@kendoo.agentdesk/agentdesk 0.27.0 → 0.28.0
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/CHANGELOG.md +28 -0
- package/bin/agentdesk.mjs +35 -45
- package/cli/agents.mjs +4 -256
- package/cli/bootstrap.mjs +40 -59
- package/cli/config.mjs +29 -4
- package/cli/daemon.mjs +72 -44
- package/cli/dotenv.mjs +96 -13
- package/cli/engine/agents/index.mjs +151 -0
- package/cli/engine/claude-auth.mjs +72 -0
- package/cli/engine/env.mjs +56 -0
- package/cli/engine/events.mjs +214 -0
- package/cli/engine/hooks.mjs +112 -0
- package/cli/engine/phases/EXECUTION.md +45 -0
- package/cli/engine/phases/INTAKE.md +34 -0
- package/cli/engine/phases/PLAN.md +26 -0
- package/cli/engine/phases/REVIEW.md +21 -0
- package/cli/engine/phases/SOLO.md +115 -0
- package/cli/engine/phases/SUMMARY.md +23 -0
- package/cli/engine/prompts.mjs +181 -0
- package/cli/engine/query.mjs +63 -0
- package/cli/engine/schemas.mjs +180 -0
- package/cli/engine/session.mjs +285 -0
- package/cli/engine/spawn.mjs +83 -0
- package/cli/engine/tracker/github.md +19 -0
- package/cli/engine/tracker/jira.md +23 -0
- package/cli/engine/tracker/linear.md +24 -0
- package/cli/engine/verdict.mjs +83 -0
- package/cli/init.mjs +290 -147
- package/cli/login.mjs +11 -3
- package/cli/phase-loop.mjs +78 -0
- package/cli/proc.mjs +131 -0
- package/cli/project-key.mjs +56 -0
- package/cli/prompt.mjs +9 -503
- package/cli/prompts.mjs +20 -1
- package/cli/security-check.mjs +1 -1
- package/cli/session-isolation.mjs +65 -9
- package/cli/session-sandbox.mjs +13 -1
- package/cli/setup-helpers.mjs +83 -36
- package/cli/team.mjs +41 -34
- package/cli/tracker-check.mjs +12 -2
- package/cli/tracker-project.mjs +93 -0
- package/cli/update-check.mjs +62 -0
- package/package.json +12 -3
- package/cli/orchestrator.mjs +0 -461
- package/cli/stream-parser.mjs +0 -216
- package/prompts/phased.md +0 -549
- package/prompts/team.md +0 -505
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// Phase prompt rendering for the main thread (the lead).
|
|
2
|
+
//
|
|
3
|
+
// Templates live in ./phases/<PHASE>.md and ./tracker/<tracker>.md. The
|
|
4
|
+
// template language is the same minimal one prompts/phased.md used:
|
|
5
|
+
// {{VAR}} substitution
|
|
6
|
+
// {{#FLAG}} ... {{/FLAG}} kept when FLAG is on, removed otherwise
|
|
7
|
+
// Untrusted content (task description) is wrapped with wrapUntrusted() and
|
|
8
|
+
// the security header from cli/prompt.mjs is prepended, as before.
|
|
9
|
+
|
|
10
|
+
import { readFileSync } from "fs";
|
|
11
|
+
import { dirname, join } from "path";
|
|
12
|
+
import { fileURLToPath } from "url";
|
|
13
|
+
import { wrapUntrusted, PROMPT_SECURITY_HEADER, MEMORY_INSTRUCTIONS, loadProjectMemory } from "../prompt.mjs";
|
|
14
|
+
import { generateContext } from "../detect.mjs";
|
|
15
|
+
import { formatFindingsForRetry } from "./verdict.mjs";
|
|
16
|
+
|
|
17
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
|
|
19
|
+
const cache = new Map();
|
|
20
|
+
function template(rel) {
|
|
21
|
+
if (!cache.has(rel)) cache.set(rel, readFileSync(join(here, rel), "utf-8"));
|
|
22
|
+
return cache.get(rel);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const SECTION_RE = /\{\{#([A-Z_]+)\}\}([\s\S]*?)\{\{\/\1\}\}/g;
|
|
26
|
+
|
|
27
|
+
export function renderTemplate(text, { flags = new Set(), vars = {} } = {}) {
|
|
28
|
+
let out = text;
|
|
29
|
+
// Sections nest (e.g. CHILD_BRANCH inside CHILD_TASKS). A kept outer body
|
|
30
|
+
// is emitted verbatim, so its inner markers need another pass; loop until
|
|
31
|
+
// a pass changes nothing. Bounded, in case of a malformed template.
|
|
32
|
+
for (let pass = 0; pass < 10; pass++) {
|
|
33
|
+
const next = out.replace(SECTION_RE, (_, flag, body) => (flags.has(flag) ? body : ""));
|
|
34
|
+
if (next === out) break;
|
|
35
|
+
out = next;
|
|
36
|
+
}
|
|
37
|
+
out = out.replace(/\{\{([A-Z_]+)\}\}/g, (_, name) => (name in vars ? String(vars[name] ?? "") : ""));
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function trackerSection(tracker, phases, vars) {
|
|
42
|
+
if (!tracker) return "";
|
|
43
|
+
let text;
|
|
44
|
+
try { text = template(`tracker/${tracker}.md`); } catch { return ""; }
|
|
45
|
+
const flags = new Set(["COMMON", ...(Array.isArray(phases) ? phases : [phases])]);
|
|
46
|
+
return renderTemplate(text, { flags, vars }).trim();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function soloSearchAndCreate({ tracker, config }) {
|
|
50
|
+
const jiraBase = (config.jira?.baseUrl || "").replace(/\/+$/, "");
|
|
51
|
+
const search = {
|
|
52
|
+
linear: "Search Linear for an existing issue related to the task description using the GraphQL API (endpoint https://api.linear.app/graphql, header `Authorization: $LINEAR_API_KEY`). Search by keywords from the description; look for open/in-progress issues that match.",
|
|
53
|
+
jira: `Search Jira for an existing issue related to the task description: POST ${jiraBase}/rest/api/3/search with basic auth (\`$JIRA_EMAIL:$JIRA_API_TOKEN\`) and a JQL text query built from keywords in the description; look for open/in-progress issues.`,
|
|
54
|
+
github: 'Search GitHub for an existing issue: `gh issue list --search "<keywords from description>" --state open`.',
|
|
55
|
+
};
|
|
56
|
+
const create = {
|
|
57
|
+
linear: "Create a Linear issue with the GraphQL API (`issueCreate`), title from the description.",
|
|
58
|
+
jira: `Create a Jira issue: POST ${jiraBase}/rest/api/3/issue with basic auth, summary from the description.`,
|
|
59
|
+
github: 'Create a GitHub issue: `gh issue create --title "..." --body "..."`.',
|
|
60
|
+
};
|
|
61
|
+
return { SEARCH_INSTR: search[tracker] || "", CREATE_INSTR: create[tracker] || "" };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// The single-agent variant: one query, the agent is the main thread with full
|
|
65
|
+
// tools, no lead and no review gate. Ported from the legacy buildSoloPrompt.
|
|
66
|
+
export function renderSoloPrompt({
|
|
67
|
+
agent, taskId, taskLink, description, tracker, config = {}, project = {},
|
|
68
|
+
sessionUrl, cwd, childStrategy,
|
|
69
|
+
}) {
|
|
70
|
+
const hasRealTaskId = !!taskId && !String(taskId).startsWith("new-") && !String(taskId).startsWith("task-");
|
|
71
|
+
const vars = {
|
|
72
|
+
AGENT_NAME: agent.name,
|
|
73
|
+
AGENT_ROLE: agent.role,
|
|
74
|
+
AGENT_DESCRIPTION: agent.description,
|
|
75
|
+
GROUND_RULES: agent.groundRules || "",
|
|
76
|
+
CODE_PRINCIPLE: agent.codePrinciple || "",
|
|
77
|
+
RESPONSIBILITIES: Array.isArray(agent.execution?.tasks) ? agent.execution.tasks.map(t => `- ${t}`).join("\n") : "",
|
|
78
|
+
TASK_ID: hasRealTaskId ? taskId : "TBD",
|
|
79
|
+
TASK_LINK: taskLink || "",
|
|
80
|
+
SESSION_URL: sessionUrl || "",
|
|
81
|
+
JIRA_BASE_URL: config.jira?.baseUrl || "",
|
|
82
|
+
TASK_DESCRIPTION: description ? wrapUntrusted("task_description", description) : "",
|
|
83
|
+
...soloSearchAndCreate({ tracker, config }),
|
|
84
|
+
};
|
|
85
|
+
const flags = new Set();
|
|
86
|
+
if (vars.GROUND_RULES) flags.add("GROUND_RULES");
|
|
87
|
+
if (vars.CODE_PRINCIPLE) flags.add("CODE_PRINCIPLE");
|
|
88
|
+
if (vars.RESPONSIBILITIES) flags.add("RESPONSIBILITIES");
|
|
89
|
+
if (description) flags.add("TASK_DESCRIPTION");
|
|
90
|
+
if (tracker) {
|
|
91
|
+
flags.add(tracker.toUpperCase());
|
|
92
|
+
flags.add(hasRealTaskId ? "HAS_TASK_ID" : "FIND_OR_CREATE");
|
|
93
|
+
flags.add("CHILD_TASKS");
|
|
94
|
+
flags.add((childStrategy || "inline") === "branch" ? "CHILD_BRANCH" : "CHILD_INLINE");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
vars.TRACKER_SECTION = trackerSection(tracker, ["INTAKE", "EXECUTION", "SUMMARY"], vars);
|
|
98
|
+
|
|
99
|
+
let body = renderTemplate(template("phases/SOLO.md"), { flags, vars }).trim();
|
|
100
|
+
if (tracker) {
|
|
101
|
+
body += `\n\n## TRACKER LOCK\n\nThis project uses **${tracker.toUpperCase()}**. Do NOT use any other tracker, even if its API returns errors — troubleshoot credentials and permissions instead.`;
|
|
102
|
+
}
|
|
103
|
+
if (config.instructions) body += `\n\n## ADDITIONAL INSTRUCTIONS\n\n${config.instructions}`;
|
|
104
|
+
|
|
105
|
+
body += `\n\n${MEMORY_INSTRUCTIONS}`;
|
|
106
|
+
const memory = loadProjectMemory(cwd);
|
|
107
|
+
if (memory) body += `\n\n### Current memory\n\n${memory}`;
|
|
108
|
+
|
|
109
|
+
const context = generateContext(project);
|
|
110
|
+
const now = new Date();
|
|
111
|
+
const timeInfo = `Current date/time: ${now.toLocaleDateString("en-US", { weekday: "long", year: "numeric", month: "long", day: "numeric" })} ${now.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" })}`;
|
|
112
|
+
return `${PROMPT_SECURITY_HEADER}\n\n${body}\n\n---\n\n## PROJECT CONTEXT\n\n${context}\n\n${timeInfo}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function createTaskSection({ tracker, config, description }) {
|
|
116
|
+
const lines = ["## CREATE TASK (MANDATORY — FIRST ACTION)", "",
|
|
117
|
+
"No task id was provided. Have Dennis create the task in the tracker as the very first action, then announce it on its own line as `TASK_ID: <identifier>`.", ""];
|
|
118
|
+
if (tracker === "linear") {
|
|
119
|
+
lines.push("Create a Linear issue with the GraphQL API.");
|
|
120
|
+
if (config.linear?.teamKey) lines.push(`**Team MUST be \`${config.linear.teamKey}\`.** Resolve its id via \`{ teams(filter: { key: { eq: "${config.linear.teamKey}" } }) { nodes { id } } }\` and pass it as \`teamId\` in \`issueCreate\`.`);
|
|
121
|
+
lines.push("**Assign to the connected user:** fetch `{ viewer { id } }` and pass that id as `assigneeId`.");
|
|
122
|
+
} else if (tracker === "jira") {
|
|
123
|
+
lines.push(`Create a Jira issue at ${config.jira?.baseUrl || ""}.`);
|
|
124
|
+
if (config.jira?.project) lines.push(`**Project MUST be \`${config.jira.project}\`.** Set \`fields.project.key = "${config.jira.project}"\`.`);
|
|
125
|
+
lines.push(`**Assign to the connected user:** GET \`${config.jira?.baseUrl || ""}/rest/api/3/myself\` for the \`accountId\`, then set \`fields.assignee = { "accountId": "<id>" }\`.`);
|
|
126
|
+
} else if (tracker === "github") {
|
|
127
|
+
lines.push('Create a GitHub issue: `gh issue create --title "..." --body "..." --assignee @me`.');
|
|
128
|
+
}
|
|
129
|
+
lines.push("", `Task description: ${wrapUntrusted("task_description", description)}`, "");
|
|
130
|
+
return lines.join("\n");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Returns the full user prompt for one phase's query().
|
|
134
|
+
export function renderPhasePrompt({
|
|
135
|
+
phase, taskId, taskLink, description, createTask, tracker, config = {}, project = {},
|
|
136
|
+
sessionUrl, cwd, sessionMemory = "", retryVerdict = null,
|
|
137
|
+
}) {
|
|
138
|
+
const vars = {
|
|
139
|
+
TASK_ID: taskId,
|
|
140
|
+
TASK_LINK: taskLink || "",
|
|
141
|
+
SESSION_URL: sessionUrl || "",
|
|
142
|
+
JIRA_BASE_URL: config.jira?.baseUrl || "",
|
|
143
|
+
TASK_DESCRIPTION: description ? wrapUntrusted("task_description", description) : "",
|
|
144
|
+
RETRY_FINDINGS: retryVerdict ? formatFindingsForRetry(retryVerdict) : "",
|
|
145
|
+
};
|
|
146
|
+
const flags = new Set();
|
|
147
|
+
if (description) flags.add("TASK_DESCRIPTION");
|
|
148
|
+
if (config.screenshots !== false) flags.add("SCREENSHOTS_ENABLED"); else flags.add("SCREENSHOTS_DISABLED");
|
|
149
|
+
if (tracker) flags.add(tracker.toUpperCase()); else flags.add("NO_TRACKER");
|
|
150
|
+
if (retryVerdict) flags.add("RETRY");
|
|
151
|
+
|
|
152
|
+
vars.TRACKER_SECTION = trackerSection(tracker, phase, vars);
|
|
153
|
+
|
|
154
|
+
let body = renderTemplate(template(`phases/${phase}.md`), { flags, vars }).trim();
|
|
155
|
+
|
|
156
|
+
if (phase === "INTAKE" && createTask && description) {
|
|
157
|
+
body += `\n\n${createTaskSection({ tracker, config, description })}`;
|
|
158
|
+
}
|
|
159
|
+
if (tracker) {
|
|
160
|
+
body += `\n\n## TRACKER LOCK\n\nThis project uses **${tracker.toUpperCase()}**. Do NOT use any other tracker.`;
|
|
161
|
+
}
|
|
162
|
+
if (config.instructions) {
|
|
163
|
+
body += `\n\n## ADDITIONAL INSTRUCTIONS\n\n${config.instructions}`;
|
|
164
|
+
}
|
|
165
|
+
if (sessionMemory) {
|
|
166
|
+
body += `\n\n## SESSION MEMORY (previous phases)\n\n${sessionMemory}`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
body += `\n\n${MEMORY_INSTRUCTIONS}`;
|
|
170
|
+
const memory = loadProjectMemory(cwd);
|
|
171
|
+
if (memory) body += `\n\n### Current memory\n\n${memory}`;
|
|
172
|
+
|
|
173
|
+
if (config.projectAgents?.length) {
|
|
174
|
+
project.configAgents = config.projectAgents.map(a => ({ ...a, type: a.type || "declared", source: ".agentdesk.json" }));
|
|
175
|
+
}
|
|
176
|
+
const context = generateContext(project);
|
|
177
|
+
const now = new Date();
|
|
178
|
+
const timeInfo = `Current date/time: ${now.toLocaleDateString("en-US", { weekday: "long", year: "numeric", month: "long", day: "numeric" })} ${now.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" })}`;
|
|
179
|
+
|
|
180
|
+
return `${PROMPT_SECURITY_HEADER}\n\n${body}\n\n---\n\n## PROJECT CONTEXT\n\n${context}\n\n${timeInfo}`;
|
|
181
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// One phase = one Agent SDK query(). This module builds the Options object
|
|
2
|
+
// and owns the only import of the SDK's query(), so the session loop can be
|
|
3
|
+
// exercised in tests with a scripted message stream instead.
|
|
4
|
+
|
|
5
|
+
import { query as sdkQuery } from "@anthropic-ai/claude-agent-sdk";
|
|
6
|
+
import { createSandboxedSpawn } from "./spawn.mjs";
|
|
7
|
+
import { PHASE_OUTPUT_SCHEMAS } from "./schemas.mjs";
|
|
8
|
+
import { hooksForPhase } from "./hooks.mjs";
|
|
9
|
+
|
|
10
|
+
// Agentic turns per phase before the SDK stops the query (error_max_turns →
|
|
11
|
+
// the phase counts as failed). Overridable per project via config.phaseMaxTurns.
|
|
12
|
+
export const DEFAULT_MAX_TURNS = Object.freeze({
|
|
13
|
+
INTAKE: 40,
|
|
14
|
+
PLAN: 60,
|
|
15
|
+
EXECUTION: 250,
|
|
16
|
+
REVIEW: 80,
|
|
17
|
+
SUMMARY: 40,
|
|
18
|
+
SOLO: 250,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export function maxTurnsFor(phase, config = {}) {
|
|
22
|
+
const v = Number(config.phaseMaxTurns?.[phase]);
|
|
23
|
+
return Number.isFinite(v) && v > 0 ? v : DEFAULT_MAX_TURNS[phase];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Pure: returns the SDK Options for one phase. Everything that must be true
|
|
27
|
+
// of every unattended session is decided here, once:
|
|
28
|
+
// - permissionMode "dontAsk": pre-approved tools run, anything else is denied,
|
|
29
|
+
// nothing ever waits on a prompt
|
|
30
|
+
// - settingSources []: the user's ~/.claude settings never leak into a session
|
|
31
|
+
// - persistSession false: no transcripts written to the user's profile
|
|
32
|
+
// - forwardSubagentText: subagent messages arrive with parent_tool_use_id
|
|
33
|
+
// - outputFormat: the phase's handoff schema (schemas.mjs)
|
|
34
|
+
export function buildQueryOptions({
|
|
35
|
+
phase, cwd, env, model, agents, allowedTools, lead, state, config = {},
|
|
36
|
+
abortController, sandbox, onChild, onIsolation, hookCallbacks = {},
|
|
37
|
+
claudePath = process.env.AGENTDESK_CLAUDE_PATH,
|
|
38
|
+
}) {
|
|
39
|
+
const options = {
|
|
40
|
+
cwd,
|
|
41
|
+
env,
|
|
42
|
+
agent: lead,
|
|
43
|
+
agents,
|
|
44
|
+
allowedTools,
|
|
45
|
+
permissionMode: "dontAsk",
|
|
46
|
+
settingSources: [],
|
|
47
|
+
persistSession: false,
|
|
48
|
+
maxTurns: maxTurnsFor(phase, config),
|
|
49
|
+
abortController,
|
|
50
|
+
forwardSubagentText: true,
|
|
51
|
+
outputFormat: { type: "json_schema", schema: PHASE_OUTPUT_SCHEMAS[phase] },
|
|
52
|
+
hooks: hooksForPhase({ phase, state, ...hookCallbacks }),
|
|
53
|
+
spawnClaudeCodeProcess: createSandboxedSpawn({ sandbox, onChild, onIsolation }),
|
|
54
|
+
};
|
|
55
|
+
if (model) options.model = model;
|
|
56
|
+
if (Number(config.maxBudgetUsd) > 0) options.maxBudgetUsd = Number(config.maxBudgetUsd);
|
|
57
|
+
if (claudePath) options.pathToClaudeCodeExecutable = claudePath;
|
|
58
|
+
return options;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function* defaultRunQuery({ prompt, options }) {
|
|
62
|
+
yield* sdkQuery({ prompt, options });
|
|
63
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// Per-phase structured output: what each phase must hand to the next.
|
|
2
|
+
//
|
|
3
|
+
// The old design asked the model to "write .agentdesk/session-memory.md
|
|
4
|
+
// before finishing" and then read the file back. Phases forgot, wrote
|
|
5
|
+
// partial sections, or wrote a different shape. Now every phase ends its
|
|
6
|
+
// query with `outputFormat: { type: "json_schema" }`, the SDK validates the
|
|
7
|
+
// object, and the engine renders the markdown section itself. The file keeps
|
|
8
|
+
// the same headings as before so anything that reads it still works.
|
|
9
|
+
|
|
10
|
+
import { VERDICT_SCHEMA } from "./verdict.mjs";
|
|
11
|
+
|
|
12
|
+
const strList = { type: "array", items: { type: "string" } };
|
|
13
|
+
|
|
14
|
+
export const PHASE_OUTPUT_SCHEMAS = Object.freeze({
|
|
15
|
+
INTAKE: {
|
|
16
|
+
type: "object",
|
|
17
|
+
additionalProperties: false,
|
|
18
|
+
required: ["title", "taskSummary", "requirements", "assessment", "subtasks", "nextPhaseFocus"],
|
|
19
|
+
properties: {
|
|
20
|
+
title: { type: "string", description: "4-8 word session title" },
|
|
21
|
+
taskSummary: { type: "string" },
|
|
22
|
+
requirements: strList,
|
|
23
|
+
assessment: { ...strList, description: "existing branches/PRs, code patterns, resume context" },
|
|
24
|
+
subtasks: { ...strList, description: "subtasks created, if the task was decomposed; else empty" },
|
|
25
|
+
nextPhaseFocus: strList,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
PLAN: {
|
|
29
|
+
type: "object",
|
|
30
|
+
additionalProperties: false,
|
|
31
|
+
required: ["approach", "filesToModify", "decisions", "risks", "assignments", "steps"],
|
|
32
|
+
properties: {
|
|
33
|
+
approach: strList,
|
|
34
|
+
filesToModify: strList,
|
|
35
|
+
decisions: strList,
|
|
36
|
+
risks: strList,
|
|
37
|
+
assignments: { ...strList, description: "who does what during execution" },
|
|
38
|
+
steps: { ...strList, description: "ordered implementation steps" },
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
EXECUTION: {
|
|
42
|
+
type: "object",
|
|
43
|
+
additionalProperties: false,
|
|
44
|
+
required: ["implemented", "filesChanged", "prUrl", "qaResults", "issuesFixed", "reviewerAttention"],
|
|
45
|
+
properties: {
|
|
46
|
+
implemented: strList,
|
|
47
|
+
filesChanged: strList,
|
|
48
|
+
prUrl: { type: "string", description: "empty string if no PR was created" },
|
|
49
|
+
qaResults: strList,
|
|
50
|
+
issuesFixed: strList,
|
|
51
|
+
reviewerAttention: strList,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
REVIEW: VERDICT_SCHEMA,
|
|
55
|
+
SOLO: {
|
|
56
|
+
type: "object",
|
|
57
|
+
additionalProperties: false,
|
|
58
|
+
required: ["summary", "filesChanged", "prUrl", "deferred", "manualSteps"],
|
|
59
|
+
properties: {
|
|
60
|
+
summary: { type: "string" },
|
|
61
|
+
filesChanged: strList,
|
|
62
|
+
prUrl: { type: "string", description: "empty string if no PR was created" },
|
|
63
|
+
deferred: strList,
|
|
64
|
+
manualSteps: strList,
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
SUMMARY: {
|
|
68
|
+
type: "object",
|
|
69
|
+
additionalProperties: false,
|
|
70
|
+
required: ["status", "prUrl", "deferred", "manualSteps", "summaryComment"],
|
|
71
|
+
properties: {
|
|
72
|
+
status: { type: "string" },
|
|
73
|
+
prUrl: { type: "string" },
|
|
74
|
+
deferred: strList,
|
|
75
|
+
manualSteps: strList,
|
|
76
|
+
summaryComment: { type: "string", description: "the final tracker comment as posted" },
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const bullets = (items, empty = "- none") =>
|
|
82
|
+
Array.isArray(items) && items.length ? items.map(i => `- ${String(i)}`).join("\n") : empty;
|
|
83
|
+
|
|
84
|
+
// Markdown appended to .agentdesk/session-memory.md after each phase. Same
|
|
85
|
+
// headings the previous prompts asked the model to write.
|
|
86
|
+
export function renderMemorySection(phase, out) {
|
|
87
|
+
if (!out || typeof out !== "object") {
|
|
88
|
+
return `## ${phase}\n- (phase produced no structured summary)\n`;
|
|
89
|
+
}
|
|
90
|
+
switch (phase) {
|
|
91
|
+
case "INTAKE":
|
|
92
|
+
return [
|
|
93
|
+
"## Task",
|
|
94
|
+
`- Title: ${out.title || ""}`,
|
|
95
|
+
`- Summary: ${out.taskSummary || ""}`,
|
|
96
|
+
"",
|
|
97
|
+
"## Requirements",
|
|
98
|
+
bullets(out.requirements),
|
|
99
|
+
"",
|
|
100
|
+
"## Assessment",
|
|
101
|
+
bullets(out.assessment),
|
|
102
|
+
"",
|
|
103
|
+
"## Subtasks (if decomposed)",
|
|
104
|
+
bullets(out.subtasks),
|
|
105
|
+
"",
|
|
106
|
+
"## Next Phase: PLAN",
|
|
107
|
+
bullets(out.nextPhaseFocus),
|
|
108
|
+
"",
|
|
109
|
+
].join("\n");
|
|
110
|
+
case "PLAN":
|
|
111
|
+
return [
|
|
112
|
+
"## Plan",
|
|
113
|
+
bullets(out.approach),
|
|
114
|
+
bullets(out.filesToModify?.map(f => `File: ${f}`), ""),
|
|
115
|
+
bullets(out.decisions?.map(d => `Decision: ${d}`), ""),
|
|
116
|
+
bullets(out.risks?.map(r => `Risk: ${r}`), ""),
|
|
117
|
+
"",
|
|
118
|
+
"## Agent Assignments",
|
|
119
|
+
bullets(out.assignments),
|
|
120
|
+
"",
|
|
121
|
+
"## Next Phase: EXECUTION",
|
|
122
|
+
bullets(out.steps),
|
|
123
|
+
"",
|
|
124
|
+
].filter(l => l !== "").join("\n") + "\n";
|
|
125
|
+
case "EXECUTION":
|
|
126
|
+
return [
|
|
127
|
+
"## Execution Summary",
|
|
128
|
+
bullets(out.implemented),
|
|
129
|
+
bullets(out.filesChanged?.map(f => `Changed: ${f}`), ""),
|
|
130
|
+
`- PR: ${out.prUrl || "(none)"}`,
|
|
131
|
+
"",
|
|
132
|
+
"## QA Results",
|
|
133
|
+
bullets(out.qaResults),
|
|
134
|
+
bullets(out.issuesFixed?.map(i => `Fixed: ${i}`), ""),
|
|
135
|
+
"",
|
|
136
|
+
"## Next Phase: REVIEW",
|
|
137
|
+
bullets(out.reviewerAttention),
|
|
138
|
+
"",
|
|
139
|
+
].filter(l => l !== "").join("\n") + "\n";
|
|
140
|
+
case "REVIEW":
|
|
141
|
+
return [
|
|
142
|
+
"## Review",
|
|
143
|
+
`- Verdict: ${out.verdict || "MISSING"}`,
|
|
144
|
+
`- Findings: ${Array.isArray(out.findings) && out.findings.length ? "" : "none"}`,
|
|
145
|
+
...(Array.isArray(out.findings) ? out.findings.map(f => ` - ${f.title}${f.file ? ` (${f.file}${f.line ? `:${f.line}` : ""})` : ""} — ${f.detail} [${f.reviewer}]`) : []),
|
|
146
|
+
`- Out of scope: ${Array.isArray(out.deferred) && out.deferred.length ? out.deferred.join("; ") : "none"}`,
|
|
147
|
+
`- Unverified claims: ${Array.isArray(out.unverifiedClaims) && out.unverifiedClaims.length ? out.unverifiedClaims.join("; ") : "none"}`,
|
|
148
|
+
"",
|
|
149
|
+
].join("\n");
|
|
150
|
+
case "SOLO":
|
|
151
|
+
return [
|
|
152
|
+
"## Solo Summary",
|
|
153
|
+
`- ${out.summary || ""}`,
|
|
154
|
+
bullets(out.filesChanged?.map(f => `Changed: ${f}`), ""),
|
|
155
|
+
`- PR: ${out.prUrl || "(none)"}`,
|
|
156
|
+
"",
|
|
157
|
+
"## Deferred",
|
|
158
|
+
bullets(out.deferred),
|
|
159
|
+
"",
|
|
160
|
+
"## Manual Steps",
|
|
161
|
+
bullets(out.manualSteps),
|
|
162
|
+
"",
|
|
163
|
+
].filter(l => l !== "").join("\n") + "\n";
|
|
164
|
+
case "SUMMARY":
|
|
165
|
+
return [
|
|
166
|
+
"## Final Status",
|
|
167
|
+
`- ${out.status || ""}`,
|
|
168
|
+
`- PR: ${out.prUrl || "(none)"}`,
|
|
169
|
+
"",
|
|
170
|
+
"## Deferred",
|
|
171
|
+
bullets(out.deferred),
|
|
172
|
+
"",
|
|
173
|
+
"## Manual Steps",
|
|
174
|
+
bullets(out.manualSteps),
|
|
175
|
+
"",
|
|
176
|
+
].join("\n");
|
|
177
|
+
default:
|
|
178
|
+
return `## ${phase}\n${JSON.stringify(out, null, 2)}\n`;
|
|
179
|
+
}
|
|
180
|
+
}
|