@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.
@@ -12,12 +12,13 @@ Task: {{TASK_ID}}
12
12
 
13
13
  ## Your mission
14
14
 
15
- 1. Dictate the final summary in product terms. It must include:
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** — actions the developer must perform (migrations, config, deploys).
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, and the summary comment exactly as posted. Do not repeat the JSON object in chat; keep any closing chat message brief and human-readable.
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.
@@ -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, MEMORY_INSTRUCTIONS, loadProjectMemory } from "../prompt.mjs";
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, cwd, childStrategy,
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 += `\n\n${MEMORY_INSTRUCTIONS}`;
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, cwd, sessionMemory = "", retryVerdict = null, evidence = null, openItems = [],
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 += `\n\n${MEMORY_INSTRUCTIONS}`;
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
+ }
@@ -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: "who does what during execution" },
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
  });