-
Notifications
You must be signed in to change notification settings - Fork 12
software factory: explore running it only using Claude Code + Skills + Boxel CLI #4843
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
jurgenwerk
wants to merge
20
commits into
main
Choose a base branch
from
cs-11149-create-a-set-of-skills-extracted-from-the-orchestrator
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
281df90
Add Phase 1 interactive runbook for software factory
jurgenwerk 65baddc
Add `boxel lint` top-level command
jurgenwerk f58a6dd
Add `boxel parse` top-level command
jurgenwerk 72429e2
Add `boxel test` top-level command
jurgenwerk eb7317d
Update Phase 1 runbook: validator CLIs are landed
jurgenwerk 82b8698
Add software-factory-scheduling skill
jurgenwerk ffb5cc4
Rewrite software-factory-operations skill for interactive flow
jurgenwerk 29c7c40
Rewrite software-factory-bootstrap skill for interactive flow
jurgenwerk 267dc4d
Fix-ups from first interactive test run
jurgenwerk a1df95c
Default to dev `boxel` CLI in skills (don't expect it on PATH)
jurgenwerk eb69d34
Fix boxel realm push/pull/sync command syntax in skills
jurgenwerk cac3f05
Fix `boxel realm create` realm-name argument guidance
jurgenwerk 0c71451
Fix validator input shapes + loader.import typing in skills
jurgenwerk 55f7e48
Put validation artifact cards back into the Phase 1 flow
jurgenwerk 8259edf
Collapse runbook to single prompt + add bootstrap-seed Issue
jurgenwerk 0845c88
Drop sticky-note specifics from the runbook
jurgenwerk 39d88cf
Rename interactive-runbook.md → runbook.md
jurgenwerk b06dfe1
Make the validator-iteration loop explicit
jurgenwerk 622dc9f
Add validator-loop bail-out limits
jurgenwerk 2e31989
Drop spec-tracking content from runbook
jurgenwerk File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,280 @@ | ||
| import type { Command } from 'commander'; | ||
| import { ensureTrailingSlash } from '@cardstack/runtime-common/paths'; | ||
| import { SupportedMimeType } from '@cardstack/runtime-common/supported-mime-type'; | ||
| import { | ||
| getProfileManager, | ||
| NO_ACTIVE_PROFILE_ERROR, | ||
| type ProfileManager, | ||
| } from '../lib/profile-manager'; | ||
| import { FG_RED, FG_YELLOW, DIM, RESET } from '../lib/colors'; | ||
| import { cliLog } from '../lib/cli-log'; | ||
| import { lint as lintSingleFile, type LintMessage } from './file/lint'; | ||
| import { listFiles } from './file/list'; | ||
|
|
||
| const LINTABLE_EXTENSIONS = ['.gts', '.gjs', '.ts', '.js'] as const; | ||
|
|
||
| export interface LintRealmViolation { | ||
| rule: string | null; | ||
| file: string; | ||
| line: number; | ||
| column: number; | ||
| message: string; | ||
| severity: 'error' | 'warning'; | ||
| } | ||
|
|
||
| export interface LintRealmResult { | ||
| status: 'passed' | 'failed' | 'error'; | ||
| filesChecked: number; | ||
| filesWithErrors: number; | ||
| errorCount: number; | ||
| warningCount: number; | ||
| durationMs: number; | ||
| lintableFiles: string[]; | ||
| violations: LintRealmViolation[]; | ||
| errorMessage?: string; | ||
| } | ||
|
|
||
| export interface LintRealmOptions { | ||
| /** Optional realm-relative path. When set, lints only that file. */ | ||
| path?: string; | ||
| profileManager?: ProfileManager; | ||
| } | ||
|
|
||
| /** | ||
| * Lint every lintable file (`.gts`, `.gjs`, `.ts`, `.js`) in a realm, | ||
| * or a single file when `options.path` is set. Source is fetched from | ||
| * the realm; the realm's `_lint` endpoint runs ESLint + Prettier with | ||
| * the `@cardstack/boxel` rules. | ||
| */ | ||
| export async function lintRealm( | ||
| realmUrl: string, | ||
| options?: LintRealmOptions, | ||
| ): Promise<LintRealmResult> { | ||
| let pm = options?.profileManager ?? getProfileManager(); | ||
| let active = pm.getActiveProfile(); | ||
| if (!active) { | ||
| return emptyErrorResult(NO_ACTIVE_PROFILE_ERROR); | ||
| } | ||
|
|
||
| let normalizedRealmUrl = ensureTrailingSlash(realmUrl); | ||
| let startedAt = Date.now(); | ||
|
|
||
| let lintableFiles: string[]; | ||
| if (options?.path) { | ||
| let path = options.path; | ||
| if (!LINTABLE_EXTENSIONS.some((ext) => path.endsWith(ext))) { | ||
| return emptyErrorResult( | ||
| `Path "${path}" is not lintable — must end with one of ${LINTABLE_EXTENSIONS.join(', ')}`, | ||
| ); | ||
| } | ||
| lintableFiles = [path]; | ||
| } else { | ||
| let listResult = await listFiles(normalizedRealmUrl, { | ||
| profileManager: pm, | ||
| }); | ||
| if (listResult.error) { | ||
| return emptyErrorResult( | ||
| `Failed to list realm files: ${listResult.error}`, | ||
| ); | ||
| } | ||
| lintableFiles = listResult.filenames.filter((f) => | ||
| LINTABLE_EXTENSIONS.some((ext) => f.endsWith(ext)), | ||
| ); | ||
| } | ||
|
|
||
| if (lintableFiles.length === 0) { | ||
| return { | ||
| status: 'passed', | ||
| filesChecked: 0, | ||
| filesWithErrors: 0, | ||
| errorCount: 0, | ||
| warningCount: 0, | ||
| durationMs: Date.now() - startedAt, | ||
| lintableFiles: [], | ||
| violations: [], | ||
| }; | ||
| } | ||
|
|
||
| let violations: LintRealmViolation[] = []; | ||
| let filesWithErrors = 0; | ||
| let errorCount = 0; | ||
| let warningCount = 0; | ||
|
|
||
| for (let file of lintableFiles) { | ||
| let source: string; | ||
| try { | ||
| let readUrl = new URL(file, normalizedRealmUrl).href; | ||
| let response = await pm.authedRealmFetch(readUrl, { | ||
| method: 'GET', | ||
| headers: { Accept: SupportedMimeType.CardSource }, | ||
| }); | ||
| if (!response.ok) { | ||
| let body = await response.text().catch(() => '(no body)'); | ||
| recordReadError( | ||
| file, | ||
| `HTTP ${response.status}: ${body.slice(0, 300)}`, | ||
| violations, | ||
| ); | ||
| filesWithErrors += 1; | ||
| errorCount += 1; | ||
| continue; | ||
| } | ||
| source = await response.text(); | ||
| } catch (err) { | ||
| recordReadError( | ||
| file, | ||
| err instanceof Error ? err.message : String(err), | ||
| violations, | ||
| ); | ||
| filesWithErrors += 1; | ||
| errorCount += 1; | ||
| continue; | ||
| } | ||
|
|
||
| let result = await lintSingleFile(normalizedRealmUrl, source, file, { | ||
| profileManager: pm, | ||
| }); | ||
|
|
||
| if (!result.ok) { | ||
| recordReadError(file, result.error ?? 'lint failed', violations); | ||
| filesWithErrors += 1; | ||
| errorCount += 1; | ||
| continue; | ||
| } | ||
|
|
||
| let fileHasError = false; | ||
| for (let msg of result.messages ?? []) { | ||
| let severity: 'error' | 'warning' = | ||
| msg.severity === 2 ? 'error' : 'warning'; | ||
| violations.push({ | ||
| rule: msg.ruleId, | ||
| file, | ||
| line: msg.line, | ||
| column: msg.column, | ||
| message: msg.message, | ||
| severity, | ||
| }); | ||
| if (severity === 'error') { | ||
| errorCount += 1; | ||
| fileHasError = true; | ||
| } else { | ||
| warningCount += 1; | ||
| } | ||
| } | ||
| if (fileHasError) filesWithErrors += 1; | ||
| } | ||
|
|
||
| return { | ||
| status: errorCount === 0 ? 'passed' : 'failed', | ||
| filesChecked: lintableFiles.length, | ||
| filesWithErrors, | ||
| errorCount, | ||
| warningCount, | ||
| durationMs: Date.now() - startedAt, | ||
| lintableFiles, | ||
| violations, | ||
| }; | ||
| } | ||
|
|
||
| function recordReadError( | ||
| file: string, | ||
| detail: string, | ||
| violations: LintRealmViolation[], | ||
| ): void { | ||
| violations.push({ | ||
| rule: 'lint-error', | ||
| file, | ||
| line: 0, | ||
| column: 0, | ||
| message: detail, | ||
| severity: 'error', | ||
| }); | ||
| } | ||
|
|
||
| function emptyErrorResult(message: string): LintRealmResult { | ||
| return { | ||
| status: 'error', | ||
| filesChecked: 0, | ||
| filesWithErrors: 0, | ||
| errorCount: 0, | ||
| warningCount: 0, | ||
| durationMs: 0, | ||
| lintableFiles: [], | ||
| violations: [], | ||
| errorMessage: message, | ||
| }; | ||
| } | ||
|
|
||
| interface LintCliOptions { | ||
| realm: string; | ||
| json?: boolean; | ||
| } | ||
|
|
||
| export function registerLintCommand(program: Command): void { | ||
| program | ||
| .command('lint') | ||
| .description( | ||
| 'Lint every lintable (.gts/.gjs/.ts/.js) file in a realm via the realm lint endpoint. Pass a realm-relative path to lint a single file.', | ||
| ) | ||
| .argument( | ||
| '[path]', | ||
| 'Optional realm-relative file path. When omitted, lints every lintable file in the realm.', | ||
| ) | ||
| .requiredOption('--realm <realm-url>', 'The realm URL to lint against') | ||
| .option('--json', 'Output structured JSON result') | ||
| .action(async (path: string | undefined, opts: LintCliOptions) => { | ||
| let result: LintRealmResult; | ||
| try { | ||
| result = await lintRealm(opts.realm, path ? { path } : {}); | ||
| } catch (err) { | ||
| console.error( | ||
| `${FG_RED}Error:${RESET} ${err instanceof Error ? err.message : String(err)}`, | ||
| ); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| if (opts.json) { | ||
| cliLog.output(JSON.stringify(result, null, 2)); | ||
| if (result.status !== 'passed') { | ||
| process.exit(1); | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| if (result.errorMessage) { | ||
| console.error(`${FG_RED}Error:${RESET} ${result.errorMessage}`); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| if (result.violations.length === 0) { | ||
| console.log( | ||
| `${DIM}No lint issues found (${result.filesChecked} file(s) checked).${RESET}`, | ||
| ); | ||
| return; | ||
| } | ||
|
|
||
| let currentFile: string | undefined; | ||
| for (let v of result.violations) { | ||
| if (v.file !== currentFile) { | ||
| currentFile = v.file; | ||
| console.log(`\n${DIM}${v.file}${RESET}`); | ||
| } | ||
| let color = v.severity === 'error' ? FG_RED : FG_YELLOW; | ||
| let rule = v.rule ? ` (${v.rule})` : ''; | ||
| console.log( | ||
| ` ${color}${v.severity}${RESET} ${v.line}:${v.column} ${v.message}${DIM}${rule}${RESET}`, | ||
| ); | ||
| } | ||
|
|
||
| console.log( | ||
| `\n${DIM}${result.errorCount} error(s), ${result.warningCount} warning(s) across ${result.filesChecked} file(s)${RESET}`, | ||
| ); | ||
|
|
||
| if (result.errorCount > 0) { | ||
| process.exit(1); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| // Re-export for callers that want the type alongside the function. | ||
| export type { LintMessage }; |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@playwright/testis added underdevDependencies, but the CLI eagerly imports./commands/testduring startup and that module has a top-levelimport { chromium } from '@playwright/test'. In normal npm/global installs, dev dependencies are not installed, so even unrelated commands (for exampleboxel --helporboxel profile list) will fail at process start with a module-resolution error before argument parsing. This needs either a runtime dependency or deferred import inside thetestcommand path.Useful? React with 👍 / 👎.