@stigmer/runner 3.1.3 → 3.1.5
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/.build-fingerprint +1 -1
- package/dist/activities/execute-cursor/attachment-resolver.js +1 -1
- package/dist/activities/execute-cursor/attachment-resolver.js.map +1 -1
- package/dist/activities/execute-cursor/capture-flow.d.ts +26 -15
- package/dist/activities/execute-cursor/capture-flow.js +56 -18
- package/dist/activities/execute-cursor/capture-flow.js.map +1 -1
- package/dist/activities/execute-cursor/command-provenance.d.ts +11 -25
- package/dist/activities/execute-cursor/command-provenance.js +25 -115
- package/dist/activities/execute-cursor/command-provenance.js.map +1 -1
- package/dist/activities/execute-cursor/index.js +297 -478
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/skill-resolver.js +1 -1
- package/dist/activities/execute-cursor/skill-resolver.js.map +1 -1
- package/dist/activities/execute-cursor/todo-tracker.d.ts +6 -1
- package/dist/activities/execute-cursor/todo-tracker.js +15 -43
- package/dist/activities/execute-cursor/todo-tracker.js.map +1 -1
- package/dist/activities/execute-cursor/turn-stream.d.ts +141 -0
- package/dist/activities/execute-cursor/turn-stream.js +249 -0
- package/dist/activities/execute-cursor/turn-stream.js.map +1 -0
- package/dist/activities/execute-deep-agent/command-provenance.d.ts +61 -0
- package/dist/activities/execute-deep-agent/command-provenance.js +72 -0
- package/dist/activities/execute-deep-agent/command-provenance.js.map +1 -0
- package/dist/activities/execute-deep-agent/index.js +88 -20
- package/dist/activities/execute-deep-agent/index.js.map +1 -1
- package/dist/activities/execute-deep-agent/status-builder.js +8 -1
- package/dist/activities/execute-deep-agent/status-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/v3-status-builder.js +12 -1
- package/dist/activities/execute-deep-agent/v3-status-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/writeback-coordinator.js +5 -1
- package/dist/activities/execute-deep-agent/writeback-coordinator.js.map +1 -1
- package/dist/otel.js +10 -0
- package/dist/otel.js.map +1 -1
- package/dist/shared/filereview/cas-progress.d.ts +63 -0
- package/dist/shared/filereview/cas-progress.js +128 -0
- package/dist/shared/filereview/cas-progress.js.map +1 -0
- package/dist/shared/filereview/cas-substrate.d.ts +29 -0
- package/dist/shared/filereview/cas-substrate.js +46 -21
- package/dist/shared/filereview/cas-substrate.js.map +1 -1
- package/dist/shared/filereview/command-provenance.d.ts +93 -0
- package/dist/shared/filereview/command-provenance.js +132 -0
- package/dist/shared/filereview/command-provenance.js.map +1 -0
- package/dist/shared/filereview/git-substrate.d.ts +9 -3
- package/dist/shared/filereview/git-substrate.js +4 -0
- package/dist/shared/filereview/git-substrate.js.map +1 -1
- package/dist/shared/filereview/index.d.ts +4 -3
- package/dist/shared/filereview/index.js +3 -2
- package/dist/shared/filereview/index.js.map +1 -1
- package/dist/shared/filereview/progress.d.ts +105 -34
- package/dist/shared/filereview/progress.js +96 -34
- package/dist/shared/filereview/progress.js.map +1 -1
- package/dist/shared/plan-mode-prompt.d.ts +9 -0
- package/dist/shared/plan-mode-prompt.js +18 -0
- package/dist/shared/plan-mode-prompt.js.map +1 -1
- package/dist/shared/todos.d.ts +56 -0
- package/dist/shared/todos.js +98 -0
- package/dist/shared/todos.js.map +1 -0
- package/dist/shared/tool-row.d.ts +16 -0
- package/dist/shared/tool-row.js +31 -0
- package/dist/shared/tool-row.js.map +1 -1
- package/dist/shared/workspace/git-identity.d.ts +36 -0
- package/dist/shared/workspace/git-identity.js +39 -0
- package/dist/shared/workspace/git-identity.js.map +1 -0
- package/dist/shared/workspace/local-backend.d.ts +6 -2
- package/dist/shared/workspace/local-backend.js +6 -2
- package/dist/shared/workspace/local-backend.js.map +1 -1
- package/dist/shared/workspace/stigmer-link.d.ts +54 -0
- package/dist/shared/workspace/stigmer-link.js +92 -0
- package/dist/shared/workspace/stigmer-link.js.map +1 -0
- package/dist/shared/workspace/types.d.ts +6 -4
- package/package.json +2 -2
- package/src/__tests__/otel-turn-span.test.ts +61 -0
- package/src/activities/execute-cursor/__tests__/progress-substrate.test.ts +169 -0
- package/src/activities/execute-cursor/__tests__/turn-stream.test.ts +349 -0
- package/src/activities/execute-cursor/attachment-resolver.ts +1 -1
- package/src/activities/execute-cursor/capture-flow.ts +71 -25
- package/src/activities/execute-cursor/command-provenance.ts +25 -120
- package/src/activities/execute-cursor/index.ts +345 -505
- package/src/activities/execute-cursor/skill-resolver.ts +1 -1
- package/src/activities/execute-cursor/todo-tracker.ts +17 -59
- package/src/activities/execute-cursor/turn-stream.ts +418 -0
- package/src/activities/execute-deep-agent/__tests__/command-provenance.test.ts +252 -0
- package/src/activities/execute-deep-agent/__tests__/status-builder.test.ts +78 -0
- package/src/activities/execute-deep-agent/__tests__/v3-status-builder.test.ts +105 -1
- package/src/activities/execute-deep-agent/__tests__/writeback-coordinator.test.ts +30 -3
- package/src/activities/execute-deep-agent/command-provenance.ts +102 -0
- package/src/activities/execute-deep-agent/index.ts +107 -20
- package/src/activities/execute-deep-agent/status-builder.ts +9 -0
- package/src/activities/execute-deep-agent/v3-status-builder.ts +13 -0
- package/src/activities/execute-deep-agent/writeback-coordinator.ts +5 -1
- package/src/otel.ts +8 -0
- package/src/shared/__tests__/todos.test.ts +216 -0
- package/src/shared/filereview/__tests__/cas-progress.test.ts +228 -0
- package/src/shared/filereview/__tests__/cas-substrate.test.ts +66 -0
- package/src/shared/filereview/__tests__/command-provenance.test.ts +252 -0
- package/src/shared/filereview/__tests__/progress.test.ts +112 -10
- package/src/shared/filereview/cas-progress.ts +170 -0
- package/src/shared/filereview/cas-substrate.ts +69 -24
- package/src/shared/filereview/command-provenance.ts +180 -0
- package/src/shared/filereview/git-substrate.ts +13 -3
- package/src/shared/filereview/index.ts +15 -1
- package/src/shared/filereview/progress.ts +171 -47
- package/src/shared/plan-mode-prompt.ts +18 -0
- package/src/shared/todos.ts +126 -0
- package/src/shared/tool-row.ts +34 -0
- package/src/shared/workspace/__tests__/git-identity.test.ts +124 -0
- package/src/shared/workspace/__tests__/stigmer-link.test.ts +173 -0
- package/src/shared/workspace/git-identity.ts +41 -0
- package/src/shared/workspace/local-backend.ts +6 -2
- package/src/shared/workspace/stigmer-link.ts +97 -0
- package/src/shared/workspace/types.ts +6 -4
- package/dist/activities/execute-cursor/stigmer-link.d.ts +0 -35
- package/dist/activities/execute-cursor/stigmer-link.js +0 -73
- package/dist/activities/execute-cursor/stigmer-link.js.map +0 -1
- package/src/activities/execute-cursor/stigmer-link.ts +0 -78
|
@@ -26,6 +26,10 @@ import {
|
|
|
26
26
|
WorkspaceLockTimeoutError,
|
|
27
27
|
type ReleaseWorkspaceLock,
|
|
28
28
|
} from "../../shared/workspace/workspace-lock.js";
|
|
29
|
+
import {
|
|
30
|
+
ensureStigmerSymlink,
|
|
31
|
+
removeStigmerSymlink,
|
|
32
|
+
} from "../../shared/workspace/stigmer-link.js";
|
|
29
33
|
import type { ToolOutputOffloadContext } from "../../shared/status-offload.js";
|
|
30
34
|
import { publishPlanArtifact } from "../../shared/plan-artifact.js";
|
|
31
35
|
import { classifyTool } from "../../shared/tool-kind.js";
|
|
@@ -52,14 +56,26 @@ import {
|
|
|
52
56
|
} from "../../shared/filereview/capture.js";
|
|
53
57
|
import {
|
|
54
58
|
captureFileChangeProgress,
|
|
59
|
+
createGitProgressSubstrate,
|
|
60
|
+
createHybridProgressSubstrate,
|
|
55
61
|
newProgressCaptureState,
|
|
62
|
+
type ProgressSubstrate,
|
|
56
63
|
} from "../../shared/filereview/progress.js";
|
|
64
|
+
import {
|
|
65
|
+
createCasProgressSubstrate,
|
|
66
|
+
type CasTouchedSnapshot,
|
|
67
|
+
} from "../../shared/filereview/cas-progress.js";
|
|
57
68
|
import { hasCandidateCaptured } from "../../shared/filereview/events.js";
|
|
58
69
|
import { casBlobReader, type CasPathCapture } from "../../shared/filereview/cas-substrate.js";
|
|
59
70
|
import { partitionIgnoredPathsBySecret } from "../../shared/filereview/secret-paths.js";
|
|
60
71
|
import type { CasCaptureObserver } from "./cas-capture-observer.js";
|
|
61
|
-
import {
|
|
72
|
+
import {
|
|
73
|
+
collectSettledToolCallIds,
|
|
74
|
+
collectSubAgentToolCallIds,
|
|
75
|
+
withholdSecretContentFromMessages,
|
|
76
|
+
} from "../../shared/tool-row.js";
|
|
62
77
|
import { stampFlowedFileEditRows, stampFlowedSubAgentFileEditRows } from "./stamp-flowed-rows.js";
|
|
78
|
+
import { deriveTurnCommandProvenance } from "./command-provenance.js";
|
|
63
79
|
|
|
64
80
|
/** The harness id stamped on the deep-agent's file-review ledger events. */
|
|
65
81
|
const DEEP_AGENT_HARNESS_ID = "deep-agent";
|
|
@@ -217,6 +233,22 @@ export function createDeepAgentActivities(config: Config) {
|
|
|
217
233
|
throw lockErr;
|
|
218
234
|
}
|
|
219
235
|
|
|
236
|
+
// Bridge the workspace to the session's platform dir so the agent's
|
|
237
|
+
// file tools can read platform-mounted content (`.stigmer/inputs/…`
|
|
238
|
+
// attachments incl. the approved plan, `.stigmer/skills/…`) — the
|
|
239
|
+
// deepagents FilesystemBackend resolves paths against the workspace
|
|
240
|
+
// root and knows nothing of the LocalWorkspaceBackend's `.stigmer`
|
|
241
|
+
// routing. Two ordering invariants (see shared/workspace/stigmer-link.ts):
|
|
242
|
+
// - AFTER the lock: the link is a tree mutation; sessions sharing one
|
|
243
|
+
// attached directory must not re-point it under a running turn.
|
|
244
|
+
// - BEFORE the baseline capture below: present in both the baseline
|
|
245
|
+
// and candidate trees, the link cancels out of the file-review diff
|
|
246
|
+
// even when the workspace's git excludes do not list `.stigmer`.
|
|
247
|
+
// Removed in cleanup() (the finally below), before the lock releases.
|
|
248
|
+
if (setup.workspaceBackend.platformDir) {
|
|
249
|
+
await ensureStigmerSymlink(gitRoot, setup.workspaceBackend.platformDir);
|
|
250
|
+
}
|
|
251
|
+
|
|
220
252
|
// (1) Capture-mode resume — reconcile any DECIDED change set FIRST (this
|
|
221
253
|
// drops the per-execution refs), before the next baseline re-pins them
|
|
222
254
|
// (refs are executionId-keyed). Then (2) short-circuit a pure file-review
|
|
@@ -303,8 +335,10 @@ export function createDeepAgentActivities(config: Config) {
|
|
|
303
335
|
}
|
|
304
336
|
|
|
305
337
|
// Turn start (capture mode): pin the pre-turn tree + author BASELINE. Taken
|
|
306
|
-
// AFTER any reconcile above and AFTER performSetup's workspace writes
|
|
307
|
-
// the
|
|
338
|
+
// AFTER any reconcile above and AFTER performSetup's workspace writes and
|
|
339
|
+
// the `.stigmer` symlink above, so the baseline absorbs the reconciled
|
|
340
|
+
// state and the runner-owned files (the symlink then cancels out of the
|
|
341
|
+
// baseline→candidate diff).
|
|
308
342
|
let captureBaselineTree = "";
|
|
309
343
|
// Snapshot the sub-agent tool-call ids that exist BEFORE this turn's
|
|
310
344
|
// stream, so the turn-boundary stamp scopes itself to sub-agent rows
|
|
@@ -312,6 +346,12 @@ export function createDeepAgentActivities(config: Config) {
|
|
|
312
346
|
// seeds prior sub-agents — but computed, not assumed, so it stays correct
|
|
313
347
|
// if that ever changes; see collectSubAgentToolCallIds).
|
|
314
348
|
const priorSubAgentToolCallIds = collectSubAgentToolCallIds(initialStatus.subAgentExecutions);
|
|
349
|
+
// Snapshot the top-level tool-call ids already SETTLED before this turn's
|
|
350
|
+
// stream, so the approved-command provenance (DD-28) scopes itself to THIS
|
|
351
|
+
// turn's executed commands by identity. The deep-agent's approved shell
|
|
352
|
+
// executes in place at its seeded position, so the Cursor positional scope
|
|
353
|
+
// would miss it — see execute-deep-agent/command-provenance.ts.
|
|
354
|
+
const priorSettledToolCallIds = collectSettledToolCallIds(initialStatus.messages);
|
|
315
355
|
if (setup.captureMode) {
|
|
316
356
|
captureBaselineTree = await captureBaselineToLedger({
|
|
317
357
|
status: initialStatus,
|
|
@@ -323,14 +363,34 @@ export function createDeepAgentActivities(config: Config) {
|
|
|
323
363
|
});
|
|
324
364
|
}
|
|
325
365
|
|
|
326
|
-
// Per-turn state for mid-run live capture (DD-32
|
|
327
|
-
//
|
|
328
|
-
//
|
|
329
|
-
//
|
|
330
|
-
//
|
|
366
|
+
// Per-turn state + substrate for mid-run live capture (DD-32 / DD-33). The
|
|
367
|
+
// floor lives in progressState; the substrate is chosen for this turn's
|
|
368
|
+
// workspace shape and owns its own short-circuit cache. An atomic snapshot
|
|
369
|
+
// of the shared observer feeds the CAS/HYBRID substrates — copied
|
|
370
|
+
// synchronously so a concurrent sub-agent write cannot mutate it mid-read.
|
|
331
371
|
const progressState = newProgressCaptureState();
|
|
332
|
-
const
|
|
333
|
-
const
|
|
372
|
+
const casObserver = setup.casObserver;
|
|
373
|
+
const readObserverTouched = (): CasTouchedSnapshot => ({
|
|
374
|
+
before: new Map(casObserver.before),
|
|
375
|
+
blockedSecretPaths: new Set(casObserver.blockedSecretPaths),
|
|
376
|
+
});
|
|
377
|
+
// Git tree -> hybrid (numstat for tracked + observer for gitignored);
|
|
378
|
+
// non-git -> cas over the observer (no baseline tree needed). Undefined
|
|
379
|
+
// outside capture mode (writes are deny-gated, nothing is captured).
|
|
380
|
+
const progressSubstrate: ProgressSubstrate | undefined = !setup.captureMode
|
|
381
|
+
? undefined
|
|
382
|
+
: setup.gitWorkspace
|
|
383
|
+
? captureBaselineTree
|
|
384
|
+
? createHybridProgressSubstrate(
|
|
385
|
+
createGitProgressSubstrate({
|
|
386
|
+
workspaceRoot: gitRoot,
|
|
387
|
+
executionId,
|
|
388
|
+
baselineTree: captureBaselineTree,
|
|
389
|
+
}),
|
|
390
|
+
createCasProgressSubstrate({ workspaceRoot: gitRoot, read: readObserverTouched }),
|
|
391
|
+
)
|
|
392
|
+
: undefined
|
|
393
|
+
: createCasProgressSubstrate({ workspaceRoot: gitRoot, read: readObserverTouched });
|
|
334
394
|
|
|
335
395
|
const cancellationSignal = Context.current().cancellationSignal;
|
|
336
396
|
|
|
@@ -357,20 +417,20 @@ export function createDeepAgentActivities(config: Config) {
|
|
|
357
417
|
globalBypass: setup.globalBypass,
|
|
358
418
|
},
|
|
359
419
|
streamVersion: setup.streamVersion,
|
|
360
|
-
// Mid-run live capture (DD-32): attach file_change_progress
|
|
361
|
-
// scheduled persist
|
|
362
|
-
//
|
|
363
|
-
//
|
|
420
|
+
// Mid-run live capture (DD-32 / DD-33): attach file_change_progress
|
|
421
|
+
// before each scheduled persist, throttled by the floor inside
|
|
422
|
+
// captureFileChangeProgress. The substrate (git / non-git CAS / hybrid)
|
|
423
|
+
// was chosen for this turn above; the deep-agent's only runner-owned
|
|
424
|
+
// tree entry is the `.stigmer` symlink, created before the baseline
|
|
425
|
+
// so it appears identically in every capture and cancels out of the
|
|
426
|
+
// diff — the git slice therefore needs no excludePaths, matching its
|
|
427
|
+
// turn-boundary candidate capture.
|
|
364
428
|
beforePersist: async (status) => {
|
|
365
|
-
if (!
|
|
366
|
-
return;
|
|
367
|
-
}
|
|
429
|
+
if (!progressSubstrate) return;
|
|
368
430
|
await captureFileChangeProgress({
|
|
369
431
|
status,
|
|
370
|
-
gitRoot,
|
|
371
|
-
executionId,
|
|
372
432
|
changeSetId,
|
|
373
|
-
|
|
433
|
+
substrate: progressSubstrate,
|
|
374
434
|
state: progressState,
|
|
375
435
|
});
|
|
376
436
|
},
|
|
@@ -429,6 +489,25 @@ export function createDeepAgentActivities(config: Config) {
|
|
|
429
489
|
gitRoot,
|
|
430
490
|
casCaptureClass,
|
|
431
491
|
);
|
|
492
|
+
// Approved-command turn facts (DD-28): when every mutation-capable call
|
|
493
|
+
// this turn was a consented shell command, attach the provenance so the
|
|
494
|
+
// backend can verify the cited consent rows and auto-keep the set
|
|
495
|
+
// instead of arming a second review gate. Fail-closed: any non-qualifying
|
|
496
|
+
// turn attaches nothing and reviews manually exactly as before. Attached
|
|
497
|
+
// only when captureCandidateToLedger actually authors a CANDIDATE.
|
|
498
|
+
const commandProvenance = deriveTurnCommandProvenance({
|
|
499
|
+
status: initialStatus,
|
|
500
|
+
priorSettledToolCallIds,
|
|
501
|
+
priorSubAgentToolCallIds,
|
|
502
|
+
globalBypass: setup.globalBypass,
|
|
503
|
+
});
|
|
504
|
+
if (commandProvenance) {
|
|
505
|
+
console.log(
|
|
506
|
+
`[ExecuteDeepAgent] capture: turn qualifies for approved-command auto-keep ` +
|
|
507
|
+
`(consent rows: ${commandProvenance.consentToolCallIds.join(",") || "(auto_approve_all)"}); ` +
|
|
508
|
+
`attaching provenance to candidate (execution=${executionId})`,
|
|
509
|
+
);
|
|
510
|
+
}
|
|
432
511
|
await captureCandidateToLedger({
|
|
433
512
|
status: initialStatus,
|
|
434
513
|
gitRoot,
|
|
@@ -441,6 +520,7 @@ export function createDeepAgentActivities(config: Config) {
|
|
|
441
520
|
unreviewablePaths,
|
|
442
521
|
unreviewableCaptureClass: casCaptureClass,
|
|
443
522
|
gitWorkspace: setup.gitWorkspace,
|
|
523
|
+
commandProvenance,
|
|
444
524
|
});
|
|
445
525
|
// Review is pending iff a CANDIDATE was actually authored (the seam
|
|
446
526
|
// drops no-op captures), so a turn that only touched-then-unchanged an
|
|
@@ -872,5 +952,12 @@ async function cleanup(setup: SetupResult | null): Promise<void> {
|
|
|
872
952
|
console.warn("[ExecuteDeepAgent] MCP connection cleanup failed:", err);
|
|
873
953
|
}
|
|
874
954
|
}
|
|
955
|
+
|
|
956
|
+
// Drop the workspace→platform `.stigmer` symlink so an attached repo is
|
|
957
|
+
// left untouched between turns (issue #173 semantics; a multi-turn session
|
|
958
|
+
// recreates it next turn). Runs in the activity finally, BEFORE the
|
|
959
|
+
// workspace lock releases, so the next queued turn baselines a clean tree.
|
|
960
|
+
// Best-effort and symlink-only: a real `.stigmer` directory is never removed.
|
|
961
|
+
await removeStigmerSymlink(setup.workspaceBackend.rootDir);
|
|
875
962
|
}
|
|
876
963
|
|
|
@@ -24,9 +24,11 @@ import {
|
|
|
24
24
|
ExecutionPhase,
|
|
25
25
|
MessageType,
|
|
26
26
|
ToolCallStatus,
|
|
27
|
+
ToolKind,
|
|
27
28
|
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
28
29
|
import { resolveApprovalMessage as resolveApprovalMsg } from "../../shared/approval-policy.js";
|
|
29
30
|
import { classifyTool } from "../../shared/tool-kind.js";
|
|
31
|
+
import { applyTodoUpdate } from "../../shared/todos.js";
|
|
30
32
|
import { ExecutionState } from "./execution-state.js";
|
|
31
33
|
import { utcTimestamp } from "../../shared/status.js";
|
|
32
34
|
import type { ExecutionStatusWriter } from "./execution-status-writer.js";
|
|
@@ -348,6 +350,13 @@ export class StatusBuilder {
|
|
|
348
350
|
// (see v3 builder note). Truncating here would corrupt image base64.
|
|
349
351
|
tc.status = ToolCallStatus.TOOL_CALL_COMPLETED;
|
|
350
352
|
tc.result = extractToolResult(event.data);
|
|
353
|
+
|
|
354
|
+
// Project a completed to-do write into status.todos — success branch only,
|
|
355
|
+
// since a failed write_todos never ran its state-mutating Command. Shares
|
|
356
|
+
// the mapper and rationale documented in V3StatusBuilder.handleToolFinished.
|
|
357
|
+
if (tc.toolKind === ToolKind.TODO) {
|
|
358
|
+
applyTodoUpdate(this.state.proto.todos, tc.args?.todos, { merge: false });
|
|
359
|
+
}
|
|
351
360
|
}
|
|
352
361
|
|
|
353
362
|
tc.completedAt = utcTimestamp();
|
|
@@ -23,9 +23,11 @@ import {
|
|
|
23
23
|
ExecutionPhase,
|
|
24
24
|
MessageType,
|
|
25
25
|
ToolCallStatus,
|
|
26
|
+
ToolKind,
|
|
26
27
|
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
27
28
|
import { resolveApprovalMessage as resolveApprovalMsg } from "../../shared/approval-policy.js";
|
|
28
29
|
import { classifyTool } from "../../shared/tool-kind.js";
|
|
30
|
+
import { applyTodoUpdate } from "../../shared/todos.js";
|
|
29
31
|
import { ExecutionState } from "./execution-state.js";
|
|
30
32
|
import { utcTimestamp } from "../../shared/status.js";
|
|
31
33
|
import type { ExecutionStatusWriter } from "./execution-status-writer.js";
|
|
@@ -340,6 +342,17 @@ export class V3StatusBuilder implements ExecutionStatusWriter {
|
|
|
340
342
|
this.state.toolStartTimes.delete(callId);
|
|
341
343
|
this.toolArgBuffers.delete(callId);
|
|
342
344
|
|
|
345
|
+
// Project a completed to-do write into status.todos. deepagents' write_todos
|
|
346
|
+
// runs its state-mutating Command when the tool node COMPLETES, so we mirror
|
|
347
|
+
// it here (not at tool-start) — a call cancelled before finishing correctly
|
|
348
|
+
// projects nothing. Keyed on the harness-agnostic ToolKind.TODO (stamped at
|
|
349
|
+
// tool-start) and fed by the same shared mapper the Cursor tracker uses;
|
|
350
|
+
// deepagents always full-replaces (no merge field). The tool call itself
|
|
351
|
+
// stays in messages (the client filters ToolKind.TODO from the thread).
|
|
352
|
+
if (tc.toolKind === ToolKind.TODO) {
|
|
353
|
+
applyTodoUpdate(this.state.proto.todos, tc.args?.todos, { merge: false });
|
|
354
|
+
}
|
|
355
|
+
|
|
343
356
|
this._forceNextUpdate = true;
|
|
344
357
|
}
|
|
345
358
|
|
|
@@ -27,6 +27,7 @@ import { GitWriteBackMode } from "@stigmer/protos/ai/stigmer/agentic/session/v1/
|
|
|
27
27
|
import type { WorkspaceEntry } from "@stigmer/protos/ai/stigmer/agentic/session/v1/workspace_pb";
|
|
28
28
|
import type { WorkspaceBackend, ProvisionResult } from "../../shared/workspace/types.js";
|
|
29
29
|
import { SourceType } from "../../shared/workspace/types.js";
|
|
30
|
+
import { gitCommitAsAgent } from "../../shared/workspace/git-identity.js";
|
|
30
31
|
import type { ExecutionStatusWriter } from "./execution-status-writer.js";
|
|
31
32
|
|
|
32
33
|
const WRITE_BACK_ENABLED_MODES = new Set([
|
|
@@ -273,7 +274,10 @@ export class WriteBackCoordinator {
|
|
|
273
274
|
commitMsg: string,
|
|
274
275
|
): Promise<void> {
|
|
275
276
|
await exec("git add -A");
|
|
276
|
-
|
|
277
|
+
// Committed as the agent identity: the cloud sandbox has no git identity
|
|
278
|
+
// configured (a bare commit fails with "Author identity unknown"), and in
|
|
279
|
+
// local mode the agent's work should not be attributed to the host user.
|
|
280
|
+
await exec(gitCommitAsAgent(commitMsg));
|
|
277
281
|
|
|
278
282
|
entryState.commitCount++;
|
|
279
283
|
const shaOutput = await exec("git rev-parse HEAD");
|
package/src/otel.ts
CHANGED
|
@@ -125,12 +125,20 @@ export async function startCursorTurnSpan(attrs: {
|
|
|
125
125
|
[ATTR_SESSION_ID]: attrs.sessionId,
|
|
126
126
|
},
|
|
127
127
|
});
|
|
128
|
+
// Guard against a double end / a late setTokens: the turn span is ended from
|
|
129
|
+
// the activity's finally (finishTurnTelemetry), and OTel warns when a span is
|
|
130
|
+
// ended twice or mutated after end. Making this a no-op keeps the caller free
|
|
131
|
+
// to end idempotently on every exit path.
|
|
132
|
+
let ended = false;
|
|
128
133
|
return {
|
|
129
134
|
setTokens(input: number, output: number) {
|
|
135
|
+
if (ended) return;
|
|
130
136
|
if (input) span.setAttribute(ATTR_LLM_INPUT_TOKENS, input);
|
|
131
137
|
if (output) span.setAttribute(ATTR_LLM_OUTPUT_TOKENS, output);
|
|
132
138
|
},
|
|
133
139
|
end() {
|
|
140
|
+
if (ended) return;
|
|
141
|
+
ended = true;
|
|
134
142
|
span.end();
|
|
135
143
|
},
|
|
136
144
|
};
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for applyTodoUpdate — the harness-agnostic to-do map projection
|
|
3
|
+
* shared by the Cursor TodoTracker and the native v2/v3 status builders.
|
|
4
|
+
*
|
|
5
|
+
* These pin the exact mapping contract (status coercion, id synthesis,
|
|
6
|
+
* created_at preservation, merge vs. full-replace, clear-on-empty, defensive
|
|
7
|
+
* input handling, dirty return) so no two writers of status.todos can drift.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, it, expect } from "vitest";
|
|
11
|
+
import { TodoStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
12
|
+
import type { TodoItem } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/todo_pb";
|
|
13
|
+
import { applyTodoUpdate } from "../todos.js";
|
|
14
|
+
|
|
15
|
+
function emptyMap(): { [key: string]: TodoItem } {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("applyTodoUpdate", () => {
|
|
20
|
+
describe("status mapping", () => {
|
|
21
|
+
it("maps pending", () => {
|
|
22
|
+
const m = emptyMap();
|
|
23
|
+
applyTodoUpdate(m, [{ content: "A", status: "pending" }], { merge: false });
|
|
24
|
+
expect(m["todo-0"].status).toBe(TodoStatus.TODO_PENDING);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("maps in_progress (snake_case)", () => {
|
|
28
|
+
const m = emptyMap();
|
|
29
|
+
applyTodoUpdate(m, [{ content: "A", status: "in_progress" }], { merge: false });
|
|
30
|
+
expect(m["todo-0"].status).toBe(TodoStatus.TODO_IN_PROGRESS);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("maps inProgress (camelCase, via toLowerCase)", () => {
|
|
34
|
+
const m = emptyMap();
|
|
35
|
+
applyTodoUpdate(m, [{ content: "A", status: "inProgress" }], { merge: false });
|
|
36
|
+
expect(m["todo-0"].status).toBe(TodoStatus.TODO_IN_PROGRESS);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("maps completed", () => {
|
|
40
|
+
const m = emptyMap();
|
|
41
|
+
applyTodoUpdate(m, [{ content: "A", status: "completed" }], { merge: false });
|
|
42
|
+
expect(m["todo-0"].status).toBe(TodoStatus.TODO_COMPLETED);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("maps cancelled (Cursor superset)", () => {
|
|
46
|
+
const m = emptyMap();
|
|
47
|
+
applyTodoUpdate(m, [{ content: "A", status: "cancelled" }], { merge: false });
|
|
48
|
+
expect(m["todo-0"].status).toBe(TodoStatus.TODO_CANCELLED);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("defaults unknown status to pending", () => {
|
|
52
|
+
const m = emptyMap();
|
|
53
|
+
applyTodoUpdate(m, [{ content: "A", status: "bogus" }], { merge: false });
|
|
54
|
+
expect(m["todo-0"].status).toBe(TodoStatus.TODO_PENDING);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("defaults missing status to pending", () => {
|
|
58
|
+
const m = emptyMap();
|
|
59
|
+
applyTodoUpdate(m, [{ content: "A" }], { merge: false });
|
|
60
|
+
expect(m["todo-0"].status).toBe(TodoStatus.TODO_PENDING);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("id synthesis", () => {
|
|
65
|
+
it("uses a provided id", () => {
|
|
66
|
+
const m = emptyMap();
|
|
67
|
+
applyTodoUpdate(m, [{ id: "t1", content: "A", status: "pending" }], { merge: false });
|
|
68
|
+
expect(Object.keys(m)).toEqual(["t1"]);
|
|
69
|
+
expect(m["t1"].id).toBe("t1");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("falls back to todo-<index> when id is absent", () => {
|
|
73
|
+
const m = emptyMap();
|
|
74
|
+
applyTodoUpdate(
|
|
75
|
+
m,
|
|
76
|
+
[
|
|
77
|
+
{ content: "A", status: "pending" },
|
|
78
|
+
{ content: "B", status: "completed" },
|
|
79
|
+
],
|
|
80
|
+
{ merge: false },
|
|
81
|
+
);
|
|
82
|
+
expect(Object.keys(m)).toEqual(["todo-0", "todo-1"]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("falls back to todo-<index> when id is an empty string", () => {
|
|
86
|
+
const m = emptyMap();
|
|
87
|
+
applyTodoUpdate(m, [{ id: "", content: "A", status: "pending" }], { merge: false });
|
|
88
|
+
expect(Object.keys(m)).toEqual(["todo-0"]);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("timestamps", () => {
|
|
93
|
+
it("sets created_at from raw.created_at on full-replace", () => {
|
|
94
|
+
const m = emptyMap();
|
|
95
|
+
applyTodoUpdate(
|
|
96
|
+
m,
|
|
97
|
+
[{ id: "t1", content: "A", status: "pending", created_at: "2026-01-01T00:00:00Z" }],
|
|
98
|
+
{ merge: false, now: "2026-06-06T06:06:06Z" },
|
|
99
|
+
);
|
|
100
|
+
expect(m["t1"].createdAt).toBe("2026-01-01T00:00:00Z");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("uses now for created_at when raw.created_at is absent (full-replace)", () => {
|
|
104
|
+
const m = emptyMap();
|
|
105
|
+
applyTodoUpdate(m, [{ content: "A", status: "pending" }], {
|
|
106
|
+
merge: false,
|
|
107
|
+
now: "2026-06-06T06:06:06Z",
|
|
108
|
+
});
|
|
109
|
+
expect(m["todo-0"].createdAt).toBe("2026-06-06T06:06:06Z");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("always bumps updated_at to now", () => {
|
|
113
|
+
const m = emptyMap();
|
|
114
|
+
applyTodoUpdate(m, [{ content: "A", status: "pending" }], {
|
|
115
|
+
merge: false,
|
|
116
|
+
now: "2026-06-06T06:06:06Z",
|
|
117
|
+
});
|
|
118
|
+
expect(m["todo-0"].updatedAt).toBe("2026-06-06T06:06:06Z");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("preserves created_at from the prior entry ONLY on merge", () => {
|
|
122
|
+
const m = emptyMap();
|
|
123
|
+
applyTodoUpdate(m, [{ id: "t1", content: "A", status: "pending" }], {
|
|
124
|
+
merge: false,
|
|
125
|
+
now: "2026-01-01T00:00:00Z",
|
|
126
|
+
});
|
|
127
|
+
applyTodoUpdate(m, [{ id: "t1", content: "A", status: "completed" }], {
|
|
128
|
+
merge: true,
|
|
129
|
+
now: "2026-02-02T00:00:00Z",
|
|
130
|
+
});
|
|
131
|
+
expect(m["t1"].createdAt).toBe("2026-01-01T00:00:00Z");
|
|
132
|
+
expect(m["t1"].updatedAt).toBe("2026-02-02T00:00:00Z");
|
|
133
|
+
expect(m["t1"].status).toBe(TodoStatus.TODO_COMPLETED);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("does NOT preserve created_at on full-replace of the same id", () => {
|
|
137
|
+
const m = emptyMap();
|
|
138
|
+
applyTodoUpdate(m, [{ id: "t1", content: "A", status: "pending" }], {
|
|
139
|
+
merge: false,
|
|
140
|
+
now: "2026-01-01T00:00:00Z",
|
|
141
|
+
});
|
|
142
|
+
applyTodoUpdate(m, [{ id: "t1", content: "A", status: "pending" }], {
|
|
143
|
+
merge: false,
|
|
144
|
+
now: "2026-02-02T00:00:00Z",
|
|
145
|
+
});
|
|
146
|
+
expect(m["t1"].createdAt).toBe("2026-02-02T00:00:00Z");
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("merge vs. full-replace", () => {
|
|
151
|
+
it("full-replace clears prior entries", () => {
|
|
152
|
+
const m = emptyMap();
|
|
153
|
+
applyTodoUpdate(m, [{ id: "a", content: "A", status: "pending" }], { merge: false });
|
|
154
|
+
applyTodoUpdate(m, [{ id: "b", content: "B", status: "pending" }], { merge: false });
|
|
155
|
+
expect(Object.keys(m)).toEqual(["b"]);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("merge preserves other existing entries", () => {
|
|
159
|
+
const m = emptyMap();
|
|
160
|
+
applyTodoUpdate(m, [{ id: "a", content: "A", status: "pending" }], { merge: false });
|
|
161
|
+
applyTodoUpdate(m, [{ id: "b", content: "B", status: "pending" }], { merge: true });
|
|
162
|
+
expect(Object.keys(m).sort()).toEqual(["a", "b"]);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("merge updates an existing entry by id", () => {
|
|
166
|
+
const m = emptyMap();
|
|
167
|
+
applyTodoUpdate(m, [{ id: "a", content: "A", status: "pending" }], { merge: false });
|
|
168
|
+
applyTodoUpdate(m, [{ id: "a", content: "A2", status: "completed" }], { merge: true });
|
|
169
|
+
expect(m["a"].content).toBe("A2");
|
|
170
|
+
expect(m["a"].status).toBe(TodoStatus.TODO_COMPLETED);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe("clear-on-empty", () => {
|
|
175
|
+
it("clears the map on empty array + full-replace and reports a change", () => {
|
|
176
|
+
const m = emptyMap();
|
|
177
|
+
applyTodoUpdate(m, [{ id: "a", content: "A", status: "pending" }], { merge: false });
|
|
178
|
+
const changed = applyTodoUpdate(m, [], { merge: false });
|
|
179
|
+
expect(changed).toBe(true);
|
|
180
|
+
expect(Object.keys(m)).toHaveLength(0);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("does NOT clear and reports no change on empty array + merge", () => {
|
|
184
|
+
const m = emptyMap();
|
|
185
|
+
applyTodoUpdate(m, [{ id: "a", content: "A", status: "pending" }], { merge: false });
|
|
186
|
+
const changed = applyTodoUpdate(m, [], { merge: true });
|
|
187
|
+
expect(changed).toBe(false);
|
|
188
|
+
expect(Object.keys(m)).toEqual(["a"]);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("treats a non-array payload like empty (clears on full-replace)", () => {
|
|
192
|
+
const m = emptyMap();
|
|
193
|
+
applyTodoUpdate(m, [{ id: "a", content: "A", status: "pending" }], { merge: false });
|
|
194
|
+
const changed = applyTodoUpdate(m, undefined, { merge: false });
|
|
195
|
+
expect(changed).toBe(true);
|
|
196
|
+
expect(Object.keys(m)).toHaveLength(0);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe("defensive input handling", () => {
|
|
201
|
+
it("coerces non-object array entries to empty todos", () => {
|
|
202
|
+
const m = emptyMap();
|
|
203
|
+
const changed = applyTodoUpdate(m, ["oops", null, 42], { merge: false });
|
|
204
|
+
expect(changed).toBe(true);
|
|
205
|
+
expect(Object.keys(m)).toEqual(["todo-0", "todo-1", "todo-2"]);
|
|
206
|
+
expect(m["todo-0"].content).toBe("");
|
|
207
|
+
expect(m["todo-0"].status).toBe(TodoStatus.TODO_PENDING);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe("dirty return value", () => {
|
|
212
|
+
it("returns true when items are written", () => {
|
|
213
|
+
expect(applyTodoUpdate(emptyMap(), [{ content: "A" }], { merge: false })).toBe(true);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
});
|