granttap-mcp 0.8.66 → 0.8.68
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/apps/bridge/src/bin/cli/phone.ts +2 -1
- package/apps/bridge/src/bin/hooks/claude-prompt-hook.ts +7 -1
- package/apps/bridge/src/bin/runtime/monitor.ts +11 -0
- package/apps/bridge/src/capabilities/index.ts +1 -0
- package/apps/bridge/src/capabilities/routing/index.ts +28 -0
- package/apps/bridge/src/mesh/admin/convergence.ts +1 -0
- package/apps/bridge/src/mesh/catalog/binding-state.ts +3 -1
- package/apps/bridge/src/mesh/catalog/index.ts +1 -0
- package/apps/bridge/src/mesh/identity/call-scope.ts +1 -1
- package/apps/bridge/src/mesh/runtime/dependencies.ts +8 -0
- package/apps/bridge/src/mesh/runtime/graph/README.md +26 -0
- package/apps/bridge/src/mesh/runtime/graph/analysis.ts +3 -1
- package/apps/bridge/src/mesh/runtime/graph/binding-sync.ts +6 -2
- package/apps/bridge/src/mesh/runtime/graph/checkout-identity.ts +29 -0
- package/apps/bridge/src/mesh/runtime/handoff.ts +45 -17
- package/apps/bridge/src/mesh/runtime/index.ts +64 -7
- package/apps/bridge/src/mesh/runtime/prepared-transfer/index.ts +27 -0
- package/apps/bridge/src/mesh/runtime/snapshot/enriched.ts +3 -1
- package/apps/bridge/src/mesh/runtime/worktree.ts +25 -4
- package/apps/bridge/src/mesh/store/base.ts +6 -0
- package/apps/bridge/src/mesh/store/index.ts +14 -23
- package/apps/bridge/src/mesh/store/receipts/index.ts +57 -0
- package/apps/bridge/src/mesh/store/sync.ts +2 -101
- package/apps/bridge/src/mesh/tasks/state.ts +23 -3
- package/apps/bridge/src/monitor/handlers/inbound.ts +21 -10
- package/apps/bridge/src/monitor/handlers/session-commands.ts +15 -83
- package/apps/bridge/src/monitor/handlers/task-create.ts +5 -1
- package/apps/bridge/src/monitor/handlers/user-message.ts +15 -28
- package/apps/bridge/src/monitor/support/runtime.ts +2 -0
- package/apps/bridge/src/policy/effective-action.ts +8 -0
- package/apps/bridge/src/reply/index.ts +31 -7
- package/apps/bridge/src/sessions/cursor/activity.ts +8 -7
- package/apps/bridge/src/sessions/index.ts +5 -2
- package/apps/bridge/src/sessions/scan/codex/parse.ts +4 -0
- package/apps/bridge/src/sessions/scan/grok.ts +9 -5
- package/apps/bridge/src/task-control/README.md +13 -0
- package/apps/bridge/src/task-control/index.ts +72 -0
- package/apps/bridge/src/task-dispatch/README.md +22 -0
- package/apps/bridge/src/task-dispatch/inbox.ts +168 -0
- package/apps/bridge/src/task-dispatch/index.ts +4 -0
- package/apps/bridge/src/task-dispatch/journal.ts +65 -0
- package/apps/bridge/src/task-dispatch/receipts/index.ts +54 -0
- package/apps/bridge/src/task-dispatch/records.ts +30 -0
- package/apps/bridge/src/task-dispatch/sender/index.ts +101 -0
- package/apps/bridge/src/task-dispatch/sender/store.ts +91 -0
- package/apps/bridge/src/task-dispatch/worker/index.ts +120 -0
- package/apps/bridge/src/task-dispatch/worker/monitor.ts +62 -0
- package/apps/bridge/src/task-dispatch/worker/remote.ts +20 -0
- package/apps/bridge/src/task-network/README.md +34 -0
- package/apps/bridge/src/task-network/configuration/README.md +18 -0
- package/apps/bridge/src/task-network/configuration/ciphertext.ts +36 -0
- package/apps/bridge/src/task-network/configuration/runtime.ts +43 -0
- package/apps/bridge/src/task-network/configuration/sync.ts +35 -0
- package/apps/bridge/src/task-network/index.ts +95 -0
- package/apps/bridge/src/task-network/runtime/configuration.ts +34 -0
- package/apps/bridge/src/task-network/runtime/index.ts +3 -0
- package/apps/bridge/src/task-network/runtime/monitor.ts +53 -0
- package/apps/bridge/src/task-network/runtime/preparation.ts +119 -0
- package/apps/bridge/src/task-network/runtime/receiver.ts +114 -0
- package/apps/bridge/src/task-network/source/index.ts +160 -0
- package/apps/bridge/src/task-network/source/runtime.ts +62 -0
- package/apps/bridge/src/task-network/storage/incoming.ts +109 -0
- package/apps/bridge/src/task-network/storage/outgoing.ts +97 -0
- package/apps/bridge/src/task-network/storage/private-file.ts +25 -0
- package/apps/bridge/src/task-network/transport/ciphertext.ts +36 -0
- package/apps/bridge/src/task-network/transport/http.ts +62 -0
- package/apps/bridge/src/task-network/transport/receive.ts +30 -0
- package/apps/bridge/src/task-routing/README.md +18 -0
- package/apps/bridge/src/task-routing/admission/index.ts +67 -0
- package/apps/bridge/src/task-routing/index.ts +92 -0
- package/apps/bridge/src/task-routing/observed-turn/codex.ts +48 -0
- package/apps/bridge/src/task-routing/observed-turn/index.ts +15 -0
- package/apps/bridge/src/task-routing/requests/index.ts +123 -0
- package/apps/bridge/src/task-routing/requests/processor.ts +58 -0
- package/apps/bridge/src/task-routing/resources/README.md +19 -0
- package/apps/bridge/src/task-routing/resources/hardware.ts +56 -0
- package/apps/bridge/src/task-routing/resources/index.ts +31 -0
- package/apps/bridge/src/task-routing/resources/observations.ts +53 -0
- package/apps/bridge/src/task-routing/resources/reporter.ts +60 -0
- package/apps/bridge/src/task-routing/resources/runtime.ts +30 -0
- package/apps/bridge/src/task-routing/resources/transport.ts +52 -0
- package/apps/bridge/src/task-routing/runtime/admission.ts +37 -0
- package/apps/bridge/src/task-routing/runtime/index.ts +61 -0
- package/apps/bridge/src/task-routing/runtime/observation.ts +39 -0
- package/apps/bridge/src/task-routing/runtime/router.ts +84 -0
- package/apps/bridge/src/task-transfer/README.md +16 -0
- package/apps/bridge/src/task-transfer/context/archive.ts +10 -0
- package/apps/bridge/src/task-transfer/context/images.ts +32 -0
- package/apps/bridge/src/task-transfer/context/materialize.ts +36 -0
- package/apps/bridge/src/task-transfer/context.ts +57 -0
- package/apps/bridge/src/task-transfer/continuation/README.md +8 -0
- package/apps/bridge/src/task-transfer/continuation/index.ts +124 -0
- package/apps/bridge/src/task-transfer/index.ts +111 -0
- package/apps/bridge/src/task-transfer/persistence.ts +109 -0
- package/apps/bridge/src/task-transfer/projection/index.ts +29 -0
- package/apps/bridge/src/task-transfer/projection/storage.ts +68 -0
- package/apps/bridge/src/task-transfer/records.ts +16 -0
- package/apps/bridge/src/task-transfer/runtime/README.md +7 -0
- package/apps/bridge/src/task-transfer/runtime/commands.ts +12 -0
- package/apps/bridge/src/task-transfer/runtime/continuation.ts +14 -0
- package/apps/bridge/src/task-transfer/runtime/index.ts +3 -0
- package/apps/bridge/src/task-transfer/runtime/local.ts +10 -0
- package/apps/bridge/src/transcript-history/images/native.ts +5 -1
- package/apps/bridge/src/transcript-history/index.ts +10 -3
- package/apps/mcp/src/desktop/delivery/README.md +5 -0
- package/apps/mcp/src/desktop/delivery/attachments.ts +19 -4
- package/apps/mcp/src/desktop/delivery/task-handoff.ts +1 -0
- package/apps/mcp/src/desktop/delivery/task-scope.ts +26 -0
- package/apps/mcp/src/desktop/delivery/task-send.ts +17 -26
- package/apps/mcp/src/desktop/engine-bridge.ts +10 -3
- package/apps/mcp/src/desktop/native-access/README.md +4 -0
- package/apps/mcp/src/desktop/native-access/routes.ts +1 -1
- package/apps/mcp/src/desktop/operations/index.ts +11 -0
- package/apps/mcp/src/desktop/project-transport/README.md +13 -0
- package/apps/mcp/src/desktop/project-transport/index.ts +71 -0
- package/apps/mcp/src/desktop/task-activity-runner.ts +8 -1
- package/apps/mcp/src/desktop/task-activity-worker.ts +4 -0
- package/apps/mcp/src/desktop/task-control/README.md +26 -0
- package/apps/mcp/src/desktop/task-control/capabilities.ts +29 -0
- package/apps/mcp/src/desktop/task-control/index.ts +30 -0
- package/apps/mcp/src/desktop/task-control/preferences.ts +57 -0
- package/apps/mcp/src/desktop/task-queue/README.md +14 -0
- package/apps/mcp/src/desktop/task-queue/index.ts +63 -0
- package/apps/mcp/src/desktop/task-transfer/README.md +7 -0
- package/apps/mcp/src/desktop/task-transfer/index.ts +27 -0
- package/apps/mcp/src/http/create-server.ts +2 -0
- package/apps/mcp/src/mcp-tools/routing/index.ts +27 -0
- package/apps/mcp/src/status/desktop-status.ts +2 -0
- package/bin/granttap-mcp.mjs +1 -1
- package/cursor-plugin/README.md +1 -1
- package/cursor-plugin/mcp.json +1 -1
- package/cursor-plugin/stdio-bootstrap.cjs +1 -1
- package/package.json +1 -1
- package/packages/core/README.md +24 -0
- package/packages/core/relay-client-types.ts +3 -0
- package/packages/core/relay-client.ts +39 -97
- package/packages/core/relay-inbound/index.ts +50 -0
- package/packages/core/replay-store.ts +29 -0
- package/packages/core/socket-lifecycle/index.ts +2 -0
- package/packages/core/socket-lifecycle/upgrade-headers.ts +11 -0
- package/packages/core/store-file-lock/README.md +11 -0
- package/packages/core/store-file-lock/index.ts +103 -0
- package/packages/core/task-dispatch/index.ts +37 -0
- package/packages/core/task-job-http/README.md +9 -0
- package/packages/core/task-job-http/index.ts +31 -0
- package/packages/protocol/messages/mesh/events.ts +6 -0
- package/packages/protocol/messages/mesh/tasks.ts +2 -0
- package/packages/protocol/messages/sessions/index.ts +4 -0
- package/packages/protocol/schema.ts +11 -0
- package/packages/protocol/task-dispatch/README.md +17 -0
- package/packages/protocol/task-dispatch/index.ts +40 -0
- package/packages/protocol/task-network/index.ts +92 -0
- package/packages/protocol/task-resources/index.ts +33 -0
- package/packages/protocol/task-transfer/README.md +7 -0
- package/packages/protocol/task-transfer/index.ts +18 -0
|
@@ -5,7 +5,8 @@ import { createOneTimePairing, DEFAULT_RELAY, PAIRING_CODE_TTL_MINUTES } from ".
|
|
|
5
5
|
import { isMachineConfigured, readOnlyMachineConfigPath } from "../../../../mcp/src/status/pairing-status";
|
|
6
6
|
import { installHttpMcpService } from "../../../../mcp/src/http-service";
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
// Direct invocation may retain the `phone` route; the published launcher strips it.
|
|
9
|
+
const mode = process.argv[2] === "phone" ? process.argv[3] : process.argv[2];
|
|
9
10
|
|
|
10
11
|
async function main(): Promise<void> {
|
|
11
12
|
if (mode !== "add" && mode !== "reconnect") {
|
|
@@ -5,11 +5,13 @@
|
|
|
5
5
|
* Registered in ~/.claude/settings.json so that when a person submits a
|
|
6
6
|
* prompt, this program adds what the chat could not know by itself: the
|
|
7
7
|
* background runs that answered phone messages in it, and the Mesh brief for
|
|
8
|
-
* its Task.
|
|
8
|
+
* its Task. Transferred chats require explicit continuation; ordinary prompts retain the existing context behavior.
|
|
9
9
|
*
|
|
10
10
|
* Reads: UserPromptSubmit JSON on stdin ({ session_id, prompt, cwd, ... })
|
|
11
11
|
* Writes: { hookSpecificOutput: { hookEventName, additionalContext } } on stdout
|
|
12
12
|
*/
|
|
13
|
+
import { localTransferContinuations } from "../../task-transfer";
|
|
14
|
+
import { localRoutingRequests } from '../../task-routing/requests';
|
|
13
15
|
import { promptContext } from "../../mesh/context/prompt";
|
|
14
16
|
|
|
15
17
|
async function readStdin(): Promise<string> {
|
|
@@ -30,6 +32,10 @@ async function main(): Promise<void> {
|
|
|
30
32
|
return;
|
|
31
33
|
}
|
|
32
34
|
const sessionId = typeof input.session_id === "string" ? input.session_id.trim() : "";
|
|
35
|
+
const transferred = localTransferContinuations().blockReason(sessionId) ?? localRoutingRequests().blockReason(sessionId);
|
|
36
|
+
if (transferred) {
|
|
37
|
+
process.stdout.write(JSON.stringify({ decision: "block", reason: transferred })); return;
|
|
38
|
+
}
|
|
33
39
|
const text = promptContext(sessionId);
|
|
34
40
|
if (!text) return;
|
|
35
41
|
process.stdout.write(JSON.stringify({
|
|
@@ -12,11 +12,16 @@ import { applyNetworkRoute } from "../../device-network/settings";
|
|
|
12
12
|
import { startEndpointPublisher } from "../../device-network/directory";
|
|
13
13
|
import { managedAdmissionOptions } from "../../managed-admission";
|
|
14
14
|
|
|
15
|
+
import { startTaskDispatchMonitor } from "../../task-dispatch/worker/monitor";
|
|
16
|
+
import { startTaskNetworkMonitor } from '../../task-network/runtime';
|
|
17
|
+
|
|
15
18
|
let client: RelayClient;
|
|
16
19
|
let monitor: ReturnType<typeof startSessionMonitor>;
|
|
17
20
|
const engine = new EngineSupervisor();
|
|
18
21
|
let stopping = false;
|
|
19
22
|
let stopEndpointPublisher = () => {};
|
|
23
|
+
let stopTaskDispatch = () => {};
|
|
24
|
+
let stopTaskNetwork = () => {};
|
|
20
25
|
|
|
21
26
|
try {
|
|
22
27
|
const cfg = applyNetworkRoute(loadConfig(machineConfigPath()));
|
|
@@ -27,6 +32,10 @@ try {
|
|
|
27
32
|
peerAllowed: controllerPeerGate(cfg),
|
|
28
33
|
});
|
|
29
34
|
monitor = startSessionMonitor(client);
|
|
35
|
+
stopTaskDispatch = startTaskDispatchMonitor({ config: cfg, client,
|
|
36
|
+
isLeader: monitor.isLeader, peerAllowed: controllerPeerGate(cfg), publish: monitor.publish });
|
|
37
|
+
stopTaskNetwork = startTaskNetworkMonitor({ config: cfg, client,
|
|
38
|
+
isLeader: monitor.isLeader, peerAllowed: controllerPeerGate(cfg), publish: monitor.publish });
|
|
30
39
|
stopEndpointPublisher = startEndpointPublisher(cfg);
|
|
31
40
|
} catch (error) {
|
|
32
41
|
process.stderr.write(
|
|
@@ -38,6 +47,8 @@ try {
|
|
|
38
47
|
const stop = (): void => {
|
|
39
48
|
if (stopping) return;
|
|
40
49
|
stopping = true;
|
|
50
|
+
stopTaskDispatch();
|
|
51
|
+
stopTaskNetwork();
|
|
41
52
|
monitor.close();
|
|
42
53
|
stopEndpointPublisher();
|
|
43
54
|
client.close();
|
|
@@ -3,6 +3,7 @@ import { descriptorsForProvider, descriptorsForSession, mcpConfigDigest } from "
|
|
|
3
3
|
import { cachedMetadata, refreshDescriptorMetadata } from "./metadata";
|
|
4
4
|
|
|
5
5
|
export { projectSharedSkills, workspaceSkills } from "./skills";
|
|
6
|
+
export { deliveryRoutingError } from "./routing";
|
|
6
7
|
export { mcpConfigDigest } from "./descriptors";
|
|
7
8
|
|
|
8
9
|
/** Resolve real MCP initialization metadata and cache it for a session. */
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { SessionInfo } from "../../../../../packages/protocol/schema";
|
|
2
|
+
import { loadRuntimeConfig } from "../../config";
|
|
3
|
+
import type { DeliveryOptions } from "../../reply/payload/types";
|
|
4
|
+
import { mcpServersForSession, workspaceSkills } from "..";
|
|
5
|
+
|
|
6
|
+
/** Validate explicit routing against current machine and Task capability rules. */
|
|
7
|
+
export function deliveryRoutingError(target: SessionInfo, options: DeliveryOptions): string | undefined {
|
|
8
|
+
const runtime = loadRuntimeConfig();
|
|
9
|
+
const settings = runtime.providerSettings;
|
|
10
|
+
if (target.agent in settings && settings[target.agent as keyof typeof settings] === false) {
|
|
11
|
+
return "This agent is disabled in GrantTap Settings.";
|
|
12
|
+
}
|
|
13
|
+
if (options.preferredMcp) {
|
|
14
|
+
const disabled = runtime.sessionMcpDisabled[target.sessionId] ?? [];
|
|
15
|
+
if (!mcpServersForSession(target, disabled).some(server => server.name === options.preferredMcp && server.allowed)) {
|
|
16
|
+
return "The selected MCP server is not allowed for this task.";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
if (options.skill) {
|
|
20
|
+
if (!workspaceSkills(target.cwd).some(skill => skill.name === options.skill)) {
|
|
21
|
+
return "The selected project skill is no longer available in this task's folder.";
|
|
22
|
+
}
|
|
23
|
+
if ((runtime.sessionSkillsDisabled[target.sessionId] ?? []).includes(options.skill)) {
|
|
24
|
+
return "The selected project skill is disabled for this task.";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
@@ -54,6 +54,7 @@ export function preferExecution(
|
|
|
54
54
|
if ((current.endedAt != null) !== (incoming.endedAt != null)) {
|
|
55
55
|
const ended = current.endedAt != null ? current : incoming;
|
|
56
56
|
const live = current.endedAt != null ? incoming : current;
|
|
57
|
+
if (live.continuedAt != null && live.continuedAt > ended.endedAt!) return live;
|
|
57
58
|
return { ...live, endedAt: ended.endedAt };
|
|
58
59
|
}
|
|
59
60
|
const currentAt = current.updatedAt ?? current.startedAt;
|
|
@@ -81,7 +81,9 @@ export function workspaceForRepository(
|
|
|
81
81
|
.filter((task) => projectIds.has(task.projectId)).map((task) => task.taskId));
|
|
82
82
|
const local = state.executions.find((item) =>
|
|
83
83
|
taskIds.has(item.taskId) && (computerId == null || item.computerId === computerId));
|
|
84
|
-
if (computerId != null) return local?.worktree ?? local?.workspace
|
|
84
|
+
if (computerId != null) return local?.worktree ?? local?.workspace
|
|
85
|
+
?? state.bindings.find(binding => binding.endpointId === computerId
|
|
86
|
+
&& binding.repositoryId === canonicalRepositoryId && binding.available)?.localPathHint;
|
|
85
87
|
return local?.worktree ?? local?.workspace
|
|
86
88
|
?? state.projects.find((item) => projectIds.has(item.projectId))?.repositoryRoot;
|
|
87
89
|
}
|
|
@@ -224,6 +224,7 @@ function linkSession(input: {
|
|
|
224
224
|
workspace: cwd,
|
|
225
225
|
repositoryId: repository.canonicalRepositoryId,
|
|
226
226
|
activeAt: session.lastActivityAt,
|
|
227
|
+
state: session.state, continuedAt: session.transfer?.continuedAt,
|
|
227
228
|
branch: repository.branch ?? session.branch,
|
|
228
229
|
worktree: repository.worktree,
|
|
229
230
|
uncommitted: hasUncommittedWork(repository.worktree ?? cwd),
|
|
@@ -49,7 +49,7 @@ function canonical(value: unknown): unknown {
|
|
|
49
49
|
export function grantTapToolName(rawToolName: unknown): string | undefined {
|
|
50
50
|
const raw = String(rawToolName ?? "").trim();
|
|
51
51
|
const tool = raw.includes("__") ? raw.split("__").at(-1)! : raw;
|
|
52
|
-
return ["notify", "connect", "ask", "ask_yes_no"].includes(tool) ? tool : undefined;
|
|
52
|
+
return ["notify", "connect", "ask", "ask_yes_no", "offload_task"].includes(tool) ? tool : undefined;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
export function meshCallKey(tool: string, args: unknown): string {
|
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
MeshProvider,
|
|
6
6
|
MeshSnapshot,
|
|
7
7
|
SessionInfo,
|
|
8
|
+
TaskCapsule,
|
|
8
9
|
} from "../../../../../packages/protocol/schema";
|
|
9
10
|
import type { createCodexSession, deliverToSession } from "../../reply";
|
|
10
11
|
import type { MeshStore } from "../store";
|
|
@@ -15,6 +16,12 @@ type WorktreeResult = NonNullable<ReturnType<typeof createHandoffWorktree>>;
|
|
|
15
16
|
|
|
16
17
|
export type MeshRuntimeDependencies = {
|
|
17
18
|
store: () => MeshStore;
|
|
19
|
+
continuations?: () => import("../../task-transfer").TransferContinuations;
|
|
20
|
+
transfers?: () => import('../../task-transfer').PreparedTaskTransfers;
|
|
21
|
+
captureTransferContext?: typeof import('../../task-transfer').captureTransferContext;
|
|
22
|
+
sourceCaptureCurrent?: (session: SessionInfo, capsule: TaskCapsule) => boolean;
|
|
23
|
+
dispatchPreparedTransfer?: (event: MeshEvent, session: SessionInfo,
|
|
24
|
+
confirmContext: () => Promise<boolean>) => Promise<boolean>;
|
|
18
25
|
sessions: () => SessionInfo[];
|
|
19
26
|
/** Observed native configuration for a session on this endpoint. */
|
|
20
27
|
capabilityInventory?: (session: SessionInfo) => SessionInfo;
|
|
@@ -34,6 +41,7 @@ export type MeshRuntimeDependencies = {
|
|
|
34
41
|
taskId: string,
|
|
35
42
|
provider: MeshProvider,
|
|
36
43
|
revision: string,
|
|
44
|
+
operationId?: string,
|
|
37
45
|
) => WorktreeResult | undefined | null;
|
|
38
46
|
hasCommit: (repository: string, revision: string) => boolean;
|
|
39
47
|
/** Publish the branch a capsule names, when the person asked for it. */
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Repository graph refresh
|
|
2
|
+
|
|
3
|
+
Public entry point: `analysis.ts` for explicit builds; `binding-sync.ts` for
|
|
4
|
+
verified local admission and recovery; `checkout-identity.ts` for checkout scope.
|
|
5
|
+
|
|
6
|
+
A remote identity must match the checkout's observed Git origin. A `local:`
|
|
7
|
+
identity must resolve to the exact Git root it names. Gaining an origin does not
|
|
8
|
+
invalidate an existing local-directory binding or move it to another Project.
|
|
9
|
+
Local bindings are admitted to Engine as path identities without a canonical
|
|
10
|
+
remote claim. Changed remote identities and local paths to different checkouts
|
|
11
|
+
remain rejected. Neither Task identity nor Project membership is rewritten.
|
|
12
|
+
|
|
13
|
+
After admission, `graphAnalysisBindings` selects one graph per endpoint and real
|
|
14
|
+
checkout path, preferring an admitted canonical binding over a duplicate local
|
|
15
|
+
alias. Failed bindings remain visible with their original error. Both the Mac
|
|
16
|
+
projection and phone refresh use this rule. The graph contains actual Engine
|
|
17
|
+
analysis; unavailable analysis is never synthesized as a successful graph.
|
|
18
|
+
|
|
19
|
+
Behavior tests: `tests/`, root `tests/graph/graph-binding-sync.test.ts`, and
|
|
20
|
+
`apps/mcp/src/desktop/projection/tests/graph-refresh.test.ts`. The optional
|
|
21
|
+
`engine-selection-live.test.ts` uses `GRANTTAP_ENGINE_TEST_BINARY` to verify the
|
|
22
|
+
actual native scanner respects `.git/info/exclude` and retains untracked source.
|
|
23
|
+
|
|
24
|
+
## License
|
|
25
|
+
|
|
26
|
+
Apache License 2.0. See [LICENSE](../../../../../../LICENSE).
|
|
@@ -5,6 +5,7 @@ import { sendMeshPayload } from "../../../host/session-keys";
|
|
|
5
5
|
import { publishedCatalog } from "../../../session-catalog";
|
|
6
6
|
import { meshSnapshot, meshSnapshots } from "..";
|
|
7
7
|
import { refreshLocalGraphBindings } from "./binding-sync";
|
|
8
|
+
import { graphAnalysisBindings } from "./checkout-identity";
|
|
8
9
|
|
|
9
10
|
/** A person's explicit Graph refresh bypasses background job backoff. */
|
|
10
11
|
type GraphAnalysisDependencies = {
|
|
@@ -48,7 +49,8 @@ export async function analyzeProjectGraphNow(
|
|
|
48
49
|
const rejected = new Set(failures.map(report => report.repositoryId));
|
|
49
50
|
const [backbone, repositoryGraphs] = await Promise.all([
|
|
50
51
|
dependencies.backbone(projectId),
|
|
51
|
-
dependencies.graphs(projectId, (current.bindings ?? [])
|
|
52
|
+
dependencies.graphs(projectId, graphAnalysisBindings((current.bindings ?? [])
|
|
53
|
+
.filter(binding => !rejected.has(binding.repositoryId))), { background: false }),
|
|
52
54
|
]);
|
|
53
55
|
const observed = [...repositoryGraphs, ...failures];
|
|
54
56
|
const reports = observed.length > 0 ? observed : [{
|
|
@@ -3,6 +3,7 @@ import type { MeshSnapshot, ProjectRepositoryGraph } from "../../../../../../pac
|
|
|
3
3
|
import { queueProjectBindingSync } from "../../../engine/runtime/engine-projects";
|
|
4
4
|
import { inspectRepository } from "../../catalog";
|
|
5
5
|
import { computerId } from "../../identity/computer";
|
|
6
|
+
import { matchesLocalCheckout } from "./checkout-identity";
|
|
6
7
|
|
|
7
8
|
/** Re-admit this computer's verified checkouts before an explicit Graph build. */
|
|
8
9
|
export async function refreshLocalGraphBindings(
|
|
@@ -21,8 +22,9 @@ export async function refreshLocalGraphBindings(
|
|
|
21
22
|
try {
|
|
22
23
|
const path = binding.localPathHint!;
|
|
23
24
|
const facts = dependencies.inspect(path);
|
|
25
|
+
const pathIdentity = matchesLocalCheckout(binding.repositoryId, facts.root);
|
|
24
26
|
if (!facts.worktree || realpathSync(facts.root) !== realpathSync(path)
|
|
25
|
-
|| facts.canonicalRepositoryId !== binding.repositoryId) {
|
|
27
|
+
|| (facts.canonicalRepositoryId !== binding.repositoryId && !pathIdentity)) {
|
|
26
28
|
failures.push({ projectId: snapshot.projectId, repositoryId: binding.repositoryId,
|
|
27
29
|
revision: "unverified", weavatrixVersion: "unknown", analysisStatus: "UNAVAILABLE",
|
|
28
30
|
analysisErrorCode: facts.worktree ? "REPOSITORY_IDENTITY_MISMATCH" : "REPOSITORY_IDENTITY_UNVERIFIED",
|
|
@@ -32,7 +34,9 @@ export async function refreshLocalGraphBindings(
|
|
|
32
34
|
const admitted = await dependencies.sync(snapshot.project, {
|
|
33
35
|
summary: binding,
|
|
34
36
|
localRoot: facts.root,
|
|
35
|
-
|
|
37
|
+
// A local binding is scoped by its verified path. Do not relabel it as
|
|
38
|
+
// a remote repository or move it into that repository's Project scope.
|
|
39
|
+
canonicalRemote: pathIdentity ? undefined : facts.baseRemote,
|
|
36
40
|
lastSeenAt: Date.now(),
|
|
37
41
|
});
|
|
38
42
|
if (!admitted) failures.push(admissionFailure(snapshot, binding));
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
2
|
+
import { isAbsolute } from "node:path";
|
|
3
|
+
import type { ProjectBindingSummary } from "../../../../../../packages/protocol/schema";
|
|
4
|
+
|
|
5
|
+
/** Local identities name an exact directory, even after that directory gains an origin. */
|
|
6
|
+
export function matchesLocalCheckout(repositoryId: string, root: string): boolean {
|
|
7
|
+
if (!repositoryId.startsWith("local:")) return false;
|
|
8
|
+
const path = repositoryId.slice("local:".length);
|
|
9
|
+
if (!isAbsolute(path)) return false;
|
|
10
|
+
try { return realpathSync(path) === realpathSync(root); }
|
|
11
|
+
catch { return false; }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Once admitted, one checkout contributes one graph to this Mesh. */
|
|
15
|
+
export function graphAnalysisBindings(bindings: ProjectBindingSummary[]): ProjectBindingSummary[] {
|
|
16
|
+
const selected = new Map<string, ProjectBindingSummary>();
|
|
17
|
+
for (const binding of bindings) {
|
|
18
|
+
let path: string | undefined;
|
|
19
|
+
if (binding.available && binding.localPathHint) {
|
|
20
|
+
try { path = realpathSync(binding.localPathHint); } catch { /* Keep an unavailable checkout distinct. */ }
|
|
21
|
+
}
|
|
22
|
+
const key = JSON.stringify([binding.endpointId, path ?? binding.repositoryId]);
|
|
23
|
+
const previous = selected.get(key);
|
|
24
|
+
if (!previous || (previous.repositoryId.startsWith("local:") && !binding.repositoryId.startsWith("local:"))) {
|
|
25
|
+
selected.set(key, binding);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return [...selected.values()];
|
|
29
|
+
}
|
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
* catalog, snapshots, and inbound routing.
|
|
7
7
|
*/
|
|
8
8
|
import type { RelayClient } from "../../../../../packages/core/relay-client";
|
|
9
|
-
import type { MeshEvent, MeshHandoffPrepare, TaskCapsule } from "../../../../../packages/protocol/schema";
|
|
10
|
-
import { handoffReceipt } from "../handoff";
|
|
9
|
+
import type { MeshEvent, MeshHandoffPrepare, TaskCapsule, SessionInfo } from "../../../../../packages/protocol/schema";
|
|
10
|
+
import { handoffReceipt, capsuleHash } from "../handoff";
|
|
11
|
+
import { localTaskTransfers, localTransferContinuations, transferDigest } from "../../task-transfer";
|
|
11
12
|
import type { MeshRuntimeDependencies } from "./dependencies";
|
|
12
13
|
|
|
13
14
|
function capsulePrompt(capsule: TaskCapsule): string {
|
|
@@ -132,23 +133,42 @@ async function acceptHandoff(
|
|
|
132
133
|
+ "or hand off from a commit this computer already has.",
|
|
133
134
|
);
|
|
134
135
|
}
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
136
|
+
const transfers = capsule.launch === "on_message" ? (deps.transfers ?? localTaskTransfers)() : undefined;
|
|
137
|
+
const previous = transfers?.forEvent(event.eventId);
|
|
138
|
+
if (previous && (previous.projectId !== event.projectId
|
|
139
|
+
|| previous.sourceSessionId !== event.sourceSessionId
|
|
140
|
+
|| capsuleHash(previous.capsule) !== capsuleHash(capsule))) {
|
|
141
|
+
return rejectHandoff(deps, client, event, "This transfer id already names different work.");
|
|
142
|
+
}
|
|
143
|
+
if (transfers && !previous && (!event.payload.transferContext || !capsule.title
|
|
144
|
+
|| transferDigest(event.payload.transferContext) !== capsule.contextHash)) {
|
|
145
|
+
return rejectHandoff(deps, client, event, "The complete transfer context could not be confirmed.");
|
|
146
|
+
}
|
|
147
|
+
const worktree = previous ? { path: previous.workspace, branch: previous.branch }
|
|
148
|
+
: transfers ? deps.worktree(repository, event.taskId, capsule.targetProvider, revision, event.eventId)
|
|
149
|
+
: deps.worktree(repository, event.taskId, capsule.targetProvider, revision);
|
|
141
150
|
if (!worktree) return rejectHandoff(deps, client, event, "A separate handoff worktree could not be created.");
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
151
|
+
let targetSessionId: string;
|
|
152
|
+
if (transfers) {
|
|
153
|
+
try {
|
|
154
|
+
targetSessionId = previous?.sessionId ?? transfers.prepare({ eventId: event.eventId,
|
|
155
|
+
projectId: event.projectId, sourceSessionId: event.sourceSessionId,
|
|
156
|
+
title: capsule.title!, capsule, workspace: worktree.path, branch: worktree.branch,
|
|
157
|
+
context: event.payload.transferContext!, acceptedAt: deps.now() }).sessionId;
|
|
158
|
+
} catch {
|
|
159
|
+
return rejectHandoff(deps, client, event, "The complete transfer could not be saved on this computer.");
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
const result = await deps.start(capsule.targetProvider, capsulePrompt(capsule), worktree.path, capsule.targetModel);
|
|
163
|
+
if (!result.ok || !result.sessionId) {
|
|
164
|
+
const reason = result.ok ? "The target agent did not create a session." : result.error;
|
|
165
|
+
return rejectHandoff(deps, client, event, reason);
|
|
166
|
+
}
|
|
167
|
+
targetSessionId = result.sessionId;
|
|
148
168
|
}
|
|
149
169
|
store.linkExecution({
|
|
150
170
|
taskId: event.taskId,
|
|
151
|
-
sessionId:
|
|
171
|
+
sessionId: targetSessionId,
|
|
152
172
|
provider: capsule.targetProvider,
|
|
153
173
|
computerId: deps.computer(),
|
|
154
174
|
workspace: worktree.path,
|
|
@@ -156,10 +176,18 @@ async function acceptHandoff(
|
|
|
156
176
|
worktree: worktree.path,
|
|
157
177
|
startedAt: deps.now(),
|
|
158
178
|
});
|
|
159
|
-
const receipt = handoffReceipt(capsule, event.sourceSessionId,
|
|
179
|
+
const receipt = handoffReceipt(capsule, event.sourceSessionId, targetSessionId, previous?.acceptedAt ?? deps.now());
|
|
180
|
+
if (transfers) {
|
|
181
|
+
receipt.prepared = true;
|
|
182
|
+
if (capsule.sourceComputer === deps.computer()) (deps.continuations ?? localTransferContinuations)().record({
|
|
183
|
+
sourceSessionId: receipt.sourceSessionId, targetSessionId: receipt.targetSessionId,
|
|
184
|
+
taskId: receipt.taskId, projectId: event.projectId, capsuleHash: receipt.capsuleHash,
|
|
185
|
+
acceptedAt: receipt.acceptedAt, queuedAt: event.createdAt,
|
|
186
|
+
}, (deps.transfers ?? localTaskTransfers)().nativeSession({ sessionId: receipt.sourceSessionId } as SessionInfo).sessionId);
|
|
187
|
+
}
|
|
160
188
|
store.recordReceipt(receipt);
|
|
161
189
|
const accepted = responseEvent({
|
|
162
|
-
deps, source: event, sourceSessionId:
|
|
190
|
+
deps, source: event, sourceSessionId: targetSessionId, eventType: "HANDOFF_ACCEPTED",
|
|
163
191
|
payload: { receipt },
|
|
164
192
|
});
|
|
165
193
|
store.acceptEvent(accepted);
|
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
SessionInfo,
|
|
11
11
|
TaskCapsule,
|
|
12
12
|
} from "../../../../../packages/protocol/schema";
|
|
13
|
+
import { captureTransferContext, localTransferContinuations, localTaskTransfers } from "../../task-transfer";
|
|
13
14
|
import { configDir } from "../../config";
|
|
14
15
|
import {
|
|
15
16
|
createClaudeSession,
|
|
@@ -20,7 +21,7 @@ import {
|
|
|
20
21
|
} from "../../reply";
|
|
21
22
|
import { isProviderEnabled } from "../../config/runtime";
|
|
22
23
|
import { sendMeshPayload } from "../../host/session-keys";
|
|
23
|
-
import { linkSessionsToProjects, workingTreeState } from "../catalog";
|
|
24
|
+
import { inspectRepository, linkSessionsToProjects, workingTreeState } from "../catalog";
|
|
24
25
|
import { catalogFromEndpoint } from "../catalog/models";
|
|
25
26
|
import { saveProjectCapabilityRequest } from "../catalog/project/requests";
|
|
26
27
|
import { computerId } from "../identity/computer";
|
|
@@ -36,6 +37,7 @@ import { createHandoffWorktree, repositoryHasCommit } from "./worktree";
|
|
|
36
37
|
import { fetchRevision, pushBranch } from "../local-remote/remote";
|
|
37
38
|
import { projectSessionCapabilityInventory } from "./capabilities";
|
|
38
39
|
import { createMeshSessionCatalog } from "./catalog/session-catalog";
|
|
40
|
+
import { capturePreparedHandoff, preparedHandoffMetadata } from './prepared-transfer';
|
|
39
41
|
|
|
40
42
|
export type { MeshRuntimeDependencies };
|
|
41
43
|
|
|
@@ -56,6 +58,14 @@ const defaultDependencies: MeshRuntimeDependencies = {
|
|
|
56
58
|
now: Date.now,
|
|
57
59
|
eventId: randomUUID,
|
|
58
60
|
providerEnabled: isProviderEnabled,
|
|
61
|
+
sourceCaptureCurrent: (session, capsule) => {
|
|
62
|
+
const cwd = session.worktree ?? session.cwd;
|
|
63
|
+
try { return Boolean(cwd && workingTreeState(cwd) === 'clean'
|
|
64
|
+
&& inspectRepository(cwd).revision === (capsule.latestCommit ?? capsule.baseSha)); }
|
|
65
|
+
catch { return false; }
|
|
66
|
+
},
|
|
67
|
+
dispatchPreparedTransfer: async (event, session, confirm) =>
|
|
68
|
+
(await import('../../task-network/runtime')).submitNetworkTransfer(event, session, confirm),
|
|
59
69
|
start: async (provider, prompt, cwd, model) => {
|
|
60
70
|
if (provider === "claude") return createClaudeSession(prompt, cwd, 240_000, [], model);
|
|
61
71
|
if (provider === "codex") return createCodexSession(prompt, cwd, 240_000, [], model);
|
|
@@ -67,12 +77,13 @@ const defaultDependencies: MeshRuntimeDependencies = {
|
|
|
67
77
|
push: pushBranch,
|
|
68
78
|
fetch: fetchRevision,
|
|
69
79
|
send: (client, payload, options) => sendMeshPayload(client, payload, "phone", options),
|
|
70
|
-
worktree: (repository, taskId, provider, revision) => createHandoffWorktree({
|
|
80
|
+
worktree: (repository, taskId, provider, revision, operationId) => createHandoffWorktree({
|
|
71
81
|
repository,
|
|
72
82
|
worktreeRoot: join(configDir(), "worktrees"),
|
|
73
83
|
taskId,
|
|
74
84
|
provider,
|
|
75
85
|
baseSha: revision,
|
|
86
|
+
operationId,
|
|
76
87
|
}),
|
|
77
88
|
};
|
|
78
89
|
|
|
@@ -89,6 +100,10 @@ async function prepareHandoff(
|
|
|
89
100
|
if (!session || session.projectId !== request.projectId || session.taskId !== request.taskId) {
|
|
90
101
|
return false;
|
|
91
102
|
}
|
|
103
|
+
if (request.prepareOnly && session.state === "working") {
|
|
104
|
+
await blockHandoff(client, request, "Pause or finish the current turn before transferring this Task.");
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
92
107
|
const cwd = session.worktree ?? session.cwd;
|
|
93
108
|
const made = request.checkpoint && cwd && workingTreeState(cwd) === "dirty"
|
|
94
109
|
? createCheckpoint(cwd, request.taskId, session.title ?? request.taskId, request.createdAt)
|
|
@@ -100,7 +115,7 @@ async function prepareHandoff(
|
|
|
100
115
|
&& (other.worktree ?? other.cwd) === cwd)
|
|
101
116
|
: false;
|
|
102
117
|
const checkpoint = made && shared ? { ...made, status: "requires_review" as const } : made;
|
|
103
|
-
|
|
118
|
+
let capsule = buildTaskCapsule(deps.store(), session, request, deps.computer(), checkpoint);
|
|
104
119
|
const readiness = handoffReadiness({
|
|
105
120
|
capsule,
|
|
106
121
|
targetProviderEnabled: request.targetProvider === "grok_bot"
|
|
@@ -119,6 +134,19 @@ async function prepareHandoff(
|
|
|
119
134
|
return false;
|
|
120
135
|
}
|
|
121
136
|
if (!capsule) return false;
|
|
137
|
+
let transferContext: string | undefined;
|
|
138
|
+
let confirmContext: (() => Promise<boolean>) | undefined;
|
|
139
|
+
if (request.prepareOnly) {
|
|
140
|
+
try {
|
|
141
|
+
const captured = await capturePreparedHandoff(deps, session, capsule);
|
|
142
|
+
transferContext = captured.transferContext;
|
|
143
|
+
capsule = captured.capsule;
|
|
144
|
+
confirmContext = captured.confirm;
|
|
145
|
+
} catch (error) {
|
|
146
|
+
await blockHandoff(client, request, error instanceof Error ? error.message : "The complete transfer context is unavailable.");
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
122
150
|
const local = request.targetComputer === deps.computer();
|
|
123
151
|
if (request.push && !local && cwd) {
|
|
124
152
|
const pushed = deps.push(cwd, capsule.branch);
|
|
@@ -138,10 +166,22 @@ async function prepareHandoff(
|
|
|
138
166
|
eventType: "HANDOFF_REQUEST",
|
|
139
167
|
createdAt,
|
|
140
168
|
expiresAt: createdAt + 60 * 60_000,
|
|
141
|
-
payload: { capsule },
|
|
169
|
+
payload: { capsule, transferContext },
|
|
142
170
|
};
|
|
171
|
+
if (request.prepareOnly && !local && deps.dispatchPreparedTransfer) {
|
|
172
|
+
try {
|
|
173
|
+
if (await deps.dispatchPreparedTransfer(event, session, confirmContext!)) {
|
|
174
|
+
await deps.send(client, preparedHandoffMetadata(event), { ttlMs: 60 * 60_000, wake: true }).catch(() => {});
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
await blockHandoff(client, request, 'Project computer transfer is not configured for this destination. Connect its Project in GrantTap.');
|
|
178
|
+
} catch (error) {
|
|
179
|
+
await blockHandoff(client, request, error instanceof Error ? error.message : 'The transfer could not be saved.');
|
|
180
|
+
}
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
143
183
|
deps.store().acceptEvent(event);
|
|
144
|
-
await deps.send(client, event, { ttlMs: 60 * 60_000, wake: true });
|
|
184
|
+
await deps.send(client, request.prepareOnly ? preparedHandoffMetadata(event) : event, { ttlMs: 60 * 60_000, wake: true });
|
|
145
185
|
if (local) await acceptHandoff(client, event);
|
|
146
186
|
return true;
|
|
147
187
|
}
|
|
@@ -199,8 +239,20 @@ export function createMeshRuntime(deps: MeshRuntimeDependencies) {
|
|
|
199
239
|
return true;
|
|
200
240
|
}
|
|
201
241
|
const fresh = store.acceptEvent(payload);
|
|
242
|
+
if (payload.eventType === "HANDOFF_REQUEST" && origin === "relay"
|
|
243
|
+
&& (fresh || payload.payload.capsule?.launch === "on_message")) await acceptHandoff(client, payload);
|
|
202
244
|
if (!fresh) return true;
|
|
203
|
-
if (payload.eventType === "
|
|
245
|
+
if (payload.eventType === "HANDOFF_ACCEPTED" && payload.payload.receipt?.prepared) {
|
|
246
|
+
const receipt = payload.payload.receipt;
|
|
247
|
+
const request = store.eventsForProject(payload.projectId).find(event =>
|
|
248
|
+
event.eventType === "HANDOFF_REQUEST" && event.sourceSessionId === receipt.sourceSessionId
|
|
249
|
+
&& event.payload.capsule?.sourceComputer === deps.computer());
|
|
250
|
+
if (request) (deps.continuations ?? localTransferContinuations)().record({
|
|
251
|
+
sourceSessionId: receipt.sourceSessionId, targetSessionId: receipt.targetSessionId,
|
|
252
|
+
projectId: payload.projectId, taskId: receipt.taskId, capsuleHash: receipt.capsuleHash,
|
|
253
|
+
acceptedAt: receipt.acceptedAt, queuedAt: request.createdAt,
|
|
254
|
+
}, (deps.transfers ?? localTaskTransfers)().nativeSession({ sessionId: receipt.sourceSessionId } as SessionInfo).sessionId);
|
|
255
|
+
}
|
|
204
256
|
if (payload.eventType === "AGENT_QUESTION") await answerAgentQuestion(client, payload);
|
|
205
257
|
return true;
|
|
206
258
|
},
|
|
@@ -211,7 +263,12 @@ export function createMeshRuntime(deps: MeshRuntimeDependencies) {
|
|
|
211
263
|
};
|
|
212
264
|
}
|
|
213
265
|
|
|
214
|
-
|
|
266
|
+
/** Local adapters may replace a scoped transport without rebuilding runtime dependencies. */
|
|
267
|
+
export function createLocalMeshRuntime(overrides: Partial<MeshRuntimeDependencies> = {}) {
|
|
268
|
+
return createMeshRuntime({ ...defaultDependencies, ...overrides });
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const defaultRuntime = createLocalMeshRuntime();
|
|
215
272
|
|
|
216
273
|
export function meshCatalog(sessions: SessionInfo[]): SessionInfo[] {
|
|
217
274
|
return defaultRuntime.catalog(sessions);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { MeshEvent, SessionInfo, TaskCapsule } from '../../../../../../packages/protocol/schema';
|
|
2
|
+
import type { MeshRuntimeDependencies } from '../dependencies';
|
|
3
|
+
import { captureTransferContext } from '../../../task-transfer';
|
|
4
|
+
|
|
5
|
+
/** The private computer transport owns context; phone notifications are observations only. */
|
|
6
|
+
export function preparedHandoffMetadata(event: MeshEvent): MeshEvent {
|
|
7
|
+
const { transferContext: _context, ...payload } = event.payload;
|
|
8
|
+
return { ...event, payload };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Retains the original archive facts so reconfirmation hashes the same conversation. */
|
|
12
|
+
export async function capturePreparedHandoff(deps: MeshRuntimeDependencies, session: SessionInfo, facts: TaskCapsule) {
|
|
13
|
+
const capture = deps.captureTransferContext ?? captureTransferContext;
|
|
14
|
+
const captured = await capture(session, facts);
|
|
15
|
+
const task = deps.store().task(facts.taskId);
|
|
16
|
+
return { transferContext: captured.context,
|
|
17
|
+
capsule: { ...facts, title: task?.title ?? session.title ?? facts.goal.slice(0, 160),
|
|
18
|
+
launch: 'on_message' as const, contextHash: captured.hash },
|
|
19
|
+
confirm: async () => {
|
|
20
|
+
const current = deps.sessions().find(row => row.sessionId === session.sessionId);
|
|
21
|
+
if (!current || current.state === 'working' || current.model !== session.model || current.effort !== session.effort
|
|
22
|
+
|| (current.worktree ?? current.cwd) !== (session.worktree ?? session.cwd)
|
|
23
|
+
|| deps.sourceCaptureCurrent && !deps.sourceCaptureCurrent(current, facts)) return false;
|
|
24
|
+
try { return (await capture(current, facts)).hash === captured.hash; }
|
|
25
|
+
catch { return false; }
|
|
26
|
+
} };
|
|
27
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { refreshLocalGraphBindings } from "../graph/binding-sync";
|
|
2
|
+
import { graphAnalysisBindings } from "../graph/checkout-identity";
|
|
2
3
|
import type { MeshSnapshot } from "../../../../../../packages/protocol/schema";
|
|
3
4
|
import {
|
|
4
5
|
projectBackbone, projectRepositoryGraphs, waitForProjectBindingSync,
|
|
@@ -16,7 +17,8 @@ export async function enrichMeshSnapshot(
|
|
|
16
17
|
): Promise<MeshSnapshot> {
|
|
17
18
|
const identityFailures = await refreshLocalGraphBindings(snapshot);
|
|
18
19
|
const excludedRepositories = new Set(identityFailures.map((report) => report.repositoryId));
|
|
19
|
-
const bindings = (snapshot.bindings ?? [])
|
|
20
|
+
const bindings = graphAnalysisBindings((snapshot.bindings ?? [])
|
|
21
|
+
.filter((binding) => !excludedRepositories.has(binding.repositoryId)));
|
|
20
22
|
await waitForProjectBindingSync(snapshot.projectId);
|
|
21
23
|
queueProjectKnowledgeSync({
|
|
22
24
|
projectId: snapshot.projectId,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { existsSync, mkdirSync, lstatSync, realpathSync } from "node:fs";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
4
5
|
|
|
5
6
|
export type HandoffWorktree = { path: string; branch: string };
|
|
6
7
|
|
|
@@ -28,15 +29,19 @@ export function createHandoffWorktree(input: {
|
|
|
28
29
|
provider: string;
|
|
29
30
|
baseSha: string;
|
|
30
31
|
now?: number;
|
|
32
|
+
operationId?: string;
|
|
31
33
|
}): HandoffWorktree | undefined {
|
|
32
34
|
const { repository, worktreeRoot, taskId, provider, baseSha, now = Date.now() } = input;
|
|
33
35
|
if (!/^[0-9a-f]{7,64}$/i.test(baseSha)) return undefined;
|
|
36
|
+
if (input.operationId !== undefined && (!input.operationId || input.operationId.length > 128)) return undefined;
|
|
34
37
|
const task = safeSegment(taskId) || "task";
|
|
35
38
|
const agent = safeSegment(provider) || "agent";
|
|
36
|
-
const suffix =
|
|
39
|
+
const suffix = input.operationId ? createHash('sha256').update(input.operationId).digest('hex').slice(0, 24)
|
|
40
|
+
: Math.trunc(now).toString(36);
|
|
37
41
|
const branch = `granttap/${agent}/${task}-${suffix}`;
|
|
38
42
|
const path = join(worktreeRoot, `${task}-${agent}-${suffix}`);
|
|
39
|
-
if (existsSync(path)) return
|
|
43
|
+
if (existsSync(path)) return input.operationId && reusableWorktree(path, repository, baseSha, branch)
|
|
44
|
+
? { path, branch } : undefined;
|
|
40
45
|
mkdirSync(worktreeRoot, { recursive: true, mode: 0o700 });
|
|
41
46
|
try {
|
|
42
47
|
execFileSync("git", ["-C", repository, "cat-file", "-e", `${baseSha}^{commit}`], {
|
|
@@ -50,3 +55,19 @@ export function createHandoffWorktree(input: {
|
|
|
50
55
|
return undefined;
|
|
51
56
|
}
|
|
52
57
|
}
|
|
58
|
+
|
|
59
|
+
function reusableWorktree(path: string, repository: string, revision: string, branch: string): boolean {
|
|
60
|
+
const git = (cwd: string, args: string[]) => execFileSync('git', ['-C', cwd, ...args], {
|
|
61
|
+
encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 3_000, maxBuffer: 512 * 1_024,
|
|
62
|
+
}).trim();
|
|
63
|
+
try {
|
|
64
|
+
if (lstatSync(path).isSymbolicLink() || !lstatSync(path).isDirectory()) return false;
|
|
65
|
+
const root = realpathSync(path);
|
|
66
|
+
return realpathSync(git(path, ['rev-parse', '--show-toplevel'])) === root
|
|
67
|
+
&& realpathSync(resolve(path, git(path, ['rev-parse', '--git-common-dir'])))
|
|
68
|
+
=== realpathSync(resolve(repository, git(repository, ['rev-parse', '--git-common-dir'])))
|
|
69
|
+
&& git(path, ['rev-parse', 'HEAD']) === git(repository, ['rev-parse', `${revision}^{commit}`])
|
|
70
|
+
&& git(path, ['branch', '--show-current']) === branch
|
|
71
|
+
&& git(path, ['status', '--porcelain=v1', '--untracked-files=all']) === '';
|
|
72
|
+
} catch { return false; }
|
|
73
|
+
}
|