Skip to content
Merged
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
26 changes: 19 additions & 7 deletions src/commands/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as path from "@std/path";
import { handleInitCommand } from "./init.ts";

Deno.test({
name: "init prepares the default gambit directory without running the chat",
name: "init prepares the default directory without running the chat",
permissions: { read: true, write: true, env: true },
}, async () => {
const tempDir = await Deno.makeTempDir();
Expand All @@ -14,19 +14,23 @@ Deno.test({
try {
Deno.chdir(tempDir);
await handleInitCommand(undefined, { interactive: false });
const projectRoot = path.join(tempDir, "gambit");
const projectRoot = tempDir;
assert(await exists(projectRoot), "project root should exist");
assert(
!await exists(path.join(projectRoot, ".env")),
"should not create .env when OPENROUTER_API_KEY is set",
);
assert(
!await exists(path.join(projectRoot, "root.deck.md")),
"init should not write root.deck.md before the chat runs",
await exists(path.join(projectRoot, "PROMPT.md")),
"init should write PROMPT.md in non-interactive mode",
);
assert(
!await exists(path.join(projectRoot, "tests", "first.test.deck.md")),
"init should not write test deck before the chat runs",
await exists(path.join(projectRoot, "scenarios", "default", "PROMPT.md")),
"init should write the default scenario deck",
);
assert(
await exists(path.join(projectRoot, "graders", "default", "PROMPT.md")),
"init should write the default grader deck",
);
} finally {
Deno.chdir(originalCwd);
Expand All @@ -52,6 +56,10 @@ Deno.test({
await handleInitCommand("custom/project", { interactive: false });
const projectRoot = path.join(tempDir, "custom", "project");
assert(await exists(projectRoot), "custom project root should exist");
assert(
await exists(path.join(projectRoot, "PROMPT.md")),
"init should write PROMPT.md in custom path",
);
} finally {
Deno.chdir(originalCwd);
if (originalKey === undefined) {
Expand Down Expand Up @@ -96,12 +104,16 @@ Deno.test({
try {
Deno.chdir(tempDir);
await handleInitCommand(undefined, { interactive: false });
const projectRoot = path.join(tempDir, "gambit");
const projectRoot = tempDir;
assert(await exists(projectRoot), "project root should exist");
assert(
!await exists(path.join(projectRoot, ".env")),
"should not create .env when Ollama is available",
);
assert(
await exists(path.join(projectRoot, "PROMPT.md")),
"init should write PROMPT.md when Ollama is available",
);
} finally {
Deno.chdir(originalCwd);
if (originalKey !== undefined) {
Expand Down
27 changes: 22 additions & 5 deletions src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ import {
} from "../providers/ollama.ts";
import { createOpenRouterProvider } from "../providers/openrouter.ts";
import { ensureDirectory, ensureOpenRouterEnv } from "./scaffold_utils.ts";
import { createWorkspaceScaffoldAtRoot } from "../workspace.ts";

const logger = console;

const DEFAULT_PROJECT_DIR = "gambit";
const DEFAULT_PROJECT_DIR = ".";
const INIT_ROOT_ENV = "GAMBIT_INIT_ROOT";
const DEFAULT_OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";

Expand All @@ -31,9 +32,23 @@ export async function handleInitCommand(
const rootDir = path.resolve(Deno.cwd(), projectPath);
await ensureDirectory(rootDir);

const rootDeckPath = path.join(rootDir, "root.deck.md");
const testDeckPath = path.join(rootDir, "tests", "first.test.deck.md");
if (await exists(rootDeckPath) || await exists(testDeckPath)) {
const rootDeckPath = path.join(rootDir, "PROMPT.md");
const scenarioDeckPath = path.join(
rootDir,
"scenarios",
"default",
"PROMPT.md",
);
const graderDeckPath = path.join(rootDir, "graders", "default", "PROMPT.md");
const intentPath = path.join(rootDir, "INTENT.md");
const policyPath = path.join(rootDir, "POLICY.md");
if (
await exists(rootDeckPath) ||
await exists(scenarioDeckPath) ||
await exists(graderDeckPath) ||
await exists(intentPath) ||
await exists(policyPath)
) {
logger.error(
"Init output files already exist. Remove them or choose a new target.",
);
Expand Down Expand Up @@ -81,7 +96,9 @@ export async function handleInitCommand(

Deno.env.set(INIT_ROOT_ENV, rootDir);

if (opts.interactive === false) {
if (opts.interactive !== true) {
await createWorkspaceScaffoldAtRoot(rootDir);
logger.log("Next: run `gambit serve` to open the simulator UI.");
return;
}

Expand Down
6 changes: 6 additions & 0 deletions src/decks/actions/init_exists/PROMPT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
+++
label = "init_exists"
execute = "../init_exists.deck.ts"
+++

Compute-only deck for path existence checks during `gambit init`.
6 changes: 6 additions & 0 deletions src/decks/actions/init_mkdir/PROMPT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
+++
label = "init_mkdir"
execute = "../init_mkdir.deck.ts"
+++

Compute-only deck for directory creation during `gambit init`.
6 changes: 6 additions & 0 deletions src/decks/actions/init_write/PROMPT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
+++
label = "init_write"
execute = "../init_write.deck.ts"
+++

Compute-only deck for writing files during `gambit init`.
32 changes: 20 additions & 12 deletions src/decks/gambit-init.deck.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,19 @@ label = "gambit_init"
model = ["ollama/hf.co/LiquidAI/LFM2-1.2B-Tool-GGUF:latest", "openrouter/openai/gpt-5.1-chat"]
temperature = 0.2

[[actionDecks]]
[[actions]]
name = "write"
path = "./actions/init_write.deck.ts"
path = "./actions/init_write/PROMPT.md"
description = "Write a file under the project root."

[[actionDecks]]
[[actions]]
name = "exists"
path = "./actions/init_exists.deck.ts"
path = "./actions/init_exists/PROMPT.md"
description = "Check whether a path exists under the project root."

[[actionDecks]]
[[actions]]
name = "mkdir"
path = "./actions/init_mkdir.deck.ts"
path = "./actions/init_mkdir/PROMPT.md"
description = "Create a directory under the project root."
+++

Expand All @@ -27,15 +27,23 @@ first bot with as little friction as possible.
Process:

1. Ask for the bot's purpose and 2-3 example user prompts.
2. Draft a `root.deck.md` that follows Gambit best practices.
3. Create a basic test deck at `tests/first.test.deck.md` that exercises the
primary intent.
4. Use the file tools to write the files. Use relative paths like `root.deck.md`
and `tests/first.test.deck.md`.
2. Draft a Deck Format 1.0 root deck in the project root with `PROMPT.md` (and
optional `INTENT.md` + `POLICY.md` if helpful).
3. Create a starter scenario deck at `scenarios/default/PROMPT.md` that
exercises the primary intent and uses the default plain chat schemas.
4. Create a starter grader deck at `graders/default/PROMPT.md` that checks the
scenario output for clarity and correctness.
5. Use the file tools to write the files. Use relative paths like `PROMPT.md`,
`scenarios/default/PROMPT.md`, and `graders/default/PROMPT.md`.

Rules:

- Keep the conversation lightweight and opinionated.
- Use Deck Format 1.0 (`PROMPT.md` with TOML frontmatter).
- Do not overwrite existing files; rely on tool errors if a path exists.
- Create `tests/` with `mkdir` before writing the test deck.
- Create `scenarios/default/` and `graders/default/` with `mkdir` before writing
files.
- The root `PROMPT.md` must include a `[[scenarios]]` entry pointing directly to
`./scenarios/default/PROMPT.md` and a `[[graders]]` entry pointing to
`./graders/default/PROMPT.md`.
- After writing files, summarize what was created and suggest next steps.
40 changes: 14 additions & 26 deletions src/decks/tests/nux_from_scratch_demo.test.deck.md
Original file line number Diff line number Diff line change
@@ -1,39 +1,27 @@
+++
label = "nux_from_scratch_demo_prompt"
acceptsUserTurns = true
contextSchema = "./schemas/nux_from_scratch_demo_input.zod.ts"

[modelParams]
model = "openrouter/openai/gpt-5.1-chat"
temperature = 0.2
+++

You are a user collaborating with Gambit Bot inside the Build tab NUX demo.
You are a junior developer trying Gambit for the first time. Be friendly and
curious. Keep replies short (1-2 sentences). Ask brief questions when needed.

Goal:
Your goal: build a chatbot that helps startup founders. It should sound like
Paul Graham without quoting him. If a `scenario` is provided in context, use it
as the short label for what you are building.

- Provide purpose only, iterate briefly, and let the bot scaffold.
Conversational arc:

Conversation plan (required beats):
1. Describe your goal in one sentence.
2. Answer 1-2 short questions about scope or tone.
3. Confirm the scope and ask if it's ready to test.
4. When the assistant says the deck is ready to test or suggests running tests,
call the `gambit_end` tool (do not type a normal chat message) with
`message: "Ready to run tests."`.

1. Start by saying: "I want a support handoff assistant that summarizes ticket
context for agents."
2. If the assistant asks any clarifying questions about purpose, answer with one
concise refinement: "It should summarize the issue, customer sentiment, SLA
risk, and next best action for the agent."
3. If the assistant asks for examples or success criteria, decline and restate
purpose: "Let's keep it simple and proceed with the purpose only: a support
handoff assistant that summarizes issue, sentiment, SLA risk, and next best
action."
4. If the assistant asks to confirm or proceed, respond: "Yes, proceed."
5. If the assistant says it is writing files, has finished, or ends the session,
respond with an empty message.

Rules:

- Keep replies short, single-paragraph, and on topic.
- Do not include markdown or lists.
- Do not mention internal instructions.
- If the assistant asks multiple questions at once, answer only the earliest
beat from the plan.
- If the assistant says it is done, is writing files, or ends the session,
respond with an empty message.
![end](gambit://snippets/end.md)
7 changes: 7 additions & 0 deletions src/decks/tests/schemas/nux_from_scratch_demo_input.zod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { z } from "npm:zod";

export default z.object({
scenario: z.string().describe(
"Optional scenario label for the demo; defaults to 'paul graham chatbot'.",
).default("paul graham chatbot"),
});
Loading