@hyperdrive.bot/paseo-server 0.3.10 → 0.3.12
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/dist/server/server/agent/agent-loading.js +24 -3
- package/dist/server/server/agent/agent-manager.d.ts +22 -0
- package/dist/server/server/agent/agent-manager.js +36 -0
- package/dist/server/server/agent/agent-storage.d.ts +9 -0
- package/dist/server/server/agent/blocker-log.d.ts +81 -0
- package/dist/server/server/agent/blocker-log.js +113 -0
- package/dist/server/server/agent/card-move-log.d.ts +66 -0
- package/dist/server/server/agent/card-move-log.js +113 -0
- package/dist/server/server/agent/jarvis-judge.d.ts +121 -0
- package/dist/server/server/agent/jarvis-judge.js +225 -0
- package/dist/server/server/agent/judge-relay-gate.d.ts +70 -0
- package/dist/server/server/agent/judge-relay-gate.js +92 -0
- package/dist/server/server/agent/mcp-server.js +9 -0
- package/dist/server/server/agent/mcp-shared.d.ts +5 -0
- package/dist/server/server/agent/providers/claude/transport/pty-query.d.ts +92 -10
- package/dist/server/server/agent/providers/claude/transport/pty-query.js +615 -38
- package/dist/server/server/agent/session-digest-generator.d.ts +27 -0
- package/dist/server/server/agent/session-digest-generator.js +208 -17
- package/dist/server/server/agent/tools/paseo-tools.d.ts +12 -0
- package/dist/server/server/agent/tools/paseo-tools.js +48 -0
- package/dist/server/server/agent/tools/read-only-surface.d.ts +52 -0
- package/dist/server/server/agent/tools/read-only-surface.js +68 -0
- package/dist/server/server/agent/tools/types.d.ts +5 -0
- package/dist/server/server/bootstrap.js +73 -0
- package/dist/server/server/relay-transport.js +36 -5
- package/dist/server/web-ui/_expo/static/js/web/{desktop-attachment-bridge-9f03d060da06e8838a4e0fdd5c067757.js → desktop-attachment-bridge-e6e0f92f1a2959279a7c3b2b50bf1df3.js} +5 -5
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-e6e0f92f1a2959279a7c3b2b50bf1df3.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-e6e0f92f1a2959279a7c3b2b50bf1df3.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-e6e0f92f1a2959279a7c3b2b50bf1df3.js.map.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-e6e0f92f1a2959279a7c3b2b50bf1df3.js.map.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/{desktop-attachment-store-d0d9dffa612bf12bf551c5e513c28691.js → desktop-attachment-store-d5c93939344c993e52d655ccf8942928.js} +3 -3
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-d5c93939344c993e52d655ccf8942928.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-d5c93939344c993e52d655ccf8942928.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-d5c93939344c993e52d655ccf8942928.js.map.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-d5c93939344c993e52d655ccf8942928.js.map.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/{index-461cf3ab1d54d6debea0e037829768e9.js → index-853ef1a7fc9c9b7a39577b79564e0896.js} +166 -165
- package/dist/server/web-ui/_expo/static/js/web/index-853ef1a7fc9c9b7a39577b79564e0896.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/index-853ef1a7fc9c9b7a39577b79564e0896.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/index-853ef1a7fc9c9b7a39577b79564e0896.js.map.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/{index-461cf3ab1d54d6debea0e037829768e9.js.map.gz → index-853ef1a7fc9c9b7a39577b79564e0896.js.map.gz} +0 -0
- package/dist/server/web-ui/_expo/static/js/web/{indexeddb-attachment-store-54584275ea7a32061741c119074ff13f.js → indexeddb-attachment-store-35fb4ddfe925b1fb6b7a6c1953b21621.js} +3 -3
- package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-35fb4ddfe925b1fb6b7a6c1953b21621.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-35fb4ddfe925b1fb6b7a6c1953b21621.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-35fb4ddfe925b1fb6b7a6c1953b21621.js.map.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-35fb4ddfe925b1fb6b7a6c1953b21621.js.map.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/{native-file-attachment-store-b53f2ba81a9777f6f6d04a98b0a12c33.js → native-file-attachment-store-8602ccac919e2b56f2e9a252dd06483e.js} +5 -5
- package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-8602ccac919e2b56f2e9a252dd06483e.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-8602ccac919e2b56f2e9a252dd06483e.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-8602ccac919e2b56f2e9a252dd06483e.js.map.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-8602ccac919e2b56f2e9a252dd06483e.js.map.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/{profiler-7b2011c26cedb58cc00c02a7c25fc0a0.js → profiler-b3dd30ba0a88aece50e7e403785f34ba.js} +11 -11
- package/dist/server/web-ui/_expo/static/js/web/profiler-b3dd30ba0a88aece50e7e403785f34ba.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/profiler-b3dd30ba0a88aece50e7e403785f34ba.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/profiler-b3dd30ba0a88aece50e7e403785f34ba.js.map.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/profiler-b3dd30ba0a88aece50e7e403785f34ba.js.map.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/{startRecording-1b57837c4c358ef963a1392d9d96354b.js → startRecording-fd9fa11e24a3b8d94e5161aae7f42a5d.js} +60 -60
- package/dist/server/web-ui/_expo/static/js/web/startRecording-fd9fa11e24a3b8d94e5161aae7f42a5d.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/startRecording-fd9fa11e24a3b8d94e5161aae7f42a5d.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/startRecording-fd9fa11e24a3b8d94e5161aae7f42a5d.js.map.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/{startRecording-1b57837c4c358ef963a1392d9d96354b.js.map.gz → startRecording-fd9fa11e24a3b8d94e5161aae7f42a5d.js.map.gz} +0 -0
- package/dist/server/web-ui/index.html +1 -1
- package/dist/server/web-ui/index.html.br +0 -0
- package/dist/server/web-ui/index.html.gz +0 -0
- package/package.json +6 -6
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-9f03d060da06e8838a4e0fdd5c067757.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-9f03d060da06e8838a4e0fdd5c067757.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-9f03d060da06e8838a4e0fdd5c067757.js.map.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-9f03d060da06e8838a4e0fdd5c067757.js.map.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-d0d9dffa612bf12bf551c5e513c28691.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-d0d9dffa612bf12bf551c5e513c28691.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-d0d9dffa612bf12bf551c5e513c28691.js.map.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-d0d9dffa612bf12bf551c5e513c28691.js.map.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/index-461cf3ab1d54d6debea0e037829768e9.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/index-461cf3ab1d54d6debea0e037829768e9.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/index-461cf3ab1d54d6debea0e037829768e9.js.map.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-54584275ea7a32061741c119074ff13f.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-54584275ea7a32061741c119074ff13f.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-54584275ea7a32061741c119074ff13f.js.map.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-54584275ea7a32061741c119074ff13f.js.map.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-b53f2ba81a9777f6f6d04a98b0a12c33.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-b53f2ba81a9777f6f6d04a98b0a12c33.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-b53f2ba81a9777f6f6d04a98b0a12c33.js.map.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-b53f2ba81a9777f6f6d04a98b0a12c33.js.map.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/profiler-7b2011c26cedb58cc00c02a7c25fc0a0.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/profiler-7b2011c26cedb58cc00c02a7c25fc0a0.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/profiler-7b2011c26cedb58cc00c02a7c25fc0a0.js.map.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/profiler-7b2011c26cedb58cc00c02a7c25fc0a0.js.map.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/startRecording-1b57837c4c358ef963a1392d9d96354b.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/startRecording-1b57837c4c358ef963a1392d9d96354b.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/startRecording-1b57837c4c358ef963a1392d9d96354b.js.map.br +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { buildConfigOverrides, buildSessionConfig, extractTimestamps, isStoredAgentProviderAvailable, toAgentPersistenceHandle, } from "../persistence-hooks.js";
|
|
2
|
+
import { resolveUsableResumeCwd } from "./resolve-resume-cwd.js";
|
|
2
3
|
const pendingAgentInitializations = new Map();
|
|
3
4
|
export async function ensureAgentLoaded(agentId, deps) {
|
|
4
5
|
const existing = deps.agentManager.getAgent(agentId);
|
|
@@ -19,18 +20,38 @@ export async function ensureAgentLoaded(agentId, deps) {
|
|
|
19
20
|
throw new Error(`Agent ${agentId} references unavailable provider '${record.provider}'`);
|
|
20
21
|
}
|
|
21
22
|
const handle = toAgentPersistenceHandle(validProviders, record.persistence);
|
|
23
|
+
// A recorded cwd may no longer exist: the worktree was cleaned up, or the
|
|
24
|
+
// session came from another machine (`/Users/claudinho/...` on a linux box).
|
|
25
|
+
// normalizeConfig stats the cwd and throws "Working directory does not
|
|
26
|
+
// exist", so loading the agent fails outright and every READ of that session
|
|
27
|
+
// fails with it, including get_agent_activity, even though the transcript is
|
|
28
|
+
// sitting on disk and perfectly readable.
|
|
29
|
+
//
|
|
30
|
+
// The interactive resume path in session.ts already solves this with
|
|
31
|
+
// resolveUsableResumeCwd. ensureAgentLoaded did not, so schedules and the
|
|
32
|
+
// Jarvis judge hit the wall instead. Measured 2026-07-31: 27 of 1,226 agents
|
|
33
|
+
// were unreadable for this reason alone.
|
|
34
|
+
//
|
|
35
|
+
// Resuming does not need the original directory, only *a* valid one: the
|
|
36
|
+
// transcript is read from the provider store regardless.
|
|
37
|
+
const resolvedCwd = record.cwd ? await resolveUsableResumeCwd(record.cwd) : null;
|
|
38
|
+
if (resolvedCwd?.substituted) {
|
|
39
|
+
deps.logger.warn({ agentId, original: resolvedCwd.original, fallback: resolvedCwd.cwd }, "Agent cwd no longer exists; loading against an existing directory instead");
|
|
40
|
+
}
|
|
41
|
+
const cwdOverride = resolvedCwd?.substituted ? { cwd: resolvedCwd.cwd } : {};
|
|
22
42
|
let snapshot;
|
|
23
43
|
if (handle) {
|
|
24
|
-
snapshot = await deps.agentManager.resumeAgentFromPersistence(handle, buildConfigOverrides(record), agentId, extractTimestamps(record));
|
|
44
|
+
snapshot = await deps.agentManager.resumeAgentFromPersistence(handle, { ...buildConfigOverrides(record), ...cwdOverride }, agentId, extractTimestamps(record));
|
|
25
45
|
deps.logger.info({ agentId, provider: record.provider }, "Agent resumed from persistence");
|
|
26
46
|
}
|
|
27
47
|
else {
|
|
28
|
-
const
|
|
48
|
+
const baseConfig = buildSessionConfig(record, {
|
|
29
49
|
validProviders,
|
|
30
50
|
});
|
|
31
|
-
if (!
|
|
51
|
+
if (!baseConfig) {
|
|
32
52
|
throw new Error(`Agent ${agentId} references unavailable provider '${record.provider}'`);
|
|
33
53
|
}
|
|
54
|
+
const config = { ...baseConfig, ...cwdOverride };
|
|
34
55
|
snapshot = await deps.agentManager.createAgent(config, agentId, {
|
|
35
56
|
labels: record.labels,
|
|
36
57
|
workspaceId: record.workspaceId,
|
|
@@ -4,6 +4,8 @@ import type { Logger } from "pino";
|
|
|
4
4
|
import type { TerminalManager } from "../../terminal/terminal-manager.js";
|
|
5
5
|
import { type AgentCapabilityFlags, type AgentClient, type AgentFeature, type AgentSlashCommand, type AgentMode, type AgentPermissionRequest, type AgentPermissionResponse, type AgentPermissionResult, type AgentPersistenceHandle, type AgentProviderNotice, type AgentPromptInput, type AgentProvider, type AgentRunOptions, type AgentRunResult, type AgentSession, type AgentSessionConfig, type AgentStreamEvent, type AgentTimelineItem, type AgentUsage, type AgentRuntimeInfo, type ImportableProviderSession, type ListImportableSessionsOptions } from "./agent-sdk-types.js";
|
|
6
6
|
import type { StoredAgentRecord, AgentStorage } from "./agent-storage.js";
|
|
7
|
+
import { type CardMoveLog } from "./card-move-log.js";
|
|
8
|
+
import type { BlockerLog } from "./blocker-log.js";
|
|
7
9
|
import type { AgentTimelineFetchOptions, AgentTimelineFetchResult, AgentTimelineRow, AgentTimelineStore } from "./agent-timeline-store-types.js";
|
|
8
10
|
import { type ForegroundTurnWaiter } from "./foreground-run-state.js";
|
|
9
11
|
import { type RewindMode } from "./rewind/rewind.js";
|
|
@@ -85,6 +87,10 @@ export interface AgentManagerOptions {
|
|
|
85
87
|
appendSystemPrompt?: string;
|
|
86
88
|
agentStreamCoalesceWindowMs?: number;
|
|
87
89
|
rescueTimeouts?: AgentManagerRescueTimeouts;
|
|
90
|
+
/** Jarvis substrate (JARVIS-V1-SPEC §3.2). Optional: absent = no move logging. */
|
|
91
|
+
cardMoveLog?: CardMoveLog;
|
|
92
|
+
/** Jarvis substrate: blocker history. Optional: absent = no blocker logging. */
|
|
93
|
+
blockerLog?: BlockerLog;
|
|
88
94
|
logger: Logger;
|
|
89
95
|
}
|
|
90
96
|
export interface WaitForAgentOptions {
|
|
@@ -207,6 +213,10 @@ export declare class AgentManager {
|
|
|
207
213
|
private readonly subscribers;
|
|
208
214
|
private readonly idFactory;
|
|
209
215
|
private readonly registry?;
|
|
216
|
+
/** Jarvis substrate (JARVIS-V1-SPEC §3.2). Absent in tests and older callers. */
|
|
217
|
+
private readonly cardMoveLog?;
|
|
218
|
+
/** Jarvis substrate: blocker history. Absent in tests and older callers. */
|
|
219
|
+
private readonly blockerLog?;
|
|
210
220
|
private readonly durableTimelineStore?;
|
|
211
221
|
private readonly previousStatuses;
|
|
212
222
|
private readonly backgroundTasks;
|
|
@@ -265,6 +275,12 @@ export declare class AgentManager {
|
|
|
265
275
|
listDraftFeatures(config: AgentSessionConfig): Promise<AgentFeature[]>;
|
|
266
276
|
getAgent(id: string): ManagedAgent | null;
|
|
267
277
|
getTimeline(id: string): AgentTimelineItem[];
|
|
278
|
+
/**
|
|
279
|
+
* Timeline epoch for an agent. Row `seq` restarts at 1 whenever the store is
|
|
280
|
+
* re-initialized without seeded rows, so a seq is only comparable WITHIN an
|
|
281
|
+
* epoch. Anything caching a high-water seq must cache this alongside it.
|
|
282
|
+
*/
|
|
283
|
+
getTimelineEpoch(id: string): string;
|
|
268
284
|
getTimelineRows(id: string): Promise<AgentTimelineRow[]>;
|
|
269
285
|
fetchTimeline(id: string, options?: AgentTimelineFetchOptions): AgentTimelineFetchResult;
|
|
270
286
|
createAgent(config: AgentSessionConfig, agentId?: string, options?: {
|
|
@@ -331,6 +347,12 @@ export declare class AgentManager {
|
|
|
331
347
|
setAgentFeature(agentId: string, featureId: string, value: unknown): Promise<void>;
|
|
332
348
|
setTitle(agentId: string, title: string): Promise<void>;
|
|
333
349
|
setLabels(agentId: string, labels: Record<string, string>): Promise<void>;
|
|
350
|
+
/**
|
|
351
|
+
* The single chokepoint for every label write (JARVIS-V1-SPEC §3.2). Both the
|
|
352
|
+
* live-agent branch and the detached-record branch flow through here, which is
|
|
353
|
+
* why the card-move hook lives at this level and not on the public `setLabels`
|
|
354
|
+
* (that one misses the detached path entirely).
|
|
355
|
+
*/
|
|
334
356
|
private writeLabels;
|
|
335
357
|
private writeStoredMetadata;
|
|
336
358
|
detachAgent(agentId: string): Promise<{
|
|
@@ -6,6 +6,7 @@ import { getParentAgentIdFromLabels, isDelegatedAgent, PARENT_AGENT_ID_LABEL, }
|
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { getAgentStreamEventTurnId, } from "./agent-sdk-types.js";
|
|
8
8
|
import { buildArchivedAgentRecord } from "./agent-archive.js";
|
|
9
|
+
import { KANBAN_LABEL_KEY } from "./card-move-log.js";
|
|
9
10
|
import { InMemoryAgentTimelineStore, } from "./agent-timeline-store.js";
|
|
10
11
|
import { AGENT_STREAM_COALESCE_DEFAULT_WINDOW_MS, AgentStreamCoalescer, } from "./agent-stream-coalescer.js";
|
|
11
12
|
import { ForegroundRunState } from "./foreground-run-state.js";
|
|
@@ -200,6 +201,10 @@ export class AgentManager {
|
|
|
200
201
|
this.appendSystemPrompt = "";
|
|
201
202
|
this.idFactory = options?.idFactory ?? (() => randomUUID());
|
|
202
203
|
this.registry = options?.registry;
|
|
204
|
+
// Plain access, not `options?.`: the optional chain counts as a branch and
|
|
205
|
+
// tips this constructor over the oxlint complexity ceiling of 20.
|
|
206
|
+
this.cardMoveLog = options.cardMoveLog;
|
|
207
|
+
this.blockerLog = options.blockerLog;
|
|
203
208
|
this.durableTimelineStore = options?.durableTimelineStore;
|
|
204
209
|
if (options?.onAgentAttention) {
|
|
205
210
|
this.attentionListeners.add(options.onAgentAttention);
|
|
@@ -510,6 +515,14 @@ export class AgentManager {
|
|
|
510
515
|
this.requireAgent(id);
|
|
511
516
|
return this.timelineStore.getItems(id);
|
|
512
517
|
}
|
|
518
|
+
/**
|
|
519
|
+
* Timeline epoch for an agent. Row `seq` restarts at 1 whenever the store is
|
|
520
|
+
* re-initialized without seeded rows, so a seq is only comparable WITHIN an
|
|
521
|
+
* epoch. Anything caching a high-water seq must cache this alongside it.
|
|
522
|
+
*/
|
|
523
|
+
getTimelineEpoch(id) {
|
|
524
|
+
return this.timelineStore.getEpoch(id);
|
|
525
|
+
}
|
|
513
526
|
async getTimelineRows(id) {
|
|
514
527
|
this.requireAgent(id);
|
|
515
528
|
if (this.durableTimelineStore) {
|
|
@@ -1019,17 +1032,35 @@ export class AgentManager {
|
|
|
1019
1032
|
const agent = this.requireAgent(agentId);
|
|
1020
1033
|
await this.writeLabels(agent.id, labels);
|
|
1021
1034
|
}
|
|
1035
|
+
/**
|
|
1036
|
+
* The single chokepoint for every label write (JARVIS-V1-SPEC §3.2). Both the
|
|
1037
|
+
* live-agent branch and the detached-record branch flow through here, which is
|
|
1038
|
+
* why the card-move hook lives at this level and not on the public `setLabels`
|
|
1039
|
+
* (that one misses the detached path entirely).
|
|
1040
|
+
*/
|
|
1022
1041
|
async writeLabels(agentId, patch) {
|
|
1042
|
+
// Only pay for a before-read when the patch could actually move the card.
|
|
1043
|
+
const touchesColumn = Object.prototype.hasOwnProperty.call(patch, KANBAN_LABEL_KEY);
|
|
1023
1044
|
const liveAgent = this.agents.get(agentId);
|
|
1024
1045
|
if (liveAgent) {
|
|
1046
|
+
const beforeColumn = touchesColumn ? (liveAgent.labels[KANBAN_LABEL_KEY] ?? null) : null;
|
|
1025
1047
|
liveAgent.labels = applyLabelPatch(liveAgent.labels, patch);
|
|
1026
1048
|
this.touchUpdatedAt(liveAgent);
|
|
1027
1049
|
await this.persistSnapshot(liveAgent);
|
|
1028
1050
|
this.emitState(liveAgent, { persist: false });
|
|
1051
|
+
if (touchesColumn) {
|
|
1052
|
+
this.cardMoveLog?.record(agentId, beforeColumn, liveAgent.labels[KANBAN_LABEL_KEY] ?? null);
|
|
1053
|
+
}
|
|
1029
1054
|
const record = this.registry ? await this.registry.get(agentId) : null;
|
|
1030
1055
|
return { record, live: true };
|
|
1031
1056
|
}
|
|
1057
|
+
const priorColumn = touchesColumn && this.registry
|
|
1058
|
+
? ((await this.registry.get(agentId))?.labels?.[KANBAN_LABEL_KEY] ?? null)
|
|
1059
|
+
: null;
|
|
1032
1060
|
const nextRecord = await this.writeStoredMetadata(agentId, { labels: patch });
|
|
1061
|
+
if (touchesColumn) {
|
|
1062
|
+
this.cardMoveLog?.record(agentId, priorColumn, nextRecord.labels?.[KANBAN_LABEL_KEY] ?? null);
|
|
1063
|
+
}
|
|
1033
1064
|
return { record: nextRecord, live: false };
|
|
1034
1065
|
}
|
|
1035
1066
|
async writeStoredMetadata(agentId, patch) {
|
|
@@ -1096,6 +1127,11 @@ export class AgentManager {
|
|
|
1096
1127
|
const registry = this.requireRegistry();
|
|
1097
1128
|
const persisted = await registry.setDigest(agentId, digest);
|
|
1098
1129
|
agent.updatedAt = new Date(persisted.updatedAt);
|
|
1130
|
+
// Jarvis substrate: mergeDigest overwrites blockers on every derivation, so
|
|
1131
|
+
// without this append the "fourth time waiting on Marco" signal is destroyed
|
|
1132
|
+
// on every idle. Self-reported blockers win over derived ones, matching how
|
|
1133
|
+
// the judge reads them.
|
|
1134
|
+
this.blockerLog?.record(agentId, digest.blockers?.length ? digest.blockers : digest.derivedBlockers);
|
|
1099
1135
|
this.emitState(agent, { persist: false });
|
|
1100
1136
|
}
|
|
1101
1137
|
notifyAgentState(agentId) {
|
|
@@ -36,6 +36,15 @@ declare const STORED_AGENT_SCHEMA: z.ZodObject<{
|
|
|
36
36
|
accomplishments: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
37
37
|
selfReportedAt: z.ZodOptional<z.ZodString>;
|
|
38
38
|
generatedAt: z.ZodOptional<z.ZodString>;
|
|
39
|
+
derivedNextStep: z.ZodOptional<z.ZodString>;
|
|
40
|
+
derivedBlockers: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
41
|
+
urgency: z.ZodOptional<z.ZodEnum<{
|
|
42
|
+
low: "low";
|
|
43
|
+
normal: "normal";
|
|
44
|
+
high: "high";
|
|
45
|
+
}>>;
|
|
46
|
+
deadline: z.ZodOptional<z.ZodString>;
|
|
47
|
+
derivedDeadline: z.ZodOptional<z.ZodString>;
|
|
39
48
|
}, z.core.$strip>>;
|
|
40
49
|
lastStatus: z.ZodDefault<z.ZodEnum<{
|
|
41
50
|
error: "error";
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { Logger } from "pino";
|
|
2
|
+
/**
|
|
3
|
+
* Append-only blocker history (JARVIS-V1-SPEC, follow-up to §3.2).
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS EXISTS, and why the card-move log does not cover it.
|
|
6
|
+
*
|
|
7
|
+
* The move log answers "how many times was this card DEFERRED", counting column
|
|
8
|
+
* transitions. The sentence the product actually wants is different:
|
|
9
|
+
*
|
|
10
|
+
* "that is the fourth time you have been waiting on Marco"
|
|
11
|
+
*
|
|
12
|
+
* That needs a history of WHO or WHAT has been blocking, not of where the card
|
|
13
|
+
* sat. Those are different claims and the move log cannot produce the second one.
|
|
14
|
+
*
|
|
15
|
+
* Without this, blockers live only on `digest.blockers` / `digest.derivedBlockers`,
|
|
16
|
+
* and `mergeDigest` OVERWRITES both on every derivation. So the signal that makes
|
|
17
|
+
* the killer feature land is destroyed on every idle. Like the move log, this is
|
|
18
|
+
* PROSPECTIVE ONLY: nothing before it ships is recoverable.
|
|
19
|
+
*
|
|
20
|
+
* ## Deliberately no fuzzy matching here
|
|
21
|
+
*
|
|
22
|
+
* The obvious next step is to parse "Marco" out of the text and count mentions.
|
|
23
|
+
* That is resisted on purpose. A model rewording a blocker between derivations
|
|
24
|
+
* ("waiting on Marco Marques to confirm the VPN org mapping" vs "blocked pending
|
|
25
|
+
* Marco's confirmation") would break any regex or exact-match counter, and it
|
|
26
|
+
* would break SILENTLY, under-counting and making the nag weaker exactly when the
|
|
27
|
+
* blocker is most persistent.
|
|
28
|
+
*
|
|
29
|
+
* So this module stores blockers VERBATIM with timestamps and does no semantic
|
|
30
|
+
* work at all. The judge is already a language model reading a briefing; counting
|
|
31
|
+
* "this is the fourth distinct time Marco has come up" is precisely what it is
|
|
32
|
+
* good at and what a daemon is bad at. Keep the intelligence at the reader.
|
|
33
|
+
*/
|
|
34
|
+
export interface BlockerRow {
|
|
35
|
+
cardId: string;
|
|
36
|
+
/** Verbatim blocker strings as recorded at that moment. Never normalized. */
|
|
37
|
+
blockers: string[];
|
|
38
|
+
/** ISO timestamp. */
|
|
39
|
+
at: string;
|
|
40
|
+
}
|
|
41
|
+
export interface BlockerLogOptions {
|
|
42
|
+
/** Directory to write into, normally $PASEO_HOME. */
|
|
43
|
+
dir: string;
|
|
44
|
+
logger: Logger;
|
|
45
|
+
/** Test seam. Defaults to `() => new Date()`. */
|
|
46
|
+
now?: () => Date;
|
|
47
|
+
}
|
|
48
|
+
export declare class BlockerLog {
|
|
49
|
+
private readonly filePath;
|
|
50
|
+
private readonly logger;
|
|
51
|
+
private readonly now;
|
|
52
|
+
private ensured;
|
|
53
|
+
constructor(options: BlockerLogOptions);
|
|
54
|
+
/**
|
|
55
|
+
* Record the current blocker set for a card. No-ops when the set is unchanged
|
|
56
|
+
* from the last recording, so callers can hand us every digest write.
|
|
57
|
+
*
|
|
58
|
+
* NEVER throws. A digest write must not fail because bookkeeping failed.
|
|
59
|
+
*/
|
|
60
|
+
record(cardId: string, blockers: readonly string[] | undefined): void;
|
|
61
|
+
/** All rows, oldest first. Returns [] when the log does not exist yet. */
|
|
62
|
+
readAll(): BlockerRow[];
|
|
63
|
+
/** Rows for one card, oldest first. */
|
|
64
|
+
history(cardId: string): BlockerRow[];
|
|
65
|
+
/** The most recent recording for a card, or null. */
|
|
66
|
+
latest(cardId: string): BlockerRow | null;
|
|
67
|
+
/**
|
|
68
|
+
* How many separate times this card has been recorded as blocked by anything.
|
|
69
|
+
* A coarse count only. It deliberately does NOT try to say "how many times by
|
|
70
|
+
* Marco specifically", because that is a semantic question, see the module note.
|
|
71
|
+
*/
|
|
72
|
+
blockedRecordings(cardId: string): number;
|
|
73
|
+
/**
|
|
74
|
+
* Compact history for a judge briefing: one line per distinct recording,
|
|
75
|
+
* oldest first, capped. Handing the model the raw timeline lets it say "fourth
|
|
76
|
+
* time on Marco" itself rather than us guessing at entity matching.
|
|
77
|
+
*/
|
|
78
|
+
renderTimeline(cardId: string, max?: number): string[];
|
|
79
|
+
private ensureDir;
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=blocker-log.d.ts.map
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
/** Change detection only. NOT used for matching or counting. */
|
|
4
|
+
function fingerprint(blockers) {
|
|
5
|
+
return blockers
|
|
6
|
+
.map((b) => b.trim().toLowerCase())
|
|
7
|
+
.filter(Boolean)
|
|
8
|
+
.sort()
|
|
9
|
+
.join("\u0000");
|
|
10
|
+
}
|
|
11
|
+
export class BlockerLog {
|
|
12
|
+
constructor(options) {
|
|
13
|
+
this.ensured = false;
|
|
14
|
+
this.filePath = path.join(options.dir, "blockers.jsonl");
|
|
15
|
+
this.logger = options.logger;
|
|
16
|
+
this.now = options.now ?? (() => new Date());
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Record the current blocker set for a card. No-ops when the set is unchanged
|
|
20
|
+
* from the last recording, so callers can hand us every digest write.
|
|
21
|
+
*
|
|
22
|
+
* NEVER throws. A digest write must not fail because bookkeeping failed.
|
|
23
|
+
*/
|
|
24
|
+
record(cardId, blockers) {
|
|
25
|
+
const next = (blockers ?? []).map((b) => b.trim()).filter(Boolean);
|
|
26
|
+
const previous = this.latest(cardId);
|
|
27
|
+
// An empty set is worth recording once: "the blocker cleared" is real news,
|
|
28
|
+
// and it is what lets a reader see that a card came unstuck and got stuck again.
|
|
29
|
+
if (previous && fingerprint(previous.blockers) === fingerprint(next)) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (!previous && next.length === 0) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const row = { cardId, blockers: next, at: this.now().toISOString() };
|
|
36
|
+
try {
|
|
37
|
+
this.ensureDir();
|
|
38
|
+
fs.appendFileSync(this.filePath, `${JSON.stringify(row)}\n`, "utf8");
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
this.logger.error({ err: error, cardId }, "Blocker log append failed");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** All rows, oldest first. Returns [] when the log does not exist yet. */
|
|
45
|
+
readAll() {
|
|
46
|
+
let raw;
|
|
47
|
+
try {
|
|
48
|
+
raw = fs.readFileSync(this.filePath, "utf8");
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
const code = error?.code;
|
|
52
|
+
if (code !== "ENOENT") {
|
|
53
|
+
this.logger.error({ err: error }, "Blocker log read failed");
|
|
54
|
+
}
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
const rows = [];
|
|
58
|
+
for (const line of raw.split("\n")) {
|
|
59
|
+
const trimmed = line.trim();
|
|
60
|
+
if (!trimmed) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
const parsed = JSON.parse(trimmed);
|
|
65
|
+
if (typeof parsed?.cardId === "string" && Array.isArray(parsed?.blockers)) {
|
|
66
|
+
rows.push(parsed);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// A torn trailing line must not poison the whole read.
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return rows;
|
|
75
|
+
}
|
|
76
|
+
/** Rows for one card, oldest first. */
|
|
77
|
+
history(cardId) {
|
|
78
|
+
return this.readAll().filter((row) => row.cardId === cardId);
|
|
79
|
+
}
|
|
80
|
+
/** The most recent recording for a card, or null. */
|
|
81
|
+
latest(cardId) {
|
|
82
|
+
const rows = this.history(cardId);
|
|
83
|
+
return rows.length > 0 ? rows[rows.length - 1] : null;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* How many separate times this card has been recorded as blocked by anything.
|
|
87
|
+
* A coarse count only. It deliberately does NOT try to say "how many times by
|
|
88
|
+
* Marco specifically", because that is a semantic question, see the module note.
|
|
89
|
+
*/
|
|
90
|
+
blockedRecordings(cardId) {
|
|
91
|
+
return this.history(cardId).filter((r) => r.blockers.length > 0).length;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Compact history for a judge briefing: one line per distinct recording,
|
|
95
|
+
* oldest first, capped. Handing the model the raw timeline lets it say "fourth
|
|
96
|
+
* time on Marco" itself rather than us guessing at entity matching.
|
|
97
|
+
*/
|
|
98
|
+
renderTimeline(cardId, max = 6) {
|
|
99
|
+
const rows = this.history(cardId);
|
|
100
|
+
return rows.slice(-max).map((r) => {
|
|
101
|
+
const day = r.at.slice(0, 10);
|
|
102
|
+
return r.blockers.length === 0 ? `${day}: cleared` : `${day}: ${r.blockers.join(" | ")}`;
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
ensureDir() {
|
|
106
|
+
if (this.ensured) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
fs.mkdirSync(path.dirname(this.filePath), { recursive: true });
|
|
110
|
+
this.ensured = true;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=blocker-log.js.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { Logger } from "pino";
|
|
2
|
+
/**
|
|
3
|
+
* Append-only kanban card-move log (JARVIS-V1-SPEC §3.2).
|
|
4
|
+
*
|
|
5
|
+
* Why this exists: column membership rides on the agent label `labels.kanban`,
|
|
6
|
+
* and `labels.kanban_enteredAt` holds only the CURRENT value, overwritten on
|
|
7
|
+
* every move. There is no move history anywhere, so "fourth time you have
|
|
8
|
+
* deferred this" is unwritable. This log is the deferral history.
|
|
9
|
+
*
|
|
10
|
+
* It is PROSPECTIVE ONLY. Nothing before it ships is recoverable, which is why
|
|
11
|
+
* the spec sequences it ahead of everything else.
|
|
12
|
+
*
|
|
13
|
+
* Durability model: newline-delimited JSON appended to a single file under
|
|
14
|
+
* $PASEO_HOME. Append is atomic enough for our purposes (single writer process,
|
|
15
|
+
* one `appendFileSync` per row, rows are far below PIPE_BUF), and a torn final
|
|
16
|
+
* line is tolerated by the reader rather than being allowed to poison a read.
|
|
17
|
+
*/
|
|
18
|
+
export interface CardMoveRow {
|
|
19
|
+
cardId: string;
|
|
20
|
+
/** Previous column. Null on first placement. */
|
|
21
|
+
fromColumn: string | null;
|
|
22
|
+
/** New column. Null when the card was removed from the board entirely. */
|
|
23
|
+
toColumn: string | null;
|
|
24
|
+
/** ISO timestamp. */
|
|
25
|
+
at: string;
|
|
26
|
+
}
|
|
27
|
+
export interface CardMoveLogOptions {
|
|
28
|
+
/** Directory to write into, normally $PASEO_HOME. */
|
|
29
|
+
dir: string;
|
|
30
|
+
logger: Logger;
|
|
31
|
+
/** Test seam. Defaults to `() => new Date()`. */
|
|
32
|
+
now?: () => Date;
|
|
33
|
+
}
|
|
34
|
+
export declare class CardMoveLog {
|
|
35
|
+
private readonly filePath;
|
|
36
|
+
private readonly logger;
|
|
37
|
+
private readonly now;
|
|
38
|
+
private ensured;
|
|
39
|
+
constructor(options: CardMoveLogOptions);
|
|
40
|
+
/**
|
|
41
|
+
* Record a column change. No-ops when the column did not actually change, so
|
|
42
|
+
* callers can hand us every label write without filtering first.
|
|
43
|
+
*
|
|
44
|
+
* NEVER throws. A label write must not fail because bookkeeping failed.
|
|
45
|
+
*/
|
|
46
|
+
record(cardId: string, fromColumn: string | null, toColumn: string | null): void;
|
|
47
|
+
/** All rows, oldest first. Returns [] when the log does not exist yet. */
|
|
48
|
+
readAll(): CardMoveRow[];
|
|
49
|
+
/** Rows for one card, oldest first. */
|
|
50
|
+
history(cardId: string): CardMoveRow[];
|
|
51
|
+
/**
|
|
52
|
+
* How many times this card has been pushed away from active work.
|
|
53
|
+
*
|
|
54
|
+
* Counts transitions from an active column (now/urgent/verify) into a
|
|
55
|
+
* deferral column (later/future/awaiting). A card that has never been active
|
|
56
|
+
* and simply sits in `later` has not been deferred, it has merely never been
|
|
57
|
+
* picked up, and conflating those two would make the judge's headline number
|
|
58
|
+
* a lie.
|
|
59
|
+
*/
|
|
60
|
+
deferralCount(cardId: string): number;
|
|
61
|
+
/** When the card last entered its current column, from the log. Null if unknown. */
|
|
62
|
+
lastMoveAt(cardId: string): string | null;
|
|
63
|
+
private ensureDir;
|
|
64
|
+
}
|
|
65
|
+
export declare const KANBAN_LABEL_KEY = "kanban";
|
|
66
|
+
//# sourceMappingURL=card-move-log.d.ts.map
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
/** Columns that mean "pushed away from now", used for the deferral count. */
|
|
4
|
+
const DEFERRAL_TARGETS = new Set(["later", "future", "awaiting"]);
|
|
5
|
+
/** Columns that mean "actively committed to". */
|
|
6
|
+
const ACTIVE_SOURCES = new Set(["now", "urgent", "verify"]);
|
|
7
|
+
export class CardMoveLog {
|
|
8
|
+
constructor(options) {
|
|
9
|
+
this.ensured = false;
|
|
10
|
+
this.filePath = path.join(options.dir, "card-moves.jsonl");
|
|
11
|
+
this.logger = options.logger;
|
|
12
|
+
this.now = options.now ?? (() => new Date());
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Record a column change. No-ops when the column did not actually change, so
|
|
16
|
+
* callers can hand us every label write without filtering first.
|
|
17
|
+
*
|
|
18
|
+
* NEVER throws. A label write must not fail because bookkeeping failed.
|
|
19
|
+
*/
|
|
20
|
+
record(cardId, fromColumn, toColumn) {
|
|
21
|
+
const from = fromColumn ?? null;
|
|
22
|
+
const to = toColumn ?? null;
|
|
23
|
+
if (from === to) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const row = {
|
|
27
|
+
cardId,
|
|
28
|
+
fromColumn: from,
|
|
29
|
+
toColumn: to,
|
|
30
|
+
at: this.now().toISOString(),
|
|
31
|
+
};
|
|
32
|
+
try {
|
|
33
|
+
this.ensureDir();
|
|
34
|
+
fs.appendFileSync(this.filePath, `${JSON.stringify(row)}\n`, "utf8");
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
// Deliberately swallowed. Losing a log row is bad; failing the user's
|
|
38
|
+
// card move because we could not log it is worse.
|
|
39
|
+
this.logger.error({ err: error, cardId, from, to }, "Card move log append failed");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** All rows, oldest first. Returns [] when the log does not exist yet. */
|
|
43
|
+
readAll() {
|
|
44
|
+
let raw;
|
|
45
|
+
try {
|
|
46
|
+
raw = fs.readFileSync(this.filePath, "utf8");
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
const code = error?.code;
|
|
50
|
+
if (code !== "ENOENT") {
|
|
51
|
+
this.logger.error({ err: error }, "Card move log read failed");
|
|
52
|
+
}
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
const rows = [];
|
|
56
|
+
for (const line of raw.split("\n")) {
|
|
57
|
+
const trimmed = line.trim();
|
|
58
|
+
if (!trimmed) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const parsed = JSON.parse(trimmed);
|
|
63
|
+
if (typeof parsed?.cardId === "string" && typeof parsed?.at === "string") {
|
|
64
|
+
rows.push(parsed);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// A torn trailing line (crash mid-append) must not poison the whole read.
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return rows;
|
|
73
|
+
}
|
|
74
|
+
/** Rows for one card, oldest first. */
|
|
75
|
+
history(cardId) {
|
|
76
|
+
return this.readAll().filter((row) => row.cardId === cardId);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* How many times this card has been pushed away from active work.
|
|
80
|
+
*
|
|
81
|
+
* Counts transitions from an active column (now/urgent/verify) into a
|
|
82
|
+
* deferral column (later/future/awaiting). A card that has never been active
|
|
83
|
+
* and simply sits in `later` has not been deferred, it has merely never been
|
|
84
|
+
* picked up, and conflating those two would make the judge's headline number
|
|
85
|
+
* a lie.
|
|
86
|
+
*/
|
|
87
|
+
deferralCount(cardId) {
|
|
88
|
+
let count = 0;
|
|
89
|
+
for (const row of this.history(cardId)) {
|
|
90
|
+
if (row.fromColumn &&
|
|
91
|
+
ACTIVE_SOURCES.has(row.fromColumn) &&
|
|
92
|
+
row.toColumn &&
|
|
93
|
+
DEFERRAL_TARGETS.has(row.toColumn)) {
|
|
94
|
+
count += 1;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return count;
|
|
98
|
+
}
|
|
99
|
+
/** When the card last entered its current column, from the log. Null if unknown. */
|
|
100
|
+
lastMoveAt(cardId) {
|
|
101
|
+
const history = this.history(cardId);
|
|
102
|
+
return history.length > 0 ? history[history.length - 1].at : null;
|
|
103
|
+
}
|
|
104
|
+
ensureDir() {
|
|
105
|
+
if (this.ensured) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
fs.mkdirSync(path.dirname(this.filePath), { recursive: true });
|
|
109
|
+
this.ensured = true;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
export const KANBAN_LABEL_KEY = "kanban";
|
|
113
|
+
//# sourceMappingURL=card-move-log.js.map
|