From 4aeb0d245517b6ca5522b1e00c028a7f6923b27c Mon Sep 17 00:00:00 2001 From: asepharyana Date: Wed, 9 Sep 2026 19:12:29 +0700 Subject: [PATCH] =?UTF-8?q?workflow:=20polish=20=E2=80=94=20/context=20gro?= =?UTF-8?q?ups=20trackers,=20/workflow=20in=20README,=20memoize=20git=20ro?= =?UTF-8?q?ot?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- README.md | 2 ++ src/session.ts | 18 +++++++++++++++--- src/ui/panel-bodies.ts | 17 ++++++++++------- test/workflow.test.ts | 18 ++++++++++++++++-- 4 files changed, 43 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 3e84ed4..50db42c 100644 --- a/README.md +++ b/README.md @@ -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 | | [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 | +| [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 | | [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 | @@ -151,6 +152,7 @@ Type `/` and a menu appears, narrowing as you type. /skills /plugins /registry [search|add|remove] /mcp [add|remove] /init /context /todos /notes /memory /tools /compact /cost /sessions /resume /save /clear /exit +/undo /redo /changes /search /fork /workflow ``` `esc` dismisses a panel, interrupts a running turn, and clears the queue. `ctrl-c` kills the diff --git a/src/session.ts b/src/session.ts index 508d8f6..b511094 100644 --- a/src/session.ts +++ b/src/session.ts @@ -423,18 +423,29 @@ export class Session { * Node's existsSync over Bun.file(...).exists(). */ private gitRoot(): string | undefined { + if (this.gitRootResolved) return this.cachedGitRoot ?? undefined; + this.gitRootResolved = true; try { let dir = resolve(this.opts.cwd ?? process.cwd()); 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); - if (parent === dir) return undefined; + if (parent === dir) { + this.cachedGitRoot = null; + return undefined; + } dir = parent; } } catch { + this.cachedGitRoot = null; return undefined; } } + private cachedGitRoot: string | null | undefined; + private gitRootResolved = false; /** * 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 memoryBlock = mem ? mem.render() : ''; + const workflowPolicy = this.workflowPolicy(); const text = systemPrompt({ cwd: this.opts.cwd ?? process.cwd(), instructions: this.opts.instructions ?? [], @@ -775,7 +787,7 @@ export class Session { canAsk: this.opts.ask !== undefined && this.activeTools().includes('ask'), ...(this.mcpServerNamesForPrompt() ? { mcpServers: this.mcpServerNamesForPrompt() } : {}), ...(this.workspaceFiles && this.workspaceFiles.length > 0 ? { workspaceFiles: this.workspaceFiles } : {}), - ...(this.workflowPolicy() ? { workflowPolicy: this.workflowPolicy() } : {}), + ...(workflowPolicy ? { workflowPolicy } : {}), }); this.promptCache = { key: versionKey, text }; return text; diff --git a/src/ui/panel-bodies.ts b/src/ui/panel-bodies.ts index 4b1a84a..1867603 100644 --- a/src/ui/panel-bodies.ts +++ b/src/ui/panel-bodies.ts @@ -72,13 +72,16 @@ export function costPanel( } export function contextPanel(files: readonly string[]): Panel { - return { - title: 'project instructions', - body: - files.length > 0 - ? files.map((f) => `- \`${f}\``).join('\n') - : 'No `AGENTS.md`, `CLAUDE.md`, or `.shiro.md` found. Run `/init` to write one.', - }; + const tracker = files.filter((f) => f.endsWith('TODO.md') || f.endsWith('ROADMAP.md')); + const instructions = files.filter((f) => !tracker.includes(f)); + const body: string[] = []; + if (instructions.length > 0) body.push('instructions:', ...instructions.map((f) => `- \`${f}\``)); + if (tracker.length > 0) body.push('trackers:', ...tracker.map((f) => `- \`${f}\``)); + 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 => ({ diff --git a/test/workflow.test.ts b/test/workflow.test.ts index 4ce87b0..79843cf 100644 --- a/test/workflow.test.ts +++ b/test/workflow.test.ts @@ -169,10 +169,24 @@ test('/workflow parses to the workflow action', () => { test('/workflow panel renders the status rows', () => inGitRepo(async () => { 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 panel = workflowPanel(session); expect(panel.title).toBe('workflow'); expect(panel.body).toContain('TODO.md: yes'); expect(panel.body).toContain('workflow: on'); - })); \ No newline at end of file + })); + +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'); +}); \ No newline at end of file