@kendoo.agentdesk/agentdesk 0.32.1 → 0.34.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 +23 -0
- package/README.md +22 -4
- package/cli/agents.mjs +8 -8
- package/cli/daemon.mjs +63 -9
- package/cli/engine/agents/index.mjs +19 -10
- package/cli/engine/lessons.mjs +171 -0
- package/cli/engine/phases/EXECUTION.md +2 -0
- package/cli/engine/phases/INTAKE.md +2 -1
- package/cli/engine/phases/PLAN.md +4 -3
- package/cli/engine/phases/REVIEW.md +2 -2
- package/cli/engine/phases/SOLO.md +5 -1
- package/cli/engine/phases/SUMMARY.md +5 -4
- package/cli/engine/prompts.mjs +17 -10
- package/cli/engine/recovery.mjs +109 -0
- package/cli/engine/schemas.mjs +39 -8
- package/cli/engine/session.mjs +174 -29
- package/cli/engine/tracker/github.md +2 -2
- package/cli/engine/tracker/jira.md +1 -1
- package/cli/engine/tracker/linear.md +1 -1
- package/cli/engine/verdict.mjs +2 -2
- package/cli/prompt.mjs +5 -12
- package/cli/session-queue.mjs +3 -1
- package/package.json +4 -2
- package/shared/recovery.mjs +28 -0
- package/shared/session-status.mjs +1 -1
|
@@ -12,12 +12,13 @@ Task: {{TASK_ID}}
|
|
|
12
12
|
|
|
13
13
|
## Your mission
|
|
14
14
|
|
|
15
|
-
1.
|
|
15
|
+
1. **Own the delivery report.** Reconcile the latest review and engine verification before dictating the summary. Distinguish delivered, unverified, blocked and user-approved deferred items. If review remains unresolved, report the work as incomplete; do not describe it as ready or substitute a successful test for an unmet acceptance criterion. It must include:
|
|
16
16
|
- **What was done** — outcome-focused, non-technical.
|
|
17
17
|
- **What was omitted / deferred** — everything from REVIEW's out-of-scope list plus anything the team explicitly skipped.
|
|
18
|
-
- **Manual steps** —
|
|
18
|
+
- **Manual steps** — every remaining action outside this session, including access recovery, migrations, config and deploys. Name the responsible person or role and how completion will be confirmed. Required work blocked on access is not an out-of-scope deferral.
|
|
19
19
|
- **PR link**.
|
|
20
20
|
- **Session link**: {{SESSION_URL}}
|
|
21
|
-
2. Delegate the tracker writes to Dennis and require the command output for each: verify the PR link is attached (attach it if missing), transition the task to "In Review", post the final comment.
|
|
21
|
+
2. Delegate the tracker writes to Dennis and require the command output for each: verify the existing PR link is attached (attach it if missing), transition the task to "In Review" only when the review and engine verification permit it, and post the accurate final comment. If a write fails, identify the blocker in `status` and put the recovery owner/action in `manualSteps`, including who retries delivery after access is restored and the receipt needed to confirm it. Do not leave `manualSteps` empty when an access fix is required. Put only the confirmed posted text in `summaryComment`, leaving it empty if posting was not confirmed. The final user-facing update must state the outcome and any remaining action with its owner.
|
|
22
|
+
3. **Lessons for future sessions.** The handoff has `lessons` and `retireLessons`. Propose a lesson only for something non-obvious that cost this session time and would cost the next one too — a setup step, seed data, an environment quirk, a deployment step, a review finding that keeps recurring. Each needs the evidence (what happened: the command and its outcome) and a scope: `project`, `area:<ui|copy|docs|api|data>`, or `path:<file or directory prefix>`. Never store secrets — name the env var instead. Do not restate what README, CLAUDE.md or the code already says. If an entry under PROJECT LESSONS proved wrong or obsolete in this session, retire it by its id with the reason. Empty arrays are the normal case.
|
|
22
23
|
|
|
23
|
-
The structured output required by the schema is captured automatically — status, PR URL, deferred items, manual steps,
|
|
24
|
+
The structured output required by the schema is captured automatically — status, PR URL, deferred items, manual steps, the summary comment exactly as posted, lessons, and retirements. Do not repeat the JSON object in chat; keep any closing chat message brief and human-readable.
|
package/cli/engine/prompts.mjs
CHANGED
|
@@ -10,11 +10,12 @@
|
|
|
10
10
|
import { readFileSync } from "fs";
|
|
11
11
|
import { dirname, join } from "path";
|
|
12
12
|
import { fileURLToPath } from "url";
|
|
13
|
-
import { wrapUntrusted, PROMPT_SECURITY_HEADER,
|
|
13
|
+
import { wrapUntrusted, PROMPT_SECURITY_HEADER, LEGACY_NOTES_HEADER } from "../prompt.mjs";
|
|
14
14
|
import { generateContext } from "../detect.mjs";
|
|
15
15
|
import { formatFindingsForRetry } from "./verdict.mjs";
|
|
16
16
|
import { formatEvidenceForPrompt } from "./evidence.mjs";
|
|
17
17
|
import { renderOpenItems } from "./handoff.mjs";
|
|
18
|
+
import { renderLessonsSection } from "./lessons.mjs";
|
|
18
19
|
|
|
19
20
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
20
21
|
|
|
@@ -40,6 +41,16 @@ export function renderTemplate(text, { flags = new Set(), vars = {} } = {}) {
|
|
|
40
41
|
return out;
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
// Engine-provided project knowledge: active lessons in scope, then the
|
|
45
|
+
// hand-written notes from before the ledger existed (read-only).
|
|
46
|
+
function projectKnowledge({ lessons = [], projectNotes = "" }) {
|
|
47
|
+
let out = "";
|
|
48
|
+
const block = renderLessonsSection(lessons);
|
|
49
|
+
if (block) out += `\n\n${block}`;
|
|
50
|
+
if (projectNotes) out += `\n\n${LEGACY_NOTES_HEADER}\n\n${projectNotes}`;
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
|
|
43
54
|
function trackerSection(tracker, phases, vars) {
|
|
44
55
|
if (!tracker) return "";
|
|
45
56
|
let text;
|
|
@@ -67,7 +78,7 @@ function soloSearchAndCreate({ tracker, config }) {
|
|
|
67
78
|
// tools, no lead and no review gate. Ported from the legacy buildSoloPrompt.
|
|
68
79
|
export function renderSoloPrompt({
|
|
69
80
|
agent, taskId, taskLink, description, tracker, config = {}, project = {},
|
|
70
|
-
sessionUrl,
|
|
81
|
+
sessionUrl, childStrategy, lessons = [], projectNotes = "",
|
|
71
82
|
}) {
|
|
72
83
|
const hasRealTaskId = !!taskId && !String(taskId).startsWith("new-") && !String(taskId).startsWith("task-");
|
|
73
84
|
const vars = {
|
|
@@ -104,9 +115,7 @@ export function renderSoloPrompt({
|
|
|
104
115
|
}
|
|
105
116
|
if (config.instructions) body += `\n\n## ADDITIONAL INSTRUCTIONS\n\n${config.instructions}`;
|
|
106
117
|
|
|
107
|
-
body +=
|
|
108
|
-
const memory = loadProjectMemory(cwd);
|
|
109
|
-
if (memory) body += `\n\n### Current memory\n\n${memory}`;
|
|
118
|
+
body += projectKnowledge({ lessons, projectNotes });
|
|
110
119
|
|
|
111
120
|
const context = generateContext(project);
|
|
112
121
|
const now = new Date();
|
|
@@ -135,8 +144,8 @@ function createTaskSection({ tracker, config, description }) {
|
|
|
135
144
|
// Returns the full user prompt for one phase's query().
|
|
136
145
|
export function renderPhasePrompt({
|
|
137
146
|
phase, taskId, taskLink, description, createTask, tracker, config = {}, project = {},
|
|
138
|
-
sessionUrl,
|
|
139
|
-
roster = null, profile = null, handoffRetry = false,
|
|
147
|
+
sessionUrl, sessionMemory = "", retryVerdict = null, evidence = null, openItems = [],
|
|
148
|
+
roster = null, profile = null, handoffRetry = false, lessons = [], projectNotes = "",
|
|
140
149
|
}) {
|
|
141
150
|
const vars = {
|
|
142
151
|
TASK_ID: taskId,
|
|
@@ -187,9 +196,7 @@ export function renderPhasePrompt({
|
|
|
187
196
|
body += `\n\n## SESSION MEMORY (previous phases)\n\n${sessionMemory}`;
|
|
188
197
|
}
|
|
189
198
|
|
|
190
|
-
body +=
|
|
191
|
-
const memory = loadProjectMemory(cwd);
|
|
192
|
-
if (memory) body += `\n\n### Current memory\n\n${memory}`;
|
|
199
|
+
body += projectKnowledge({ lessons, projectNotes });
|
|
193
200
|
|
|
194
201
|
if (config.projectAgents?.length) {
|
|
195
202
|
project.configAgents = config.projectAgents.map(a => ({ ...a, type: a.type || "declared", source: ".agentdesk.json" }));
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, renameSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { PHASE_OUTPUT_SCHEMAS } from "./schemas.mjs";
|
|
5
|
+
|
|
6
|
+
export function checkpointStore(directory, sessionId, { resume = false } = {}) {
|
|
7
|
+
mkdirSync(directory, { recursive: true });
|
|
8
|
+
const path = join(directory, `recovery-${createHash("sha256").update(sessionId).digest("hex").slice(0, 24)}.json`);
|
|
9
|
+
let data = { version: 1, sessionId, queue: null, receipts: {}, outcomes: [], instructions: [], transcript: [], totals: null };
|
|
10
|
+
if (resume) {
|
|
11
|
+
if (!existsSync(path)) throw new Error("No recovery checkpoint exists for this session. Start a separately reviewed continuation instead.");
|
|
12
|
+
data = JSON.parse(readFileSync(path, "utf8"));
|
|
13
|
+
if (data.version !== 1 || data.sessionId !== sessionId) throw new Error("Recovery checkpoint does not match this session.");
|
|
14
|
+
}
|
|
15
|
+
const save = patch => {
|
|
16
|
+
data = { ...data, ...patch };
|
|
17
|
+
writeFileSync(`${path}.tmp`, JSON.stringify(data), { mode: 0o600 });
|
|
18
|
+
renameSync(`${path}.tmp`, path);
|
|
19
|
+
};
|
|
20
|
+
return { get data() { return data; }, save, path };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function validHandoff(phase, value) {
|
|
24
|
+
const matches = (schema, v) => {
|
|
25
|
+
if (schema.type === "object") return !!v && typeof v === "object" && !Array.isArray(v)
|
|
26
|
+
&& (schema.required || []).every(k => k in v)
|
|
27
|
+
&& Object.entries(v).every(([k, item]) => schema.properties?.[k] ? matches(schema.properties[k], item) : schema.additionalProperties !== false);
|
|
28
|
+
if (schema.type === "array") return Array.isArray(v) && v.every(item => matches(schema.items, item));
|
|
29
|
+
if (schema.type === "string") return typeof v === "string" && (!schema.enum || schema.enum.includes(v));
|
|
30
|
+
if (schema.type === "integer") return Number.isInteger(v);
|
|
31
|
+
return true;
|
|
32
|
+
};
|
|
33
|
+
return !!PHASE_OUTPUT_SCHEMAS[phase] && matches(PHASE_OUTPUT_SCHEMAS[phase], value);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Conservative journal for externally visible commands. Exact repeats are
|
|
37
|
+
// refused; uncertain responses require reconciliation, never automatic replay.
|
|
38
|
+
export function externalActionKey(tool, input = {}) {
|
|
39
|
+
const command = String(input.command || "");
|
|
40
|
+
const external = tool.startsWith("mcp__") && /create|update|delete|transition|comment|post|send/i.test(tool) || (tool === "Bash" && (
|
|
41
|
+
/\bgh\s+(?:pr|issue)\s+(?:create|comment|edit|merge|close|reopen)\b/.test(command)
|
|
42
|
+
|| /\bcurl\b/.test(command) && /(?:--data|-d\b|-X\s*(?:POST|PUT|PATCH|DELETE)|--request\s+(?:POST|PUT|PATCH|DELETE))/.test(command)
|
|
43
|
+
));
|
|
44
|
+
if (!external) return null;
|
|
45
|
+
const canonical = value => Array.isArray(value) ? value.map(canonical)
|
|
46
|
+
: value && typeof value === "object" ? Object.fromEntries(Object.keys(value).sort().map(k => [k, canonical(value[k])])) : value;
|
|
47
|
+
// Bash descriptions/timeouts are presentation, not the external action.
|
|
48
|
+
return createHash("sha256").update(JSON.stringify([tool, tool === "Bash" ? command.trim() : canonical(input)])).digest("hex");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function journalExternalActions(options, recovery) {
|
|
52
|
+
const previousPre = options.hooks.PreToolUse[0].hooks[0];
|
|
53
|
+
options.hooks.PreToolUse[0].hooks[0] = async input => {
|
|
54
|
+
const decision = await previousPre(input);
|
|
55
|
+
if (decision.hookSpecificOutput?.permissionDecision === "deny") return decision;
|
|
56
|
+
const key = externalActionKey(input.tool_name, input.tool_input);
|
|
57
|
+
if (!key) return decision;
|
|
58
|
+
const receipt = recovery.data.receipts[key];
|
|
59
|
+
if (receipt) return { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny",
|
|
60
|
+
permissionDecisionReason: `This external action is recorded as ${receipt.state}. Do not repeat or reword it to bypass this guard. Inspect the provider to reconcile its result; preserve the existing PR/comment.` } };
|
|
61
|
+
recovery.save({ receipts: { ...recovery.data.receipts, [key]: { state: "pending", tool: input.tool_name, at: Date.now() } } });
|
|
62
|
+
return decision;
|
|
63
|
+
};
|
|
64
|
+
const previousPost = options.hooks.PostToolUse[0].hooks[0];
|
|
65
|
+
options.hooks.PostToolUse[0].hooks[0] = async input => {
|
|
66
|
+
const result = await previousPost(input);
|
|
67
|
+
const key = externalActionKey(input.tool_name, input.tool_input);
|
|
68
|
+
if (key && recovery.data.receipts[key]) {
|
|
69
|
+
// A tool returning is not proof of an external write succeeding. Only
|
|
70
|
+
// the provider-specific outcome parser can produce a confirmed receipt.
|
|
71
|
+
recovery.save({ receipts: { ...recovery.data.receipts, [key]: { ...recovery.data.receipts[key], state: "response-received" } } });
|
|
72
|
+
}
|
|
73
|
+
return result;
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// A PreToolUse matcher that lets only the SDK's schema tool through and denies
|
|
78
|
+
// everything else with the given reason. Shared by the engine's handoff repair
|
|
79
|
+
// and the opt-in SDK smoke tests, so the production guard and the diagnostics
|
|
80
|
+
// cannot drift apart.
|
|
81
|
+
export function structuredOutputOnlyHook(reason) {
|
|
82
|
+
return { hooks: [async input => input.tool_name === "StructuredOutput" ? {} : {
|
|
83
|
+
hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: reason },
|
|
84
|
+
}] };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function repairQueryOptions(options) {
|
|
88
|
+
return { ...options, agents: {}, agent: undefined, allowedTools: ["StructuredOutput"], tools: ["StructuredOutput"], mcpServers: {},
|
|
89
|
+
hooks: { PreToolUse: [structuredOutputOnlyHook("Handoff repair cannot execute tools.")] }, maxTurns: 2 };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function trackerAccessDenied(denials) {
|
|
93
|
+
return denials.some(d => /mcp__.*(?:jira|atlassian|linear|github)/i.test(d.tool_name || "")
|
|
94
|
+
|| d.tool_name === "Bash" && /\bcurl\b|\bgh\s+(?:issue|pr)\s+(?:view|list)\b/.test(d.tool_input?.command || ""));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function recoveryBrief(data, tree) {
|
|
98
|
+
return [
|
|
99
|
+
"## Authoritative task and user instructions",
|
|
100
|
+
`Original task: ${data.taskId || ""}\n${data.description || ""}`,
|
|
101
|
+
"Stay within this objective. Propose unrelated discoveries separately; do not expand scope to repair lost context.",
|
|
102
|
+
...data.instructions.map((i, n) => `${n + 1}. ${i.text}`),
|
|
103
|
+
"Latest user corrections override earlier plans. Acknowledge their effect before working.",
|
|
104
|
+
`Current revision: ${tree.revision || "unknown"}; working tree clean: ${tree.clean}. Preserve unfinished edits. Inspect the diff and existing PR before continuing. Prior verification must be re-established.`,
|
|
105
|
+
`Recorded external actions: ${JSON.stringify(data.receipts)}`,
|
|
106
|
+
`Confirmed provider outcomes: ${JSON.stringify(data.outcomes || [])}`,
|
|
107
|
+
"Do not repeat confirmed actions. A pending or response-received entry is uncertain: reconcile it against the provider first. Never reword commands to bypass the replay guard. Never claim an action failed merely because this phase cannot access the provider.",
|
|
108
|
+
].join("\n\n");
|
|
109
|
+
}
|
package/cli/engine/schemas.mjs
CHANGED
|
@@ -11,6 +11,33 @@ import { VERDICT_SCHEMA } from "./verdict.mjs";
|
|
|
11
11
|
|
|
12
12
|
const strList = { type: "array", items: { type: "string" } };
|
|
13
13
|
|
|
14
|
+
// What SUMMARY/SOLO hand back for the project lessons ledger. The engine
|
|
15
|
+
// records these with provenance; agents never write the ledger themselves.
|
|
16
|
+
const lessonList = {
|
|
17
|
+
type: "array",
|
|
18
|
+
description: "non-obvious things that cost this session time and would cost the next one too; empty when nothing qualifies",
|
|
19
|
+
items: {
|
|
20
|
+
type: "object",
|
|
21
|
+
additionalProperties: false,
|
|
22
|
+
required: ["text", "scope", "evidence"],
|
|
23
|
+
properties: {
|
|
24
|
+
text: { type: "string", description: "one actionable sentence" },
|
|
25
|
+
scope: { type: "string", description: "project | area:<ui|copy|docs|api|data> | path:<file or directory prefix>" },
|
|
26
|
+
evidence: { type: "string", description: "what happened that proves it (command and outcome)" },
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
const retireList = {
|
|
31
|
+
type: "array",
|
|
32
|
+
description: "ids from PROJECT LESSONS that proved wrong or obsolete in this session; empty when none did",
|
|
33
|
+
items: {
|
|
34
|
+
type: "object",
|
|
35
|
+
additionalProperties: false,
|
|
36
|
+
required: ["id", "reason"],
|
|
37
|
+
properties: { id: { type: "string" }, reason: { type: "string" } },
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
14
41
|
export const PHASE_OUTPUT_SCHEMAS = Object.freeze({
|
|
15
42
|
INTAKE: {
|
|
16
43
|
type: "object",
|
|
@@ -44,8 +71,8 @@ export const PHASE_OUTPUT_SCHEMAS = Object.freeze({
|
|
|
44
71
|
filesToModify: strList,
|
|
45
72
|
decisions: strList,
|
|
46
73
|
risks: strList,
|
|
47
|
-
assignments: { ...strList, description: "
|
|
48
|
-
steps: { ...strList, description: "ordered implementation steps" },
|
|
74
|
+
assignments: { ...strList, description: "each assignment names one owner, a bounded deliverable, dependencies, and acceptance evidence" },
|
|
75
|
+
steps: { ...strList, description: "ordered implementation steps with owners; implementation and the subsequent audit both happen in EXECUTION, with audit approval required before publishing" },
|
|
49
76
|
},
|
|
50
77
|
},
|
|
51
78
|
EXECUTION: {
|
|
@@ -65,25 +92,29 @@ export const PHASE_OUTPUT_SCHEMAS = Object.freeze({
|
|
|
65
92
|
SOLO: {
|
|
66
93
|
type: "object",
|
|
67
94
|
additionalProperties: false,
|
|
68
|
-
required: ["summary", "filesChanged", "prUrl", "deferred", "manualSteps"],
|
|
95
|
+
required: ["summary", "filesChanged", "prUrl", "deferred", "manualSteps", "lessons", "retireLessons"],
|
|
69
96
|
properties: {
|
|
70
97
|
summary: { type: "string" },
|
|
71
98
|
filesChanged: strList,
|
|
72
99
|
prUrl: { type: "string", description: "empty string if no PR was created" },
|
|
73
100
|
deferred: strList,
|
|
74
101
|
manualSteps: strList,
|
|
102
|
+
lessons: lessonList,
|
|
103
|
+
retireLessons: retireList,
|
|
75
104
|
},
|
|
76
105
|
},
|
|
77
106
|
SUMMARY: {
|
|
78
107
|
type: "object",
|
|
79
108
|
additionalProperties: false,
|
|
80
|
-
required: ["status", "prUrl", "deferred", "manualSteps", "summaryComment"],
|
|
109
|
+
required: ["status", "prUrl", "deferred", "manualSteps", "summaryComment", "lessons", "retireLessons"],
|
|
81
110
|
properties: {
|
|
82
|
-
status: { type: "string" },
|
|
111
|
+
status: { type: "string", description: "accurate delivery outcome, including any unresolved review or tracker-write blocker" },
|
|
83
112
|
prUrl: { type: "string" },
|
|
84
|
-
deferred: strList,
|
|
85
|
-
manualSteps: strList,
|
|
86
|
-
summaryComment: { type: "string", description: "the final tracker comment as posted" },
|
|
113
|
+
deferred: { ...strList, description: "only explicitly out-of-scope or user-approved deferrals; required work blocked on access belongs in status and manualSteps" },
|
|
114
|
+
manualSteps: { ...strList, description: "each remaining manual action with its named owner and completion evidence; include access recovery and the subsequent delivery retry when a tracker write failed; empty only when no manual action remains" },
|
|
115
|
+
summaryComment: { type: "string", description: "the final tracker comment exactly as confirmed posted; empty string if posting failed or was not confirmed" },
|
|
116
|
+
lessons: lessonList,
|
|
117
|
+
retireLessons: retireList,
|
|
87
118
|
},
|
|
88
119
|
},
|
|
89
120
|
});
|