@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
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,29 @@ All user-facing changes to AgentDesk. Each entry is tagged:
|
|
|
8
8
|
|
|
9
9
|
Internal refactors, infrastructure changes, and architectural notes are not listed here.
|
|
10
10
|
|
|
11
|
+
## [0.34.0] — 2026-09-21
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- `[CLI]` Project lessons with provenance. SUMMARY and solo sessions propose lessons with evidence and a scope; the engine records them in `.agentdesk/lessons.json` with the session, task, phase, agent and revision they came from. Lessons from a session that ended complete are active at once; lessons from an interrupted session stay proposed until a later complete session confirms them. Prompts receive the active lessons in scope, newest first, and the team can retire a lesson by id when it proves wrong. Solo sessions have no review gate, so their lessons stay proposed until a team session that ends complete confirms them.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- `[Both]` Jane now leads delivery explicitly: she sets priorities, assigns an owner and required evidence for each step, resolves disagreements, follows up on incomplete work, and carries decisions into the next phase. Her reports distinguish verified outcomes from blockers and unconfirmed tracker writes; unmet requirements cannot be silently deferred or described as ready.
|
|
18
|
+
- `[CLI]` `.agentdesk/memory.md` is no longer written by the team. It is still shown as read-only legacy notes.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- `[CLI]` Team and solo agents can now submit their structured handoffs. The required submission tool was missing from their allowed tool lists, so intake could finish its research and Jira startup comment but stop before implementation or a final tracker report. Handoff repair also has access to the submission tool without execution tools.
|
|
22
|
+
- `[CLI]` When the model exhausts its structured-output retries, a solo session now ends as a handoff instead of reporting itself complete without a summary, and a review gets the same one-shot repair as the other phases instead of sending the team back into execution blindly. The underlying error is reported when repair fails too.
|
|
23
|
+
- `[CLI]` Issue label updates on GitHub are best-effort again: a missing label is not reported as a failed tracker write.
|
|
24
|
+
|
|
25
|
+
## [0.33.0] — 2026-09-21
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
- `[Both]` New dashboard sessions support pausing, saving task corrections, and resuming the same conversation and workspace after workers stop. Saved instructions and recovery status survive server restarts; the daemon retains the task checkpoint and checks access again before continuing.
|
|
29
|
+
- `[UI]` A recovery panel explains access failures, shows when intervention is needed, and offers instructions for reconnecting the model provider.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
- `[CLI]` Missing phase handoffs get one repair attempt from saved findings with execution tools disabled. Failed recovery preserves the work for intervention. Recognized external writes are recorded before dispatch, and exact repeats are blocked during continuation.
|
|
33
|
+
|
|
11
34
|
## [0.32.1] — 2026-09-21
|
|
12
35
|
|
|
13
36
|
### Fixed
|
package/README.md
CHANGED
|
@@ -46,7 +46,7 @@ AgentDesk spawns a team of AI agents inside Claude Code that collaborate on your
|
|
|
46
46
|
|
|
47
47
|
| Agent | Role |
|
|
48
48
|
|-------|------|
|
|
49
|
-
| Jane |
|
|
49
|
+
| Jane | Team Lead — owns priorities and delivery, assigns work, challenges evidence, and resolves disagreements |
|
|
50
50
|
| Dennis | Senior Developer — implements the solution |
|
|
51
51
|
| Sam | Architecture Auditor — code structure, separation of concerns |
|
|
52
52
|
| Bart | QA Engineer — edge cases, risks, acceptance criteria, screenshots |
|
|
@@ -288,14 +288,32 @@ When working on Jira or Linear tasks, agents automatically download and review a
|
|
|
288
288
|
|
|
289
289
|
### Handoff & Resume
|
|
290
290
|
|
|
291
|
-
|
|
291
|
+
The phase lead submits each handoff through the SDK's `StructuredOutput` tool. It must be included in both the selected agent's tool list and the unattended tool permissions; chat text alone cannot advance the phase. To check this against the real SDK after changes to tool configuration, run `npm run test:sdk-handoff`. This opt-in diagnostic requires a Claude login and uses model tokens. It checks every phase, solo mode, and handoff repair with fixed fixtures and execution tools blocked.
|
|
292
|
+
|
|
293
|
+
Run `npm run test:sdk-leadership` to check Jane's decisions against supplied reports: a superseded scope deferral, a completion claim missing visual evidence, and a failed Jira comment. This opt-in behavioral check also requires a Claude login and uses model tokens; delegation and external actions are blocked. It checks decisions and named follow-up owners using the production prompts.
|
|
294
|
+
|
|
295
|
+
Sessions started from the dashboard with an updated daemon can be paused and resumed in the same conversation and workspace. Use **Pause / check workers**, or enter a correction and choose **Pause & save**. Instructions are saved immediately; **Verify & resume** becomes available after the previous workers have stopped and released the workspace. A quarantined workspace remains blocked until its workers exit.
|
|
296
|
+
|
|
297
|
+
Resume restores the original task, discovered tracker task ID, pending phase, saved findings, user corrections, token totals, and recorded external actions. Corrections trigger replanning against the existing work. Access is checked again before continuing; a failed login check preserves both the checkpoint and the pending correction. For a revoked model login, reconnect Claude on the daemon computer using the same account profile, then choose **Verify & resume**. Session controls require the session owner and a connected daemon that supports recovery; older sessions need a separate continuation.
|
|
298
|
+
|
|
299
|
+
A missing phase handoff gets one repair attempt using the preserved findings with execution tools disabled. If repair fails, or required tracker access is denied during intake or planning, work pauses for intervention. Recognized external write commands are fingerprinted before dispatch and exact repeats are blocked. An uncertain result must be reconciled with the provider before continuing. This is a conservative replay guard, not general deduplication of equivalent commands or arbitrary scripts.
|
|
300
|
+
|
|
301
|
+
The recovery checkpoint lives on the daemon computer. The dashboard retains instructions and recovery status across server restarts, but cannot resume without the local checkpoint and workspace. It never silently creates a new task when a required checkpoint is missing.
|
|
302
|
+
|
|
303
|
+
Terminal sessions also save a resume snapshot (`.agentdesk-resume.md`) when interrupted. To start a continuation from the terminal, use the command printed in that snapshot, including `--resume-worktree` when present:
|
|
292
304
|
|
|
293
305
|
```bash
|
|
294
306
|
# Session hits limit → "HANDOFF" shown in terminal
|
|
295
|
-
#
|
|
296
|
-
agentdesk team KEN-517
|
|
307
|
+
# Continue in the retained workspace:
|
|
308
|
+
agentdesk team KEN-517 --resume-worktree SESSION-ID
|
|
297
309
|
```
|
|
298
310
|
|
|
311
|
+
### Project lessons
|
|
312
|
+
|
|
313
|
+
The team's cross-session memory is an engine-owned ledger, `.agentdesk/lessons.json` in the project directory (local, gitignored, shared by every session worktree). At the end of a session the SUMMARY handoff (or the solo agent's) may propose lessons — a setup step, seed data, an environment quirk — each with evidence and a scope (`project`, `area:<ui|copy|docs|api|data>`, or `path:<prefix>`). The engine records them with where they came from (session, task, phase, agent, revision). A lesson from a session that ended **complete** (verified approval) is active immediately; one from a session that ended in handoff stays proposed until a later complete session proposes the same lesson. Solo sessions have no review gate, so their lessons stay proposed until a team session that ends complete confirms them. Every phase prompt receives the active lessons in scope, newest first (at most 30). A team that finds a lesson wrong or obsolete retires it by id in the same handoff; retired lessons are kept for the record and never shown again.
|
|
314
|
+
|
|
315
|
+
`.agentdesk/memory.md`, the hand-written notes file from earlier versions, is still shown to the team as read-only legacy notes but is no longer written.
|
|
316
|
+
|
|
299
317
|
### Session protocol
|
|
300
318
|
|
|
301
319
|
At the end of each session, Jane posts a structured summary on the tracker covering:
|
package/cli/agents.mjs
CHANGED
|
@@ -3,16 +3,16 @@
|
|
|
3
3
|
export const BUILT_IN_AGENTS = {
|
|
4
4
|
Jane: {
|
|
5
5
|
badge: "●● JANE ●●",
|
|
6
|
-
role: "
|
|
7
|
-
description: "
|
|
8
|
-
groundRules: "Jane
|
|
9
|
-
planning: "
|
|
6
|
+
role: "Team Lead",
|
|
7
|
+
description: "owns delivery against the user's requirements, sets priorities, makes decisions, assigns work, challenges unsupported reports, resolves disagreements, and decomposes large tasks into accountable steps",
|
|
8
|
+
groundRules: "Jane owns the outcome: she sets acceptance criteria, decomposes work, assigns an owner and expected evidence, and follows up until each requirement is verified or explicitly blocked. The latest user correction takes precedence over earlier plans and agent-proposed deferrals. She makes routine decisions within the authorized scope and asks the user only for a consequential choice or missing information the team cannot resolve. In team mode she delegates implementation and tracker operations; she does not edit code or run commands herself. She can discuss technical evidence, file references and tradeoffs when needed to judge a report. She never overrides a failed check or required review gate, invents evidence, or treats an agent's unsupported claim as completion. She keeps unrelated improvements out of scope and reports unresolved blockers honestly.",
|
|
9
|
+
planning: "Set the delivery target and acceptance criteria; choose the approach from the team's evidence; resolve tradeoffs; assign owners, dependencies and verification for each step.",
|
|
10
10
|
execution: {
|
|
11
|
-
step: "Jane
|
|
11
|
+
step: "Jane leads delivery and closes the loop",
|
|
12
12
|
tasks: [
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
13
|
+
"Track each acceptance criterion, its owner, and the evidence still owed; send incomplete work back with a specific next action.",
|
|
14
|
+
"Resolve disagreements using requirements, project conventions and observations; preserve all required audit and verification gates.",
|
|
15
|
+
"Dictate an accurate final tracker summary and verify the posting receipt; distinguish delivered, unverified, blocked and user-approved deferred work.",
|
|
16
16
|
],
|
|
17
17
|
order: 99,
|
|
18
18
|
},
|
package/cli/daemon.mjs
CHANGED
|
@@ -17,6 +17,7 @@ import { fileURLToPath } from "url";
|
|
|
17
17
|
import { dirname } from "path";
|
|
18
18
|
import { repositoryInfo, listWorkspaces, findWorkspacesByTask, inspectWorkspace, cleanupWorkspace, recoverWorkspaceLocks } from "./worktrees.mjs";
|
|
19
19
|
import { createSessionQueue, sessionLimit, confirmWithAbort } from "./session-queue.mjs";
|
|
20
|
+
import { classifyFailure } from "../shared/recovery.mjs";
|
|
20
21
|
|
|
21
22
|
const __dirname_daemon = dirname(fileURLToPath(import.meta.url));
|
|
22
23
|
const DAEMON_VERSION = JSON.parse(readFileSync(join(__dirname_daemon, "../package.json"), "utf-8")).version;
|
|
@@ -280,17 +281,20 @@ export async function runDaemon() {
|
|
|
280
281
|
let confirmationQueue = Promise.resolve();
|
|
281
282
|
const maxSessions = sessionLimit();
|
|
282
283
|
const sessionQueue = createSessionQueue({ limit: maxSessions, run: handleStartSession,
|
|
283
|
-
onQueued: job => sendBuffered(job.sessionId, { type: "session:update", status: "queued" }),
|
|
284
|
-
|
|
284
|
+
onQueued: job => sendBuffered(job.sessionId, { type: "session:update", status: "queued", runId: job.runId }),
|
|
285
|
+
onSettled: job => {
|
|
286
|
+
if (job.recoveryCompletion) sendBuffered(job.sessionId, { type: "session:recovery", runId: job.runId, recovery: job.recoveryCompletion });
|
|
287
|
+
},
|
|
288
|
+
onError: job => send({ type: "daemon:error", sessionId: job.sessionId, runId: job.runId, error: "Unable to start queued session" }),
|
|
285
289
|
});
|
|
286
290
|
function enqueueSession(job) {
|
|
287
291
|
const project = projects.find(p => p.id === job.projectId);
|
|
288
|
-
if (!project) { send({ type: "daemon:error", sessionId: job.sessionId, error: "Unknown project" }); return; }
|
|
292
|
+
if (!project) { send({ type: "daemon:error", sessionId: job.sessionId, runId: job.runId, error: "Unknown project" }); return; }
|
|
289
293
|
try {
|
|
290
294
|
let key = job.workspace?.resumeId ? `workspace:${job.workspace.resumeId}` : null;
|
|
291
295
|
if (job.workspace?.enabled === false) key = `repo:${repositoryInfo(project.path).repo}`;
|
|
292
296
|
sessionQueue.enqueue({ ...job, key });
|
|
293
|
-
} catch (error) { send({ type: "daemon:error", sessionId: job.sessionId, error: error.message }); }
|
|
297
|
+
} catch (error) { send({ type: "daemon:error", sessionId: job.sessionId, runId: job.runId, error: error.message }); }
|
|
294
298
|
}
|
|
295
299
|
|
|
296
300
|
function send(data) {
|
|
@@ -332,6 +336,7 @@ export async function runDaemon() {
|
|
|
332
336
|
type: "daemon:connect",
|
|
333
337
|
version: DAEMON_VERSION,
|
|
334
338
|
worktrees: true,
|
|
339
|
+
recovery: true,
|
|
335
340
|
maxSessions,
|
|
336
341
|
projects: projects.map(p => ({ id: p.id, name: p.name })),
|
|
337
342
|
});
|
|
@@ -366,6 +371,13 @@ export async function runDaemon() {
|
|
|
366
371
|
if (msg.type === "daemon:cancel-session") {
|
|
367
372
|
handleCancelSession(msg);
|
|
368
373
|
}
|
|
374
|
+
if (msg.type === "daemon:pause-session") {
|
|
375
|
+
try { handlePauseSession(msg); }
|
|
376
|
+
catch {
|
|
377
|
+
sendBuffered(msg.sessionId, { type: "session:recovery", runId: msg.runId,
|
|
378
|
+
recovery: { state: "paused", kind: "restart", ready: false, message: "Could not verify workspace ownership. Check the daemon and try checking workers again." } });
|
|
379
|
+
}
|
|
380
|
+
}
|
|
369
381
|
if (msg.type === "daemon:workspace-request") handleWorkspaceRequest(msg);
|
|
370
382
|
});
|
|
371
383
|
|
|
@@ -453,12 +465,13 @@ async function confirmIncomingSession({ project, taskId, prompt, signal }) {
|
|
|
453
465
|
}
|
|
454
466
|
}
|
|
455
467
|
|
|
456
|
-
async function handleStartSession(
|
|
468
|
+
async function handleStartSession(job) {
|
|
469
|
+
const { sessionId, projectId, taskId: remoteTaskId, prompt, screenshots: screenshotsOverride, workspace, runId, resumeSession, instructions = [] } = job;
|
|
457
470
|
// Validate project against local allowlist
|
|
458
471
|
const project = projects.find(p => p.id === projectId);
|
|
459
472
|
if (!project) {
|
|
460
473
|
console.log(` ${red}Rejected:${reset} unknown project ${dim}${projectId}${reset}`);
|
|
461
|
-
send({ type: "daemon:error", sessionId, error: `Project "${projectId}" not registered with this daemon` });
|
|
474
|
+
send({ type: "daemon:error", sessionId, runId, error: `Project "${projectId}" not registered with this daemon` });
|
|
462
475
|
return;
|
|
463
476
|
}
|
|
464
477
|
|
|
@@ -471,6 +484,7 @@ async function confirmIncomingSession({ project, taskId, prompt, signal }) {
|
|
|
471
484
|
if (activeSessions.has(sessionId)) return;
|
|
472
485
|
const activeSession = {
|
|
473
486
|
sessionId, projectId,
|
|
487
|
+
runId,
|
|
474
488
|
child: null,
|
|
475
489
|
// Cancelling must stop the whole phase pipeline, not just the child that
|
|
476
490
|
// happens to be running: killing one phase's child would otherwise let
|
|
@@ -531,6 +545,8 @@ async function confirmIncomingSession({ project, taskId, prompt, signal }) {
|
|
|
531
545
|
sessionId,
|
|
532
546
|
cwd: project.path,
|
|
533
547
|
workspace,
|
|
548
|
+
resumeSession, instructions,
|
|
549
|
+
onWorkspace: record => { activeSession.workspaceId = record?.id; },
|
|
534
550
|
apiKey,
|
|
535
551
|
serverUrl: agentdeskServer,
|
|
536
552
|
abortSignal: sessionAbort.signal,
|
|
@@ -564,6 +580,10 @@ async function confirmIncomingSession({ project, taskId, prompt, signal }) {
|
|
|
564
580
|
}
|
|
565
581
|
|
|
566
582
|
return (event) => {
|
|
583
|
+
event = { ...event, runId };
|
|
584
|
+
if (event.type === "session:start") activeSession.startedExecution = true;
|
|
585
|
+
if (event.type === "session:recovery") activeSession.recovery = event.recovery;
|
|
586
|
+
if (event.type === "session:workspace" && event.quarantined) activeSession.quarantined = true;
|
|
567
587
|
// Confirmed accomplishments may arrive during cancellation teardown.
|
|
568
588
|
// They do not revive the session or emit a second terminal event.
|
|
569
589
|
if (["session:outcome", "session:workspace", "session:usage"].includes(event.type)) { sendBuffered(sessionId, event); return; }
|
|
@@ -594,6 +614,7 @@ async function confirmIncomingSession({ project, taskId, prompt, signal }) {
|
|
|
594
614
|
};
|
|
595
615
|
})(),
|
|
596
616
|
});
|
|
617
|
+
activeSession.quarantined ||= result.quarantined;
|
|
597
618
|
|
|
598
619
|
const outcome = result.status || (result.handoff ? "handoff" : "complete");
|
|
599
620
|
const clean = outcome === "complete";
|
|
@@ -616,10 +637,42 @@ async function confirmIncomingSession({ project, taskId, prompt, signal }) {
|
|
|
616
637
|
} catch (err) {
|
|
617
638
|
console.log(` ${red}Failed to start session:${reset} ${err.message}`);
|
|
618
639
|
if (activeSession.cancelled) return;
|
|
640
|
+
activeSession.recovery = classifyFailure(err.message);
|
|
619
641
|
// Send generic error to server — don't leak internal details (paths, config, etc.)
|
|
620
|
-
send({ type: "daemon:error", sessionId, error: "Failed to start session on daemon" });
|
|
642
|
+
send({ type: "daemon:error", sessionId, runId, error: "Failed to start session on daemon" });
|
|
621
643
|
} finally {
|
|
622
644
|
if (activeSessions.get(sessionId) === activeSession) activeSessions.delete(sessionId);
|
|
645
|
+
if (activeSession.paused || activeSession.recovery?.state !== "running" && activeSession.recovery) {
|
|
646
|
+
job.recoveryCompletion = activeSession.paused
|
|
647
|
+
? { state: "paused", kind: activeSession.startedExecution || resumeSession ? "user" : "queued", message: "Work preserved. Your saved instructions will be applied when you resume." }
|
|
648
|
+
: activeSession.recovery;
|
|
649
|
+
job.recoveryCompletion = { ...job.recoveryCompletion, ready: !activeSession.quarantined };
|
|
650
|
+
if (activeSession.quarantined) job.recoveryCompletion.message = "Waiting for previous workers to exit. The workspace is quarantined; it cannot be resumed yet.";
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function handlePauseSession({ sessionId, runId, workspaceId }) {
|
|
656
|
+
const job = sessionQueue.get(sessionId);
|
|
657
|
+
if (job && job.runId !== runId) return;
|
|
658
|
+
const session = activeSessions.get(sessionId);
|
|
659
|
+
if (session && session.runId !== runId) return;
|
|
660
|
+
if (session) {
|
|
661
|
+
session.paused = true;
|
|
662
|
+
session.cancelled = true;
|
|
663
|
+
stopSessionWork(session);
|
|
664
|
+
} else if (sessionQueue.cancel(sessionId)) {
|
|
665
|
+
sendBuffered(sessionId, { type: "session:recovery", runId, recovery: { state: "paused", kind: job?.resumeSession ? "user" : "queued", ready: true,
|
|
666
|
+
message: "Paused before starting. Resume to begin with your saved instructions." } });
|
|
667
|
+
} else {
|
|
668
|
+
// After a daemon restart, no local worker owns the session. Workspace
|
|
669
|
+
// acquisition on resume still checks surviving child PIDs and leases.
|
|
670
|
+
recoverWorkspaceLocks();
|
|
671
|
+
const record = workspaceId ? listWorkspaces({ projectPaths: projects.map(p => p.path) }).find(r => r.id === workspaceId) : null;
|
|
672
|
+
const busy = sessionQueue.activeIds.includes(sessionId) || (record ? inspectWorkspace(record).active : !!workspaceId);
|
|
673
|
+
if (record && !busy) sendBuffered(sessionId, { type: "session:workspace", runId, quarantined: false });
|
|
674
|
+
sendBuffered(sessionId, { type: "session:recovery", runId, recovery: { state: "paused", kind: "restart", ready: !busy,
|
|
675
|
+
message: busy ? "The workspace is still busy or unavailable. Check workers again after they exit." : "No worker is running on this daemon. Resume will check the saved workspace lease and checkpoint." } });
|
|
623
676
|
}
|
|
624
677
|
}
|
|
625
678
|
|
|
@@ -633,8 +686,9 @@ async function confirmIncomingSession({ project, taskId, prompt, signal }) {
|
|
|
633
686
|
}
|
|
634
687
|
|
|
635
688
|
function handleCancelSession({ sessionId }) {
|
|
689
|
+
const job = sessionQueue.get(sessionId);
|
|
636
690
|
if (sessionQueue.cancel(sessionId)) {
|
|
637
|
-
sendBuffered(sessionId, { type: "session:end", status: "stopped", duration: "0s" });
|
|
691
|
+
sendBuffered(sessionId, { type: "session:end", runId: job?.runId, status: "stopped", duration: "0s" });
|
|
638
692
|
return;
|
|
639
693
|
}
|
|
640
694
|
const activeSession = activeSessions.get(sessionId);
|
|
@@ -649,7 +703,7 @@ async function confirmIncomingSession({ project, taskId, prompt, signal }) {
|
|
|
649
703
|
|
|
650
704
|
// The `cancelled` flag also suppresses the orchestrator's own
|
|
651
705
|
// session:end, so this stays the single terminal event for the session.
|
|
652
|
-
sendBuffered(sessionId, { type: "session:end", duration, steps: 0, inputTokens: 0, outputTokens: 0, status: "stopped" });
|
|
706
|
+
sendBuffered(sessionId, { type: "session:end", runId: activeSession.runId, duration, steps: 0, inputTokens: 0, outputTokens: 0, status: "stopped" });
|
|
653
707
|
}
|
|
654
708
|
}
|
|
655
709
|
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// The team (cli/agents.mjs BUILT_IN_AGENTS + project customisations via
|
|
4
4
|
// resolveTeam) becomes, per phase:
|
|
5
5
|
// - one lead definition — Jane — that the main thread runs *as* (Options.agent),
|
|
6
|
-
// with
|
|
7
|
-
// tools do not exist in her session.
|
|
6
|
+
// with Agent for delegation and StructuredOutput for the phase handoff.
|
|
7
|
+
// She cannot touch code because those tools do not exist in her session.
|
|
8
8
|
// - one AgentDefinition per engineering role, with the tool list that role
|
|
9
9
|
// needs in that phase and nothing more.
|
|
10
10
|
//
|
|
@@ -104,16 +104,23 @@ export function leadSystemPrompt(roster, phase) {
|
|
|
104
104
|
const lines = Object.entries(roster).map(([n, d]) => rosterLine(n, d));
|
|
105
105
|
return [
|
|
106
106
|
`You are ${LEAD}, ${jane.role}. ${jane.description}.`,
|
|
107
|
+
`Leadership responsibilities: ${jane.groundRules}`,
|
|
107
108
|
"",
|
|
108
|
-
"You coordinate; you do not build.
|
|
109
|
+
"You coordinate; you do not build. Use Agent to delegate to the team and StructuredOutput to submit the phase handoff when the work is done. You never read files, run commands, or edit anything yourself; when you need technical information, ask an agent for it.",
|
|
110
|
+
"Finish by calling StructuredOutput with the required schema. A closing chat message alone does not submit the handoff. Do not repeat the JSON in chat.",
|
|
109
111
|
"Always delegate with run_in_background: false. A backgrounded agent's own tool calls cannot be approved, so it will be unable to do anything.",
|
|
110
|
-
"
|
|
112
|
+
"Explain decisions in plain language tied to the user's outcome. Use technical details when they help assign work, challenge a claim, or explain a tradeoff.",
|
|
111
113
|
"",
|
|
112
114
|
`Team available in phase ${phase}:`,
|
|
113
115
|
...lines,
|
|
114
116
|
"",
|
|
115
|
-
"
|
|
116
|
-
"When
|
|
117
|
+
"Start each phase by stating its objective, the outstanding acceptance criteria, and who owns the next action. On a resumed task, work from the existing implementation and latest user feedback; reopen only what that feedback or current evidence requires.",
|
|
118
|
+
"When you delegate, name one owner and provide the objective, relevant context and user corrections, scope boundaries, dependencies, and the exact evidence needed to accept the result. Agents start with no memory of this conversation. Use only agents available in this phase.",
|
|
119
|
+
"When an agent reports, assess it against the assignment. Accept with evidence, return a specific gap to its owner, or identify a real blocker. Follow each assessment with your decision and the next action; do not merely relay the report. Never claim a result you did not receive or treat a proposed action as an executed one.",
|
|
120
|
+
"Resolve disagreements by comparing observations with the user's requirements and project conventions. Request a focused check when facts conflict, then make the call and explain why. An unsupported rejection needs evidence; a valid blocking finding must be fixed and rechecked, never voted away. Do not re-run broad discovery when a targeted check will settle the issue.",
|
|
121
|
+
"Keep a concise account of requirements, owners, accepted evidence and unresolved blockers in the phase's existing handoff fields. Carry your decisions and next actions forward. A prior agent's deferral does not authorize skipping a current user requirement; do not silently expand scope either.",
|
|
122
|
+
"If blocked, assign a bounded diagnostic or recovery step to an available agent. If it requires user access or a consequential scope decision, state what is blocked, what was tried, and the smallest input needed; never weaken acceptance criteria to call the task done. Continue independent authorized work only where the phase's gates permit it.",
|
|
123
|
+
"Report meaningful decisions, resolved blockers, and the next owner/action concisely. Avoid repeating long agent reports, narrating every delegation, or requesting permission for routine work already authorized.",
|
|
117
124
|
"Announce a new task id, if you create one, on its own line as `TASK_ID: <id>`, and a short session title as `SESSION_TITLE: <title>`.",
|
|
118
125
|
].join("\n");
|
|
119
126
|
}
|
|
@@ -124,10 +131,10 @@ export function leadSystemPrompt(roster, phase) {
|
|
|
124
131
|
export function soloDefinition(agent) {
|
|
125
132
|
const def = {
|
|
126
133
|
description: `${agent.role}: ${agent.description}`,
|
|
127
|
-
prompt: `You are ${agent.name}, ${agent.role}. You work alone on the task given to you, end to end.`,
|
|
128
|
-
tools: [...FULL],
|
|
134
|
+
prompt: `You are ${agent.name}, ${agent.role}. You work alone on the task given to you, end to end. Finish by calling StructuredOutput with the required schema; a chat message alone does not submit the handoff.`,
|
|
135
|
+
tools: [...FULL, "StructuredOutput"],
|
|
129
136
|
};
|
|
130
|
-
return { agents: { [agent.name]: def }, allowedTools: [...
|
|
137
|
+
return { agents: { [agent.name]: def }, allowedTools: [...def.tools], lead: agent.name };
|
|
131
138
|
}
|
|
132
139
|
|
|
133
140
|
// team — resolveTeam(config) output (array of { name, role, description, ... })
|
|
@@ -168,7 +175,9 @@ export function agentsForPhase({ phase, team, phaseModels = {}, profile = null }
|
|
|
168
175
|
sdkAgents[LEAD] = {
|
|
169
176
|
description: `${BUILT_IN_AGENTS.Jane.role}: ${BUILT_IN_AGENTS.Jane.description}`,
|
|
170
177
|
prompt: leadSystemPrompt(agents, phase),
|
|
171
|
-
tools
|
|
178
|
+
// The SDK filters even its schema tool against the selected agent's tools.
|
|
179
|
+
// Without this, outputFormat yields success with no structured_output.
|
|
180
|
+
tools: ["Agent", "StructuredOutput"],
|
|
172
181
|
};
|
|
173
182
|
|
|
174
183
|
const allowed = new Set(["Agent"]);
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// Project lessons with provenance — an engine-owned ledger.
|
|
2
|
+
//
|
|
3
|
+
// `.agentdesk/memory.md` was free text the agents edited themselves: no
|
|
4
|
+
// source, no scope, no way to correct or retire an entry, and in a worktree
|
|
5
|
+
// session it was read from the worktree, where it never existed. This ledger
|
|
6
|
+
// lives in the *source* project directory (shared by every worktree; local,
|
|
7
|
+
// gitignored). Agents never write it: SUMMARY and SOLO propose lessons and
|
|
8
|
+
// retirements in their structured handoff, the engine records them with
|
|
9
|
+
// where they came from, and the engine's own verdict on the session decides
|
|
10
|
+
// whether a lesson is `active` (session ended `complete`) or only `proposed`.
|
|
11
|
+
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { dirname, join } from "node:path";
|
|
14
|
+
import { randomBytes } from "node:crypto";
|
|
15
|
+
import { TOUCH_AREAS } from "./team-profile.mjs";
|
|
16
|
+
import { shortRev } from "./evidence.mjs";
|
|
17
|
+
|
|
18
|
+
export const LESSON_STATUSES = Object.freeze(["proposed", "active", "retired"]);
|
|
19
|
+
export const MAX_LESSONS = 500; // ledger size; oldest retired, then oldest proposed, go first
|
|
20
|
+
export const MAX_LESSON_TEXT = 1000; // stored text
|
|
21
|
+
export const MAX_LESSON_NOTE = 500; // stored evidence / retirement reason
|
|
22
|
+
export const INJECT_LIMIT = 30; // entries per prompt
|
|
23
|
+
export const INJECT_TEXT_LIMIT = 300; // characters per entry in a prompt
|
|
24
|
+
|
|
25
|
+
export function lessonsPath(sourceCwd) {
|
|
26
|
+
return join(sourceCwd, ".agentdesk", "lessons.json");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const EMPTY = () => ({ version: 1, lessons: [] });
|
|
30
|
+
|
|
31
|
+
export function readLessons(path) {
|
|
32
|
+
try {
|
|
33
|
+
if (!existsSync(path)) return EMPTY();
|
|
34
|
+
const data = JSON.parse(readFileSync(path, "utf8"));
|
|
35
|
+
if (!data || data.version !== 1 || !Array.isArray(data.lessons)) return { version: 1, lessons: [], unreadable: true };
|
|
36
|
+
return {
|
|
37
|
+
version: 1,
|
|
38
|
+
lessons: data.lessons
|
|
39
|
+
.filter(l => l && typeof l.id === "string" && typeof l.text === "string")
|
|
40
|
+
.map(l => ({
|
|
41
|
+
...l,
|
|
42
|
+
confirmedBy: Array.isArray(l.confirmedBy) ? l.confirmedBy.filter(s => typeof s === "string") : [],
|
|
43
|
+
status: LESSON_STATUSES.includes(l.status) ? l.status : "proposed",
|
|
44
|
+
scope: normalizeScope(l.scope),
|
|
45
|
+
evidence: typeof l.evidence === "string" ? l.evidence : "",
|
|
46
|
+
createdAt: typeof l.createdAt === "string" ? l.createdAt : "",
|
|
47
|
+
source: l.source && typeof l.source === "object" ? l.source : {},
|
|
48
|
+
})),
|
|
49
|
+
};
|
|
50
|
+
} catch { return { version: 1, lessons: [], unreadable: true }; }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function writeLessons(path, data) {
|
|
54
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
55
|
+
const tmp = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
56
|
+
writeFileSync(tmp, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
57
|
+
renameSync(tmp, path);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// "project" | "area:<ui|copy|docs|api|data>" | "path:<relative prefix>".
|
|
61
|
+
// Anything else widens to "project" — the safe direction for a hint.
|
|
62
|
+
export function normalizeScope(scope) {
|
|
63
|
+
const s = String(scope ?? "").trim();
|
|
64
|
+
if (s === "project") return s;
|
|
65
|
+
const area = s.match(/^area:([a-z]+)$/);
|
|
66
|
+
if (area) return TOUCH_AREAS.includes(area[1]) ? s : "project";
|
|
67
|
+
const path = s.match(/^path:(.+)$/);
|
|
68
|
+
if (path) {
|
|
69
|
+
const prefix = path[1].trim().replace(/^\.\//, "");
|
|
70
|
+
if (prefix && prefix.length <= 200 && !prefix.startsWith("/") && !prefix.split("/").includes("..")) return `path:${prefix}`;
|
|
71
|
+
}
|
|
72
|
+
return "project";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const clean = (text, max) => String(text ?? "").replace(/\s+/g, " ").trim().slice(0, max);
|
|
76
|
+
const textKey = text => clean(text, MAX_LESSON_TEXT).toLowerCase();
|
|
77
|
+
const newId = () => randomBytes(4).toString("hex");
|
|
78
|
+
const byAge = (a, b) => String(a.createdAt).localeCompare(String(b.createdAt));
|
|
79
|
+
|
|
80
|
+
// When the ledger outgrows MAX_LESSONS, drop the oldest retired entries, then
|
|
81
|
+
// the oldest proposed ones. Active lessons are never dropped here.
|
|
82
|
+
function bounded(list) {
|
|
83
|
+
const keep = [...list];
|
|
84
|
+
for (const status of ["retired", "proposed"]) {
|
|
85
|
+
while (keep.length > MAX_LESSONS) {
|
|
86
|
+
const victim = keep.filter(l => l.status === status).sort(byAge)[0];
|
|
87
|
+
if (!victim) break;
|
|
88
|
+
keep.splice(keep.indexOf(victim), 1);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return keep;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Apply a single proposal to the lessons list, mutating list and counts.
|
|
95
|
+
function applyProposal({ list, proposal, source, complete, at, counts }) {
|
|
96
|
+
const text = clean(proposal?.text, MAX_LESSON_TEXT);
|
|
97
|
+
if (!text) return;
|
|
98
|
+
const key = textKey(text);
|
|
99
|
+
const existing = list.find(l => l.status !== "retired" && textKey(l.text) === key);
|
|
100
|
+
if (existing) {
|
|
101
|
+
if (!complete) return; // an unverified session cannot confirm anything
|
|
102
|
+
if (!existing.confirmedBy.includes(source.sessionId)) existing.confirmedBy.push(source.sessionId);
|
|
103
|
+
if (existing.status === "proposed") { existing.status = "active"; counts.activated++; } else counts.confirmed++;
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
list.push({
|
|
107
|
+
id: newId(), text, scope: normalizeScope(proposal?.scope), evidence: clean(proposal?.evidence, MAX_LESSON_NOTE),
|
|
108
|
+
source: { sessionId: source.sessionId, taskId: source.taskId || null, phase: source.phase, agent: source.agent, revision: source.revision || null },
|
|
109
|
+
createdAt: at, status: complete ? "active" : "proposed", confirmedBy: complete ? [source.sessionId] : [],
|
|
110
|
+
});
|
|
111
|
+
if (complete) counts.activated++; else counts.proposed++;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Apply a single retirement to the lessons list, mutating list and counts.
|
|
115
|
+
function applyRetirement({ list, retirement, source, at, counts }) {
|
|
116
|
+
const index = list.findIndex(l => l.id === retirement?.id);
|
|
117
|
+
if (index < 0 || list[index].status === "retired") return;
|
|
118
|
+
if (list[index].source?.sessionId === source.sessionId) { list.splice(index, 1); counts.dropped++; return; }
|
|
119
|
+
list[index] = { ...list[index], status: "retired", retired: { sessionId: source.sessionId, reason: clean(retirement?.reason, MAX_LESSON_NOTE), at } };
|
|
120
|
+
counts.retired++;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Records what a session's SUMMARY/SOLO handed back. `complete` is the
|
|
124
|
+
// engine's verdict on the whole session (verified approval), not the agents'.
|
|
125
|
+
export function recordLessons({ path, proposals = [], retirements = [], source, complete = false, now = () => new Date().toISOString() }) {
|
|
126
|
+
const data = readLessons(path);
|
|
127
|
+
if (data.unreadable) throw new Error("lessons.json exists but is not a version-1 ledger; not overwriting it");
|
|
128
|
+
const list = data.lessons;
|
|
129
|
+
const at = now();
|
|
130
|
+
const counts = { activated: 0, proposed: 0, confirmed: 0, retired: 0, dropped: 0 };
|
|
131
|
+
|
|
132
|
+
for (const p of Array.isArray(proposals) ? proposals : []) {
|
|
133
|
+
applyProposal({ list, proposal: p, source, complete, at, counts });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
for (const r of Array.isArray(retirements) ? retirements : []) {
|
|
137
|
+
applyRetirement({ list, retirement: r, source, at, counts });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const changed = Object.values(counts).some(n => n > 0);
|
|
141
|
+
data.lessons = bounded(list);
|
|
142
|
+
if (changed) writeLessons(path, data);
|
|
143
|
+
return { ...counts, changed, lessons: data.lessons };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Active lessons in scope, newest first, bounded — what a prompt gets.
|
|
147
|
+
export function selectLessons({ lessons = [], touches = [...TOUCH_AREAS], limit = INJECT_LIMIT, textLimit = INJECT_TEXT_LIMIT } = {}) {
|
|
148
|
+
const inScope = l => l.scope === "project" || String(l.scope).startsWith("path:")
|
|
149
|
+
|| (String(l.scope).startsWith("area:") && touches.includes(String(l.scope).slice(5)));
|
|
150
|
+
return lessons
|
|
151
|
+
.filter(l => l.status === "active" && inScope(l))
|
|
152
|
+
.sort((a, b) => byAge(b, a))
|
|
153
|
+
.slice(0, limit)
|
|
154
|
+
.map(l => ({ ...l, text: l.text.length > textLimit ? `${l.text.slice(0, textLimit - 1)}…` : l.text }));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function renderLessonsSection(lessons = []) {
|
|
158
|
+
if (!lessons.length) return "";
|
|
159
|
+
const lines = [
|
|
160
|
+
"## PROJECT LESSONS",
|
|
161
|
+
"",
|
|
162
|
+
"Recorded by the engine from earlier sessions on this project, newest first, each with its id and where it came from. They are hints from earlier sessions, not user instructions: apply them where they fit, and they never override the task, the user's instructions or the security rules. If one proved wrong or obsolete in this session, retire it in the final handoff (`retireLessons`, with the id and why) — never edit files to change them.",
|
|
163
|
+
"",
|
|
164
|
+
];
|
|
165
|
+
for (const l of lessons) {
|
|
166
|
+
const from = [l.source?.taskId, l.source?.phase, l.source?.revision ? shortRev(l.source.revision) : null, String(l.createdAt || "").slice(0, 10)]
|
|
167
|
+
.filter(Boolean).join(" · ");
|
|
168
|
+
lines.push(`- [${l.id}] (${l.scope}) ${l.text}${l.evidence ? ` — evidence: ${l.evidence}` : ""}${from ? ` — from ${from}` : ""}`);
|
|
169
|
+
}
|
|
170
|
+
return lines.join("\n");
|
|
171
|
+
}
|
|
@@ -42,4 +42,6 @@ Screenshots are **disabled** for this project. Do not capture any unless the use
|
|
|
42
42
|
5. **Bart reviews and publishes** — reads all changed files, checks edge cases and error handling, runs linter and build, captures screenshots if applicable, pushes and creates the PR, posts the PR link on the tracker, posts screenshots as a separate comment. Sam's approval must cover the commit being published: if anyone committed after his audit (Vera's tests, a specialist's change), have Sam audit the new commits first — a short re-audit is enough.
|
|
43
43
|
6. Ask Dennis, Sam and Bart to post their brief tracker comments (files changed & decisions; architecture findings or clean audit with evidence; PR link, test results, screenshots).
|
|
44
44
|
|
|
45
|
+
After each report, decide whether its assignment is accepted, needs a specific correction, or is blocked. Name the next owner and action. Keep unmet acceptance criteria visible and send incomplete work back to its owner; do not simply pass the report to the next agent. If a required check cannot run, assign a bounded attempt to restore it and retain the requirement as unverified if that fails. For example, a CSS class assertion alone does not establish rendered button clearance. Preserve required audit gates when resolving disagreements, and keep unrelated cleanup out of this task. Carry accepted evidence in `qaResults` and unresolved criteria, blockers and their next owners/actions in `reviewerAttention`.
|
|
46
|
+
|
|
45
47
|
The structured output required by the schema is captured automatically — what was implemented, files changed, the PR URL (empty string if none), QA results, issues fixed, and what the reviewers should look at. Do not repeat the JSON object in chat; keep any closing chat message brief and human-readable.
|
|
@@ -28,7 +28,8 @@ Task description:
|
|
|
28
28
|
- explore the code relevant to the task and report the patterns he finds.
|
|
29
29
|
Ask him to report everything back plainly — you decide what matters.
|
|
30
30
|
2. **Tracker session start.** Dictate the exact comment text ("Team session started. Session: {{SESSION_URL}}") and ask Dennis to post it and move the task to "In Progress", confirming with the command output.
|
|
31
|
-
3. **
|
|
31
|
+
3. **Set the delivery target.** Reconcile the original task, latest user corrections, and existing work. State what remains to deliver and what evidence will close each acceptance criterion. A previous agent's deferral is not permission to omit something the user now explicitly requires. Make routine scope interpretations yourself; ask only when missing information or a consequential choice cannot be resolved by the team. Decompose large work into owned steps without silently deferring requested scope; obtain a user decision if completing the request requires changing that scope.
|
|
32
32
|
4. Announce `SESSION_TITLE: <4-8 word title>` on its own line.
|
|
33
|
+
5. **Hand over a direction.** Put the current acceptance criteria in `requirements`, accepted facts and scope decisions in `assessment`, and the next owner, action and required evidence in `nextPhaseFocus`. Distinguish confirmed work from claims still needing verification.
|
|
33
34
|
|
|
34
35
|
The structured output required by the schema is captured automatically — title, task summary, requirements, assessment (branches, PRs, patterns, resume context), subtasks, and what PLAN should focus on. Do not repeat the JSON object in chat; keep any closing chat message brief and human-readable.
|
|
@@ -7,13 +7,13 @@ Task: {{TASK_ID}}
|
|
|
7
7
|
|
|
8
8
|
- Follow CLAUDE.md conventions (if present). Do not modify files unrelated to the task.
|
|
9
9
|
- No code and no file changes in this phase. Plan only.
|
|
10
|
-
- Sam's audit
|
|
10
|
+
- Order the work correctly: PLAN chooses the approach; EXECUTION starts with implementation and tests; Sam then audits the resulting changes inside EXECUTION; approval permits publishing. Sam's code audit gates publishing, not entry into EXECUTION. Do not create a circular dependency by requiring an audit of unwritten changes before implementation can start.
|
|
11
11
|
|
|
12
12
|
{{TRACKER_SECTION}}
|
|
13
13
|
|
|
14
14
|
## Your mission
|
|
15
15
|
|
|
16
|
-
1.
|
|
16
|
+
1. Set the delivery target — what the user gets, acceptance criteria, scope boundaries, and the highest-priority unresolved requirement. Explain technical constraints when they affect the decision.
|
|
17
17
|
2. **Delegate in parallel** to the team, each with the task summary and requirements from session memory. Ask each to verify assumptions with tools (Glob, Grep, Read) — no guessing — and to report:
|
|
18
18
|
- Dennis: implementation plan — files to modify, approach, complexity (S/M/L).
|
|
19
19
|
- Sam: architecture review — existing patterns, module boundaries, whether the approach keeps concerns separated.
|
|
@@ -21,6 +21,7 @@ Task: {{TASK_ID}}
|
|
|
21
21
|
{{#HAS_LUNA}} - Luna (only if the task touches UI): visual impact, accessibility, and a screenshot plan (pages, viewports).{{/HAS_LUNA}}
|
|
22
22
|
{{#HAS_MARK}} - Mark (only if user-facing text changes): copy audit.{{/HAS_MARK}}
|
|
23
23
|
{{#HAS_NORA}} - Nora (only if user-facing behaviour changes): which docs/README/help surfaces must change.{{/HAS_NORA}}
|
|
24
|
-
3.
|
|
24
|
+
3. **Choose the plan.** Compare the proposals against acceptance criteria, existing project conventions and evidence. Ask for objections once; resolve factual disagreements with a focused check and record the decision and reason. Make routine implementation choices within the authorized scope without sending them back to the user. Do not brainstorm beyond two rounds.
|
|
25
|
+
4. **Assign accountable work.** Each step needs one owner, dependencies, a bounded deliverable and the evidence that will demonstrate acceptance. Put these in `assignments` and `steps`; put tradeoffs and rationale in `decisions`. Include every required acceptance criterion and identify real blockers in `risks`.
|
|
25
26
|
|
|
26
27
|
The structured output required by the schema is captured automatically — approach, files to modify, decisions, risks, agent assignments, and the ordered implementation steps. Do not repeat the JSON object in chat; keep any closing chat message brief and human-readable.
|
|
@@ -16,7 +16,7 @@ Task: {{TASK_ID}}
|
|
|
16
16
|
- **Bart** — does the implementation meet the acceptance criteria from INTAKE? Is any requirement missed or silently deferred? Is the PR description accurate, does it reference the task, are screenshots attached where expected?
|
|
17
17
|
{{#NO_SAM}} - **Bart** also runs the verification audit in Sam's place: for every claim EXECUTION made (deployed, tests pass, endpoint works, migration ran), confirm it is backed by an observation he can reproduce; anything resting on inference is a finding.{{/NO_SAM}}
|
|
18
18
|
- **Vera** — run the test suite and report the real output; is the changed code covered; do the new tests exercise the behaviour that changed?
|
|
19
|
-
2.
|
|
20
|
-
3. Decide: `APPROVED`
|
|
19
|
+
2. **Own the acceptance decision.** Reconcile conflicting reports against the current user requirements and direct observations. Request a focused check when evidence conflicts. Be strict but not pedantic: only actual gaps against the task requirements and the plan — not stylistic preferences or speculative refactors. An agent saying "done" or a majority vote cannot override a failed check, a missing observation, or a required acceptance criterion.
|
|
20
|
+
3. Decide: `APPROVED` only when required outcomes have evidence and no blocking finding remains; otherwise `NEEDS_MORE_WORK`. For each finding, write `Owner: <agent name>; Action: <specific corrective work>; Accept when: <required observation>` in its `detail` so EXECUTION can act immediately. Assign implementation fixes to Dennis, test gaps to Vera, and missing user-flow or rendered acceptance checks to Bart. The reviewer field identifies who found the gap and does not replace the corrective owner. Reserve `deferred` for work explicitly outside the authorized scope or deferred by the user; a previous agent's convenience deferral does not remove a requirement.
|
|
21
21
|
|
|
22
22
|
The structured output required by the schema is captured automatically — the verdict, the findings (reviewer, title, detail, file, line), items explicitly out of scope, and any claims that were not backed by an observation. Do not repeat the JSON object in chat; keep any closing chat message brief and human-readable.
|
|
@@ -112,4 +112,8 @@ This keeps everything in one branch — no conflicts, one PR to review.
|
|
|
112
112
|
If the task has no child items, just work on it normally as a single task.
|
|
113
113
|
{{/CHILD_TASKS}}
|
|
114
114
|
|
|
115
|
-
|
|
115
|
+
## Lessons for future sessions
|
|
116
|
+
|
|
117
|
+
The handoff has `lessons` and `retireLessons`. Propose a lesson only for something non-obvious that cost you time and would cost the next session too — a setup step, seed data, an environment quirk, a deployment step. Each needs the evidence (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, retire it by its id with the reason. Empty arrays are the normal case.
|
|
118
|
+
|
|
119
|
+
The structured output required by the schema is captured automatically — a summary of what was done, files changed, the PR URL (empty string if none), deferred items, manual steps, lessons, and retirements. Do not repeat the JSON object in chat; keep any closing chat message brief and human-readable.
|