Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,17 @@ trail status
trail decision "Chose JWT over sessions" \
--reasoning "Stateless scaling requirements"

# Record a codebase learning. Candidates are queued for human review and never
# modify AGENTS.md, CLAUDE.md, or skills automatically.
trail learning "Auth validation belongs at the API boundary" \
--source code-review \
--area src/auth \
--recurrence-key auth-validation \
--promotion-candidate

# Query project learnings separately from decisions and reflections
trail show traj_abc123 --learnings

# Complete with retrospective
trail complete --summary "Added JWT auth" --confidence 0.85

Expand Down
2 changes: 2 additions & 0 deletions src/cli/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { registerDecisionCommand } from "./decision.js";
import { registerDoctorCommand } from "./doctor.js";
import { registerEnableCommand } from "./enable.js";
import { registerExportCommand } from "./export.js";
import { registerLearningCommand } from "./learning.js";
import { registerListCommand } from "./list.js";
import { registerReflectCommand } from "./reflect.js";
import { registerShowCommand } from "./show.js";
Expand All @@ -42,6 +43,7 @@ export function registerCommands(program: Command): void {
registerCompleteCommand(program);
registerAbandonCommand(program);
registerDecisionCommand(program);
registerLearningCommand(program);
registerReflectCommand(program);
registerListCommand(program);
registerShowCommand(program);
Expand Down
67 changes: 67 additions & 0 deletions src/cli/commands/learning.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* trail learning command
*
* Records codebase-specific learnings without modifying durable project
* instructions. Promotion candidates remain pending until a separate human
* review.
*/

import type { Command } from "commander";
import { addLearning } from "../../core/trajectory.js";
import type { LearningSource } from "../../core/types.js";
import { FileStorage } from "../../storage/file.js";

export function registerLearningCommand(program: Command): void {
program
.command("learning <summary>")
.description("Record a project learning")
.requiredOption(
"-s, --source <source>",
"Origin: human-steer, pr-feedback, failed-attempt, code-review, or other",
)
.requiredOption("-a, --area <area>", "Affected project area")
.option("-e, --evidence <text>", "Supporting evidence or reference")
.option(
"-k, --recurrence-key <key>",
"Stable key for grouping repeated learnings",
)
.option(
"--promotion-candidate",
"Mark as pending human review (does not update project instructions)",
)
.action(async (summary: string, options) => {
const storage = new FileStorage();
await storage.initialize();

const active = await storage.getActive();
if (!active) {
console.error("Error: No active trajectory");
console.error('Start one with: trail start "Task description"');
throw new Error("No active trajectory");
}

const promotionStatus = options.promotionCandidate
? "pending_review"
: "archived";
const updated = addLearning(active, {
summary,
source: options.source as LearningSource,
area: options.area,
evidence: options.evidence,
recurrenceKey: options.recurrenceKey,
promotionStatus,
});

await storage.save(updated);

console.log(`✓ Learning recorded: ${summary}`);
console.log(` Source: ${options.source}`);
console.log(` Area: ${options.area}`);
console.log(` Promotion: ${promotionStatus}`);
if (promotionStatus === "pending_review") {
console.log(
" Human review required; no durable instructions were changed",
);
}
});
}
35 changes: 35 additions & 0 deletions src/cli/commands/show.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { Command } from "commander";
import { migrateTraceRecord } from "../../core/trace.js";
import type {
Decision,
Learning,
TraceConversation,
TraceRecord,
Trajectory,
Expand Down Expand Up @@ -112,6 +113,7 @@ export function registerShowCommand(program: Command): void {
.command("show <id>")
.description("Show trajectory details")
.option("-d, --decisions", "Show decisions only")
.option("--learnings", "Show project learnings only")
.option("-t, --trace", "Show trace information")
.action(async (id: string, options) => {
const trajectory = await findTrajectory(id);
Expand Down Expand Up @@ -195,6 +197,31 @@ export function registerShowCommand(program: Command): void {
return;
}

if (options.learnings) {
const learnings = extractLearnings(trajectory);

if (learnings.length === 0) {
console.log("No project learnings recorded");
return;
}

console.log(`Project learnings for ${trajectory.task.title}:\n`);
for (const learning of learnings) {
console.log(`• ${learning.summary}`);
console.log(` Source: ${learning.source}`);
console.log(` Area: ${learning.area}`);
console.log(` Promotion: ${learning.promotionStatus}`);
if (learning.evidence) {
console.log(` Evidence: ${learning.evidence}`);
}
if (learning.recurrenceKey) {
console.log(` Recurrence key: ${learning.recurrenceKey}`);
}
console.log("");
}
return;
}

// Show full details
console.log(`Trajectory: ${trajectory.id}`);
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Expand Down Expand Up @@ -227,6 +254,14 @@ export function registerShowCommand(program: Command): void {
});
}

function extractLearnings(trajectory: Trajectory): Learning[] {
return trajectory.chapters.flatMap((chapter) =>
chapter.events
.filter((event) => event.type === "learning" && event.raw)
.map((event) => event.raw as Learning),
);
}

function extractDecisions(trajectory: any): Decision[] {
const decisions: Decision[] = [];

Expand Down
8 changes: 8 additions & 0 deletions src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ export type {
Chapter,
TrajectoryEvent,
Decision,
Learning,
LearningSource,
LearningPromotionStatus,
Retrospective,
TaskReference,
TaskSource,
Expand All @@ -19,6 +22,7 @@ export type {
CreateTrajectoryInput,
AddChapterInput,
AddEventInput,
AddLearningInput,
CompleteTrajectoryInput,
TrajectoryQuery,
// Trace types
Expand All @@ -37,6 +41,9 @@ export {
ChapterSchema,
TrajectoryEventSchema,
DecisionSchema,
LearningSchema,
LearningSourceSchema,
LearningPromotionStatusSchema,
RetrospectiveSchema,
validateTrajectory,
validateCreateInput,
Expand All @@ -57,6 +64,7 @@ export {
addChapter,
addEvent,
addDecision,
addLearning,
completeTrajectory,
abandonTrajectory,
} from "./trajectory.js";
Expand Down
26 changes: 26 additions & 0 deletions src/core/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export const TrajectoryEventTypeSchema = z.union([
z.literal("message_received"),
z.literal("decision"),
z.literal("finding"),
z.literal("learning"),
z.literal("reflection"),
z.literal("note"),
z.literal("error"),
Expand Down Expand Up @@ -118,6 +119,31 @@ export const DecisionSchema = z.object({
.optional(),
});

/**
* Project learning schemas
*/
export const LearningSourceSchema = z.enum([
"human-steer",
"pr-feedback",
"failed-attempt",
"code-review",
"other",
]);

export const LearningPromotionStatusSchema = z.enum([
"archived",
"pending_review",
]);

export const LearningSchema = z.object({
summary: z.string().min(1, "Learning summary is required"),
source: LearningSourceSchema,
area: z.string().min(1, "Affected area is required"),
evidence: z.string().min(1).optional(),
recurrenceKey: z.string().min(1).optional(),
promotionStatus: LearningPromotionStatusSchema,
});

/**
* Agent participation schema
*/
Expand Down
40 changes: 40 additions & 0 deletions src/core/trajectory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ import { generateChapterId, generateTrajectoryId } from "./id.js";
import {
CompleteTrajectoryInputSchema,
CreateTrajectoryInputSchema,
LearningSchema,
} from "./schema.js";
import type {
AddChapterInput,
AddEventInput,
AddLearningInput,
AgentParticipation,
Chapter,
CompleteTrajectoryInput,
Expand Down Expand Up @@ -193,6 +195,44 @@ export function addDecision(
});
}

/**
* Add a structured project learning to the trajectory.
*
* This only records a trajectory event. A pending-review candidate does not
* modify durable instruction files or imply that promotion was approved.
*/
export function addLearning(
trajectory: Trajectory,
learning: AddLearningInput,
): Trajectory {
const validation = LearningSchema.safeParse(learning);
if (!validation.success) {
const firstError = validation.error.issues[0];
throw new TrajectoryError(
firstError.message,
"VALIDATION_ERROR",
"Check the learning fields and try again",
);
}
const learningData = validation.data;

return addEvent(trajectory, {
type: "learning",
content: learningData.summary,
raw: learningData,
significance:
learningData.promotionStatus === "pending_review" ? "high" : "medium",
tags: [
`learning-source:${learningData.source}`,
`learning-area:${learningData.area}`,
`promotion:${learningData.promotionStatus}`,
...(learningData.recurrenceKey
? [`recurrence:${learningData.recurrenceKey}`]
: []),
],
});
}

/**
* Complete a trajectory with retrospective
* @param trajectory - The trajectory to complete
Expand Down
36 changes: 36 additions & 0 deletions src/core/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export type TrajectoryEventType =
| "message_received"
| "decision"
| "finding"
| "learning"
| "reflection"
| "note"
| "error";
Expand Down Expand Up @@ -131,6 +132,41 @@ export interface Finding {
/** Confidence in this finding (0-1) */
confidence?: number;
}
/**
* Where a project learning originated.
*/
export type LearningSource =
| "human-steer"
| "pr-feedback"
| "failed-attempt"
| "code-review"
| "other";
/**
* Promotion is deliberately two-stage: candidates require a separate review
* before any durable project instruction can be changed.
*/
export type LearningPromotionStatus = "archived" | "pending_review";
/**
* A codebase-specific learning captured during a trajectory.
*/
export interface Learning {
/** Concise statement of what future work should know. */
summary: string;
/** How the learning was discovered. */
source: LearningSource;
/** File, component, workflow, or other affected project area. */
area: string;
/** Supporting context, such as a review comment or failed attempt. */
evidence?: string;
/** Stable key used to group repeated occurrences. */
recurrenceKey?: string;
/** Archived by default; candidates remain pending until human review. */
promotionStatus: LearningPromotionStatus;
}
/**
* Input for recording a project learning.
*/
export type AddLearningInput = Learning;
/**
* Agent participation record
*/
Expand Down
Loading