Skip to content
Closed
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
2 changes: 1 addition & 1 deletion codev-skeleton/protocols/aspir/builder-prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ You are running in STRICT mode. This means:
{{/if}}

## Protocol
Follow the SPIR protocol: `codev/protocols/spir/protocol.md`
Follow the ASPIR protocol: `codev/protocols/aspir/protocol.md`
Read and internalize the protocol before starting any work.

{{#if spec}}
Expand Down
2 changes: 1 addition & 1 deletion codev-skeleton/protocols/aspir/protocol.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
},
"checks": {
"plan_exists": "test -f codev/plans/${PROJECT_TITLE}.md",
"has_phases_json": "grep -q '\"phases\":' codev/plans/${PROJECT_TITLE}.md",
"has_phases_json": "grep -qE '\"phases\"\\s*:' codev/plans/${PROJECT_TITLE}.md",
"min_two_phases": "grep -o '\"id\": *\"[^\"]*\"' codev/plans/${PROJECT_TITLE}.md | wc -l | awk '$1 >= 2 {exit 0} {exit 1}'"
},
"next": "implement"
Expand Down
2 changes: 1 addition & 1 deletion codev-skeleton/protocols/spir/protocol.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
},
"checks": {
"plan_exists": "test -f codev/plans/${PROJECT_TITLE}.md",
"has_phases_json": "grep -q '\"phases\":' codev/plans/${PROJECT_TITLE}.md",
"has_phases_json": "grep -qE '\"phases\"\\s*:' codev/plans/${PROJECT_TITLE}.md",
"min_two_phases": "grep -o '\"id\": *\"[^\"]*\"' codev/plans/${PROJECT_TITLE}.md | wc -l | awk '$1 >= 2 {exit 0} {exit 1}'"
},
"gate": "plan-approval",
Expand Down
2 changes: 1 addition & 1 deletion codev/protocols/aspir/builder-prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ You are running in STRICT mode. This means:
{{/if}}

## Protocol
Follow the SPIR protocol: `codev/protocols/spir/protocol.md`
Follow the ASPIR protocol: `codev/protocols/aspir/protocol.md`
Read and internalize the protocol before starting any work.

{{#if spec}}
Expand Down
2 changes: 1 addition & 1 deletion codev/protocols/aspir/protocol.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
},
"checks": {
"plan_exists": "test -f codev/plans/${PROJECT_TITLE}.md",
"has_phases_json": "grep -q '\"phases\":' codev/plans/${PROJECT_TITLE}.md",
"has_phases_json": "grep -qE '\"phases\"\\s*:' codev/plans/${PROJECT_TITLE}.md",
"min_two_phases": "grep -o '\"id\": *\"[^\"]*\"' codev/plans/${PROJECT_TITLE}.md | wc -l | awk '$1 >= 2 {exit 0} {exit 1}'"
},
"next": "implement"
Expand Down
2 changes: 1 addition & 1 deletion codev/protocols/spir/protocol.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
},
"checks": {
"plan_exists": "test -f codev/plans/${PROJECT_TITLE}.md",
"has_phases_json": "grep -q '\"phases\":' codev/plans/${PROJECT_TITLE}.md",
"has_phases_json": "grep -qE '\"phases\"\\s*:' codev/plans/${PROJECT_TITLE}.md",
"min_two_phases": "grep -o '\"id\": *\"[^\"]*\"' codev/plans/${PROJECT_TITLE}.md | wc -l | awk '$1 >= 2 {exit 0} {exit 1}'"
},
"gate": "plan-approval",
Expand Down
2 changes: 1 addition & 1 deletion packages/codev/dashboard/src/components/AnalyticsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ function MiniBarChart({ data, dataKey, nameKey, color, formatter }: {
<XAxis type="number" hide />
<YAxis type="category" dataKey={nameKey} width={80} tick={{ fontSize: 10, fill: 'var(--text-secondary)' }} />
<Tooltip
formatter={formatter ? (v: number | undefined) => v != null ? formatter(v) : '' : undefined}
formatter={formatter ? ((v: unknown) => typeof v === 'number' ? formatter(v) : '') as any : undefined}
contentStyle={{ background: 'var(--bg-primary)', border: '1px solid var(--border-color)', fontSize: 11, borderRadius: 4 }}
labelStyle={{ color: 'var(--text-primary)' }}
itemStyle={{ color: 'var(--text-secondary)' }}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/**
* Regression tests for issue #619:
* 1. ASPIR builder-prompt referenced SPIR protocol path
* 2. af spawn --task with --protocol skipped porch init
* 3. has_phases_json check used fragile literal string (now uses -E regex)
*/

import { describe, it, expect, vi, beforeEach } from 'vitest';
import * as fs from 'node:fs';
import * as path from 'node:path';

// ============================================================================
// Bug 1: ASPIR builder-prompt must reference ASPIR, not SPIR
// ============================================================================

describe('ASPIR builder-prompt protocol reference', () => {
const skeletonPrompt = path.resolve(
__dirname,
'../../../../../codev-skeleton/protocols/aspir/builder-prompt.md',
);

it('references aspir/protocol.md, not spir/protocol.md', () => {
const content = fs.readFileSync(skeletonPrompt, 'utf-8');
expect(content).not.toContain('codev/protocols/spir/protocol.md');
expect(content).toContain('codev/protocols/aspir/protocol.md');
});

it('says "ASPIR protocol", not "SPIR protocol"', () => {
const content = fs.readFileSync(skeletonPrompt, 'utf-8');
expect(content).not.toMatch(/Follow the SPIR protocol/);
expect(content).toMatch(/Follow the ASPIR protocol/);
});
});

// ============================================================================
// Bug 2: spawnTask with explicit protocol must call initPorchInWorktree
// ============================================================================

describe('spawnTask porch init when --protocol is provided', () => {
beforeEach(() => {
vi.clearAllMocks();
});

it('initPorchInWorktree is called when hasExplicitProtocol is true', async () => {
const { initPorchInWorktree } = await import('../commands/spawn-worktree.js');
// Verify the function is exported (it must be importable for spawnTask to call it)
expect(typeof initPorchInWorktree).toBe('function');
});

it('spawnTask source calls initPorchInWorktree in the hasExplicitProtocol branch', () => {
// Read the spawn.ts source to verify the fix is present
const spawnSrc = path.resolve(__dirname, '../commands/spawn.ts');
expect(fs.existsSync(spawnSrc)).toBe(true);
const content = fs.readFileSync(spawnSrc, 'utf-8');

// The initPorchInWorktree call must appear inside the hasExplicitProtocol block
// (i.e., after the `if (hasExplicitProtocol)` line and before the closing `} else {`)
const hasExplicitBlock = content.match(
/if \(hasExplicitProtocol\)([\s\S]*?)(?=\} else \{)/,
);
expect(hasExplicitBlock).not.toBeNull();
expect(hasExplicitBlock![1]).toContain('initPorchInWorktree');
});
});

// ============================================================================
// Bug 3: has_phases_json check must use regex (-E) not literal string match
// ============================================================================

describe('has_phases_json check uses regex', () => {
const protocolFiles = [
path.resolve(__dirname, '../../../../../codev-skeleton/protocols/aspir/protocol.json'),
path.resolve(__dirname, '../../../../../codev-skeleton/protocols/spir/protocol.json'),
path.resolve(__dirname, '../../../../../codev/protocols/aspir/protocol.json'),
path.resolve(__dirname, '../../../../../codev/protocols/spir/protocol.json'),
];

for (const file of protocolFiles) {
const label = file.split('codev').pop() ?? file;

it(`${label}: has_phases_json uses grep -qE (regex-aware)`, () => {
const data = JSON.parse(fs.readFileSync(file, 'utf-8'));
const planPhase = data.phases.find((p: { id: string }) => p.id === 'plan');
expect(planPhase).toBeDefined();
const check = planPhase.checks?.has_phases_json as string | undefined;
expect(check).toBeDefined();
// Must use -E (extended regex) so whitespace variants like `"phases" :` also match
expect(check).toMatch(/grep -qE/);
// Must not use the old fragile literal-only form
expect(check).not.toMatch(/grep -q '/);
});
}
});
3 changes: 3 additions & 0 deletions packages/codev/src/agent-farm/commands/spawn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,9 @@ async function spawnTask(options: SpawnOptions, config: Config): Promise<void> {
};
const prompt = buildPromptFromTemplate(config, protocol, templateContext);
builderPrompt = `You are a Builder. Read codev/roles/builder.md for your full role definition.\n${resumeNotice}\n${prompt}`;
if (!options.resume) {
await initPorchInWorktree(worktreePath, protocol, builderId, worktreeName);
}
} else {
builderPrompt = `You are a Builder. Read codev/roles/builder.md for your full role definition.\n${resumeNotice}\n# Task\n\n${taskDescription}`;
}
Expand Down