Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
124ba47
🎉 feat(smithers): add project scaffolding
roninjin10 Apr 2, 2026
0e3fbbe
✨ feat(smithers): add workflow components
roninjin10 Apr 2, 2026
3000101
📝 feat(smithers): add prompt templates
roninjin10 Apr 2, 2026
9ca377f
🔧 feat(smithers): add workflow definitions
roninjin10 Apr 2, 2026
3398076
📄 docs(smithers-tui): add TUI design documents
roninjin10 Apr 2, 2026
19caede
feat: add Smithers domain system prompt support
roninjin10 Apr 3, 2026
24b546c
feat: add smithers helpbar shortcuts
roninjin10 Apr 3, 2026
7e17dc4
feat: implement smithers chat branding and status scaffolding
roninjin10 Apr 3, 2026
0515f27
🔧 chore: add .worktrees to gitignore and scheduled tasks lock
roninjin10 Apr 3, 2026
4cff80b
🏷️ refactor: rebrand crush to smithers-tui across Go codebase
roninjin10 Apr 3, 2026
ed8a70c
🧪 test: update tests for smithers-tui rebrand
roninjin10 Apr 3, 2026
c170cf3
✨ feat(smithers): add cloud API client package with types and tests
roninjin10 Apr 3, 2026
b05fcc4
✨ feat(ui): add views router with agents, tickets, and approvals
roninjin10 Apr 3, 2026
34e1ca5
🧪 test(e2e): add smithers domain system prompt test and VHS recording
roninjin10 Apr 3, 2026
53acc6d
📄 docs(smithers-tui): update PRD, design, and engineering documents
roninjin10 Apr 3, 2026
35f37d5
📋 feat(smithers): add specs pipeline artifacts (engineering, research…
roninjin10 Apr 3, 2026
4cd33e8
🎫 feat(smithers): add ticket definitions
roninjin10 Apr 3, 2026
b00b2b5
🔧 feat(smithers): add workflows, prompts, and agent configuration
roninjin10 Apr 3, 2026
7537e17
feat(mcp): add Smithers MCP default configuration and injection
roninjin10 Apr 5, 2026
2fbd61a
test(mcp): add Smithers MCP discovery integration tests
roninjin10 Apr 5, 2026
8334c49
test(e2e): add MCP tool discovery VHS happy-path recording
roninjin10 Apr 5, 2026
9923769
feat(chat): enhance router with chat-root semantics and default-to-ch…
roninjin10 Apr 5, 2026
4938e1f
test(chat): add router unit tests and default-console E2E placeholder
roninjin10 Apr 5, 2026
69c6710
docs(chat-default-console): add implementation summary
roninjin10 Apr 5, 2026
46121a2
feat(smithers): add workflow management methods to client
roninjin10 Apr 5, 2026
dd7ea01
docs(smithers): add implementation summary for eng-smithers-workflows…
roninjin10 Apr 5, 2026
dc1c864
🔧 chore: update gitignore and clean up worktrees
roninjin10 Apr 5, 2026
41758d1
📝 docs(smithers): add specs pipeline — research, plans, engineering, …
roninjin10 Apr 5, 2026
17d5ac3
✨ feat(smithers): add API clients for runs, prompts, systems, tickets…
roninjin10 Apr 5, 2026
fb50f26
✨ feat(ui): add split-pane, toast, DAG, run-table components and all …
roninjin10 Apr 5, 2026
9aaf025
✨ feat(agent): enhance smithers prompt, coordinator, and templates
roninjin10 Apr 5, 2026
124501f
🔧 chore: update CLI commands, config, app wiring, and oauth
roninjin10 Apr 5, 2026
16845a9
✨ feat(jjhub): add client and gh-dash-inspired TUI proof-of-concept
roninjin10 Apr 5, 2026
62ff75b
✅ test(e2e): add E2E test suite, VHS tapes, and TUI test infrastructure
roninjin10 Apr 5, 2026
94f9bce
chore(deps): bump the all group across 1 directory with 3 updates
dependabot[bot] Apr 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 1 addition & 0 deletions .claude/scheduled_tasks.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"sessionId":"5c1e7a42-7e7d-4fe4-90e9-604dd995eb94","pid":5187,"acquiredAt":1775271415148}
1 change: 1 addition & 0 deletions .claude/worktrees/agent-a47f3aeb
Submodule agent-a47f3aeb added at c87fef
1 change: 1 addition & 0 deletions .claude/worktrees/agent-a514a89b
Submodule agent-a514a89b added at c87fef
1 change: 1 addition & 0 deletions .claude/worktrees/agent-ae4d8f53
Submodule agent-ae4d8f53 added at c87fef
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,10 @@ manpages/
completions/crush.*sh
.prettierignore
.task
.worktrees
tests/node_modules/
tests/smithers-tui
tests/.tui-test/
jjhub-tui
/poc/jjhub-tui/jjhub-tui
smithers.db*
8 changes: 8 additions & 0 deletions .smithers/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
node_modules/
executions/
*.db
*.db-shm
*.db-wal
*.sqlite
dist/
.DS_Store
26 changes: 26 additions & 0 deletions .smithers/agents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// smithers-source: generated
import { ClaudeCodeAgent, CodexAgent, GeminiAgent, PiAgent, KimiAgent, AmpAgent, type AgentLike } from "smithers-orchestrator";

export const providers = {
claude: new ClaudeCodeAgent({ model: "claude-opus-4-6" }),
codex: new CodexAgent({ model: "gpt-5.3-codex", skipGitRepoCheck: true }),
gemini: new GeminiAgent({ model: "gemini-3.1-pro-preview" }),
pi: new PiAgent({ provider: "openai", model: "gpt-5.3-codex" }),
kimi: new KimiAgent({ model: "kimi-latest" }),
amp: new AmpAgent(),
} as const;

export const roleChains = {
spec: [providers.claude, providers.codex],
research: [providers.codex, providers.kimi, providers.gemini, providers.claude],
plan: [providers.codex, providers.gemini, providers.claude, providers.kimi],
implement: [providers.codex, providers.amp, providers.gemini, providers.claude, providers.kimi],
validate: [providers.codex, providers.amp, providers.gemini],
review: [providers.claude, providers.amp, providers.codex],
} as const satisfies Record<string, AgentLike[]>;

export function pickAgent(role: keyof typeof roleChains): AgentLike {
const agent = roleChains[role][0];
if (!agent) throw new Error(`No agent configured for role: ${role}`);
return agent;
}
1 change: 1 addition & 0 deletions .smithers/bunfig.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
preload = ["./preload.ts"]
17 changes: 17 additions & 0 deletions .smithers/components/CommandProbe.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// smithers-source: seeded
/** @jsxImportSource smithers-orchestrator */
import { Task } from "smithers-orchestrator";
import { z } from "zod";

export const commandProbeOutputSchema = z.object({
command: z.string(),
available: z.boolean(),
}).passthrough();

export function CommandProbe({ id, command }: { id: string; command: string }) {
return (
<Task id={id} output={commandProbeOutputSchema}>
{{ command, available: true }}
</Task>
);
}
132 changes: 132 additions & 0 deletions .smithers/components/FeatureEnum.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// smithers-source: seeded
/** @jsxImportSource smithers-orchestrator */
import { Sequence, Task, type AgentLike } from "smithers-orchestrator";
import { z } from "zod";
import FeatureEnumScanPrompt from "../prompts/feature-enum-scan.mdx";
import FeatureEnumRefinePrompt from "../prompts/feature-enum-refine.mdx";

export const featureEnumOutputSchema = z.object({
featureGroups: z.record(z.string(), z.array(z.string())).default({}),
totalFeatures: z.number().int().default(0),
lastCommitHash: z.string().nullable().optional(),
markdownBody: z.string(),
}).passthrough();

type FeatureEnumProps = {
idPrefix: string;
agent: AgentLike | AgentLike[];
refineIterations?: number;
existingFeatures?: Record<string, string[]> | null;
lastCommitHash?: string | null;
additionalContext?: string;
};

const memoryNamespace = { kind: "workflow", id: "feature-enum" } as const;

export function FeatureEnum({
idPrefix,
agent,
refineIterations,
existingFeatures = null,
lastCommitHash = null,
additionalContext = "",
}: FeatureEnumProps) {
const isFirstRun = !existingFeatures;
const totalRefineIterations = Math.max(1, refineIterations ?? (isFirstRun ? 5 : 1));
const scanTaskId = `${idPrefix}:scan`;
const refineTaskIds = Array.from({ length: totalRefineIterations }, (_, index) => `${idPrefix}:refine:${index + 1}`);
const finalTaskId = `${idPrefix}:result`;

return (
<Sequence>
{isFirstRun && (
<Task
id={scanTaskId}
output={featureEnumOutputSchema}
agent={agent}
memory={{
remember: {
namespace: memoryNamespace,
key: scanTaskId,
},
}}
>
<FeatureEnumScanPrompt additionalContext={additionalContext} />
</Task>
)}

{refineTaskIds.map((taskId, index) => {
const previousTaskId = index === 0
? (isFirstRun ? scanTaskId : null)
: refineTaskIds[index - 1];

if (previousTaskId) {
return (
<Task
key={taskId}
id={taskId}
output={featureEnumOutputSchema}
agent={agent}
needs={{ previous: previousTaskId }}
deps={{ previous: featureEnumOutputSchema }}
memory={{
recall: {
namespace: memoryNamespace,
query: "feature inventory feature enum grouped features",
topK: 5,
},
remember: {
namespace: memoryNamespace,
key: taskId,
},
}}
>
{(deps) => (
<FeatureEnumRefinePrompt
existingFeatures={deps.previous.featureGroups}
lastCommitHash={deps.previous.lastCommitHash ?? lastCommitHash}
iteration={index + 1}
/>
)}
</Task>
);
}

return (
<Task
key={taskId}
id={taskId}
output={featureEnumOutputSchema}
agent={agent}
memory={{
recall: {
namespace: memoryNamespace,
query: "feature inventory feature enum grouped features",
topK: 5,
},
remember: {
namespace: memoryNamespace,
key: taskId,
},
}}
>
<FeatureEnumRefinePrompt
existingFeatures={existingFeatures ?? {}}
lastCommitHash={lastCommitHash}
iteration={index + 1}
/>
</Task>
);
})}

<Task
id={finalTaskId}
output={featureEnumOutputSchema}
needs={{ final: refineTaskIds[refineTaskIds.length - 1] ?? scanTaskId }}
deps={{ final: featureEnumOutputSchema }}
>
{(deps) => deps.final}
</Task>
</Sequence>
);
}
152 changes: 152 additions & 0 deletions .smithers/components/ForEachFeature.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
// smithers-source: seeded
/** @jsxImportSource smithers-orchestrator */
import { Parallel, Sequence, Task, type AgentLike } from "smithers-orchestrator";
import { z } from "zod";

export const forEachFeatureResultSchema = z.object({
groupName: z.string(),
result: z.string(),
featuresCovered: z.array(z.string()).default([]),
score: z.number().min(0).max(100).optional(),
}).passthrough();

export const forEachFeatureMergeSchema = z.object({
totalGroups: z.number().int(),
summary: z.string(),
mergedResult: z.string(),
markdownBody: z.string(),
}).passthrough();

type ForEachFeatureProps = {
idPrefix: string;
agent: AgentLike | AgentLike[];
features: Record<string, string[]>;
prompt: string;
maxConcurrency?: number;
mergeAgent?: AgentLike | AgentLike[];
granularity?: "group" | "feature";
};

type FeatureWorkItem = {
id: string;
groupName: string;
features: string[];
};

function slugifyFeatureToken(value: string) {
const normalized = value
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
return normalized.length > 0 ? normalized : "item";
}

export function ForEachFeature({
idPrefix,
agent,
features,
prompt,
maxConcurrency = 5,
mergeAgent,
granularity = "group",
}: ForEachFeatureProps) {
const featureEntries = Object.entries(features ?? {}).filter(([, groupFeatures]) => Array.isArray(groupFeatures) && groupFeatures.length > 0);
const workItems: FeatureWorkItem[] = granularity === "feature"
? featureEntries.flatMap(([groupName, groupFeatures]) =>
groupFeatures.map((feature, index) => ({
id: `${slugifyFeatureToken(groupName)}:${slugifyFeatureToken(feature)}:${index}`,
groupName,
features: [feature],
})),
)
: featureEntries.map(([groupName, groupFeatures], index) => ({
id: `${slugifyFeatureToken(groupName)}:${index}`,
groupName,
features: groupFeatures,
}));

const mergeNeeds: Record<string, string> = Object.fromEntries(
workItems.map((item, index) => [`item${index}`, `${idPrefix}:group:${item.id}`]),
);
const mergeDeps = Object.fromEntries(
workItems.map((_, index) => [`item${index}`, forEachFeatureResultSchema]),
) as Record<string, typeof forEachFeatureResultSchema>;

if (workItems.length === 0) {
return (
<Sequence>
<Task id={`${idPrefix}:merge`} output={forEachFeatureMergeSchema}>
{{
totalGroups: 0,
summary: "No feature groups were provided.",
mergedResult: "",
markdownBody: "# Feature Audit\n\nNo feature groups were provided.",
}}
</Task>
</Sequence>
);
}

return (
<Sequence>
<Parallel maxConcurrency={maxConcurrency}>
{workItems.map((item) => (
<Task
key={`${idPrefix}:${item.id}`}
id={`${idPrefix}:group:${item.id}`}
output={forEachFeatureResultSchema}
agent={agent}
continueOnFail
>
{[
`# ${granularity === "feature" ? "Feature" : "Feature Group"} Task`,
"",
`Group: ${item.groupName}`,
`Granularity: ${granularity}`,
"",
"Features:",
...item.features.map((feature) => `- ${feature}`),
"",
"REQUEST:",
prompt,
].join("\n")}
</Task>
))}
</Parallel>
<Task
id={`${idPrefix}:merge`}
output={forEachFeatureMergeSchema}
agent={mergeAgent ?? agent}
needs={mergeNeeds}
deps={mergeDeps}
>
{(deps) => {
const results = workItems.map((_, index) => deps[`item${index}`]);
const totalGroups = new Set(workItems.map((item) => item.groupName)).size;
return [
"# Merge Feature Results",
"",
`Granularity: ${granularity}`,
`Distinct groups: ${totalGroups}`,
`Work items: ${workItems.length}`,
`Set totalGroups to ${totalGroups}.`,
"",
"Combine the per-group results below into a single coherent output.",
"Preserve group boundaries, highlight the most important gaps, and produce a markdownBody suitable for a report.",
"",
...results.flatMap((result, index) => {
const groupLabel = workItems[index]?.groupName ?? `Group ${index + 1}`;
return [
`## ${groupLabel}`,
`Features covered: ${(result?.featuresCovered ?? []).join(", ") || "none"}`,
result?.score != null ? `Score: ${result.score}` : null,
result?.result ?? "",
"",
].filter(Boolean);
}),
].join("\n");
}}
</Task>
</Sequence>
);
}
Loading
Loading