Skip to content
Draft
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
58 changes: 58 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,63 @@ planeteer list
3. **Refine** — Navigate the task tree, edit details, or type refinement requests (e.g., "split the auth task into login and signup"). Press `s` to save, `x` to execute.
4. **Execute** — Tasks are dispatched to Copilot agents in parallel batches that respect the dependency graph. Progress is shown in real time.

### Environment Variables

Planeteer supports configuring environment variables that are available to Copilot agents during task execution. This is useful when agents need access to external services via MCP (Model Context Protocol) tools that require API keys, database URLs, or other configuration.

#### Global Environment Variables

Global environment variables apply to all tasks in all plans. Configure them in `.planeteer/settings.json`:

```json
{
"model": "claude-sonnet-4",
"globalEnv": {
"DATABASE_URL": "postgresql://localhost:5432/dev",
"LOG_LEVEL": "info"
}
}
```

#### Task-Specific Environment Variables

Individual tasks can have their own environment variables. These override global variables with the same name. To configure task-specific env vars:

1. In the Refine screen, press `/` then `e` to edit a task
2. Navigate to the "Environment Variables" field
3. Press Enter to edit
4. Enter variables as comma-separated `KEY=VALUE` pairs:
```
API_KEY=sk-test-123, REGION=us-west-2
```

Task-specific environment variables are saved in the plan JSON file.

#### Security Considerations

⚠️ **Important**: Environment variables containing sensitive data (API keys, passwords, tokens) are stored in plain text in plan files.

**Best practices:**
- Use global env vars in `.planeteer/settings.json` for sensitive values (add `.planeteer/` to `.gitignore`)
- For production deployments, use environment variables set at the system level instead of storing them in plans
- Planeteer will warn you when saving plans with environment variables that appear sensitive (contain "key", "token", "password", etc.)
- Sensitive values are masked with `***` in the task editor UI

#### How It Works

When a task executes:
1. Global environment variables from settings are loaded
2. Task-specific environment variables override globals with the same name
3. All variables are set in `process.env` before creating the Copilot agent session
4. MCP servers spawned by the Copilot CLI inherit these environment variables (requires Copilot SDK 0.1.25+ with `envValueMode: direct` support)

#### Example Use Cases

- **Database access**: Pass `DATABASE_URL` to agents that need to query or migrate databases
- **API integration**: Provide `API_KEY` for agents using external APIs via MCP tools
- **Multi-environment**: Use different `ENV=development|staging|production` values per task
- **Cloud providers**: Pass `AWS_REGION`, `AZURE_SUBSCRIPTION_ID`, etc. for cloud infrastructure tasks

### Keyboard Shortcuts

| Key | Action |
Expand Down Expand Up @@ -181,6 +238,7 @@ src/
│ └── plan.ts # Types: Plan, Task, ChatMessage
└── utils/
├── dependency-graph.ts # Topological sort & cycle detection
├── env-validation.ts # Environment variable security checks
└── markdown.ts # Plan → Markdown renderer
```

Expand Down
73 changes: 33 additions & 40 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"test:watch": "vitest"
},
"dependencies": {
"@github/copilot-sdk": "^0.1.0",
"@github/copilot-sdk": "^0.1.25",
"ink": "^5.1.0",
"ink-select-input": "^6.0.0",
"ink-spinner": "^5.0.0",
Expand Down
57 changes: 56 additions & 1 deletion src/components/task-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import { Box, Text, useInput } from 'ink';
import TextInput from 'ink-text-input';
import type { Task } from '../models/plan.js';

type EditField = 'title' | 'description' | 'acceptance' | 'dependsOn';
type EditField = 'title' | 'description' | 'acceptance' | 'dependsOn' | 'env';

const FIELDS: { key: EditField; label: string }[] = [
{ key: 'title', label: 'Title' },
{ key: 'description', label: 'Description' },
{ key: 'acceptance', label: 'Acceptance Criteria' },
{ key: 'dependsOn', label: 'Dependencies' },
{ key: 'env', label: 'Environment Variables' },
];

interface TaskEditorProps {
Expand All @@ -29,6 +30,7 @@ export default function TaskEditor({ task, allTaskIds, onSave, onCancel }: TaskE
const [description, setDescription] = useState(task.description);
const [acceptanceCriteria, setAcceptanceCriteria] = useState([...task.acceptanceCriteria]);
const [dependsOn, setDependsOn] = useState([...task.dependsOn]);
const [env, setEnv] = useState<Record<string, string>>(task.env || {});

// For acceptance criteria editing
const [acIndex, setAcIndex] = useState(0);
Expand Down Expand Up @@ -66,6 +68,11 @@ export default function TaskEditor({ task, allTaskIds, onSave, onCancel }: TaskE
} else if (field === 'dependsOn') {
setEditValue(dependsOn.join(', '));
setEditing(true);
} else if (field === 'env') {
// Format env vars as KEY=VALUE pairs, one per line
const envStr = Object.entries(env).map(([k, v]) => `${k}=${v}`).join(', ');
setEditValue(envStr);
setEditing(true);
}
return;
}
Expand Down Expand Up @@ -108,6 +115,7 @@ export default function TaskEditor({ task, allTaskIds, onSave, onCancel }: TaskE
let newDescription = description;
let newAcceptanceCriteria = acceptanceCriteria;
let newDependsOn = dependsOn;
let newEnv = env;

if (field === 'title') {
newTitle = value;
Expand Down Expand Up @@ -137,6 +145,21 @@ export default function TaskEditor({ task, allTaskIds, onSave, onCancel }: TaskE
.filter((d) => d && allTaskIds.includes(d) && d !== task.id);
newDependsOn = deps;
setDependsOn(deps);
} else if (field === 'env') {
// Parse KEY=VALUE pairs separated by commas
const pairs = value
.split(',')
.map((pair) => pair.trim())
.filter((pair) => pair.includes('='));
const parsed: Record<string, string> = {};
for (const pair of pairs) {
const [key, ...valueParts] = pair.split('=');
if (key && valueParts.length > 0) {
parsed[key.trim()] = valueParts.join('=').trim();
}
}
newEnv = parsed;
setEnv(parsed);
}

setEditing(false);
Expand All @@ -149,6 +172,7 @@ export default function TaskEditor({ task, allTaskIds, onSave, onCancel }: TaskE
description: newDescription,
acceptanceCriteria: newAcceptanceCriteria,
dependsOn: newDependsOn,
env: Object.keys(newEnv).length > 0 ? newEnv : undefined,
});
};

Expand Down Expand Up @@ -245,6 +269,37 @@ export default function TaskEditor({ task, allTaskIds, onSave, onCancel }: TaskE
);
}

if (field.key === 'env') {
// Helper to mask sensitive values
const maskValue = (key: string, value: string): string => {
const sensitiveKeys = ['key', 'token', 'password', 'secret', 'auth', 'credential'];
const isSensitive = sensitiveKeys.some(k => key.toLowerCase().includes(k));
return isSensitive ? '***' : value;
};

const envEntries = Object.entries(env);
return (
<Box key={field.key}>
<Text color={isActive ? 'green' : 'gray'}>{indicator}</Text>
<Text color="cyan" bold>{field.label}: </Text>
{editing && isActive ? (
<TextInput
value={editValue}
onChange={setEditValue}
onSubmit={handleEditSubmit}
placeholder="KEY=value, API_KEY=secret"
/>
) : (
<Text>
{envEntries.length > 0
? envEntries.map(([k, v]) => `${k}=${maskValue(k, v)}`).join(', ')
: '(none)'}
</Text>
)}
</Box>
);
}

return null;
})}

Expand Down
1 change: 1 addition & 0 deletions src/models/plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface Task {
dependsOn: string[];
status: TaskStatus;
agentResult?: string;
env?: Record<string, string>;
}

export interface Plan {
Expand Down
Loading