@koan-labs/koan 0.2.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/LICENSE +21 -0
- package/README.md +138 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +399 -0
- package/dist/cli/prompt.d.ts +5 -0
- package/dist/cli/prompt.js +48 -0
- package/dist/core/answers.d.ts +27 -0
- package/dist/core/answers.js +86 -0
- package/dist/core/commandLog.d.ts +8 -0
- package/dist/core/commandLog.js +51 -0
- package/dist/core/commands.d.ts +71 -0
- package/dist/core/commands.js +252 -0
- package/dist/core/constants.d.ts +32 -0
- package/dist/core/constants.js +36 -0
- package/dist/core/crystallize.d.ts +15 -0
- package/dist/core/crystallize.js +124 -0
- package/dist/core/documents.d.ts +11 -0
- package/dist/core/documents.js +72 -0
- package/dist/core/gitPolicy.d.ts +2 -0
- package/dist/core/gitPolicy.js +25 -0
- package/dist/core/handoff.d.ts +5 -0
- package/dist/core/handoff.js +20 -0
- package/dist/core/hostAdapter.d.ts +8 -0
- package/dist/core/hostAdapter.js +34 -0
- package/dist/core/lock.d.ts +5 -0
- package/dist/core/lock.js +142 -0
- package/dist/core/mcpCache.d.ts +4 -0
- package/dist/core/mcpCache.js +37 -0
- package/dist/core/prd.d.ts +33 -0
- package/dist/core/prd.js +151 -0
- package/dist/core/profile.d.ts +8 -0
- package/dist/core/profile.js +47 -0
- package/dist/core/profileRef.d.ts +3 -0
- package/dist/core/profileRef.js +41 -0
- package/dist/core/project.d.ts +17 -0
- package/dist/core/project.js +126 -0
- package/dist/core/qa.d.ts +6 -0
- package/dist/core/qa.js +26 -0
- package/dist/core/questions.d.ts +10 -0
- package/dist/core/questions.js +272 -0
- package/dist/core/reconstruct.d.ts +7 -0
- package/dist/core/reconstruct.js +62 -0
- package/dist/core/schemas.d.ts +331 -0
- package/dist/core/schemas.js +132 -0
- package/dist/core/scoring.d.ts +9 -0
- package/dist/core/scoring.js +72 -0
- package/dist/core/session.d.ts +6 -0
- package/dist/core/session.js +88 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +18 -0
- package/dist/mcp/server.d.ts +5 -0
- package/dist/mcp/server.js +539 -0
- package/package.json +55 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const StrictnessSchema = z.enum(["advisory", "strict"]);
|
|
3
|
+
export const LanguageSchema = z.enum(["ko", "en", "mixed"]);
|
|
4
|
+
export const DevelopmentUnderstandingSchema = z.enum([
|
|
5
|
+
"non_technical",
|
|
6
|
+
"beginner",
|
|
7
|
+
"intermediate",
|
|
8
|
+
"expert"
|
|
9
|
+
]);
|
|
10
|
+
export const ExplanationStyleSchema = z.enum([
|
|
11
|
+
"short",
|
|
12
|
+
"example_first",
|
|
13
|
+
"step_by_step",
|
|
14
|
+
"technical_ok"
|
|
15
|
+
]);
|
|
16
|
+
export const OutputUseSchema = z.enum([
|
|
17
|
+
"self_implementation",
|
|
18
|
+
"agent_execution",
|
|
19
|
+
"team_sharing",
|
|
20
|
+
"learning"
|
|
21
|
+
]);
|
|
22
|
+
export const LearningModeSchema = z.enum(["approval_required", "auto_with_review"]);
|
|
23
|
+
export const UserProfileSchema = z.object({
|
|
24
|
+
developmentUnderstanding: DevelopmentUnderstandingSchema,
|
|
25
|
+
explanationStyle: ExplanationStyleSchema,
|
|
26
|
+
language: LanguageSchema,
|
|
27
|
+
outputUse: OutputUseSchema,
|
|
28
|
+
domainBackground: z.string(),
|
|
29
|
+
learningMode: LearningModeSchema
|
|
30
|
+
});
|
|
31
|
+
export const DEFAULT_CONVERGENCE_THRESHOLD = 0.7;
|
|
32
|
+
export const ProjectSettingsSchema = z
|
|
33
|
+
.object({ convergenceThreshold: z.number().min(0).max(1) })
|
|
34
|
+
.default({ convergenceThreshold: DEFAULT_CONVERGENCE_THRESHOLD });
|
|
35
|
+
export const ProjectConfigSchema = z.object({
|
|
36
|
+
version: z.literal(1),
|
|
37
|
+
koanVersion: z.string(),
|
|
38
|
+
projectRoot: z.string(),
|
|
39
|
+
strictness: StrictnessSchema,
|
|
40
|
+
experimentalHandoff: z.boolean(),
|
|
41
|
+
documents: z.object({
|
|
42
|
+
readme: z.string(),
|
|
43
|
+
goal: z.string(),
|
|
44
|
+
status: z.string(),
|
|
45
|
+
plan: z.string()
|
|
46
|
+
}),
|
|
47
|
+
settings: ProjectSettingsSchema
|
|
48
|
+
});
|
|
49
|
+
export const AmbiguityAxisSchema = z.enum([
|
|
50
|
+
"purpose",
|
|
51
|
+
"target_users",
|
|
52
|
+
"current_goal",
|
|
53
|
+
"scope",
|
|
54
|
+
"non_goals",
|
|
55
|
+
"constraints",
|
|
56
|
+
"success_criteria",
|
|
57
|
+
"philosophical_intent",
|
|
58
|
+
"implementation_plan",
|
|
59
|
+
"qa_criteria",
|
|
60
|
+
"handoff_readiness"
|
|
61
|
+
]);
|
|
62
|
+
export const AxisScoreSchema = z.object({
|
|
63
|
+
axis: AmbiguityAxisSchema,
|
|
64
|
+
clarity: z.number().min(0).max(1),
|
|
65
|
+
evidence: z.array(z.string()),
|
|
66
|
+
updatedAt: z.string()
|
|
67
|
+
});
|
|
68
|
+
export const AmbiguityLedgerSchema = z.object({
|
|
69
|
+
version: z.literal(1),
|
|
70
|
+
goalId: z.string(),
|
|
71
|
+
axes: z.array(AxisScoreSchema)
|
|
72
|
+
});
|
|
73
|
+
export const AnswerRecordSchema = z.object({
|
|
74
|
+
questionId: z.string(),
|
|
75
|
+
axis: AmbiguityAxisSchema,
|
|
76
|
+
question: z.string(),
|
|
77
|
+
answer: z.string(),
|
|
78
|
+
recordedAt: z.string()
|
|
79
|
+
});
|
|
80
|
+
export const SessionStateSchema = z.object({
|
|
81
|
+
version: z.literal(1),
|
|
82
|
+
sessionId: z.string(),
|
|
83
|
+
activeGoalId: z.string().nullable(),
|
|
84
|
+
phase: z.enum(["setup", "questioning", "crystallizing", "ready", "archived"]),
|
|
85
|
+
lastQuestionId: z.string().nullable(),
|
|
86
|
+
answers: z.array(AnswerRecordSchema).default([]),
|
|
87
|
+
updatedAt: z.string()
|
|
88
|
+
});
|
|
89
|
+
export const WritePlanOperationSchema = z.discriminatedUnion("type", [
|
|
90
|
+
z.object({ type: z.literal("write"), path: z.string(), content: z.string() }),
|
|
91
|
+
z.object({
|
|
92
|
+
type: z.literal("append"),
|
|
93
|
+
path: z.string(),
|
|
94
|
+
content: z.string(),
|
|
95
|
+
headerIfMissing: z.string().optional()
|
|
96
|
+
}),
|
|
97
|
+
z.object({
|
|
98
|
+
type: z.literal("managed-region"),
|
|
99
|
+
path: z.string(),
|
|
100
|
+
name: z.string(),
|
|
101
|
+
content: z.string()
|
|
102
|
+
})
|
|
103
|
+
]);
|
|
104
|
+
export const WritePlanSchema = z.object({
|
|
105
|
+
description: z.string(),
|
|
106
|
+
operations: z.array(WritePlanOperationSchema)
|
|
107
|
+
});
|
|
108
|
+
export const CommandLogEntrySchema = z.object({
|
|
109
|
+
at: z.string(),
|
|
110
|
+
command: z.string(),
|
|
111
|
+
summary: z.string()
|
|
112
|
+
});
|
|
113
|
+
export const CommandLogSchema = z.object({
|
|
114
|
+
version: z.literal(1),
|
|
115
|
+
entries: z.array(CommandLogEntrySchema)
|
|
116
|
+
});
|
|
117
|
+
export const UserProfileRefSchema = z.object({
|
|
118
|
+
version: z.literal(1),
|
|
119
|
+
profilePath: z.string(),
|
|
120
|
+
overrides: UserProfileSchema.partial()
|
|
121
|
+
});
|
|
122
|
+
export const QuestionContextSchema = z.object({
|
|
123
|
+
sessionId: z.string(),
|
|
124
|
+
axis: AmbiguityAxisSchema,
|
|
125
|
+
questionId: z.string(),
|
|
126
|
+
askedAt: z.string()
|
|
127
|
+
});
|
|
128
|
+
export const McpCacheSchema = z.object({
|
|
129
|
+
version: z.literal(1),
|
|
130
|
+
lastQuestion: QuestionContextSchema.nullable(),
|
|
131
|
+
rawIntent: z.string().nullable().default(null)
|
|
132
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type AmbiguityAxis, type AmbiguityLedger } from "./schemas.js";
|
|
2
|
+
export declare const ANSWERED_CLARITY = 0.8;
|
|
3
|
+
export declare const AXIS_PRIORITY: readonly AmbiguityAxis[];
|
|
4
|
+
export declare function createInitialLedger(goalId: string, isoDate?: string): AmbiguityLedger;
|
|
5
|
+
export declare function selectMostUnclearAxis(ledger: AmbiguityLedger): AmbiguityAxis;
|
|
6
|
+
export declare function updateAxisScore(ledger: AmbiguityLedger, axis: AmbiguityAxis, clarity: number, evidence: string, isoDate?: string): AmbiguityLedger;
|
|
7
|
+
export declare function loadLedger(projectRoot: string): Promise<AmbiguityLedger | null>;
|
|
8
|
+
export declare function isConverged(ledger: AmbiguityLedger, threshold: number): boolean;
|
|
9
|
+
export declare function unresolvedAxes(ledger: AmbiguityLedger, threshold: number): AmbiguityAxis[];
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { STATE_FILES } from "./constants.js";
|
|
4
|
+
import { AmbiguityAxisSchema, AmbiguityLedgerSchema } from "./schemas.js";
|
|
5
|
+
export const ANSWERED_CLARITY = 0.8;
|
|
6
|
+
// Question priority for equally-unclear axes: the why layer (purpose,
|
|
7
|
+
// philosophical_intent) leads a fresh session before goal shaping and
|
|
8
|
+
// implementation planning.
|
|
9
|
+
export const AXIS_PRIORITY = [
|
|
10
|
+
"purpose",
|
|
11
|
+
"philosophical_intent",
|
|
12
|
+
"current_goal",
|
|
13
|
+
"target_users",
|
|
14
|
+
"scope",
|
|
15
|
+
"non_goals",
|
|
16
|
+
"constraints",
|
|
17
|
+
"success_criteria",
|
|
18
|
+
"implementation_plan",
|
|
19
|
+
"qa_criteria",
|
|
20
|
+
"handoff_readiness"
|
|
21
|
+
];
|
|
22
|
+
const priorityIndex = new Map(AXIS_PRIORITY.map((axis, index) => [axis, index]));
|
|
23
|
+
function axisPriority(axis) {
|
|
24
|
+
return priorityIndex.get(axis) ?? AXIS_PRIORITY.length;
|
|
25
|
+
}
|
|
26
|
+
export function createInitialLedger(goalId, isoDate = new Date().toISOString()) {
|
|
27
|
+
return {
|
|
28
|
+
version: 1,
|
|
29
|
+
goalId,
|
|
30
|
+
axes: AmbiguityAxisSchema.options.map((axis) => ({
|
|
31
|
+
axis,
|
|
32
|
+
clarity: 0,
|
|
33
|
+
evidence: [],
|
|
34
|
+
updatedAt: isoDate
|
|
35
|
+
}))
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export function selectMostUnclearAxis(ledger) {
|
|
39
|
+
const sorted = [...ledger.axes].sort((a, b) => a.clarity - b.clarity || axisPriority(a.axis) - axisPriority(b.axis));
|
|
40
|
+
return sorted[0]?.axis ?? "purpose";
|
|
41
|
+
}
|
|
42
|
+
export function updateAxisScore(ledger, axis, clarity, evidence, isoDate = new Date().toISOString()) {
|
|
43
|
+
return {
|
|
44
|
+
...ledger,
|
|
45
|
+
axes: ledger.axes.map((entry) => entry.axis === axis
|
|
46
|
+
? {
|
|
47
|
+
...entry,
|
|
48
|
+
clarity: Math.max(0, Math.min(1, clarity)),
|
|
49
|
+
evidence: evidence ? [...entry.evidence, evidence] : entry.evidence,
|
|
50
|
+
updatedAt: isoDate
|
|
51
|
+
}
|
|
52
|
+
: entry)
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export async function loadLedger(projectRoot) {
|
|
56
|
+
try {
|
|
57
|
+
const raw = await readFile(join(projectRoot, STATE_FILES.ambiguityLedger), "utf8");
|
|
58
|
+
return AmbiguityLedgerSchema.parse(JSON.parse(raw));
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export function isConverged(ledger, threshold) {
|
|
65
|
+
return ledger.axes.every((entry) => entry.clarity >= threshold);
|
|
66
|
+
}
|
|
67
|
+
export function unresolvedAxes(ledger, threshold) {
|
|
68
|
+
return ledger.axes
|
|
69
|
+
.filter((entry) => entry.clarity < threshold)
|
|
70
|
+
.sort((a, b) => a.clarity - b.clarity || axisPriority(a.axis) - axisPriority(b.axis))
|
|
71
|
+
.map((entry) => entry.axis);
|
|
72
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type CommandLogInput } from "./commandLog.js";
|
|
2
|
+
import { type SessionState } from "./schemas.js";
|
|
3
|
+
export declare function createSessionState(activeGoalId: string | null, isoDate?: string): SessionState;
|
|
4
|
+
export declare function loadSessionState(projectRoot: string): Promise<SessionState | null>;
|
|
5
|
+
export declare function goalIdFromDate(isoDate?: string): string;
|
|
6
|
+
export declare function archiveGoal(projectRoot: string, goalId: string, log?: CommandLogInput): Promise<void>;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { access, copyFile, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { basename, dirname, join } from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { appendCommandLogInLock } from "./commandLog.js";
|
|
5
|
+
import { CORE_DOCUMENTS, LAZY_DOCUMENTS, STATE_FILES } from "./constants.js";
|
|
6
|
+
import { replaceManagedRegion } from "./documents.js";
|
|
7
|
+
import { ensureStateGitignore } from "./gitPolicy.js";
|
|
8
|
+
import { withFileLock } from "./lock.js";
|
|
9
|
+
import { SessionStateSchema } from "./schemas.js";
|
|
10
|
+
export function createSessionState(activeGoalId, isoDate = new Date().toISOString()) {
|
|
11
|
+
return {
|
|
12
|
+
version: 1,
|
|
13
|
+
sessionId: randomUUID(),
|
|
14
|
+
activeGoalId,
|
|
15
|
+
phase: activeGoalId ? "questioning" : "setup",
|
|
16
|
+
lastQuestionId: null,
|
|
17
|
+
answers: [],
|
|
18
|
+
updatedAt: isoDate
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export async function loadSessionState(projectRoot) {
|
|
22
|
+
try {
|
|
23
|
+
const raw = await readFile(join(projectRoot, STATE_FILES.sessionState), "utf8");
|
|
24
|
+
return SessionStateSchema.parse(JSON.parse(raw));
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// Caller must hold the project write lock or be the locked write-plan path.
|
|
31
|
+
async function persistSessionState(projectRoot, state) {
|
|
32
|
+
const path = join(projectRoot, STATE_FILES.sessionState);
|
|
33
|
+
await mkdir(dirname(path), { recursive: true });
|
|
34
|
+
await writeFile(path, `${JSON.stringify(state, null, 2)}\n`, "utf8");
|
|
35
|
+
}
|
|
36
|
+
export function goalIdFromDate(isoDate = new Date().toISOString()) {
|
|
37
|
+
return `goal-${isoDate.replace(/[:.]/g, "-")}`;
|
|
38
|
+
}
|
|
39
|
+
async function exists(path) {
|
|
40
|
+
try {
|
|
41
|
+
await access(path);
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export async function archiveGoal(projectRoot, goalId, log) {
|
|
49
|
+
const safeGoalId = basename(goalId);
|
|
50
|
+
if (!safeGoalId || safeGoalId === "." || safeGoalId === "..") {
|
|
51
|
+
throw new Error(`Invalid goal id: ${goalId}`);
|
|
52
|
+
}
|
|
53
|
+
await withFileLock(projectRoot, async () => {
|
|
54
|
+
await ensureStateGitignore(projectRoot);
|
|
55
|
+
const archiveRoot = join(projectRoot, "koan/archive", safeGoalId);
|
|
56
|
+
await mkdir(archiveRoot, { recursive: true });
|
|
57
|
+
const files = [
|
|
58
|
+
CORE_DOCUMENTS.goal,
|
|
59
|
+
CORE_DOCUMENTS.plan,
|
|
60
|
+
CORE_DOCUMENTS.status,
|
|
61
|
+
LAZY_DOCUMENTS.decisions,
|
|
62
|
+
LAZY_DOCUMENTS.qa,
|
|
63
|
+
LAZY_DOCUMENTS.handoff
|
|
64
|
+
];
|
|
65
|
+
for (const relative of files) {
|
|
66
|
+
const source = join(projectRoot, relative);
|
|
67
|
+
if (await exists(source)) {
|
|
68
|
+
await copyFile(source, join(archiveRoot, basename(relative)));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const goalPath = join(projectRoot, CORE_DOCUMENTS.goal);
|
|
72
|
+
const currentGoal = await readFile(goalPath, "utf8").catch(() => "# Goal\n");
|
|
73
|
+
await writeFile(goalPath, replaceManagedRegion(currentGoal, "active-goal", `No active goal yet.\n\nArchived goal: ${safeGoalId}`), "utf8");
|
|
74
|
+
await rm(join(projectRoot, STATE_FILES.ambiguityLedger), { force: true });
|
|
75
|
+
const state = await loadSessionState(projectRoot);
|
|
76
|
+
if (state) {
|
|
77
|
+
await persistSessionState(projectRoot, {
|
|
78
|
+
...state,
|
|
79
|
+
activeGoalId: null,
|
|
80
|
+
phase: "archived",
|
|
81
|
+
updatedAt: new Date().toISOString()
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
if (log) {
|
|
85
|
+
await appendCommandLogInLock(projectRoot, log);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export * from "./core/constants.js";
|
|
2
|
+
export * from "./core/schemas.js";
|
|
3
|
+
export * from "./core/lock.js";
|
|
4
|
+
export * from "./core/project.js";
|
|
5
|
+
export * from "./core/documents.js";
|
|
6
|
+
export * from "./core/profile.js";
|
|
7
|
+
export * from "./core/profileRef.js";
|
|
8
|
+
export * from "./core/commandLog.js";
|
|
9
|
+
export * from "./core/mcpCache.js";
|
|
10
|
+
export * from "./core/questions.js";
|
|
11
|
+
export * from "./core/scoring.js";
|
|
12
|
+
export * from "./core/session.js";
|
|
13
|
+
export * from "./core/reconstruct.js";
|
|
14
|
+
export * from "./core/answers.js";
|
|
15
|
+
export * from "./core/crystallize.js";
|
|
16
|
+
export * from "./core/commands.js";
|
|
17
|
+
export * from "./core/hostAdapter.js";
|
|
18
|
+
export * from "./core/prd.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export * from "./core/constants.js";
|
|
2
|
+
export * from "./core/schemas.js";
|
|
3
|
+
export * from "./core/lock.js";
|
|
4
|
+
export * from "./core/project.js";
|
|
5
|
+
export * from "./core/documents.js";
|
|
6
|
+
export * from "./core/profile.js";
|
|
7
|
+
export * from "./core/profileRef.js";
|
|
8
|
+
export * from "./core/commandLog.js";
|
|
9
|
+
export * from "./core/mcpCache.js";
|
|
10
|
+
export * from "./core/questions.js";
|
|
11
|
+
export * from "./core/scoring.js";
|
|
12
|
+
export * from "./core/session.js";
|
|
13
|
+
export * from "./core/reconstruct.js";
|
|
14
|
+
export * from "./core/answers.js";
|
|
15
|
+
export * from "./core/crystallize.js";
|
|
16
|
+
export * from "./core/commands.js";
|
|
17
|
+
export * from "./core/hostAdapter.js";
|
|
18
|
+
export * from "./core/prd.js";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
export declare const toolNames: readonly ["koan_get_profile", "koan_update_profile", "koan_inspect_project", "koan_start_session", "koan_get_next_question", "koan_record_answer", "koan_crystallize_documents", "koan_get_status", "koan_update_status", "koan_record_bright_idea", "koan_record_insight", "koan_synthesize_prd", "koan_prepare_qa", "koan_prepare_handoff"];
|
|
4
|
+
export declare function createServer(): Server;
|
|
5
|
+
export declare function runServer(): Promise<void>;
|