Skip to main content
Nightshift is currently in its infancy and is subject to change and incompleteness. If you find a bug, or have an idea to improve Nightshift, please raise an issue on Github.
When you run nightshift install --prefix <prefix> and a workspace doesn’t exist yet, Nightshift launches an interactive TUI that guides you through connecting a provider, selecting a model, and describing your use case. The agent then autonomously configures the workspace by:
  1. Interviewing you to understand your needs
  2. Installing appropriate Python packages
  3. Writing code
  4. Generating a custom AGENTS.md file
  5. Creating skills in .opencode/skills/

Launch Sequence

The bootstrap routine is triggered in the install() function in src/index.ts:875. When a new workspace is detected:
const isNewWorkspace = !existsSync(workspacePath);

if (isNewWorkspace) {
  // Create scaffold without AGENTS.md - that will be generated by opencode
  await createWorkspaceScaffold(workspacePath, libraryName, packages, { skipAgentsMd: true });
  await syncWorkspace(prefix, workspacePath);

  try {
    const result = await runBootstrapPrompt(
      async (userIntent, ui, client, serverUrl, model) => {
        await bootstrapWithOpencode(prefix, workspacePath, userIntent, xdgEnv, ui, client, serverUrl, model);
      },
      {
        prefix,
        workspacePath,
        xdgEnv,
      }
    );
    // ...
  }
}
The workspace scaffold is created first (without AGENTS.md), dependencies are synced, and then the bootstrap prompt UI launches.

TUI Flow

The runBootstrapPrompt() function in src/bootstrap-prompt.ts:232 manages the interactive TUI. It progresses through several view states:
type ViewState =
  | { type: "loading" }
  | { type: "provider-select"; selectedIndex: number }
  | { type: "auth-method-select"; providerId: string; methods: Array<{ type: string; label: string }>; selectedIndex: number }
  | { type: "oauth-auto"; providerId: string; methodIndex: number; url: string; instructions: string }
  | { type: "oauth-code"; providerId: string; methodIndex: number; url: string; instructions: string; error?: boolean }
  | { type: "api-key"; providerId: string; error?: boolean }
  | { type: "model-select"; models: ModelOption[] }
  | { type: "intent-prompt" }
  | { type: "bootstrap-output" }
  | { type: "question"; /* ... */ };
StateDescription
loadingStarting the OpenCode server
provider-selectChoose Anthropic, OpenAI, or Skip
auth-method-selectChoose OAuth or API key (if multiple options)
oauth-auto / oauth-codeBrowser-based authentication flow
api-keyManual API key entry
model-selectChoose which model to use
intent-promptDescribe your use case
bootstrap-outputShows real-time agent output
questionDisplays agent questions for user input

The Bootstrap Prompt

After you describe your intent, the buildBootstrapPrompt() function in src/index.ts:618 constructs the prompt sent to the agent:
function buildBootstrapPrompt(userIntent: string): string {
  return `
You are bootstrapping a new workspace for the user. Their stated purpose is:
"${userIntent}"

## Important: Interview the User

If asked to read from a BOOT.md file, you must read it first. There will be information useful for you to help the user.

If there is a mention of Skills in either Markdown or JSON in the BOOT.md, you MUST use those to create skill files in your current working directory in the path .opencode/skills/<name>/SKILL.md.

Before taking any action outside of reading the BOOT.md, interview the user extensively to understand their needs:
- What specific problems are they trying to solve?
- What data sources will they work with?
- What are their preferred tools or libraries?
- What is their experience level with Python?
- Any specific requirements or constraints?

Use the AskUserQuestion tool to gather this information. Ask 2-4 focused questions before proceeding.

## After gathering information:

1. **TODO project tracker**: You need to set up a TODO for this bootstrapping task so you don't forget any steps. Validate by reading back the TODOs and the environment.
1. **Install packages**: Run \`uv add <packages>\` to install Python libraries appropriate for this use case
2. **Create library structure**: Add modules to src/agent_lib/ that will help with the stated purpose
3. **Generate SKILLS.md for each skill needed**: Create a SKILL.md at .opencode/skills/<name>/SKILL.md file with the SKILL.
3. **Generate AGENTS.md**: Create an AGENTS.md file following these best practices:

## SKILLS.md Guidelines:

You should always look up best practices for creating SKILLs.md files online before generating one. An extensive web search is recommended.

## AGENTS.md Guidelines:

You should always look up best practices for creating AGENTS.md files online before generating one. An extensive web search is recommended.

### Required Sections:
- **Project Overview**: One-sentence description tailored to "${userIntent}"
- **Commands**: Exact commands for build, test, run (use bun, uv, pytest)
- **Tech Stack**: Python 3.13, Bun, uv, and installed packages
- **Project Structure**: Key file paths and their purposes
- **Code Style**: Formatting rules, design patterns (use ruff, black)
- **Do's and Don'ts**: Specific, actionable guidelines for this use case
- **Safety Boundaries**:
  - Always do: Read files, run tests, format code
  - Ask first: Install new packages, modify pyproject.toml
  - Never do: Delete data, run destructive commands
`.trim();
}
The prompt instructs the agent to:
  1. Read BOOT.md if present (for pre-configured setups)
  2. Interview the user with 2-4 questions using the AskUserQuestion tool
  3. Create a TODO list to track bootstrap steps
  4. Install packages with uv add
  5. Generate skill files and AGENTS.md

Event Handling

The bootstrapWithOpencode() function in src/index.ts:728 subscribes to OpenCode events and routes them to the UI:
const events = await client.event.subscribe({}, { signal: abort.signal });

for await (const event of events.stream) {
  // Auto-approve all permission requests during bootstrap
  if (event.type === "permission.asked") {
    const request = event.properties;
    if (request.sessionID === sessionId) {
      await autoApprovePermission(client, ui, request);
    }
  }

  // Stream text output
  if (event.type === "message.part.updated") {
    const { part, delta } = event.properties;
    if (part.sessionID !== sessionId) continue;

    if (part.type === "text" && delta) {
      ui.appendText(delta);
    }

    if (part.type === "tool") {
      // Show tool execution status (running, completed, error)
    }
  }

  // Handle question events
  if (event.type === "question.asked") {
    const request = event.properties;
    if (request.sessionID === sessionId) {
      const answers = await ui.showQuestion(request);
      await client.question.reply({ requestID: request.id, answers });
    }
  }

  // Check if session is idle (completed)
  if (event.type === "session.idle" && event.properties.sessionID === sessionId) {
    resolve();
  }
}
Event TypeHandling
permission.askedAuto-approved during bootstrap
message.part.updatedStreams text deltas and tool status to UI
session.diffDisplays file changes with diffs
question.askedShows interactive question UI, sends response back
session.idleSignals bootstrap completion
session.errorRejects the bootstrap promise

BootstrapUI Interface

The UI interface passed to the bootstrap callback in src/bootstrap-prompt.ts:54 provides methods for displaying agent output:
export interface BootstrapUI {
  appendText: (text: string) => void;
  appendToolStatus: (status: "running" | "completed" | "error", text: string) => void;
  setStatus: (status: string) => void;
  showDiff: (diffs: FileDiff[]) => void;
  showBashOutput: (command: string, output: string, description?: string) => void;
  showWriteOutput: (filePath: string, content: string) => void;
  showEditOutput: (filePath: string, diff: string) => void;
  setSpinnerActive: (active: boolean) => void;
  showQuestion: (request: QuestionRequest) => Promise<QuestionAnswer[]>;
}
MethodPurpose
appendTextStreams text chunks (supports delta updates to same line)
appendToolStatusShows tool execution with colored status icons
setStatusUpdates the header status line
showDiffRenders unified diffs with syntax highlighting
showBashOutputDisplays shell command and output in a collapsible block
showWriteOutputShows file creation with content preview
showEditOutputShows file edits with diff highlighting
showQuestionDisplays multiple-choice questions from the agent

Tool Completion Handling

When tools complete, handleToolCompletion() in src/index.ts:689 routes output to the appropriate UI method:
function handleToolCompletion(
  ui: BootstrapUI,
  part: ToolCompletionPart,
): void {
  const { state, tool } = part;
  const output = state.output;
  const input = state.input;

  if (tool === "bash" && output?.trim()) {
    const command = (input?.command as string) || title;
    ui.showBashOutput(command, output, description);
  } else if (tool === "write" && input?.filePath) {
    ui.showWriteOutput(input.filePath as string, (input.content as string) || "");
  } else if (tool === "edit" && metadata?.diff && input?.filePath) {
    ui.showEditOutput(input.filePath as string, metadata.diff as string);
  } else if (tool === "apply_patch" && metadata?.diff) {
    ui.showEditOutput(filePath, metadata.diff as string);
  } else {
    ui.appendToolStatus("completed", title);
  }
}

BOOT.md Support

If a BOOT.md file exists in the workspace, the agent reads it first to get pre-configured instructions. This enables automated or semi-automated bootstrap scenarios where the user’s needs are already known.

Skipping Bootstrap

Users can skip the bootstrap prompt by pressing Ctrl+C. When skipped, Nightshift falls back to generating a default AGENTS.md:
if (!result) {
  // User skipped (Ctrl+C)
  console.log("\nSkipped bootstrap. Using default AGENTS.md.");
  await Bun.write(join(workspacePath, "AGENTS.md"), generateAgentsMd(libraryName));
}
The default AGENTS.md generated by generateAgentsMd() in src/index.ts:435 provides generic data science agent instructions.