Skip to content

Commit 0b1aefd

Browse files
authored
Merge pull request #37 from MaxLinCode/codex/atlas-skills
Add Atlas eval loop and repo-local skills
2 parents 92490c2 + 0e0ba18 commit 0b1aefd

36 files changed

Lines changed: 1840 additions & 909 deletions

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,6 @@ coverage
88
.env.local
99
.env.*.local
1010
*.tsbuildinfo
11+
packages/integrations/*.manual-eval-report.json
12+
packages/integrations/manual-eval-report.json
13+
packages/integrations/*.prompt-improvement.md

README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Atlas is a chat-first brain-dump scheduler MVP. A user sends freeform text, the
1515

1616
- `pnpm dev`: run the Next.js app locally
1717
- `pnpm build`: build all workspaces
18+
- `pnpm eval:all`: run the full live OpenAI prompt-eval loop and write a consolidated report to `packages/integrations/manual-eval-report.json`
1819
- `pnpm eval:confirmed-mutation-recovery`: run the live OpenAI confirmed-mutation recovery eval fixture set against the current prompt
1920
- `pnpm eval:conversation-context`: run the live OpenAI conversation-context eval fixture set against the current prompt
2021
- `pnpm eval:planner`: run the live OpenAI planner eval fixture set against the current prompt
@@ -31,6 +32,33 @@ Atlas is a chat-first brain-dump scheduler MVP. A user sends freeform text, the
3132
- `pnpm db:test:reset`: reset the local `atlas_test` database schema for integration reruns
3233
- `pnpm db:test:stop`: stop the local Homebrew Postgres test service
3334

35+
## Prompt Improvement Loop
36+
37+
Atlas treats prompt changes as product behavior changes. Use the live eval harness to iterate on prompts deliberately instead of editing blind.
38+
39+
Recommended loop:
40+
41+
1. Edit the owning prompt, schema, or parser in `packages/integrations` and `packages/core` together when the contract changes.
42+
2. Run the narrowest relevant live eval first:
43+
- `pnpm eval:planner`
44+
- `pnpm eval:turn-router`
45+
- `pnpm eval:router-confirmation`
46+
- `pnpm eval:conversation-context`
47+
- `pnpm eval:confirmed-mutation-recovery`
48+
3. Inspect the suite-specific report written under `packages/integrations/*.manual-eval-report.json`.
49+
4. If the suite fails, inspect the generated prompt-improvement brief under `packages/integrations/*.prompt-improvement.md` and use it as the starting point for the next prompt revision.
50+
5. Tighten the prompt or contract based on the actual failing model output.
51+
6. Run `pnpm eval:all` before merging to confirm the full prompt surface still passes together.
52+
53+
Notes:
54+
55+
- `pnpm eval:all` writes the canonical consolidated report to `packages/integrations/manual-eval-report.json`.
56+
- Single-suite evals write suite-specific reports such as `packages/integrations/conversation-context.manual-eval-report.json`.
57+
- Failing suites also write suite-specific prompt-improvement briefs such as `packages/integrations/conversation-context.prompt-improvement.md`.
58+
- Generated eval reports are ignored by git and should not be committed.
59+
- Generated prompt-improvement briefs are also ignored by git and should not be committed.
60+
- Live evals help judge prompt quality, but they do not replace deterministic tests and schema validation.
61+
3462
## Local Integration DB
3563

3664
Atlas supports an explicit local Postgres workflow for integration tests on macOS with Homebrew `postgresql@16`.
@@ -88,6 +116,22 @@ This repo is designed for human-plus-agent collaboration.
88116
- Update `docs/current-work.md` when priorities or handoff context changes.
89117
- Capture architectural changes in `docs/decisions/`.
90118

119+
## Repo-local Skills
120+
121+
Atlas includes repo-specific Codex skills under [`skills/`](./skills). These are versioned with the repo so they can evolve with Atlas architecture, workflow rules, and testing expectations rather than living as global defaults.
122+
123+
Current skills:
124+
125+
- `atlas-feature-delivery`
126+
- `atlas-planning-change`
127+
- `atlas-webhook-and-conversation`
128+
- `atlas-schema-and-migration`
129+
- `atlas-openai-contracts-and-evals`
130+
- `atlas-google-calendar-flow`
131+
- `atlas-reviewer`
132+
133+
Use these when working inside Atlas and the task matches the skill name. Keep the repo copy as the source of truth.
134+
91135
## MVP flow
92136

93137
1. The current messaging webhook receives a freeform message.

apps/web/src/lib/server/process-inbox-item.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1189,6 +1189,43 @@ describe("process inbox item service", () => {
11891189
);
11901190
});
11911191

1192+
it("does not leak internal planner validation reasons to the user", async () => {
1193+
seedInboxItemForProcessingTests({
1194+
id: "inbox-internal-reason",
1195+
userId: "123",
1196+
sourceEventId: "event-internal-reason",
1197+
rawText: "Pick an open spot for that",
1198+
normalizedText: "Pick an open spot for that",
1199+
processingStatus: "received",
1200+
linkedTaskIds: []
1201+
});
1202+
1203+
const result = await processInboxItem(
1204+
{ inboxItemId: "inbox-internal-reason" },
1205+
{
1206+
calendar: getDefaultCalendarAdapter(),
1207+
planner: async () => ({
1208+
confidence: 0.25,
1209+
summary: "Need clarification.",
1210+
actions: [
1211+
{
1212+
type: "create_task",
1213+
alias: "new_task_1",
1214+
title: "Oil change",
1215+
priority: "medium",
1216+
urgency: "medium"
1217+
}
1218+
]
1219+
})
1220+
}
1221+
);
1222+
1223+
expect(result.outcome).toBe("needs_clarification");
1224+
expect(result.followUpMessage).toBe(
1225+
"I couldn't safely apply that update. Tell me the exact task and what you'd like me to change."
1226+
);
1227+
});
1228+
11921229
it("asks for clarification when multiple existing tasks share the same title", async () => {
11931230
const store = getDefaultInboxProcessingStore();
11941231

apps/web/src/lib/server/process-inbox-item.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -822,15 +822,30 @@ function saveClarification(input: ApplyPlanningResultInput, reason: string) {
822822
}
823823

824824
function buildClarificationReply(reason: string) {
825+
if (!isSafeUserFacingClarificationReason(reason)) {
826+
return "I couldn't safely apply that update. Tell me the exact task and what you'd like me to change.";
827+
}
828+
829+
return reason;
830+
}
831+
832+
function isSafeUserFacingClarificationReason(reason: string) {
825833
if (
826834
reason.startsWith("Model returned") ||
827835
reason.startsWith("Could not resolve") ||
828-
reason.startsWith("Expected ")
836+
reason.startsWith("Expected ") ||
837+
reason.startsWith("Each created task must") ||
838+
reason.startsWith("Model did not provide") ||
839+
reason.startsWith("Please connect Google Calendar before") ||
840+
reason.startsWith("The linked Google Calendar event changed outside Atlas.") ||
841+
reason.startsWith("I couldn't safely apply that update.")
829842
) {
830-
return "I couldn't safely apply that update. Tell me the exact task and what you'd like me to change.";
843+
return reason.startsWith("Please connect Google Calendar before") ||
844+
reason.startsWith("The linked Google Calendar event changed outside Atlas.") ||
845+
reason.startsWith("I couldn't safely apply that update.");
831846
}
832847

833-
return reason;
848+
return true;
834849
}
835850

836851
function hasAmbiguousTaskTitle(tasks: Task[], task: Task) {

docs/current-work.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
## Active focus
44

55
Atlas is a schedule-forward, Google-calendar-gated product with a working mutation pipeline.
6-
Current implementation focus: harden the model-driven planning layer now that the Google Calendar path is locked down. The active branch restructures the OpenAI prompts into explicit prompt assets, expands live eval coverage around ambiguous routing and confirmed-mutation recovery, shifts product language from Telegram-first to chat-first at the prompt and docs layer, and threads `referenceTime` consistently through scheduling so temporal interpretation and busy-calendar lookups use the same anchor.
6+
Current implementation focus: harden the model-driven planning layer now that the Google Calendar path is locked down. The active branch restructures the OpenAI prompts into explicit prompt assets, expands live eval coverage around ambiguous routing and confirmed-mutation recovery, adds a consolidated `pnpm eval:all` loop plus suite-specific eval reports for prompt iteration, shifts product language from Telegram-first to chat-first at the prompt and docs layer, and threads `referenceTime` consistently through scheduling so temporal interpretation and busy-calendar lookups use the same anchor. Atlas also now carries repo-local Codex skills under `skills/` so workflow guidance can evolve with the codebase instead of living only in global agent defaults.
77

88
## Near-term milestones
99

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"scripts": {
66
"dev": "pnpm --filter @atlas/web dev",
77
"build": "pnpm -r build",
8+
"eval:all": "pnpm --filter @atlas/integrations eval:all",
89
"eval:confirmed-mutation-recovery": "pnpm --filter @atlas/integrations eval:confirmed-mutation-recovery",
910
"eval:conversation-context": "pnpm --filter @atlas/integrations eval:conversation-context",
1011
"eval:planner": "pnpm --filter @atlas/integrations eval:planner",

packages/integrations/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
},
99
"scripts": {
1010
"build": "tsc -p tsconfig.json",
11+
"eval:all": "vitest run --config vitest.manual.config.ts src/manual/all.eval.test.ts",
1112
"eval:confirmed-mutation-recovery": "vitest run --config vitest.manual.config.ts src/manual/confirmed-mutation-recovery.eval.test.ts",
1213
"eval:conversation-context": "vitest run --config vitest.manual.config.ts src/manual/conversation-context.eval.test.ts",
1314
"eval:planner": "vitest run --config vitest.manual.config.ts src/manual/planner.eval.test.ts",
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { beforeAll, describe, expect, it } from "vitest";
2+
3+
import { runConfirmedMutationRecoveryEvalSuite } from "./confirmed-mutation-recovery.eval-suite";
4+
import { runConversationContextEvalSuite } from "./conversation-context.eval-suite";
5+
import { runPlannerEvalSuite } from "./planner.eval-suite";
6+
import { runRouterConfirmationEvalSuite } from "./router-confirmation.eval-suite";
7+
import {
8+
ensureManualEvalEnv,
9+
buildEvalReport,
10+
writeEvalReport,
11+
writePromptImprovementBriefsForFailures
12+
} from "./shared";
13+
import { runTurnRouterEvalSuite } from "./turn-router.eval-suite";
14+
15+
beforeAll(() => {
16+
ensureManualEvalEnv();
17+
});
18+
19+
describe.sequential("manual prompt eval loop", () => {
20+
it("runs all live prompt eval suites and writes one report", async () => {
21+
const suites = [
22+
await runPlannerEvalSuite(),
23+
await runTurnRouterEvalSuite(),
24+
await runRouterConfirmationEvalSuite(),
25+
await runConversationContextEvalSuite(),
26+
await runConfirmedMutationRecoveryEvalSuite()
27+
];
28+
29+
const report = buildEvalReport(suites);
30+
const reportPath = await writeEvalReport(report);
31+
const briefPaths = await writePromptImprovementBriefsForFailures(suites);
32+
33+
console.table(
34+
suites.map((suite) => ({
35+
suite: suite.suiteName,
36+
total: suite.total,
37+
passed: suite.passed,
38+
failed: suite.failed,
39+
durationMs: suite.durationMs
40+
}))
41+
);
42+
console.log(`Manual eval report written to ${reportPath}`);
43+
if (briefPaths.length > 0) {
44+
console.log(`Prompt improvement briefs written to ${briefPaths.join(", ")}`);
45+
}
46+
47+
expect(report.failedCases).toBe(0);
48+
}, 300_000);
49+
});
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import type { ConfirmedMutationRecoveryInput } from "@atlas/core";
2+
import { expect } from "vitest";
3+
4+
import { recoverConfirmedMutationWithResponses } from "../openai";
5+
import type { EvalCaseResult, EvalSuiteResult } from "./shared";
6+
7+
type RecoveryEvalCase = {
8+
name: string;
9+
input: ConfirmedMutationRecoveryInput;
10+
assert: (result: Awaited<ReturnType<typeof recoverConfirmedMutationWithResponses>>) => void;
11+
};
12+
13+
export const RECOVERY_EVAL_CASES: RecoveryEvalCase[] = [
14+
{
15+
name: "single concrete confirmation recovers one write-ready request",
16+
input: {
17+
rawText: "Yes",
18+
normalizedText: "Yes",
19+
recentTurns: [
20+
{
21+
role: "assistant",
22+
text: "Would you like me to schedule it at 3pm?",
23+
createdAt: "2026-03-17T16:00:00.000Z"
24+
},
25+
{
26+
role: "user",
27+
text: "Yes",
28+
createdAt: "2026-03-17T16:01:00.000Z"
29+
}
30+
],
31+
memorySummary: "The assistant proposed scheduling it at 3pm."
32+
},
33+
assert: (result) => {
34+
expect(result.outcome).toBe("recovered");
35+
expect(result.recoveredText).toMatch(/3pm/i);
36+
}
37+
},
38+
{
39+
name: "vague yes after multiple options requires clarification",
40+
input: {
41+
rawText: "Yes",
42+
normalizedText: "Yes",
43+
recentTurns: [
44+
{
45+
role: "assistant",
46+
text: "I could do 3pm or 4pm.",
47+
createdAt: "2026-03-17T16:00:00.000Z"
48+
},
49+
{
50+
role: "user",
51+
text: "Yes",
52+
createdAt: "2026-03-17T16:01:00.000Z"
53+
}
54+
],
55+
memorySummary: "Two candidate times were proposed."
56+
},
57+
assert: (result) => {
58+
expect(result.outcome).toBe("needs_clarification");
59+
expect(result.recoveredText).toBeNull();
60+
expect(result.userReplyMessage).toMatch(/\?/);
61+
}
62+
},
63+
{
64+
name: "clear completion language recovers a completion request",
65+
input: {
66+
rawText: "done",
67+
normalizedText: "done",
68+
recentTurns: [
69+
{
70+
role: "assistant",
71+
text: "Did you finish the journaling session?",
72+
createdAt: "2026-03-17T16:00:00.000Z"
73+
},
74+
{
75+
role: "user",
76+
text: "done",
77+
createdAt: "2026-03-17T16:01:00.000Z"
78+
}
79+
],
80+
memorySummary: "The recent exchange is about the journaling session."
81+
},
82+
assert: (result) => {
83+
expect(result.outcome).toBe("recovered");
84+
expect(result.recoveredText).toMatch(/journal|done/i);
85+
}
86+
}
87+
];
88+
89+
export async function runConfirmedMutationRecoveryEvalSuite(): Promise<EvalSuiteResult> {
90+
const startedAt = Date.now();
91+
const cases: EvalCaseResult[] = [];
92+
93+
for (const testCase of RECOVERY_EVAL_CASES) {
94+
const result = await recoverConfirmedMutationWithResponses(testCase.input);
95+
96+
try {
97+
testCase.assert(result);
98+
cases.push({
99+
name: testCase.name,
100+
pass: true,
101+
details: {
102+
input: testCase.input.rawText,
103+
outcome: result.outcome,
104+
recoveredText: result.recoveredText,
105+
reason: result.reason,
106+
userReplyMessage: result.userReplyMessage
107+
}
108+
});
109+
} catch (error) {
110+
cases.push({
111+
name: testCase.name,
112+
pass: false,
113+
details: {
114+
input: testCase.input.rawText,
115+
outcome: result.outcome,
116+
recoveredText: result.recoveredText,
117+
reason: result.reason,
118+
userReplyMessage: result.userReplyMessage
119+
},
120+
error: error instanceof Error ? error.message : String(error)
121+
});
122+
}
123+
}
124+
125+
const passed = cases.filter((testCase) => testCase.pass).length;
126+
127+
return {
128+
suiteName: "confirmed-mutation-recovery",
129+
total: cases.length,
130+
passed,
131+
failed: cases.length - passed,
132+
durationMs: Date.now() - startedAt,
133+
cases
134+
};
135+
}
136+

0 commit comments

Comments
 (0)