Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions super_turtle/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ This project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added
- Structured worker result files: SubTurtles now write `.superturtle/state/results/<name>.json` at completion containing `summary`, `artifacts`, `key_decisions`, and `questions_for_orchestrator`. The meta agent can read these when spawning dependent workers, enabling true multi-agent orchestration with result passing between agents.
- Python infrastructure writes a fallback result automatically if the agent forgets to, covering both clean completion and fatal failure cases.
- Conductor inbox items for completed workers now embed the result summary, key decisions, artifacts, and open questions directly in the notification text — the meta agent no longer needs to manually inspect the workspace to spawn a well-informed downstream worker.
- Dashboard SubTurtle detail page shows a "Worker result" card with the structured output.
- `DECOMPOSITION_PROMPT.md` updated with a "Result-Passing Pattern" section and injection template for wiring upstream results into downstream `CLAUDE.md` files.
- `META_SHARED.md` updated with a result-passing note under task decomposition.

## [0.2.9] - 2026-03-28

### Changed
Expand Down
41 changes: 41 additions & 0 deletions super_turtle/claude-telegram-bot/src/conductor-snapshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,46 @@ describe("loadConductorSnapshotContext", () => {
expect(context.workerStateJson).toBe("(missing canonical worker state)");
expect(context.prepErrors[0]).toContain("canonical worker state missing");
});

it("includes worker result summary when result file exists", () => {
const baseDir = makeTempDir();
const stateDir = join(baseDir, ".superturtle", "state");
mkdirSync(join(stateDir, "workers"), { recursive: true });
mkdirSync(join(stateDir, "wakeups"), { recursive: true });
mkdirSync(join(stateDir, "results"), { recursive: true });

writeJson(join(stateDir, "workers", "worker-with-result.json"), {
kind: "worker_state",
schema_version: 1,
worker_name: "worker-with-result",
run_id: "run-wr",
lifecycle_state: "completed",
workspace: "/tmp/worker-with-result",
current_task: "Build the auth system",
metadata: {},
});

writeJson(join(stateDir, "results", "worker-with-result.json"), {
schema_version: 1,
worker_name: "worker-with-result",
completed_at: "2026-04-20T14:00:00Z",
status: "completed",
summary: "Implemented JWT middleware with RS256. 12 tests passing.",
artifacts: ["src/auth/jwt.ts"],
key_decisions: ["RS256 over HS256"],
blockers: [],
questions_for_orchestrator: ["Should refresh TTL be configurable?"],
});

const context = loadConductorSnapshotContext({
stateDir,
workerName: "worker-with-result",
});

expect(context.workerResultJson).toContain("Implemented JWT middleware with RS256");
expect(context.workerResultJson).toContain("RS256 over HS256");
expect(context.conductorSummary).toContain("Result: Implemented JWT middleware with RS256");
});
});

describe("buildPreparedSnapshotPrompt", () => {
Expand All @@ -187,6 +227,7 @@ describe("buildPreparedSnapshotPrompt", () => {
workerStateJson: '{"worker_name":"worker-a"}',
recentEventsJson: '[{"event_type":"worker.checkpoint"}]',
wakeupsJson: '[{"delivery_state":"pending"}]',
workerResultJson: "(no result file)",
statusOutput: "worker-a running as 999",
stateExcerpt: "# Current task\n\nShip it",
gitLog: "abc123 Ship it",
Expand Down
13 changes: 13 additions & 0 deletions super_turtle/claude-telegram-bot/src/conductor-snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ interface ConductorSnapshotContext {
workerStateJson: string;
recentEventsJson: string;
wakeupsJson: string;
workerResultJson: string;
prepErrors: string[];
}

Expand Down Expand Up @@ -152,6 +153,12 @@ export function loadConductorSnapshotContext(
: null;
const resolvedState = supervisorResolvedState(workerState);

const resultPath = join(stateDir, "results", `${options.workerName}.json`);
const workerResult = readJsonObject<Record<string, unknown>>(resultPath);
const workerResultSummary = workerResult
? (asString(workerResult.summary) || "(no summary)")
: "(no result file)";

const summaryLines = [
`Worker: ${options.workerName}`,
`Lifecycle state: ${workerState?.lifecycle_state || "(missing)"}`,
Expand All @@ -163,6 +170,7 @@ export function loadConductorSnapshotContext(
`Last checkpoint: ${checkpointSummary || "(none)"}`,
`Resolved terminal state: ${resolvedState || "(none)"}`,
`Pending wakeups: ${pendingWakeups.length > 0 ? pendingWakeups.map(summarizeWakeup).join(" || ") : "(none)"}`,
`Result: ${workerResultSummary}`,
];

return {
Expand All @@ -174,6 +182,7 @@ export function loadConductorSnapshotContext(
),
recentEventsJson: stringifyPromptJson(recentEvents, "[]"),
wakeupsJson: stringifyPromptJson(wakeups, "[]"),
workerResultJson: stringifyPromptJson(workerResult, "(no result file)"),
prepErrors,
};
}
Expand Down Expand Up @@ -210,6 +219,10 @@ export function buildPreparedSnapshotPrompt(snapshot: PreparedSupervisionSnapsho
snapshot.wakeupsJson || "(empty)",
"</worker_wakeups_json>",
"",
"<worker_result_json>",
snapshot.workerResultJson || "(no result file)",
"</worker_result_json>",
"",
"Supporting context (secondary):",
"<ctl_status>",
snapshot.statusOutput || "(empty)",
Expand Down
68 changes: 68 additions & 0 deletions super_turtle/claude-telegram-bot/src/conductor-supervisor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,74 @@ Ship the shipped thing
expect(events).toContain('"event_type":"worker.notification_sent"');
});

it("embeds result file data in the inbox text when result file exists", async () => {
const baseDir = makeStateDir();
const stateDir = join(baseDir, ".superturtle", "state");
const archiveWorkspace = join(baseDir, ".superturtle/subturtles", ".archive", "worker-result");
mkdirSync(join(stateDir, "workers"), { recursive: true });
mkdirSync(join(stateDir, "wakeups"), { recursive: true });
mkdirSync(join(stateDir, "results"), { recursive: true });
mkdirSync(archiveWorkspace, { recursive: true });

writeFileSync(join(archiveWorkspace, "CLAUDE.md"), "# Current task\n\nDone\n# Backlog\n- [x] Ship it\n", "utf-8");

writeJson(join(stateDir, "results", "worker-result.json"), {
schema_version: 1,
worker_name: "worker-result",
completed_at: "2026-03-08T01:00:00Z",
status: "completed",
summary: "Implemented JWT middleware with RS256. 12 tests passing.",
artifacts: ["src/auth/jwt.ts", "tests/auth/jwt.test.ts"],
key_decisions: ["RS256 over HS256 for public-key verification"],
blockers: [],
questions_for_orchestrator: ["Should refresh token TTL be configurable?"],
});

writeJson(join(stateDir, "workers", "worker-result.json"), {
kind: "worker_state",
schema_version: 1,
worker_name: "worker-result",
run_id: "run-result",
lifecycle_state: "archived",
workspace: archiveWorkspace,
current_task: "Ship it",
metadata: {},
});
writeJson(join(stateDir, "wakeups", "wake-result.json"), {
kind: "wakeup",
schema_version: 1,
id: "wake-result",
worker_name: "worker-result",
run_id: "run-result",
category: "notable",
delivery_state: "pending",
summary: "worker result done",
created_at: "2026-03-08T00:00:00Z",
updated_at: "2026-03-08T00:00:00Z",
delivery: { attempts: 0 },
payload: { kind: "completion_requested" },
metadata: {},
});

await processPendingConductorWakeups({
stateDir,
defaultChatId: 123,
listJobs: () => [],
removeJob: () => false,
sendMessage: async () => {},
isWorkerRunning: () => false,
nowIso: () => "2026-03-08T01:00:00Z",
});

const inboxItem = JSON.parse(
readFileSync(join(stateDir, "inbox", "inbox_wake-result.json"), "utf-8")
);
expect(inboxItem.text).toContain("Implemented JWT middleware with RS256");
expect(inboxItem.text).toContain("RS256 over HS256");
expect(inboxItem.text).toContain("src/auth/jwt.ts");
expect(inboxItem.text).toContain("Should refresh token TTL be configurable?");
});

it("reconciles fatal worker wakeups into a failed state", async () => {
const baseDir = makeStateDir();
const stateDir = join(baseDir, ".superturtle", "state");
Expand Down
97 changes: 94 additions & 3 deletions super_turtle/claude-telegram-bot/src/conductor-supervisor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -535,11 +535,61 @@ function buildMetaAgentInboxTitle(
: `SubTurtle ${workerName} update`;
}

function loadWorkerResultForInbox(
stateDir: string,
workerName: string
): Record<string, unknown> | null {
const resultPath = join(stateDir, "results", `${workerName}.json`);
if (!existsSync(resultPath)) return null;
try {
const parsed = JSON.parse(readFileSync(resultPath, "utf-8"));
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
? (parsed as Record<string, unknown>)
: null;
} catch {
return null;
}
}

function writeFallbackResultIfAbsent(
stateDir: string,
workerName: string,
opts: {
status: string;
summary: string;
blockers?: string[];
questions?: string[];
timestamp?: string;
}
): void {
const resultPath = join(stateDir, "results", `${workerName}.json`);
if (existsSync(resultPath)) return;
try {
mkdirSync(join(stateDir, "results"), { recursive: true });
const fallback = {
schema_version: 1,
worker_name: workerName,
completed_at: opts.timestamp || new Date().toISOString(),
status: opts.status,
summary: opts.summary,
artifacts: [] as string[],
key_decisions: [] as string[],
blockers: opts.blockers || [],
questions_for_orchestrator: opts.questions || [],
_generated_by: "infrastructure_fallback",
};
writeFileSync(resultPath, `${JSON.stringify(fallback, null, 2)}\n`, "utf-8");
} catch {
// Best-effort — never throw from result file writing.
}
}

function buildMetaAgentInboxText(
wakeup: WakeupRecord,
workerState: WorkerStateRecord | null,
stateText: string | null,
chatId: number | null
chatId: number | null,
stateDir: string
): string {
const payloadKind = typeof wakeup.payload?.kind === "string" ? wakeup.payload.kind : "";
const lines: string[] = [];
Expand All @@ -551,10 +601,38 @@ function buildMetaAgentInboxText(
lines.push(`Task: ${workerState.current_task.trim()}`);
}
if (payloadKind === "completion_requested") {
const result = loadWorkerResultForInbox(stateDir, wakeup.worker_name);
if (result) {
const isFallback = result._generated_by === "infrastructure_fallback";
const summary = typeof result.summary === "string" ? result.summary.trim() : "";
if (summary) lines.push(`Result: ${summary}`);

const decisions = Array.isArray(result.key_decisions)
? result.key_decisions.filter((d): d is string => typeof d === "string")
: [];
if (!isFallback && decisions.length > 0) {
lines.push(`Key decisions: ${decisions.slice(0, 3).join(" | ")}`);
}

const artifacts = Array.isArray(result.artifacts)
? result.artifacts.filter((a): a is string => typeof a === "string")
: [];
if (!isFallback && artifacts.length > 0) {
lines.push(`Key artifacts: ${artifacts.slice(0, 4).join(", ")}`);
}

const questions = Array.isArray(result.questions_for_orchestrator)
? result.questions_for_orchestrator.filter((q): q is string => typeof q === "string")
: [];
if (!isFallback && questions.length > 0) {
lines.push(`Questions for you: ${questions.slice(0, 2).join(" | ")}`);
}
}

const completedItems = stateText ? parseCompletedBacklogItems(stateText).slice(0, 4) : [];
if (completedItems.length > 0) {
lines.push(`Completed items: ${completedItems.join(" | ")}`);
} else {
} else if (!result) {
lines.push("Completion was reconciled and cleanup verified.");
}
} else if (payloadKind === "fatal_error") {
Expand Down Expand Up @@ -1387,11 +1465,24 @@ export async function processPendingConductorWakeups(
writeWorkerState(stateDir, workingState);
}

// Write a fallback result for terminal wakeup kinds that have no Python-side result writer.
// completion_requested and fatal_error are covered by Python's record_completion_pending /
// record_failure_pending. Timeout is resolved entirely on the TypeScript side.
if (payloadKind === "timeout") {
writeFallbackResultIfAbsent(stateDir, wakeup.worker_name, {
status: "blocked",
summary: `${wakeup.worker_name} timed out before completing all backlog items.`,
blockers: ["Worker exceeded its configured timeout."],
questions: ["Should this worker be restarted with a longer timeout, or is the task done?"],
timestamp: now,
});
}

const chatId = deriveChatId(wakeup, options.defaultChatId ?? ALLOWED_USERS[0] ?? null);
const stateText = readWorkspaceStateText(workingState);
if (wakeup.category !== "silent") {
const inboxTitle = buildMetaAgentInboxTitle(wakeup, workingState);
const inboxText = buildMetaAgentInboxText(wakeup, workingState, stateText, chatId);
const inboxText = buildMetaAgentInboxText(wakeup, workingState, stateText, chatId, stateDir);
const { item: inboxItem, created } = ensureMetaAgentInboxItem({
stateDir,
item: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ describe("cron supervision queue", () => {
workerStateJson: "{}",
recentEventsJson: "[]",
wakeupsJson: "[]",
workerResultJson: "(no result file)",
statusOutput: "ok",
stateExcerpt: "state",
gitLog: "log",
Expand All @@ -45,6 +46,7 @@ describe("cron supervision queue", () => {
workerStateJson: "{}",
recentEventsJson: "[]",
wakeupsJson: "[]",
workerResultJson: "(no result file)",
statusOutput: "ok",
stateExcerpt: "state",
gitLog: "log",
Expand Down Expand Up @@ -78,6 +80,7 @@ describe("cron supervision queue", () => {
workerStateJson: "{}",
recentEventsJson: "[]",
wakeupsJson: "[]",
workerResultJson: "(no result file)",
statusOutput: "ok",
stateExcerpt: "s",
gitLog: "g",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export interface PreparedSupervisionSnapshot {
workerStateJson: string;
recentEventsJson: string;
wakeupsJson: string;
workerResultJson: string;
statusOutput: string;
stateExcerpt: string;
gitLog: string;
Expand Down
13 changes: 13 additions & 0 deletions super_turtle/claude-telegram-bot/src/dashboard-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,18 @@ export type SubturtleEventView = {
lifecycleState: string | null;
};

export type WorkerResultRecord = {
schema_version: number;
worker_name: string;
completed_at: string;
status: string;
summary: string;
artifacts: string[];
key_decisions: string[];
blockers: string[];
questions_for_orchestrator: string[];
};

export type SubturtleDetailResponse = {
generatedAt: string;
name: string;
Expand All @@ -137,6 +149,7 @@ export type SubturtleDetailResponse = {
backlogSummary: BacklogSummary;
conductor: SubturtleConductorView | null;
events: SubturtleEventView[];
workerResult: WorkerResultRecord | null;
};

export type SubturtleLogsResponse = {
Expand Down
Loading