workflow: polish — /context groups trackers, /workflow in README, memoize git root
- contextPanel splits 'instructions' from 'trackers' (TODO.md/ROADMAP.md), so /context no longer presents project trackers as standing orders, and its empty-state mentions both instruction files and trackers - README: docs table gets Project workflow, command list gets the new commands - session: gitRoot memoized (one sync fs walk per session, not per prompt build); workflowPolicy computed once per systemFor - test: context panel grouping covered 10 workflow tests, 830+ suite green
This commit is contained in:
@@ -134,6 +134,7 @@ the flags are.
|
|||||||
| [Custom commands](docs/custom-commands.md) | a Markdown file becomes a slash command, with arguments and shell substitution |
|
| [Custom commands](docs/custom-commands.md) | a Markdown file becomes a slash command, with arguments and shell substitution |
|
||||||
| [Extensions](docs/extensions.md) | auto-loaded external skills, tools, and plugins — data, never code |
|
| [Extensions](docs/extensions.md) | auto-loaded external skills, tools, and plugins — data, never code |
|
||||||
| [Memory and state](docs/memory.md) | memory, task lists, sessions, compaction and its repair |
|
| [Memory and state](docs/memory.md) | memory, task lists, sessions, compaction and its repair |
|
||||||
|
| [Project workflow](docs/workflow.md) | when a repo tracks its own progress, the agent follows its TODO/ROADMAP/docs |
|
||||||
| [MCP](docs/mcp.md) | connecting servers, namespacing, cost, debugging one |
|
| [MCP](docs/mcp.md) | connecting servers, namespacing, cost, debugging one |
|
||||||
| [Headless mode](docs/headless.md) | `-p`, JSON events, exit codes, CI recipes |
|
| [Headless mode](docs/headless.md) | `-p`, JSON events, exit codes, CI recipes |
|
||||||
| [Architecture](docs/architecture.md) | how the loop works and why it is built this way |
|
| [Architecture](docs/architecture.md) | how the loop works and why it is built this way |
|
||||||
@@ -151,6 +152,7 @@ Type `/` and a menu appears, narrowing as you type.
|
|||||||
/skills /plugins /registry [search|add|remove] /mcp [add|remove] /init /context
|
/skills /plugins /registry [search|add|remove] /mcp [add|remove] /init /context
|
||||||
/todos /notes /memory /tools /compact /cost
|
/todos /notes /memory /tools /compact /cost
|
||||||
/sessions /resume <id> /save /clear /exit
|
/sessions /resume <id> /save /clear /exit
|
||||||
|
/undo /redo /changes /search <query> /fork /workflow
|
||||||
```
|
```
|
||||||
|
|
||||||
`esc` dismisses a panel, interrupts a running turn, and clears the queue. `ctrl-c` kills the
|
`esc` dismisses a panel, interrupts a running turn, and clears the queue. `ctrl-c` kills the
|
||||||
|
|||||||
+15
-3
@@ -423,18 +423,29 @@ export class Session {
|
|||||||
* Node's existsSync over Bun.file(...).exists().
|
* Node's existsSync over Bun.file(...).exists().
|
||||||
*/
|
*/
|
||||||
private gitRoot(): string | undefined {
|
private gitRoot(): string | undefined {
|
||||||
|
if (this.gitRootResolved) return this.cachedGitRoot ?? undefined;
|
||||||
|
this.gitRootResolved = true;
|
||||||
try {
|
try {
|
||||||
let dir = resolve(this.opts.cwd ?? process.cwd());
|
let dir = resolve(this.opts.cwd ?? process.cwd());
|
||||||
while (true) {
|
while (true) {
|
||||||
if (existsSync(join(dir, '.git', 'HEAD'))) return dir;
|
if (existsSync(join(dir, '.git', 'HEAD'))) {
|
||||||
|
this.cachedGitRoot = dir;
|
||||||
|
return dir;
|
||||||
|
}
|
||||||
const parent = dirname(dir);
|
const parent = dirname(dir);
|
||||||
if (parent === dir) return undefined;
|
if (parent === dir) {
|
||||||
|
this.cachedGitRoot = null;
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
dir = parent;
|
dir = parent;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
this.cachedGitRoot = null;
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
private cachedGitRoot: string | null | undefined;
|
||||||
|
private gitRootResolved = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rendered only when the project tracks its own progress (TODO.md/ROADMAP.md
|
* Rendered only when the project tracks its own progress (TODO.md/ROADMAP.md
|
||||||
@@ -763,6 +774,7 @@ export class Session {
|
|||||||
|
|
||||||
const mem = this.opts.memory;
|
const mem = this.opts.memory;
|
||||||
const memoryBlock = mem ? mem.render() : '';
|
const memoryBlock = mem ? mem.render() : '';
|
||||||
|
const workflowPolicy = this.workflowPolicy();
|
||||||
const text = systemPrompt({
|
const text = systemPrompt({
|
||||||
cwd: this.opts.cwd ?? process.cwd(),
|
cwd: this.opts.cwd ?? process.cwd(),
|
||||||
instructions: this.opts.instructions ?? [],
|
instructions: this.opts.instructions ?? [],
|
||||||
@@ -775,7 +787,7 @@ export class Session {
|
|||||||
canAsk: this.opts.ask !== undefined && this.activeTools().includes('ask'),
|
canAsk: this.opts.ask !== undefined && this.activeTools().includes('ask'),
|
||||||
...(this.mcpServerNamesForPrompt() ? { mcpServers: this.mcpServerNamesForPrompt() } : {}),
|
...(this.mcpServerNamesForPrompt() ? { mcpServers: this.mcpServerNamesForPrompt() } : {}),
|
||||||
...(this.workspaceFiles && this.workspaceFiles.length > 0 ? { workspaceFiles: this.workspaceFiles } : {}),
|
...(this.workspaceFiles && this.workspaceFiles.length > 0 ? { workspaceFiles: this.workspaceFiles } : {}),
|
||||||
...(this.workflowPolicy() ? { workflowPolicy: this.workflowPolicy() } : {}),
|
...(workflowPolicy ? { workflowPolicy } : {}),
|
||||||
});
|
});
|
||||||
this.promptCache = { key: versionKey, text };
|
this.promptCache = { key: versionKey, text };
|
||||||
return text;
|
return text;
|
||||||
|
|||||||
+10
-7
@@ -72,13 +72,16 @@ export function costPanel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function contextPanel(files: readonly string[]): Panel {
|
export function contextPanel(files: readonly string[]): Panel {
|
||||||
return {
|
const tracker = files.filter((f) => f.endsWith('TODO.md') || f.endsWith('ROADMAP.md'));
|
||||||
title: 'project instructions',
|
const instructions = files.filter((f) => !tracker.includes(f));
|
||||||
body:
|
const body: string[] = [];
|
||||||
files.length > 0
|
if (instructions.length > 0) body.push('instructions:', ...instructions.map((f) => `- \`${f}\``));
|
||||||
? files.map((f) => `- \`${f}\``).join('\n')
|
if (tracker.length > 0) body.push('trackers:', ...tracker.map((f) => `- \`${f}\``));
|
||||||
: 'No `AGENTS.md`, `CLAUDE.md`, or `.shiro.md` found. Run `/init` to write one.',
|
if (body.length === 0) {
|
||||||
};
|
body.push('No `AGENTS.md`, `CLAUDE.md`, or `.shiro.md` found. Run `/init` to write one.');
|
||||||
|
body.push('No `TODO.md` or `ROADMAP.md` found — no project tracker is loaded.');
|
||||||
|
}
|
||||||
|
return { title: 'project instructions & trackers', body: body.join('\n') };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const todosPanel = (session: Session): Panel => ({
|
export const todosPanel = (session: Session): Panel => ({
|
||||||
|
|||||||
+16
-2
@@ -169,10 +169,24 @@ test('/workflow parses to the workflow action', () => {
|
|||||||
test('/workflow panel renders the status rows', () =>
|
test('/workflow panel renders the status rows', () =>
|
||||||
inGitRepo(async () => {
|
inGitRepo(async () => {
|
||||||
await Bun.write(join(process.cwd(), 'TODO.md'), '# Todo\n- [ ] thing\n');
|
await Bun.write(join(process.cwd(), 'TODO.md'), '# Todo\n- [ ] thing\n');
|
||||||
const session = new Session({ model: new MockLanguageModelV4({ doStream: async () => stream([]) }), askApproval: async () => "deny" });
|
const session = new Session({ model: new MockLanguageModelV4({ doStream: async () => stream([]) }), askApproval: async () => 'deny' });
|
||||||
const { workflowPanel } = await import('../src/ui/panel-bodies');
|
const { workflowPanel } = await import('../src/ui/panel-bodies');
|
||||||
const panel = workflowPanel(session);
|
const panel = workflowPanel(session);
|
||||||
expect(panel.title).toBe('workflow');
|
expect(panel.title).toBe('workflow');
|
||||||
expect(panel.body).toContain('TODO.md: yes');
|
expect(panel.body).toContain('TODO.md: yes');
|
||||||
expect(panel.body).toContain('workflow: on');
|
expect(panel.body).toContain('workflow: on');
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
test('context panel groups instructions and trackers separately', () => {
|
||||||
|
const pm = require('../src/ui/panel-bodies') as typeof import('../src/ui/panel-bodies');
|
||||||
|
const panel = pm.contextPanel(['/repo/AGENTS.md', '/repo/TODO.md', '/repo/docs/a.md']);
|
||||||
|
expect(panel.title).toBe('project instructions & trackers');
|
||||||
|
expect(panel.body).toContain('instructions:');
|
||||||
|
expect(panel.body).toContain('- `/repo/AGENTS.md`');
|
||||||
|
expect(panel.body).toContain('trackers:');
|
||||||
|
expect(panel.body).toContain('- `/repo/TODO.md`');
|
||||||
|
|
||||||
|
const empty = pm.contextPanel([]);
|
||||||
|
expect(empty.body).toContain('No `AGENTS.md`');
|
||||||
|
expect(empty.body).toContain('no project tracker is loaded');
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user