diff --git a/codev-skeleton/protocols/aspir/builder-prompt.md b/codev-skeleton/protocols/aspir/builder-prompt.md
index 1f932b6d46..73c3ba6071 100644
--- a/codev-skeleton/protocols/aspir/builder-prompt.md
+++ b/codev-skeleton/protocols/aspir/builder-prompt.md
@@ -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}}
diff --git a/codev-skeleton/protocols/aspir/protocol.json b/codev-skeleton/protocols/aspir/protocol.json
index 3c7dbc5567..1277c69934 100644
--- a/codev-skeleton/protocols/aspir/protocol.json
+++ b/codev-skeleton/protocols/aspir/protocol.json
@@ -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"
diff --git a/codev-skeleton/protocols/spir/protocol.json b/codev-skeleton/protocols/spir/protocol.json
index b046252bb1..f2b99be4f4 100644
--- a/codev-skeleton/protocols/spir/protocol.json
+++ b/codev-skeleton/protocols/spir/protocol.json
@@ -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",
diff --git a/codev/protocols/aspir/builder-prompt.md b/codev/protocols/aspir/builder-prompt.md
index 1f932b6d46..73c3ba6071 100644
--- a/codev/protocols/aspir/builder-prompt.md
+++ b/codev/protocols/aspir/builder-prompt.md
@@ -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}}
diff --git a/codev/protocols/aspir/protocol.json b/codev/protocols/aspir/protocol.json
index 3c7dbc5567..1277c69934 100644
--- a/codev/protocols/aspir/protocol.json
+++ b/codev/protocols/aspir/protocol.json
@@ -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"
diff --git a/codev/protocols/spir/protocol.json b/codev/protocols/spir/protocol.json
index b046252bb1..f2b99be4f4 100644
--- a/codev/protocols/spir/protocol.json
+++ b/codev/protocols/spir/protocol.json
@@ -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",
diff --git a/packages/codev/dashboard/src/components/AnalyticsView.tsx b/packages/codev/dashboard/src/components/AnalyticsView.tsx
index 82e39014e0..8b39576ea4 100644
--- a/packages/codev/dashboard/src/components/AnalyticsView.tsx
+++ b/packages/codev/dashboard/src/components/AnalyticsView.tsx
@@ -83,7 +83,7 @@ function MiniBarChart({ data, dataKey, nameKey, color, formatter }: {
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)' }}
diff --git a/packages/codev/src/agent-farm/__tests__/bugfix-619-aspir-prompt.test.ts b/packages/codev/src/agent-farm/__tests__/bugfix-619-aspir-prompt.test.ts
new file mode 100644
index 0000000000..ae6175c096
--- /dev/null
+++ b/packages/codev/src/agent-farm/__tests__/bugfix-619-aspir-prompt.test.ts
@@ -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 '/);
+ });
+ }
+});
diff --git a/packages/codev/src/agent-farm/commands/spawn.ts b/packages/codev/src/agent-farm/commands/spawn.ts
index db37371b30..b603918300 100644
--- a/packages/codev/src/agent-farm/commands/spawn.ts
+++ b/packages/codev/src/agent-farm/commands/spawn.ts
@@ -424,6 +424,9 @@ async function spawnTask(options: SpawnOptions, config: Config): Promise {
};
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}`;
}