@stigmer/runner 3.1.2 → 3.1.4
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.d.ts +1 -1
- package/dist/activities/execute-cursor/attachment-resolver.js +1 -1
- package/dist/activities/execute-cursor/capture-flow.d.ts +29 -0
- package/dist/activities/execute-cursor/capture-flow.js +59 -0
- 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 +358 -557
- package/dist/activities/execute-cursor/index.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-boundary.d.ts +108 -0
- package/dist/activities/execute-cursor/turn-boundary.js +163 -0
- package/dist/activities/execute-cursor/turn-boundary.js.map +1 -0
- 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 +73 -4
- 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/streaming-v3.js +6 -1
- package/dist/activities/execute-deep-agent/streaming-v3.js.map +1 -1
- package/dist/activities/execute-deep-agent/streaming.d.ts +7 -0
- package/dist/activities/execute-deep-agent/streaming.js +5 -1
- package/dist/activities/execute-deep-agent/streaming.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/otel.js +10 -0
- package/dist/otel.js.map +1 -1
- package/dist/shared/filereview/capture.d.ts +18 -1
- package/dist/shared/filereview/capture.js +105 -28
- package/dist/shared/filereview/capture.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 +38 -0
- package/dist/shared/filereview/cas-substrate.js +48 -15
- 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/events.d.ts +16 -0
- package/dist/shared/filereview/events.js +37 -0
- package/dist/shared/filereview/events.js.map +1 -1
- package/dist/shared/filereview/git-substrate.d.ts +48 -0
- package/dist/shared/filereview/git-substrate.js +78 -0
- package/dist/shared/filereview/git-substrate.js.map +1 -1
- package/dist/shared/filereview/index.d.ts +5 -2
- package/dist/shared/filereview/index.js +5 -2
- package/dist/shared/filereview/index.js.map +1 -1
- package/dist/shared/filereview/line-counts.d.ts +48 -0
- package/dist/shared/filereview/line-counts.js +72 -0
- package/dist/shared/filereview/line-counts.js.map +1 -0
- package/dist/shared/filereview/progress.d.ts +167 -0
- package/dist/shared/filereview/progress.js +196 -0
- package/dist/shared/filereview/progress.js.map +1 -0
- package/dist/shared/implement-plan-prompt.d.ts +6 -4
- package/dist/shared/implement-plan-prompt.js +38 -6
- package/dist/shared/implement-plan-prompt.js.map +1 -1
- package/dist/shared/plan-artifact.d.ts +59 -11
- package/dist/shared/plan-artifact.js +144 -16
- package/dist/shared/plan-artifact.js.map +1 -1
- package/dist/shared/plan-mode-prompt.d.ts +15 -4
- package/dist/shared/plan-mode-prompt.js +28 -5
- 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/package.json +3 -2
- package/src/__tests__/otel-turn-span.test.ts +61 -0
- package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +7 -0
- package/src/activities/execute-cursor/__tests__/delta-enricher.test.ts +68 -0
- package/src/activities/execute-cursor/__tests__/progress-substrate.test.ts +169 -0
- package/src/activities/execute-cursor/__tests__/turn-boundary.test.ts +280 -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 +78 -0
- package/src/activities/execute-cursor/command-provenance.ts +25 -120
- package/src/activities/execute-cursor/index.ts +419 -602
- package/src/activities/execute-cursor/todo-tracker.ts +17 -59
- package/src/activities/execute-cursor/turn-boundary.ts +274 -0
- 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__/prompt-builder.test.ts +11 -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/command-provenance.ts +102 -0
- package/src/activities/execute-deep-agent/index.ts +91 -4
- package/src/activities/execute-deep-agent/status-builder.ts +9 -0
- package/src/activities/execute-deep-agent/streaming-v3.ts +6 -1
- package/src/activities/execute-deep-agent/streaming.ts +12 -1
- package/src/activities/execute-deep-agent/v3-status-builder.ts +13 -0
- package/src/otel.ts +8 -0
- package/src/shared/__tests__/implement-plan-prompt.test.ts +40 -2
- package/src/shared/__tests__/plan-artifact.test.ts +142 -9
- package/src/shared/__tests__/todos.test.ts +216 -0
- package/src/shared/filereview/__tests__/capture.test.ts +182 -1
- package/src/shared/filereview/__tests__/cas-progress.test.ts +228 -0
- package/src/shared/filereview/__tests__/cas-substrate.test.ts +100 -0
- package/src/shared/filereview/__tests__/command-provenance.test.ts +252 -0
- package/src/shared/filereview/__tests__/events.test.ts +105 -0
- package/src/shared/filereview/__tests__/line-counts.test.ts +73 -0
- package/src/shared/filereview/__tests__/progress.test.ts +338 -0
- package/src/shared/filereview/capture.ts +134 -30
- package/src/shared/filereview/cas-progress.ts +170 -0
- package/src/shared/filereview/cas-substrate.ts +79 -18
- package/src/shared/filereview/command-provenance.ts +180 -0
- package/src/shared/filereview/events.ts +50 -0
- package/src/shared/filereview/git-substrate.ts +114 -0
- package/src/shared/filereview/index.ts +36 -1
- package/src/shared/filereview/line-counts.ts +83 -0
- package/src/shared/filereview/progress.ts +310 -0
- package/src/shared/implement-plan-prompt.ts +39 -8
- package/src/shared/plan-artifact.ts +152 -16
- package/src/shared/plan-mode-prompt.ts +28 -5
- package/src/shared/todos.ts +126 -0
- package/src/shared/tool-row.ts +34 -0
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
ExecutionPhase,
|
|
11
11
|
MessageType,
|
|
12
12
|
ToolCallStatus,
|
|
13
|
+
ToolKind,
|
|
14
|
+
TodoStatus,
|
|
13
15
|
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
14
16
|
import { StatusBuilder, type StreamEvent, type ApprovalPolicyProvider } from "../status-builder.js";
|
|
15
17
|
import type { MergedToolPolicy } from "../../../shared/approval-policy.js";
|
|
@@ -1852,4 +1854,80 @@ describe("StatusBuilder", () => {
|
|
|
1852
1854
|
expect(status.phase).not.toBe(ExecutionPhase.EXECUTION_WAITING_FOR_APPROVAL);
|
|
1853
1855
|
});
|
|
1854
1856
|
});
|
|
1857
|
+
|
|
1858
|
+
// ── Todo extraction (write_todos → status.todos) ─────────────────
|
|
1859
|
+
//
|
|
1860
|
+
// v2 parity with V3StatusBuilder: a completed write_todos projects into
|
|
1861
|
+
// status.todos through the same shared mapper; a failed one does not.
|
|
1862
|
+
|
|
1863
|
+
describe("todo extraction", () => {
|
|
1864
|
+
it("projects a completed write_todos into status.todos", () => {
|
|
1865
|
+
const sb = makeBuilder();
|
|
1866
|
+
sb.processEvent(
|
|
1867
|
+
toolStartEvent("run-1", "write_todos", {
|
|
1868
|
+
todos: [
|
|
1869
|
+
{ content: "Step one", status: "in_progress" },
|
|
1870
|
+
{ content: "Step two", status: "pending" },
|
|
1871
|
+
],
|
|
1872
|
+
}),
|
|
1873
|
+
);
|
|
1874
|
+
sb.processEvent(toolEndEvent("run-1", "Todos updated"));
|
|
1875
|
+
|
|
1876
|
+
const todos = sb.currentStatus.todos;
|
|
1877
|
+
expect(Object.keys(todos)).toEqual(["todo-0", "todo-1"]);
|
|
1878
|
+
expect(todos["todo-0"].content).toBe("Step one");
|
|
1879
|
+
expect(todos["todo-0"].status).toBe(TodoStatus.TODO_IN_PROGRESS);
|
|
1880
|
+
expect(todos["todo-1"].status).toBe(TodoStatus.TODO_PENDING);
|
|
1881
|
+
});
|
|
1882
|
+
|
|
1883
|
+
it("keeps the write_todos ToolCall in messages stamped ToolKind.TODO", () => {
|
|
1884
|
+
const sb = makeBuilder();
|
|
1885
|
+
sb.processEvent(
|
|
1886
|
+
toolStartEvent("run-1", "write_todos", {
|
|
1887
|
+
todos: [{ content: "A", status: "pending" }],
|
|
1888
|
+
}),
|
|
1889
|
+
);
|
|
1890
|
+
sb.processEvent(toolEndEvent("run-1", "ok"));
|
|
1891
|
+
|
|
1892
|
+
const tc = sb.currentStatus.messages
|
|
1893
|
+
.flatMap((m) => m.toolCalls)
|
|
1894
|
+
.find((t) => t.name === "write_todos");
|
|
1895
|
+
expect(tc).toBeDefined();
|
|
1896
|
+
expect(tc!.toolKind).toBe(ToolKind.TODO);
|
|
1897
|
+
});
|
|
1898
|
+
|
|
1899
|
+
it("does not project on a failed write_todos (Command never ran)", () => {
|
|
1900
|
+
const sb = makeBuilder();
|
|
1901
|
+
sb.processEvent(
|
|
1902
|
+
toolStartEvent("run-1", "write_todos", {
|
|
1903
|
+
todos: [{ content: "A", status: "pending" }],
|
|
1904
|
+
}),
|
|
1905
|
+
);
|
|
1906
|
+
sb.processEvent(toolEndEvent("run-1", { error: "boom" }));
|
|
1907
|
+
expect(Object.keys(sb.currentStatus.todos)).toHaveLength(0);
|
|
1908
|
+
});
|
|
1909
|
+
|
|
1910
|
+
it("full-replaces the map on a subsequent write_todos", () => {
|
|
1911
|
+
const sb = makeBuilder();
|
|
1912
|
+
sb.processEvent(
|
|
1913
|
+
toolStartEvent("run-1", "write_todos", {
|
|
1914
|
+
todos: [
|
|
1915
|
+
{ content: "one", status: "completed" },
|
|
1916
|
+
{ content: "two", status: "in_progress" },
|
|
1917
|
+
],
|
|
1918
|
+
}),
|
|
1919
|
+
);
|
|
1920
|
+
sb.processEvent(toolEndEvent("run-1", "ok"));
|
|
1921
|
+
sb.processEvent(
|
|
1922
|
+
toolStartEvent("run-2", "write_todos", {
|
|
1923
|
+
todos: [{ content: "two", status: "completed" }],
|
|
1924
|
+
}),
|
|
1925
|
+
);
|
|
1926
|
+
sb.processEvent(toolEndEvent("run-2", "ok"));
|
|
1927
|
+
|
|
1928
|
+
const todos = sb.currentStatus.todos;
|
|
1929
|
+
expect(Object.keys(todos)).toEqual(["todo-0"]);
|
|
1930
|
+
expect(todos["todo-0"].status).toBe(TodoStatus.TODO_COMPLETED);
|
|
1931
|
+
});
|
|
1932
|
+
});
|
|
1855
1933
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
2
|
import { create } from "@bufbuild/protobuf";
|
|
3
3
|
import { AgentExecutionStatusSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
4
|
-
import { ExecutionPhase, MessageType, ToolCallStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
4
|
+
import { ExecutionPhase, MessageType, ToolCallStatus, ToolKind, TodoStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
5
5
|
import { V3StatusBuilder } from "../v3-status-builder.js";
|
|
6
6
|
import { normalize } from "../v3-protocol-normalizer.js";
|
|
7
7
|
import type { ApprovalPolicyProvider } from "../status-builder.js";
|
|
@@ -504,4 +504,108 @@ describe("V3StatusBuilder", () => {
|
|
|
504
504
|
expect(sb.currentStatus.messages).toHaveLength(0);
|
|
505
505
|
});
|
|
506
506
|
});
|
|
507
|
+
|
|
508
|
+
// ── Todo extraction (write_todos → status.todos) ─────────────────
|
|
509
|
+
//
|
|
510
|
+
// The native harness emits write_todos; projecting it into status.todos is
|
|
511
|
+
// what lights up the client TodoCard at parity with the Cursor harness.
|
|
512
|
+
|
|
513
|
+
describe("todo extraction", () => {
|
|
514
|
+
it("projects a completed write_todos into status.todos", () => {
|
|
515
|
+
const sb = makeBuilder();
|
|
516
|
+
feedAll(sb, [
|
|
517
|
+
makeMessageStart("run-1"),
|
|
518
|
+
makeTextDelta("run-1", "Planning the build."),
|
|
519
|
+
makeMessageFinish("run-1", { usage: { input_tokens: 10, output_tokens: 5 } }),
|
|
520
|
+
makeToolStarted("todo-1", "write_todos", {
|
|
521
|
+
todos: [
|
|
522
|
+
{ content: "Step one", status: "in_progress" },
|
|
523
|
+
{ content: "Step two", status: "pending" },
|
|
524
|
+
],
|
|
525
|
+
}),
|
|
526
|
+
makeToolFinished("todo-1", "Todos updated"),
|
|
527
|
+
]);
|
|
528
|
+
|
|
529
|
+
const todos = sb.currentStatus.todos;
|
|
530
|
+
expect(Object.keys(todos)).toEqual(["todo-0", "todo-1"]);
|
|
531
|
+
expect(todos["todo-0"].content).toBe("Step one");
|
|
532
|
+
expect(todos["todo-0"].status).toBe(TodoStatus.TODO_IN_PROGRESS);
|
|
533
|
+
expect(todos["todo-1"].content).toBe("Step two");
|
|
534
|
+
expect(todos["todo-1"].status).toBe(TodoStatus.TODO_PENDING);
|
|
535
|
+
expect(sb.forceNextUpdate).toBe(true);
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
it("keeps the write_todos ToolCall in messages stamped ToolKind.TODO", () => {
|
|
539
|
+
const sb = makeBuilder();
|
|
540
|
+
feedAll(sb, [
|
|
541
|
+
makeToolStarted("todo-1", "write_todos", {
|
|
542
|
+
todos: [{ content: "Step one", status: "pending" }],
|
|
543
|
+
}),
|
|
544
|
+
makeToolFinished("todo-1", "Todos updated"),
|
|
545
|
+
]);
|
|
546
|
+
|
|
547
|
+
const toolCalls = sb.currentStatus.messages.flatMap((m) => m.toolCalls);
|
|
548
|
+
const tc = toolCalls.find((t) => t.name === "write_todos");
|
|
549
|
+
expect(tc).toBeDefined();
|
|
550
|
+
expect(tc!.toolKind).toBe(ToolKind.TODO);
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
it("does not project until the call completes", () => {
|
|
554
|
+
const sb = makeBuilder();
|
|
555
|
+
feedAll(sb, [
|
|
556
|
+
makeToolStarted("todo-1", "write_todos", {
|
|
557
|
+
todos: [{ content: "Step one", status: "pending" }],
|
|
558
|
+
}),
|
|
559
|
+
]);
|
|
560
|
+
// Tool started but not finished — the state Command has not run yet.
|
|
561
|
+
expect(Object.keys(sb.currentStatus.todos)).toHaveLength(0);
|
|
562
|
+
|
|
563
|
+
feedAll(sb, [makeToolFinished("todo-1", "Todos updated")]);
|
|
564
|
+
expect(Object.keys(sb.currentStatus.todos)).toHaveLength(1);
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
it("full-replaces the map on a subsequent write_todos", () => {
|
|
568
|
+
const sb = makeBuilder();
|
|
569
|
+
feedAll(sb, [
|
|
570
|
+
makeToolStarted("todo-1", "write_todos", {
|
|
571
|
+
todos: [
|
|
572
|
+
{ content: "Step one", status: "completed" },
|
|
573
|
+
{ content: "Step two", status: "in_progress" },
|
|
574
|
+
],
|
|
575
|
+
}),
|
|
576
|
+
makeToolFinished("todo-1", "ok"),
|
|
577
|
+
makeToolStarted("todo-2", "write_todos", {
|
|
578
|
+
todos: [{ content: "Step two", status: "completed" }],
|
|
579
|
+
}),
|
|
580
|
+
makeToolFinished("todo-2", "ok"),
|
|
581
|
+
]);
|
|
582
|
+
|
|
583
|
+
const todos = sb.currentStatus.todos;
|
|
584
|
+
expect(Object.keys(todos)).toEqual(["todo-0"]);
|
|
585
|
+
expect(todos["todo-0"].content).toBe("Step two");
|
|
586
|
+
expect(todos["todo-0"].status).toBe(TodoStatus.TODO_COMPLETED);
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
it("does not project a sub-agent's write_todos into parent status.todos", () => {
|
|
590
|
+
const sb = makeBuilder();
|
|
591
|
+
const subNs = ["tools:task-1", "tools:sub-todo"];
|
|
592
|
+
feedAll(sb, [
|
|
593
|
+
// Register a sub-agent so its namespace routes to the SubAgentTracker.
|
|
594
|
+
makeToolStarted("task-1", "task", {
|
|
595
|
+
subagent_type: "worker",
|
|
596
|
+
description: "delegate",
|
|
597
|
+
}),
|
|
598
|
+
// The sub-agent writes todos — must not leak into the parent map.
|
|
599
|
+
makeToolStarted(
|
|
600
|
+
"sub-todo",
|
|
601
|
+
"write_todos",
|
|
602
|
+
{ todos: [{ content: "sub step", status: "pending" }] },
|
|
603
|
+
{ namespace: subNs },
|
|
604
|
+
),
|
|
605
|
+
makeToolFinished("sub-todo", "ok", { namespace: subNs }),
|
|
606
|
+
]);
|
|
607
|
+
|
|
608
|
+
expect(Object.keys(sb.currentStatus.todos)).toHaveLength(0);
|
|
609
|
+
});
|
|
610
|
+
});
|
|
507
611
|
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Approved-command turn provenance (DD-28) — the DEEP-AGENT (native) harness
|
|
3
|
+
* adapter over the shared qualification rule ({@link qualifyTurnCommandProvenance}).
|
|
4
|
+
*
|
|
5
|
+
* The deep-agent's consent model differs from Cursor's in exactly two ways, and
|
|
6
|
+
* this adapter is precisely those two differences:
|
|
7
|
+
*
|
|
8
|
+
* 1. IDENTITY turn scoping (not positional). Cursor scopes a turn by the message
|
|
9
|
+
* index its stream started at. The deep-agent cannot: an approved command
|
|
10
|
+
* executes IN PLACE at its seeded transcript position (it was proposed in a
|
|
11
|
+
* prior segment, seeded into this segment, and updated to COMPLETED by
|
|
12
|
+
* `StatusBuilder.findResumableSeededToolCall`). So "this turn's calls" are
|
|
13
|
+
* those whose id is absent from a pre-stream {@link collectSettledToolCallIds}
|
|
14
|
+
* snapshot, and "executed" is a COMPLETED status.
|
|
15
|
+
*
|
|
16
|
+
* 2. SAME-ROW direct consent (not a grant token). A gated deep-agent tool pauses
|
|
17
|
+
* on a LangGraph interrupt; SubmitApproval writes `approval_action` onto that
|
|
18
|
+
* same row, and the row keeps its id + `approval_action` when it executes on
|
|
19
|
+
* resume. So the executed command IS its own consent row — cite its own id.
|
|
20
|
+
* (The server's PreserveApprovalFields keeps that `approval_action` on the row
|
|
21
|
+
* across the status write regardless of its new status, so the backend's
|
|
22
|
+
* verification finds it.)
|
|
23
|
+
*
|
|
24
|
+
* A turn that delegated ANY sub-agent fails closed (DD-28 D1: "delegated zero
|
|
25
|
+
* sub-agents"). A sub-agent's writes fold into the parent turn's change set
|
|
26
|
+
* (DD-19) but are not attributable to a consented top-level command. The
|
|
27
|
+
* top-level `task` call (a SUBAGENT kind) already trips the shared rule's
|
|
28
|
+
* `!== SHELL` fail-closed; the explicit sub-agent-novelty guard below also covers
|
|
29
|
+
* the (currently-impossible under the replace-per-turn model) case of a sub-agent
|
|
30
|
+
* that mutates across a turn boundary without a fresh top-level `task` row.
|
|
31
|
+
*
|
|
32
|
+
* (Trust boundary + fail-closed contract: see the shared module.)
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import {
|
|
36
|
+
ApprovalAction,
|
|
37
|
+
ToolCallStatus,
|
|
38
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
39
|
+
import type { AgentExecutionStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
40
|
+
import type { TurnCommandProvenance } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/filereview_pb";
|
|
41
|
+
import { qualifyTurnCommandProvenance } from "../../shared/filereview/command-provenance.js";
|
|
42
|
+
import { collectSubAgentToolCallIds } from "../../shared/tool-row.js";
|
|
43
|
+
|
|
44
|
+
export interface DeepAgentCommandProvenanceInputs {
|
|
45
|
+
/** The status at capture time — top-level messages + sub-agent executions. */
|
|
46
|
+
readonly status: AgentExecutionStatus;
|
|
47
|
+
/**
|
|
48
|
+
* The top-level tool-call ids that had already SETTLED before this turn's
|
|
49
|
+
* stream (from {@link collectSettledToolCallIds}). A top-level call whose id is
|
|
50
|
+
* absent is this-turn's; the shared rule then keeps only the COMPLETED ones.
|
|
51
|
+
*/
|
|
52
|
+
readonly priorSettledToolCallIds: ReadonlySet<string>;
|
|
53
|
+
/**
|
|
54
|
+
* The sub-agent tool-call ids that existed before this turn's stream (from
|
|
55
|
+
* {@link collectSubAgentToolCallIds}). Any growth means a sub-agent acted this
|
|
56
|
+
* turn → fail closed.
|
|
57
|
+
*/
|
|
58
|
+
readonly priorSubAgentToolCallIds: ReadonlySet<string>;
|
|
59
|
+
/** True when the pre-armed spec.auto_approve_all bypassed the gate. */
|
|
60
|
+
readonly globalBypass: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Derive the {@link TurnCommandProvenance} for a deep-agent turn, or undefined
|
|
65
|
+
* when it does not qualify. Scopes this turn's top-level calls by id-novelty,
|
|
66
|
+
* resolves consent from the row's own server-authored `approval_action`, fails
|
|
67
|
+
* closed on any sub-agent activity, and delegates the DD-28 rule to
|
|
68
|
+
* {@link qualifyTurnCommandProvenance}.
|
|
69
|
+
*/
|
|
70
|
+
export function deriveTurnCommandProvenance(
|
|
71
|
+
inputs: DeepAgentCommandProvenanceInputs,
|
|
72
|
+
): TurnCommandProvenance | undefined {
|
|
73
|
+
const { status, priorSettledToolCallIds, priorSubAgentToolCallIds, globalBypass } = inputs;
|
|
74
|
+
|
|
75
|
+
// DD-28 D1 cond 2: any sub-agent activity this turn disqualifies. A sub-agent
|
|
76
|
+
// that ran this turn contributes at least one tool-call id absent from the
|
|
77
|
+
// pre-stream snapshot.
|
|
78
|
+
for (const id of collectSubAgentToolCallIds(status.subAgentExecutions)) {
|
|
79
|
+
if (!priorSubAgentToolCallIds.has(id)) return undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const messages = status.messages;
|
|
83
|
+
// This turn's top-level calls: those not already settled before the stream.
|
|
84
|
+
const turnToolCalls = messages
|
|
85
|
+
.flatMap((m) => m.toolCalls)
|
|
86
|
+
.filter((tc) => !priorSettledToolCallIds.has(tc.id));
|
|
87
|
+
|
|
88
|
+
return qualifyTurnCommandProvenance({
|
|
89
|
+
turnToolCalls,
|
|
90
|
+
messages,
|
|
91
|
+
// A deep-agent command executed iff its row reached COMPLETED this turn.
|
|
92
|
+
isExecutedCommand: (tc) => tc.status === ToolCallStatus.TOOL_CALL_COMPLETED,
|
|
93
|
+
// The gated command carries its own server-authored approval_action, written
|
|
94
|
+
// in place by SubmitApproval and preserved when the row executes on resume.
|
|
95
|
+
resolveDirectConsent: (tc) =>
|
|
96
|
+
tc.approvalAction === ApprovalAction.APPROVE ||
|
|
97
|
+
tc.approvalAction === ApprovalAction.APPROVE_ALL
|
|
98
|
+
? tc.id
|
|
99
|
+
: undefined,
|
|
100
|
+
globalBypass,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
@@ -50,12 +50,28 @@ import {
|
|
|
50
50
|
captureBaselineToLedger,
|
|
51
51
|
captureCandidateToLedger,
|
|
52
52
|
} from "../../shared/filereview/capture.js";
|
|
53
|
+
import {
|
|
54
|
+
captureFileChangeProgress,
|
|
55
|
+
createGitProgressSubstrate,
|
|
56
|
+
createHybridProgressSubstrate,
|
|
57
|
+
newProgressCaptureState,
|
|
58
|
+
type ProgressSubstrate,
|
|
59
|
+
} from "../../shared/filereview/progress.js";
|
|
60
|
+
import {
|
|
61
|
+
createCasProgressSubstrate,
|
|
62
|
+
type CasTouchedSnapshot,
|
|
63
|
+
} from "../../shared/filereview/cas-progress.js";
|
|
53
64
|
import { hasCandidateCaptured } from "../../shared/filereview/events.js";
|
|
54
65
|
import { casBlobReader, type CasPathCapture } from "../../shared/filereview/cas-substrate.js";
|
|
55
66
|
import { partitionIgnoredPathsBySecret } from "../../shared/filereview/secret-paths.js";
|
|
56
67
|
import type { CasCaptureObserver } from "./cas-capture-observer.js";
|
|
57
|
-
import {
|
|
68
|
+
import {
|
|
69
|
+
collectSettledToolCallIds,
|
|
70
|
+
collectSubAgentToolCallIds,
|
|
71
|
+
withholdSecretContentFromMessages,
|
|
72
|
+
} from "../../shared/tool-row.js";
|
|
58
73
|
import { stampFlowedFileEditRows, stampFlowedSubAgentFileEditRows } from "./stamp-flowed-rows.js";
|
|
74
|
+
import { deriveTurnCommandProvenance } from "./command-provenance.js";
|
|
59
75
|
|
|
60
76
|
/** The harness id stamped on the deep-agent's file-review ledger events. */
|
|
61
77
|
const DEEP_AGENT_HARNESS_ID = "deep-agent";
|
|
@@ -308,6 +324,12 @@ export function createDeepAgentActivities(config: Config) {
|
|
|
308
324
|
// seeds prior sub-agents — but computed, not assumed, so it stays correct
|
|
309
325
|
// if that ever changes; see collectSubAgentToolCallIds).
|
|
310
326
|
const priorSubAgentToolCallIds = collectSubAgentToolCallIds(initialStatus.subAgentExecutions);
|
|
327
|
+
// Snapshot the top-level tool-call ids already SETTLED before this turn's
|
|
328
|
+
// stream, so the approved-command provenance (DD-28) scopes itself to THIS
|
|
329
|
+
// turn's executed commands by identity. The deep-agent's approved shell
|
|
330
|
+
// executes in place at its seeded position, so the Cursor positional scope
|
|
331
|
+
// would miss it — see execute-deep-agent/command-provenance.ts.
|
|
332
|
+
const priorSettledToolCallIds = collectSettledToolCallIds(initialStatus.messages);
|
|
311
333
|
if (setup.captureMode) {
|
|
312
334
|
captureBaselineTree = await captureBaselineToLedger({
|
|
313
335
|
status: initialStatus,
|
|
@@ -319,6 +341,35 @@ export function createDeepAgentActivities(config: Config) {
|
|
|
319
341
|
});
|
|
320
342
|
}
|
|
321
343
|
|
|
344
|
+
// Per-turn state + substrate for mid-run live capture (DD-32 / DD-33). The
|
|
345
|
+
// floor lives in progressState; the substrate is chosen for this turn's
|
|
346
|
+
// workspace shape and owns its own short-circuit cache. An atomic snapshot
|
|
347
|
+
// of the shared observer feeds the CAS/HYBRID substrates — copied
|
|
348
|
+
// synchronously so a concurrent sub-agent write cannot mutate it mid-read.
|
|
349
|
+
const progressState = newProgressCaptureState();
|
|
350
|
+
const casObserver = setup.casObserver;
|
|
351
|
+
const readObserverTouched = (): CasTouchedSnapshot => ({
|
|
352
|
+
before: new Map(casObserver.before),
|
|
353
|
+
blockedSecretPaths: new Set(casObserver.blockedSecretPaths),
|
|
354
|
+
});
|
|
355
|
+
// Git tree -> hybrid (numstat for tracked + observer for gitignored);
|
|
356
|
+
// non-git -> cas over the observer (no baseline tree needed). Undefined
|
|
357
|
+
// outside capture mode (writes are deny-gated, nothing is captured).
|
|
358
|
+
const progressSubstrate: ProgressSubstrate | undefined = !setup.captureMode
|
|
359
|
+
? undefined
|
|
360
|
+
: setup.gitWorkspace
|
|
361
|
+
? captureBaselineTree
|
|
362
|
+
? createHybridProgressSubstrate(
|
|
363
|
+
createGitProgressSubstrate({
|
|
364
|
+
workspaceRoot: gitRoot,
|
|
365
|
+
executionId,
|
|
366
|
+
baselineTree: captureBaselineTree,
|
|
367
|
+
}),
|
|
368
|
+
createCasProgressSubstrate({ workspaceRoot: gitRoot, read: readObserverTouched }),
|
|
369
|
+
)
|
|
370
|
+
: undefined
|
|
371
|
+
: createCasProgressSubstrate({ workspaceRoot: gitRoot, read: readObserverTouched });
|
|
372
|
+
|
|
322
373
|
const cancellationSignal = Context.current().cancellationSignal;
|
|
323
374
|
|
|
324
375
|
const result: StreamResult = await streamExecution({
|
|
@@ -344,6 +395,21 @@ export function createDeepAgentActivities(config: Config) {
|
|
|
344
395
|
globalBypass: setup.globalBypass,
|
|
345
396
|
},
|
|
346
397
|
streamVersion: setup.streamVersion,
|
|
398
|
+
// Mid-run live capture (DD-32 / DD-33): attach file_change_progress
|
|
399
|
+
// before each scheduled persist, throttled by the floor inside
|
|
400
|
+
// captureFileChangeProgress. The substrate (git / non-git CAS / hybrid)
|
|
401
|
+
// was chosen for this turn above; deep-agent writes no runner-owned gate
|
|
402
|
+
// files into the tree, so the git slice needs no excludePaths — matching
|
|
403
|
+
// its turn-boundary candidate capture.
|
|
404
|
+
beforePersist: async (status) => {
|
|
405
|
+
if (!progressSubstrate) return;
|
|
406
|
+
await captureFileChangeProgress({
|
|
407
|
+
status,
|
|
408
|
+
changeSetId,
|
|
409
|
+
substrate: progressSubstrate,
|
|
410
|
+
state: progressState,
|
|
411
|
+
});
|
|
412
|
+
},
|
|
347
413
|
});
|
|
348
414
|
|
|
349
415
|
await processPostStream({
|
|
@@ -399,6 +465,25 @@ export function createDeepAgentActivities(config: Config) {
|
|
|
399
465
|
gitRoot,
|
|
400
466
|
casCaptureClass,
|
|
401
467
|
);
|
|
468
|
+
// Approved-command turn facts (DD-28): when every mutation-capable call
|
|
469
|
+
// this turn was a consented shell command, attach the provenance so the
|
|
470
|
+
// backend can verify the cited consent rows and auto-keep the set
|
|
471
|
+
// instead of arming a second review gate. Fail-closed: any non-qualifying
|
|
472
|
+
// turn attaches nothing and reviews manually exactly as before. Attached
|
|
473
|
+
// only when captureCandidateToLedger actually authors a CANDIDATE.
|
|
474
|
+
const commandProvenance = deriveTurnCommandProvenance({
|
|
475
|
+
status: initialStatus,
|
|
476
|
+
priorSettledToolCallIds,
|
|
477
|
+
priorSubAgentToolCallIds,
|
|
478
|
+
globalBypass: setup.globalBypass,
|
|
479
|
+
});
|
|
480
|
+
if (commandProvenance) {
|
|
481
|
+
console.log(
|
|
482
|
+
`[ExecuteDeepAgent] capture: turn qualifies for approved-command auto-keep ` +
|
|
483
|
+
`(consent rows: ${commandProvenance.consentToolCallIds.join(",") || "(auto_approve_all)"}); ` +
|
|
484
|
+
`attaching provenance to candidate (execution=${executionId})`,
|
|
485
|
+
);
|
|
486
|
+
}
|
|
402
487
|
await captureCandidateToLedger({
|
|
403
488
|
status: initialStatus,
|
|
404
489
|
gitRoot,
|
|
@@ -411,6 +496,7 @@ export function createDeepAgentActivities(config: Config) {
|
|
|
411
496
|
unreviewablePaths,
|
|
412
497
|
unreviewableCaptureClass: casCaptureClass,
|
|
413
498
|
gitWorkspace: setup.gitWorkspace,
|
|
499
|
+
commandProvenance,
|
|
414
500
|
});
|
|
415
501
|
// Review is pending iff a CANDIDATE was actually authored (the seam
|
|
416
502
|
// drops no-op captures), so a turn that only touched-then-unchanged an
|
|
@@ -556,9 +642,10 @@ export function createDeepAgentActivities(config: Config) {
|
|
|
556
642
|
}
|
|
557
643
|
|
|
558
644
|
// Plan mode: the agent's final message is the plan. Publish it as a
|
|
559
|
-
// first-class plan
|
|
560
|
-
// card and a follow-up Implement run can
|
|
561
|
-
// produces no file to auto-publish, so
|
|
645
|
+
// first-class plan artifact (named from the plan's title) so the UI can
|
|
646
|
+
// render a reviewable Plan card and a follow-up Implement run can
|
|
647
|
+
// reference it. Read-only mode produces no file to auto-publish, so
|
|
648
|
+
// this is the only artifact path.
|
|
562
649
|
// Requires artifact storage; with none (proxy misconfig) the plan text
|
|
563
650
|
// still lives in the final message — only the reviewable artifact is
|
|
564
651
|
// skipped (mirrors Cursor's storage-guarded plan publish).
|
|
@@ -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();
|
|
@@ -118,10 +118,15 @@ export async function streamExecutionV3(
|
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
statusBuilder.syncSubAgentExecutions();
|
|
121
|
+
// Mid-run live capture (DD-32): attach file_change_progress to the live
|
|
122
|
+
// status before it is persisted. Injected so this loop stays ignorant of
|
|
123
|
+
// file-review specifics; a no-op when the hook is absent or nothing changed.
|
|
124
|
+
const statusToPersist = statusBuilder.currentStatus;
|
|
125
|
+
await deps.beforePersist?.(statusToPersist);
|
|
121
126
|
const signal = await persistStatus(
|
|
122
127
|
client,
|
|
123
128
|
executionId,
|
|
124
|
-
|
|
129
|
+
statusToPersist,
|
|
125
130
|
{ offload, retry: retryOptions },
|
|
126
131
|
);
|
|
127
132
|
scheduler.markUpdateSent(eventsProcessed);
|
|
@@ -83,6 +83,13 @@ export interface StreamDependencies {
|
|
|
83
83
|
readonly approvalProvider?: ApprovalPolicyProvider;
|
|
84
84
|
/** Streaming protocol version. Defaults to "v2" if unset. */
|
|
85
85
|
readonly streamVersion?: "v2" | "v3";
|
|
86
|
+
/**
|
|
87
|
+
* Optional hook invoked with the live status just before each scheduled
|
|
88
|
+
* persist. Used by mid-run live capture (DD-32) to attach file_change_progress.
|
|
89
|
+
* Kept as an injected callback (closing over the turn's baseline tree / change
|
|
90
|
+
* set id in index.ts) so this loop stays ignorant of file-review specifics.
|
|
91
|
+
*/
|
|
92
|
+
readonly beforePersist?: (status: AgentExecutionStatus) => Promise<void>;
|
|
86
93
|
}
|
|
87
94
|
|
|
88
95
|
export interface StreamResult {
|
|
@@ -189,10 +196,14 @@ async function streamExecutionV2(
|
|
|
189
196
|
statusBuilder.clearForceFlag();
|
|
190
197
|
}
|
|
191
198
|
|
|
199
|
+
// Mid-run live capture (DD-32): attach file_change_progress to the live
|
|
200
|
+
// status before persisting (see streaming-v3.ts for the rationale).
|
|
201
|
+
const statusToPersist = statusBuilder.currentStatus;
|
|
202
|
+
await deps.beforePersist?.(statusToPersist);
|
|
192
203
|
const signal = await persistStatus(
|
|
193
204
|
client,
|
|
194
205
|
executionId,
|
|
195
|
-
|
|
206
|
+
statusToPersist,
|
|
196
207
|
{ offload, retry: retryOptions },
|
|
197
208
|
);
|
|
198
209
|
scheduler.markUpdateSent(eventsProcessed);
|
|
@@ -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
|
|
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
|
};
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
} from "../implement-plan-prompt.js";
|
|
6
6
|
|
|
7
7
|
describe("findApprovedPlanPath", () => {
|
|
8
|
-
it("finds the plan by
|
|
8
|
+
it("finds the plan by the legacy filename among attachment paths", () => {
|
|
9
9
|
expect(
|
|
10
10
|
findApprovedPlanPath([
|
|
11
11
|
".stigmer/inputs/data.csv",
|
|
@@ -14,15 +14,28 @@ describe("findApprovedPlanPath", () => {
|
|
|
14
14
|
).toBe(".stigmer/inputs/plan.md");
|
|
15
15
|
});
|
|
16
16
|
|
|
17
|
+
it("finds a title-named <slug>_<id>.plan.md plan", () => {
|
|
18
|
+
expect(
|
|
19
|
+
findApprovedPlanPath([
|
|
20
|
+
".stigmer/inputs/data.csv",
|
|
21
|
+
".stigmer/inputs/plan-card-ux-cleanup_a1b2c3d4.plan.md",
|
|
22
|
+
]),
|
|
23
|
+
).toBe(".stigmer/inputs/plan-card-ux-cleanup_a1b2c3d4.plan.md");
|
|
24
|
+
});
|
|
25
|
+
|
|
17
26
|
it("returns undefined when no plan attachment resolved", () => {
|
|
18
27
|
expect(findApprovedPlanPath([".stigmer/inputs/data.csv"])).toBeUndefined();
|
|
19
28
|
expect(findApprovedPlanPath([])).toBeUndefined();
|
|
20
29
|
});
|
|
21
30
|
|
|
22
|
-
it("matches the
|
|
31
|
+
it("matches the plan convention only — a plan-adjacent filename is not the plan", () => {
|
|
23
32
|
expect(
|
|
24
33
|
findApprovedPlanPath([".stigmer/inputs/my-plan.md.bak"]),
|
|
25
34
|
).toBeUndefined();
|
|
35
|
+
// A file merely containing "plan" is not a plan artifact.
|
|
36
|
+
expect(
|
|
37
|
+
findApprovedPlanPath([".stigmer/inputs/myplan.md"]),
|
|
38
|
+
).toBeUndefined();
|
|
26
39
|
});
|
|
27
40
|
});
|
|
28
41
|
|
|
@@ -45,4 +58,29 @@ describe("buildImplementPlanDirective", () => {
|
|
|
45
58
|
expect(directive).toContain("conversation above");
|
|
46
59
|
expect(directive).not.toContain("plan.md");
|
|
47
60
|
});
|
|
61
|
+
|
|
62
|
+
// Tier 3 (plan-driven build progress): both variants instruct the agent to
|
|
63
|
+
// derive its to-do list from the plan and keep it current — the agent's todo
|
|
64
|
+
// tool is the single writer of status.todos, so this instruction is the
|
|
65
|
+
// entire plan→progress linkage.
|
|
66
|
+
it("instructs plan-derived progress tracking in the attached variant", () => {
|
|
67
|
+
const directive = buildImplementPlanDirective(".stigmer/inputs/plan.md");
|
|
68
|
+
|
|
69
|
+
expect(directive).toContain("to-do list");
|
|
70
|
+
expect(directive).toContain("break the plan into");
|
|
71
|
+
expect(directive).toContain("in progress");
|
|
72
|
+
expect(directive).toContain("completed");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("instructs plan-derived progress tracking in the conversation-only variant", () => {
|
|
76
|
+
const directive = buildImplementPlanDirective(undefined);
|
|
77
|
+
|
|
78
|
+
expect(directive).toContain("to-do list");
|
|
79
|
+
expect(directive).toContain("break the plan into");
|
|
80
|
+
expect(directive).toContain("in progress");
|
|
81
|
+
expect(directive).toContain("completed");
|
|
82
|
+
// The progress block must never name the file — this variant exists
|
|
83
|
+
// because no plan file materialized.
|
|
84
|
+
expect(directive).not.toContain("plan.md");
|
|
85
|
+
});
|
|
48
86
|
});
|