@teamscala/service-boot 0.23.335 → 0.23.336
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/package.json +1 -2
- package/src/auth-gate.ts +2 -3
- package/src/build-client-assets.test.ts +1 -1
- package/src/build-client-assets.ts +1 -1
- package/src/fn-routers/orchestrator-registration-router.d.ts +17 -3
- package/src/fn-routers/orchestrator-registration-router.ts +17 -3
- package/src/fn-routers/orchestrator-registration.live.test.ts +23 -2
- package/src/fn-routers/orchestrator-registration.test.ts +23 -0
- package/src/fn-routers/task-sync-assign.test.ts +1 -0
- package/src/fn-routers/task-sync-bind.test.ts +18 -18
- package/src/fn-routers/task-sync-brief.test.ts +2 -3
- package/src/fn-routers/task-sync-dispatch.d.ts +40 -54
- package/src/fn-routers/task-sync-dispatch.test.ts +51 -33
- package/src/fn-routers/task-sync-dispatch.ts +57 -100
- package/src/fn-routers/task-sync-lane.test.ts +36 -0
- package/src/fn-routers/task-sync-lane.ts +8 -19
- package/src/fn-routers/task-sync-projection.d.ts +23 -0
- package/src/fn-routers/task-sync-projection.test.ts +26 -0
- package/src/fn-routers/task-sync-projection.ts +51 -0
- package/src/fn-routers/task-sync-router.d.ts +30 -23
- package/src/fn-routers/task-sync-router.test.ts +14 -2
- package/src/fn-routers/task-sync-router.ts +72 -114
- package/src/handlers/sweep-host-cache.d.ts +21 -1
- package/src/handlers/sweep-host-cache.test.ts +85 -0
- package/src/handlers/sweep-host-cache.ts +118 -4
- package/src/mount-orpc-router.ts +1 -1
- package/src/resolve-brand-favicon.d.ts +1 -1
- package/src/resolve-brand-favicon.ts +1 -1
|
@@ -3,122 +3,79 @@
|
|
|
3
3
|
* @status handwritten
|
|
4
4
|
* @edit edit directly
|
|
5
5
|
*
|
|
6
|
-
* DB→AGENT
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* MECHANISM — reuses the existing CLI-agnostic channel delivery, no new
|
|
12
|
-
* transport: a POST to scala-channel's fleet-events publish entry
|
|
13
|
-
* (POST /api/fleet/events — @teamscala/channel/fleet-events-publish) carrying an
|
|
14
|
-
* `orchestrator.message` FleetEvent with slotName "cross-pane". That slot is
|
|
15
|
-
* the watchEvents bypass for events NOT tied to a fleet_agent_runs row
|
|
16
|
-
* (FleetEventOrchestratorMessageSchema comment) — exactly the system-originated
|
|
17
|
-
* case. scala-channel re-emits onto its eventBus and streamFleetEvents delivers
|
|
18
|
-
* to the subscriber filtered by orchestratorSessionId (the target lane pane).
|
|
19
|
-
* Both the Claude Code relay and the OpenCode scala-hooks plugin subscribe to
|
|
20
|
-
* that SSE stream for their own pane identity and inject each event natively —
|
|
21
|
-
* so ONE publish reaches every CLI that has a live projection, no per-CLI
|
|
22
|
-
* dispatch code here.
|
|
23
|
-
*
|
|
24
|
-
* NOT the dispatch entry (POST /api/channel/send): that path requires a real
|
|
25
|
-
* sender pane identity (channel-sender-identity-validated anti-spoof) and so
|
|
26
|
-
* cannot carry a system-originated notification. The publish path is
|
|
27
|
-
* internal-trusted (localhost super-admin) and accepts a synthetic
|
|
28
|
-
* fromOrchestratorId — the correct path for a system→pane message.
|
|
29
|
-
*
|
|
30
|
-
* scala-channel's address is resolved from its service_routing row (local_port)
|
|
31
|
-
* — never hardcoded (service-ports-from-registry). Fire-and-forget: a dispatch
|
|
32
|
-
* failure (scala-channel down, port unresolvable) is logged and swallowed; it
|
|
33
|
-
* must never block the work_items write that triggered it, and the lane's
|
|
34
|
-
* SessionStart/UserPromptSubmit backstop picks up the change on its next turn
|
|
35
|
-
* regardless.
|
|
6
|
+
* DB→AGENT task projection emission. A work_items change is delivered by one
|
|
7
|
+
* canonical comments row addressed to the active cli_sessions runtime; the
|
|
8
|
+
* database emission wakes that runtime adapter, which re-reads readTaskList.
|
|
9
|
+
* This module owns no pane transport, terminal fallback, or second message
|
|
10
|
+
* queue.
|
|
36
11
|
*/
|
|
37
12
|
|
|
38
|
-
import { loadRegistryConfig } from "@teamscala/db/registry/load-config";
|
|
39
|
-
import { ServiceRoutingConfigSchema } from "@teamscala/db-validation/registry-schemas/service-routing";
|
|
40
13
|
import { getAppLogger } from "@teamscala/logger/app-loggers";
|
|
41
14
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
cachedChannelBaseUrl = port ? `http://127.0.0.1:${port}` : null;
|
|
50
|
-
} catch {
|
|
51
|
-
cachedChannelBaseUrl = null;
|
|
52
|
-
}
|
|
53
|
-
return cachedChannelBaseUrl;
|
|
15
|
+
export interface TaskChangeComment {
|
|
16
|
+
work_item_id: string;
|
|
17
|
+
content: string;
|
|
18
|
+
system_key: string;
|
|
19
|
+
source_cli_session_id: string;
|
|
20
|
+
sender_role: "system";
|
|
21
|
+
sender_name: "TaskSync";
|
|
54
22
|
}
|
|
55
23
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
message: string;
|
|
70
|
-
fromOrchestratorId: string;
|
|
71
|
-
runId: string;
|
|
72
|
-
slotName: "cross-pane";
|
|
73
|
-
ts: number;
|
|
74
|
-
orchestratorSessionId: string;
|
|
24
|
+
export function buildTaskChangeComment(
|
|
25
|
+
taskId: string,
|
|
26
|
+
sessionId: string,
|
|
27
|
+
eventKey: string = "refresh",
|
|
28
|
+
): TaskChangeComment {
|
|
29
|
+
return {
|
|
30
|
+
work_item_id: taskId,
|
|
31
|
+
content: `TaskSync: work_items task ${taskId} changed; re-read fn.taskSync.readTaskList.`,
|
|
32
|
+
system_key: `task-sync:${taskId}:${sessionId}:${eventKey}`,
|
|
33
|
+
source_cli_session_id: sessionId,
|
|
34
|
+
sender_role: "system",
|
|
35
|
+
sender_name: "TaskSync",
|
|
36
|
+
};
|
|
75
37
|
}
|
|
76
38
|
|
|
77
|
-
export
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
39
|
+
export interface TaskDispatchPrisma {
|
|
40
|
+
cli_sessions: {
|
|
41
|
+
findFirst(args: unknown): Promise<{ id: string } | null>;
|
|
42
|
+
};
|
|
43
|
+
comments: {
|
|
44
|
+
upsert(args: {
|
|
45
|
+
where: { work_item_id_system_key: { work_item_id: string; system_key: string } };
|
|
46
|
+
create: TaskChangeComment;
|
|
47
|
+
update: Record<string, never>;
|
|
48
|
+
}): Promise<{ id: string }>;
|
|
86
49
|
};
|
|
87
50
|
}
|
|
88
51
|
|
|
89
|
-
/**
|
|
90
|
-
* Publish a "your task list changed" notification to the owning lane's pane.
|
|
91
|
-
* The pane identity is the stable tmux id ("tmux:%42") every CLI relay/plugin
|
|
92
|
-
* receives for. `taskId` is carried in the message so the recipient can re-read
|
|
93
|
-
* just that task. Fire-and-forget.
|
|
94
|
-
*/
|
|
52
|
+
/** Write the durable task-change comment for the active runtime, if present. */
|
|
95
53
|
export async function notifyLaneOfTaskChange(
|
|
96
|
-
|
|
54
|
+
prisma: TaskDispatchPrisma,
|
|
97
55
|
taskId: string,
|
|
98
|
-
|
|
56
|
+
eventKey: string = "refresh",
|
|
57
|
+
): Promise<{ delivered: boolean; sessionId: string | null; commentId: string | null }> {
|
|
99
58
|
const log = getAppLogger();
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
const payload = buildTaskChangedEvent(lanePane, taskId);
|
|
59
|
+
const session = await prisma.cli_sessions.findFirst({
|
|
60
|
+
where: { work_item_id: taskId, closed_at: null, lifecycle_state: { not: "closed" } },
|
|
61
|
+
orderBy: { created_at: "desc" },
|
|
62
|
+
select: { id: true },
|
|
63
|
+
});
|
|
64
|
+
if (!session) return { delivered: false, sessionId: null, commentId: null };
|
|
107
65
|
try {
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
66
|
+
const data = buildTaskChangeComment(taskId, session.id, eventKey);
|
|
67
|
+
const comment = await prisma.comments.upsert({
|
|
68
|
+
where: { work_item_id_system_key: { work_item_id: taskId, system_key: data.system_key } },
|
|
69
|
+
create: data,
|
|
70
|
+
update: {},
|
|
113
71
|
});
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
log.warn("[task-sync] notifyLaneOfTaskChange: POST failed", {
|
|
121
|
-
taskId, error: err instanceof Error ? err.message : String(err),
|
|
72
|
+
return { delivered: true, sessionId: session.id, commentId: comment.id };
|
|
73
|
+
} catch (error) {
|
|
74
|
+
log.warn("[task-sync] durable task comment emission failed", {
|
|
75
|
+
taskId,
|
|
76
|
+
sessionId: session.id,
|
|
77
|
+
error: error instanceof Error ? error.message : String(error),
|
|
122
78
|
});
|
|
79
|
+
return { delivered: false, sessionId: session.id, commentId: null };
|
|
123
80
|
}
|
|
124
81
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @system service-boot
|
|
3
|
+
* @status handwritten
|
|
4
|
+
* @edit edit directly
|
|
5
|
+
*
|
|
6
|
+
* Behavioral tests for canonical CLI-session lane resolution.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, expect, it } from "bun:test";
|
|
9
|
+
import { resolveLaneWorkName } from "./task-sync-lane.ts";
|
|
10
|
+
|
|
11
|
+
describe("resolveLaneWorkName", () => {
|
|
12
|
+
it("reads the visible label from the active cli_sessions route", async () => {
|
|
13
|
+
let query: unknown;
|
|
14
|
+
const name = await resolveLaneWorkName(
|
|
15
|
+
{
|
|
16
|
+
cli_sessions: {
|
|
17
|
+
findFirst: async (args: unknown) => {
|
|
18
|
+
query = args;
|
|
19
|
+
return { id: "session-1", label: "ZeroStack" };
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
} as never,
|
|
23
|
+
"tmux:%7",
|
|
24
|
+
);
|
|
25
|
+
expect(name).toBe("ZeroStack");
|
|
26
|
+
expect(query).toMatchObject({ where: { host_route: { path: ["pane_id"], equals: "tmux:%7" } } });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("returns no unverified title when no active session owns the pane", async () => {
|
|
30
|
+
const name = await resolveLaneWorkName(
|
|
31
|
+
{ cli_sessions: { findFirst: async () => null } } as never,
|
|
32
|
+
"tmux:%8",
|
|
33
|
+
);
|
|
34
|
+
expect(name).toBeNull();
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -28,27 +28,16 @@ import type { Prisma, WorkItem } from "./task-sync-router.ts";
|
|
|
28
28
|
|
|
29
29
|
export async function resolveLaneWorkName(prisma: Prisma, pane: string): Promise<string | null> {
|
|
30
30
|
try {
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
).fleet_agent_runs;
|
|
38
|
-
if (!runs?.findFirst) return null;
|
|
39
|
-
// Most recent run for the pane, REGARDLESS of state. A lane's work name does
|
|
40
|
-
// not vanish when its run closes, and filtering `state != closed` here is
|
|
41
|
-
// exactly why task rows were titled "Lane": a task-list write (ensureLaneTask)
|
|
42
|
-
// that fires after the run closed resolved no name and fell back to the
|
|
43
|
-
// generic sentinel — spawn debris an orchestrator cannot attribute
|
|
44
|
-
// (measured 2026-08-01: 207 "Lane" rows). Newest-first so a reused pane
|
|
45
|
-
// resolves its CURRENT lane, not a prior one.
|
|
46
|
-
const run = await runs.findFirst({
|
|
47
|
-
where: { pane },
|
|
31
|
+
const session = await prisma.cli_sessions.findFirst({
|
|
32
|
+
where: {
|
|
33
|
+
closed_at: null,
|
|
34
|
+
lifecycle_state: { not: "closed" },
|
|
35
|
+
host_route: { path: ["pane_id"], equals: pane },
|
|
36
|
+
},
|
|
48
37
|
orderBy: { created_at: "desc" },
|
|
49
|
-
select: {
|
|
38
|
+
select: { id: true, label: true },
|
|
50
39
|
});
|
|
51
|
-
const name =
|
|
40
|
+
const name = session?.label?.trim();
|
|
52
41
|
return name ? name : null;
|
|
53
42
|
} catch {
|
|
54
43
|
return null;
|
|
@@ -33,6 +33,29 @@ import type { TaskListItem, WorkItem } from "./task-sync-router.ts";
|
|
|
33
33
|
/** CLI task-list status → work_items.status. The canonical forward direction. */
|
|
34
34
|
export declare const STATUS_TO_WORK_ITEM: Record<"completed" | "in_progress" | "pending", string>;
|
|
35
35
|
export declare function normalizeStatus(s: string | null): TaskListItem["status"];
|
|
36
|
+
export interface ProgressSubtask {
|
|
37
|
+
id: string;
|
|
38
|
+
status: string | null;
|
|
39
|
+
kind_data?: Record<string, unknown> | null;
|
|
40
|
+
}
|
|
41
|
+
export interface ProgressAcceptanceEvidence {
|
|
42
|
+
work_item_id: string | null;
|
|
43
|
+
sender_role: string | null;
|
|
44
|
+
system_key: string | null;
|
|
45
|
+
}
|
|
46
|
+
export interface EvidenceProgress {
|
|
47
|
+
total: number;
|
|
48
|
+
accepted: number;
|
|
49
|
+
percentage: number;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Derive progress from the canonical checklist and independent acceptance
|
|
53
|
+
* comments. A done status without an Orchestrator acceptance comment is not
|
|
54
|
+
* evidence of completion; prose percentages and context gauges never enter
|
|
55
|
+
* this calculation. Subtasks are required by default, with an explicit
|
|
56
|
+
* `kind_data.pm_progress.required: false` opt-out for non-gating notes.
|
|
57
|
+
*/
|
|
58
|
+
export declare function deriveEvidenceProgress(subtasks: ProgressSubtask[], evidence: ProgressAcceptanceEvidence[]): EvidenceProgress;
|
|
36
59
|
/**
|
|
37
60
|
* Decide the status to WRITE for an existing row, given what the CLI list says.
|
|
38
61
|
*
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { describe, expect, it } from "bun:test";
|
|
13
13
|
import {
|
|
14
14
|
STATUS_TO_WORK_ITEM,
|
|
15
|
+
deriveEvidenceProgress,
|
|
15
16
|
normalizeStatus,
|
|
16
17
|
planReconcile,
|
|
17
18
|
resolveProjectedStatus,
|
|
@@ -29,6 +30,31 @@ describe("STATUS_TO_WORK_ITEM", () => {
|
|
|
29
30
|
});
|
|
30
31
|
});
|
|
31
32
|
|
|
33
|
+
describe("evidence-derived task progress", () => {
|
|
34
|
+
it("counts only required subtasks with independent Orchestrator acceptance", () => {
|
|
35
|
+
const out = deriveEvidenceProgress(
|
|
36
|
+
[
|
|
37
|
+
{ id: "a", status: "done" },
|
|
38
|
+
{ id: "b", status: "done" },
|
|
39
|
+
{ id: "c", status: "todo", kind_data: { pm_progress: { required: false } } },
|
|
40
|
+
],
|
|
41
|
+
[
|
|
42
|
+
{ work_item_id: "a", sender_role: "orchestrator", system_key: "acceptance:a" },
|
|
43
|
+
{ work_item_id: "b", sender_role: "worker", system_key: "acceptance:b" },
|
|
44
|
+
],
|
|
45
|
+
);
|
|
46
|
+
expect(out).toEqual({ total: 2, accepted: 1, percentage: 50 });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("does not treat prose, context, or done alone as evidence", () => {
|
|
50
|
+
expect(
|
|
51
|
+
deriveEvidenceProgress([{ id: "a", status: "done" }], [
|
|
52
|
+
{ work_item_id: "a", sender_role: "orchestrator", system_key: "progress:100" },
|
|
53
|
+
]),
|
|
54
|
+
).toEqual({ total: 1, accepted: 0, percentage: 0 });
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
32
58
|
describe("status translation is a single non-drifting source", () => {
|
|
33
59
|
// The forward map (CLI → work_items) is canonical; the reverse is DERIVED from
|
|
34
60
|
// it (+ a closed synonym set). This locks the structural fix: the two
|
|
@@ -74,6 +74,57 @@ export function normalizeStatus(s: string | null): TaskListItem["status"] {
|
|
|
74
74
|
return (s && WORK_ITEM_TO_STATUS[s]) || "pending";
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
export interface ProgressSubtask {
|
|
78
|
+
id: string;
|
|
79
|
+
status: string | null;
|
|
80
|
+
kind_data?: Record<string, unknown> | null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface ProgressAcceptanceEvidence {
|
|
84
|
+
work_item_id: string | null;
|
|
85
|
+
sender_role: string | null;
|
|
86
|
+
system_key: string | null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface EvidenceProgress {
|
|
90
|
+
total: number;
|
|
91
|
+
accepted: number;
|
|
92
|
+
percentage: number;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Derive progress from the canonical checklist and independent acceptance
|
|
97
|
+
* comments. A done status without an Orchestrator acceptance comment is not
|
|
98
|
+
* evidence of completion; prose percentages and context gauges never enter
|
|
99
|
+
* this calculation. Subtasks are required by default, with an explicit
|
|
100
|
+
* `kind_data.pm_progress.required: false` opt-out for non-gating notes.
|
|
101
|
+
*/
|
|
102
|
+
export function deriveEvidenceProgress(
|
|
103
|
+
subtasks: ProgressSubtask[],
|
|
104
|
+
evidence: ProgressAcceptanceEvidence[],
|
|
105
|
+
): EvidenceProgress {
|
|
106
|
+
const required = subtasks.filter(
|
|
107
|
+
(s) => (s.kind_data?.pm_progress as { required?: boolean } | undefined)?.required !== false,
|
|
108
|
+
);
|
|
109
|
+
const accepted = new Set(
|
|
110
|
+
evidence
|
|
111
|
+
.filter(
|
|
112
|
+
(e) =>
|
|
113
|
+
e.sender_role === "orchestrator" &&
|
|
114
|
+
typeof e.system_key === "string" &&
|
|
115
|
+
e.system_key.startsWith("acceptance:") &&
|
|
116
|
+
e.work_item_id,
|
|
117
|
+
)
|
|
118
|
+
.map((e) => e.work_item_id as string),
|
|
119
|
+
);
|
|
120
|
+
const acceptedCount = required.filter((s) => s.status === "done" && accepted.has(s.id)).length;
|
|
121
|
+
return {
|
|
122
|
+
total: required.length,
|
|
123
|
+
accepted: acceptedCount,
|
|
124
|
+
percentage: required.length === 0 ? 0 : Math.round((acceptedCount / required.length) * 100),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
77
128
|
/**
|
|
78
129
|
* Decide the status to WRITE for an existing row, given what the CLI list says.
|
|
79
130
|
*
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
* identity in kind_data.lane = { pane, session, cli_kind, role } so any surface
|
|
22
22
|
* (fleet UI, another lane) can resolve "who owns this work" and push changes
|
|
23
23
|
* back. Binding is by pane identity (the stable $TMUX_PANE), not by fleet run —
|
|
24
|
-
* so it works
|
|
25
|
-
*
|
|
24
|
+
* so it works without any second runtime-assignment join; the canonical runtime
|
|
25
|
+
* identity remains cli_sessions and the assignment remains work_items.
|
|
26
26
|
*
|
|
27
27
|
* These procedures are the ONLY CLI-agnostic surface an adapter calls:
|
|
28
28
|
* bindLane — bind (create-or-confirm) a lane's task. SessionStart.
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
*/
|
|
39
39
|
import { type Context } from "./auth-router.ts";
|
|
40
40
|
import * as v from "valibot";
|
|
41
|
+
import { type ProgressAcceptanceEvidence } from "./task-sync-projection.ts";
|
|
41
42
|
/** A lane's native task-list item, normalized. Every CLI adapter maps to this. */
|
|
42
43
|
export interface TaskListItem {
|
|
43
44
|
content: string;
|
|
@@ -104,8 +105,15 @@ export type Prisma = {
|
|
|
104
105
|
count: number;
|
|
105
106
|
}>;
|
|
106
107
|
};
|
|
108
|
+
cli_sessions: {
|
|
109
|
+
findFirst: (a: unknown) => Promise<{
|
|
110
|
+
id: string;
|
|
111
|
+
label?: string | null;
|
|
112
|
+
} | null>;
|
|
113
|
+
};
|
|
107
114
|
comments?: {
|
|
108
115
|
findFirst: (a: unknown) => Promise<CommentRow | null>;
|
|
116
|
+
findMany: (a: unknown) => Promise<Array<ProgressAcceptanceEvidence>>;
|
|
109
117
|
};
|
|
110
118
|
};
|
|
111
119
|
/** A `comments` row — only the fields readAssignedBrief touches. */
|
|
@@ -113,18 +121,6 @@ export interface CommentRow {
|
|
|
113
121
|
content: string;
|
|
114
122
|
created_at: Date;
|
|
115
123
|
}
|
|
116
|
-
/**
|
|
117
|
-
* A dispatched run already points at its fleet_tasks row. Task-sync may fill
|
|
118
|
-
* the convenience join for an adopted lane, but must never replace that
|
|
119
|
-
* dispatch identity with the lane's work_items projection id.
|
|
120
|
-
*/
|
|
121
|
-
export declare function untrackedRunForPane(pane: string): {
|
|
122
|
-
pane: string;
|
|
123
|
-
state: {
|
|
124
|
-
not: string;
|
|
125
|
-
};
|
|
126
|
-
tracked_task_id: null;
|
|
127
|
-
};
|
|
128
124
|
/**
|
|
129
125
|
* Resolve the org a lane's task is created under. Explicit when the lane knows
|
|
130
126
|
* its org (a dispatched fleet lane carries one); defaults to the platform org
|
|
@@ -179,8 +175,7 @@ export declare const taskSyncRouter: {
|
|
|
179
175
|
* adapter so the lane has a durable task row before its first todo write.
|
|
180
176
|
* Idempotent: re-binding updates the identity (role/cli_kind can change) but
|
|
181
177
|
* never orphans existing subtasks. After binding, links the lane's
|
|
182
|
-
*
|
|
183
|
-
* the join the operator's lane-labels monitor + the unified fleet view read.
|
|
178
|
+
* cli_sessions is the runtime identity and work_items is the assignment.
|
|
184
179
|
*/
|
|
185
180
|
bindLane: import("@orpc/server").DecoratedProcedure<import("@orpc/server").MergedInitialContext<Context & Record<never, never>, Context, Context>, import("@orpc/server").MergedCurrentContext<Context, {
|
|
186
181
|
headers: Headers;
|
|
@@ -292,18 +287,30 @@ export declare const taskSyncRouter: {
|
|
|
292
287
|
task_id: null;
|
|
293
288
|
task_title: null;
|
|
294
289
|
subtasks: never[];
|
|
290
|
+
progress: {
|
|
291
|
+
total: number;
|
|
292
|
+
accepted: number;
|
|
293
|
+
percentage: number;
|
|
294
|
+
};
|
|
295
295
|
} | {
|
|
296
296
|
task_id: string;
|
|
297
297
|
task_title: string;
|
|
298
298
|
subtasks: TaskListItem[];
|
|
299
|
+
progress: import("./task-sync-projection.ts").EvidenceProgress;
|
|
299
300
|
}, {
|
|
300
301
|
task_id: null;
|
|
301
302
|
task_title: null;
|
|
302
303
|
subtasks: never[];
|
|
304
|
+
progress: {
|
|
305
|
+
total: number;
|
|
306
|
+
accepted: number;
|
|
307
|
+
percentage: number;
|
|
308
|
+
};
|
|
303
309
|
} | {
|
|
304
310
|
task_id: string;
|
|
305
311
|
task_title: string;
|
|
306
312
|
subtasks: TaskListItem[];
|
|
313
|
+
progress: import("./task-sync-projection.ts").EvidenceProgress;
|
|
307
314
|
}>, Record<never, never>, Record<never, never>>;
|
|
308
315
|
/**
|
|
309
316
|
* DB→AGENT self-brief leg. A spawned lane PULLS its own brief — nothing is
|
|
@@ -399,11 +406,9 @@ export declare const taskSyncRouter: {
|
|
|
399
406
|
* there the lane is the originator (no self-echo); here the lane is the
|
|
400
407
|
* recipient.
|
|
401
408
|
*
|
|
402
|
-
*
|
|
403
|
-
*
|
|
404
|
-
*
|
|
405
|
-
* scala-channel fires the same publish, so an operator edit reaches the
|
|
406
|
-
* lane with no explicit call. Tracked in apps/agent-fleet.md task-sync.
|
|
409
|
+
* Every call writes the canonical task-change comment for the active
|
|
410
|
+
* cli_sessions row; the database comment emission is the delivery trigger.
|
|
411
|
+
* There is no pane transport or secondary channel publisher here.
|
|
407
412
|
*/
|
|
408
413
|
applyExternalChange: import("@orpc/server").DecoratedProcedure<import("@orpc/server").MergedInitialContext<Context & Record<never, never>, Context, Context>, import("@orpc/server").MergedCurrentContext<Context, {
|
|
409
414
|
headers: Headers;
|
|
@@ -413,11 +418,13 @@ export declare const taskSyncRouter: {
|
|
|
413
418
|
orgId: string | null | undefined;
|
|
414
419
|
}>, import("@orpc/contract").Schema<unknown, unknown>, import("@orpc/contract").Schema<{
|
|
415
420
|
task_id: string;
|
|
416
|
-
|
|
421
|
+
cli_session_id: string | null;
|
|
422
|
+
comment_id: string | null;
|
|
417
423
|
notified: boolean;
|
|
418
424
|
}, {
|
|
419
425
|
task_id: string;
|
|
420
|
-
|
|
426
|
+
cli_session_id: string | null;
|
|
427
|
+
comment_id: string | null;
|
|
421
428
|
notified: boolean;
|
|
422
429
|
}>, Record<never, never>, Record<never, never>>;
|
|
423
430
|
};
|
|
@@ -337,8 +337,20 @@ describe("a blocked row survives a CLI list that cannot express it", () => {
|
|
|
337
337
|
it("planReconcile leaves a blocked subtask blocked when the list says pending", () => {
|
|
338
338
|
// The end-to-end shape: this is what re-blocked the lane three times.
|
|
339
339
|
const plan = planReconcile(
|
|
340
|
-
[
|
|
341
|
-
|
|
340
|
+
[
|
|
341
|
+
{
|
|
342
|
+
id: "row-1",
|
|
343
|
+
kind: "subtask",
|
|
344
|
+
parent_id: "task-1",
|
|
345
|
+
organisation_id: "org-1",
|
|
346
|
+
title: "Deploy the fix",
|
|
347
|
+
status: "blocked",
|
|
348
|
+
position: "0",
|
|
349
|
+
completed: false,
|
|
350
|
+
kind_data: { task_sync: { mark: "1" } },
|
|
351
|
+
},
|
|
352
|
+
],
|
|
353
|
+
[{ content: "Deploy the fix", status: "pending" }],
|
|
342
354
|
"claude-code",
|
|
343
355
|
);
|
|
344
356
|
const update = plan.toUpdate.find((u) => u.id === "row-1");
|