@delegance/claude-autopilot 5.2.2 → 6.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/CHANGELOG.md +1027 -1
  2. package/README.md +104 -17
  3. package/dist/src/adapters/council/claude.js +2 -1
  4. package/dist/src/adapters/council/openai.js +14 -7
  5. package/dist/src/adapters/deploy/_http.d.ts +43 -0
  6. package/dist/src/adapters/deploy/_http.js +99 -0
  7. package/dist/src/adapters/deploy/fly.d.ts +206 -0
  8. package/dist/src/adapters/deploy/fly.js +696 -0
  9. package/dist/src/adapters/deploy/generic.d.ts +39 -0
  10. package/dist/src/adapters/deploy/generic.js +98 -0
  11. package/dist/src/adapters/deploy/index.d.ts +15 -0
  12. package/dist/src/adapters/deploy/index.js +78 -0
  13. package/dist/src/adapters/deploy/render.d.ts +181 -0
  14. package/dist/src/adapters/deploy/render.js +550 -0
  15. package/dist/src/adapters/deploy/types.d.ts +221 -0
  16. package/dist/src/adapters/deploy/types.js +15 -0
  17. package/dist/src/adapters/deploy/vercel.d.ts +143 -0
  18. package/dist/src/adapters/deploy/vercel.js +426 -0
  19. package/dist/src/adapters/pricing.d.ts +36 -0
  20. package/dist/src/adapters/pricing.js +40 -0
  21. package/dist/src/adapters/review-engine/claude.js +2 -1
  22. package/dist/src/adapters/review-engine/codex.js +12 -8
  23. package/dist/src/adapters/review-engine/gemini.js +2 -1
  24. package/dist/src/adapters/review-engine/openai-compatible.js +2 -1
  25. package/dist/src/adapters/sdk-loader.d.ts +15 -0
  26. package/dist/src/adapters/sdk-loader.js +77 -0
  27. package/dist/src/cli/autopilot.d.ts +71 -0
  28. package/dist/src/cli/autopilot.js +735 -0
  29. package/dist/src/cli/brainstorm.d.ts +23 -0
  30. package/dist/src/cli/brainstorm.js +131 -0
  31. package/dist/src/cli/costs.d.ts +15 -1
  32. package/dist/src/cli/costs.js +99 -10
  33. package/dist/src/cli/deploy.d.ts +71 -0
  34. package/dist/src/cli/deploy.js +539 -0
  35. package/dist/src/cli/fix.d.ts +18 -0
  36. package/dist/src/cli/fix.js +105 -11
  37. package/dist/src/cli/help-text.d.ts +52 -0
  38. package/dist/src/cli/help-text.js +400 -0
  39. package/dist/src/cli/implement.d.ts +91 -0
  40. package/dist/src/cli/implement.js +196 -0
  41. package/dist/src/cli/index.js +784 -222
  42. package/dist/src/cli/json-envelope.d.ts +187 -0
  43. package/dist/src/cli/json-envelope.js +270 -0
  44. package/dist/src/cli/json-mode.d.ts +33 -0
  45. package/dist/src/cli/json-mode.js +201 -0
  46. package/dist/src/cli/migrate.d.ts +111 -0
  47. package/dist/src/cli/migrate.js +305 -0
  48. package/dist/src/cli/plan.d.ts +81 -0
  49. package/dist/src/cli/plan.js +149 -0
  50. package/dist/src/cli/pr.d.ts +106 -0
  51. package/dist/src/cli/pr.js +191 -19
  52. package/dist/src/cli/preflight.js +102 -1
  53. package/dist/src/cli/review.d.ts +27 -0
  54. package/dist/src/cli/review.js +126 -0
  55. package/dist/src/cli/runs-watch-renderer.d.ts +45 -0
  56. package/dist/src/cli/runs-watch-renderer.js +275 -0
  57. package/dist/src/cli/runs-watch.d.ts +41 -0
  58. package/dist/src/cli/runs-watch.js +395 -0
  59. package/dist/src/cli/runs.d.ts +122 -0
  60. package/dist/src/cli/runs.js +902 -0
  61. package/dist/src/cli/scan.d.ts +93 -0
  62. package/dist/src/cli/scan.js +166 -40
  63. package/dist/src/cli/spec.d.ts +66 -0
  64. package/dist/src/cli/spec.js +132 -0
  65. package/dist/src/cli/validate.d.ts +29 -0
  66. package/dist/src/cli/validate.js +131 -0
  67. package/dist/src/core/config/schema.d.ts +43 -0
  68. package/dist/src/core/config/schema.js +25 -0
  69. package/dist/src/core/config/types.d.ts +17 -0
  70. package/dist/src/core/council/runner.d.ts +10 -1
  71. package/dist/src/core/council/runner.js +25 -3
  72. package/dist/src/core/council/types.d.ts +7 -0
  73. package/dist/src/core/errors.d.ts +1 -1
  74. package/dist/src/core/errors.js +12 -0
  75. package/dist/src/core/logging/redaction.d.ts +13 -0
  76. package/dist/src/core/logging/redaction.js +20 -0
  77. package/dist/src/core/migrate/detector-rules.js +6 -0
  78. package/dist/src/core/migrate/schema-validator.js +22 -1
  79. package/dist/src/core/phases/static-rules.d.ts +5 -1
  80. package/dist/src/core/phases/static-rules.js +2 -5
  81. package/dist/src/core/run-state/budget.d.ts +88 -0
  82. package/dist/src/core/run-state/budget.js +141 -0
  83. package/dist/src/core/run-state/cli-internal.d.ts +21 -0
  84. package/dist/src/core/run-state/cli-internal.js +174 -0
  85. package/dist/src/core/run-state/events.d.ts +59 -0
  86. package/dist/src/core/run-state/events.js +504 -0
  87. package/dist/src/core/run-state/lock.d.ts +61 -0
  88. package/dist/src/core/run-state/lock.js +206 -0
  89. package/dist/src/core/run-state/phase-context.d.ts +60 -0
  90. package/dist/src/core/run-state/phase-context.js +108 -0
  91. package/dist/src/core/run-state/phase-registry.d.ts +137 -0
  92. package/dist/src/core/run-state/phase-registry.js +162 -0
  93. package/dist/src/core/run-state/phase-runner.d.ts +80 -0
  94. package/dist/src/core/run-state/phase-runner.js +447 -0
  95. package/dist/src/core/run-state/provider-readback.d.ts +130 -0
  96. package/dist/src/core/run-state/provider-readback.js +426 -0
  97. package/dist/src/core/run-state/replay-decision.d.ts +69 -0
  98. package/dist/src/core/run-state/replay-decision.js +144 -0
  99. package/dist/src/core/run-state/resolve-engine.d.ts +100 -0
  100. package/dist/src/core/run-state/resolve-engine.js +190 -0
  101. package/dist/src/core/run-state/resume-preflight.d.ts +66 -0
  102. package/dist/src/core/run-state/resume-preflight.js +116 -0
  103. package/dist/src/core/run-state/run-phase-with-lifecycle.d.ts +73 -0
  104. package/dist/src/core/run-state/run-phase-with-lifecycle.js +186 -0
  105. package/dist/src/core/run-state/runs.d.ts +57 -0
  106. package/dist/src/core/run-state/runs.js +288 -0
  107. package/dist/src/core/run-state/snapshot.d.ts +14 -0
  108. package/dist/src/core/run-state/snapshot.js +114 -0
  109. package/dist/src/core/run-state/state.d.ts +40 -0
  110. package/dist/src/core/run-state/state.js +164 -0
  111. package/dist/src/core/run-state/types.d.ts +278 -0
  112. package/dist/src/core/run-state/types.js +13 -0
  113. package/dist/src/core/run-state/ulid.d.ts +11 -0
  114. package/dist/src/core/run-state/ulid.js +95 -0
  115. package/dist/src/core/schema-alignment/extractor/index.d.ts +1 -1
  116. package/dist/src/core/schema-alignment/extractor/index.js +2 -2
  117. package/dist/src/core/schema-alignment/extractor/prisma.d.ts +13 -1
  118. package/dist/src/core/schema-alignment/extractor/prisma.js +65 -10
  119. package/dist/src/core/schema-alignment/git-history.d.ts +19 -0
  120. package/dist/src/core/schema-alignment/git-history.js +53 -0
  121. package/dist/src/core/static-rules/rules/brand-tokens.js +2 -2
  122. package/dist/src/core/static-rules/rules/schema-alignment.js +14 -4
  123. package/package.json +9 -5
  124. package/scripts/autoregress.ts +3 -2
  125. package/skills/claude-autopilot.md +1 -1
  126. package/skills/make-interfaces-feel-better/SKILL.md +104 -0
  127. package/skills/migrate/SKILL.md +193 -47
  128. package/skills/simplify-ui/SKILL.md +103 -0
  129. package/skills/ui/SKILL.md +117 -0
  130. package/skills/ui-ux-pro-max/SKILL.md +90 -0
@@ -5,6 +5,7 @@ import { fileURLToPath } from 'node:url';
5
5
  import { runSafe } from "../core/shell.js";
6
6
  import { detectLLMKey, loadEnvFile, LLM_KEY_NAMES } from "../core/detect/llm-key.js";
7
7
  import { findPackageRoot } from "./_pkg-root.js";
8
+ import { isSdkInstalled } from "../adapters/sdk-loader.js";
8
9
  const PASS = '\x1b[32m✓\x1b[0m';
9
10
  const FAIL = '\x1b[31m✗\x1b[0m';
10
11
  const WARN = '\x1b[33m!\x1b[0m';
@@ -29,6 +30,39 @@ function skillRoots() {
29
30
  roots.push(path.join(home, '.claude', 'plugins'));
30
31
  return roots.filter(p => fs.existsSync(p));
31
32
  }
33
+ /**
34
+ * Reads `deploy.adapter` from guardrail.config.yaml without pulling in the
35
+ * full config loader (which validates against a JSON schema and would noise
36
+ * up the doctor output). Returns `null` if the config or field is absent.
37
+ */
38
+ function readDeployAdapter(configPath) {
39
+ if (!fs.existsSync(configPath))
40
+ return null;
41
+ try {
42
+ const text = fs.readFileSync(configPath, 'utf8');
43
+ // Match `deploy:` block then look for `adapter:` two lines down. Cheap
44
+ // line-based parse — avoids js-yaml dependency in the doctor path.
45
+ const lines = text.split(/\r?\n/);
46
+ let inDeploy = false;
47
+ for (const line of lines) {
48
+ if (/^deploy\s*:\s*$/.test(line)) {
49
+ inDeploy = true;
50
+ continue;
51
+ }
52
+ if (inDeploy && /^\S/.test(line))
53
+ inDeploy = false; // dedented out of block
54
+ if (inDeploy) {
55
+ const m = line.match(/^\s+adapter\s*:\s*['"]?([a-zA-Z0-9_-]+)['"]?\s*$/);
56
+ if (m)
57
+ return m[1] ?? null;
58
+ }
59
+ }
60
+ return null;
61
+ }
62
+ catch {
63
+ return null;
64
+ }
65
+ }
32
66
  export function findMissingSuperpowersSkills() {
33
67
  // Traverse each root once, collect all discovered skill names, then diff against
34
68
  // the required set. Previous implementation did N × roots separate recursive walks.
@@ -173,7 +207,74 @@ export async function runDoctor() {
173
207
  ? 'git user.name / user.email not set — commits will fail.'
174
208
  : undefined,
175
209
  });
176
- // 9. Superpowers plugin required for pipeline phases, optional for review-only use
210
+ // 9. LLM SDK install state surfaces which providers are usable. After
211
+ // the v5.5 lazy-load refactor, three SDKs moved to optionalDependencies;
212
+ // `--omit=optional` users may have removed them. This check shows users
213
+ // which providers will work without `npm install <sdk>`.
214
+ const sdkChecks = [
215
+ { pkg: '@anthropic-ai/sdk', provider: 'claude' },
216
+ { pkg: 'openai', provider: 'openai/codex' },
217
+ { pkg: '@google/generative-ai', provider: 'gemini' },
218
+ ];
219
+ const installed = [];
220
+ const missing = [];
221
+ for (const { pkg } of sdkChecks) {
222
+ if (await isSdkInstalled(pkg))
223
+ installed.push(pkg);
224
+ else
225
+ missing.push(pkg);
226
+ }
227
+ checks.push({
228
+ name: `LLM SDKs installed (${installed.length}/${sdkChecks.length})`,
229
+ result: installed.length === 0 ? 'fail' : missing.length > 0 ? 'warn' : 'pass',
230
+ message: installed.length === 0
231
+ ? `No LLM SDKs found. Install at least one: ${sdkChecks.map(s => s.pkg).join(', ')}`
232
+ : missing.length > 0
233
+ ? `Missing (run npm install <pkg> to enable): ${missing.join(', ')}`
234
+ : undefined,
235
+ });
236
+ // 10. Vercel deploy adapter auth (Phase 6 of v5.4 spec). Detects
237
+ // `deploy.adapter: vercel` in guardrail.config.yaml and verifies the
238
+ // auth token is set. Warn-not-fail because users without deploy
239
+ // configured don't care.
240
+ const deployAdapter = readDeployAdapter(configYaml);
241
+ if (deployAdapter === 'vercel') {
242
+ const token = process.env.VERCEL_TOKEN ?? envVars['VERCEL_TOKEN'];
243
+ checks.push({
244
+ name: 'VERCEL_TOKEN (deploy adapter: vercel)',
245
+ result: token ? 'pass' : 'warn',
246
+ message: token
247
+ ? undefined
248
+ : 'deploy.adapter is "vercel" but VERCEL_TOKEN is not set. Generate one at https://vercel.com/account/tokens',
249
+ });
250
+ }
251
+ // 10b. Fly.io deploy adapter auth (Phase 6 of v5.6 spec). Mirrors the
252
+ // Vercel check above — warn-not-fail when FLY_API_TOKEN is missing
253
+ // and the configured adapter is `fly`.
254
+ if (deployAdapter === 'fly') {
255
+ const token = process.env.FLY_API_TOKEN ?? envVars['FLY_API_TOKEN'];
256
+ checks.push({
257
+ name: 'FLY_API_TOKEN (deploy adapter: fly)',
258
+ result: token ? 'pass' : 'warn',
259
+ message: token
260
+ ? undefined
261
+ : 'deploy.adapter is "fly" but FLY_API_TOKEN is not set. Generate one at https://fly.io/dashboard/personal/tokens',
262
+ });
263
+ }
264
+ // 10c. Render deploy adapter auth (Phase 6 of v5.6 spec). Mirrors the
265
+ // Vercel/Fly checks — warn-not-fail when RENDER_API_KEY is missing
266
+ // and the configured adapter is `render`.
267
+ if (deployAdapter === 'render') {
268
+ const token = process.env.RENDER_API_KEY ?? envVars['RENDER_API_KEY'];
269
+ checks.push({
270
+ name: 'RENDER_API_KEY (deploy adapter: render)',
271
+ result: token ? 'pass' : 'warn',
272
+ message: token
273
+ ? undefined
274
+ : 'deploy.adapter is "render" but RENDER_API_KEY is not set. Generate one at https://dashboard.render.com/u/settings#api-keys',
275
+ });
276
+ }
277
+ // 11. Superpowers plugin — required for pipeline phases, optional for review-only use
177
278
  const missingSkills = findMissingSuperpowersSkills();
178
279
  const allSkillsFound = missingSkills.length === 0;
179
280
  checks.push({
@@ -0,0 +1,27 @@
1
+ export interface ReviewCommandOptions {
2
+ cwd?: string;
3
+ configPath?: string;
4
+ /**
5
+ * Optional context note injected into the review log. The actual review
6
+ * content (LLM-driven code review against a PR diff or working tree) is
7
+ * produced by the Claude Code review skills (`/review`, `/review-2pass`,
8
+ * `pr-review-toolkit:review-pr`); this CLI verb is the engine-wrap shell
9
+ * so v6 pipeline runs can checkpoint a `review` phase entry.
10
+ */
11
+ context?: string;
12
+ /**
13
+ * Where to write the review log file. Defaults to
14
+ * `.guardrail-cache/reviews/<timestamp>-review.md` so it lands inside the
15
+ * cache that's already gitignored. The path is recorded on ReviewOutput so
16
+ * the engine path can persist it as `result` for replay.
17
+ */
18
+ outputPath?: string;
19
+ /**
20
+ * v6.0.4 — engine knob inputs. Same shape and precedence as scan / costs /
21
+ * fix / plan (CLI > env > config > built-in default off in v6.0.x).
22
+ */
23
+ cliEngine?: boolean;
24
+ envEngine?: string;
25
+ }
26
+ export declare function runReview(options?: ReviewCommandOptions): Promise<number>;
27
+ //# sourceMappingURL=review.d.ts.map
@@ -0,0 +1,126 @@
1
+ import * as path from 'node:path';
2
+ import * as fs from 'node:fs';
3
+ import { loadConfig } from "../core/config/loader.js";
4
+ import { runPhaseWithLifecycle } from "../core/run-state/run-phase-with-lifecycle.js";
5
+ const C = {
6
+ reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
7
+ green: '\x1b[32m', yellow: '\x1b[33m', cyan: '\x1b[36m', red: '\x1b[31m',
8
+ };
9
+ const fmt = (c, t) => `${C[c]}${t}${C.reset}`;
10
+ export async function runReview(options = {}) {
11
+ const cwd = options.cwd ?? process.cwd();
12
+ const configPath = options.configPath ?? path.join(cwd, 'guardrail.config.yaml');
13
+ let config = { configVersion: 1 };
14
+ if (fs.existsSync(configPath)) {
15
+ const loaded = await loadConfig(configPath);
16
+ if (loaded)
17
+ config = loaded;
18
+ }
19
+ // INTENTIONAL DEVIATION FROM THE SPEC TABLE (preserved in v6.0.6):
20
+ // the v6 spec (docs/specs/v6-run-state-engine.md) lists `review` with
21
+ // externalRefs `review-comments`, implying the phase posts review
22
+ // comments to a GitHub PR (which would make `hasSideEffects: true`).
23
+ // The implementation here does NOT post anywhere — it writes a review
24
+ // log to a local file under .guardrail-cache/reviews/ and stops.
25
+ // Posting per-line comments to a PR is owned by `claude-autopilot pr`
26
+ // (which already has `--inline-comments` / `--post-comments`); the
27
+ // `review` verb is the engine-wrap shell for the LLM-driven code
28
+ // review skills (`/review`, `/review-2pass`, `pr-review-toolkit:review-pr`)
29
+ // so pipeline runs can checkpoint a `review` phase entry. Therefore
30
+ // `idempotent: true, hasSideEffects: false` is correct for the wrapped
31
+ // behavior. If a future PR adds platform-side comment posting to this
32
+ // verb, both declarations will need to flip and the readback rules in
33
+ // the wrapping recipe will need to plumb a `review-comments` externalRef.
34
+ const context = options.context ?? null;
35
+ const outputPath = options.outputPath
36
+ ? path.resolve(cwd, options.outputPath)
37
+ : path.join(cwd, '.guardrail-cache', 'reviews', `${new Date().toISOString().replace(/[:.]/g, '-')}-review.md`);
38
+ const reviewInput = { cwd, context, outputPath };
39
+ // The wrapped phase body — writes a review log stub to disk. The actual
40
+ // LLM-driven review content is produced by the Claude Code review skills.
41
+ // Engine-off callers invoke this directly via `executeReviewPhase()`;
42
+ // engine-on callers route through `runPhase()`.
43
+ const phase = {
44
+ name: 'review',
45
+ // Re-running the review verb against the same context writes the same
46
+ // log file. Engine treats local file writes as overwrite-style — same
47
+ // precedent as scan's findings-cache.
48
+ idempotent: true,
49
+ // Local file write only — no PR comment posting, no git push, no
50
+ // provider-side mutation. See the long deviation note above where the
51
+ // engine resolution is computed.
52
+ hasSideEffects: false,
53
+ run: async (input) => executeReviewPhase(input),
54
+ };
55
+ // v6.0.6 — lifecycle wiring lives in `runPhaseWithLifecycle`.
56
+ let output;
57
+ try {
58
+ const result = await runPhaseWithLifecycle({
59
+ cwd,
60
+ phase,
61
+ input: reviewInput,
62
+ config,
63
+ cliEngine: options.cliEngine,
64
+ envEngine: options.envEngine,
65
+ runEngineOff: () => executeReviewPhase(reviewInput),
66
+ });
67
+ output = result.output;
68
+ }
69
+ catch {
70
+ return 1;
71
+ }
72
+ return renderReviewOutput(output, reviewInput);
73
+ }
74
+ // ---------------------------------------------------------------------------
75
+ // Phase body — write a review log stub. Pure: no console output, no exit
76
+ // codes. Returns a JSON-serializable ReviewOutput so the engine can persist
77
+ // it as `result` on the phase snapshot. The actual LLM-driven review
78
+ // content is produced by the Claude Code review skills; this CLI verb's
79
+ // job is to provide a checkpointable phase shell.
80
+ // ---------------------------------------------------------------------------
81
+ async function executeReviewPhase(input) {
82
+ const { context, outputPath } = input;
83
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
84
+ const lines = [
85
+ '# Review',
86
+ '',
87
+ `Generated: ${new Date().toISOString()}`,
88
+ '',
89
+ context ? `Context: ${context}` : 'Context: (none provided)',
90
+ '',
91
+ '<!--',
92
+ 'This is the v6 engine-wrap stub for the `review` phase. The actual',
93
+ 'LLM-driven review content is produced by the Claude Code review skills',
94
+ '(`/review`, `/review-2pass`, `pr-review-toolkit:review-pr`). The CLI',
95
+ 'verb exists to provide a checkpointable phase shell so',
96
+ '`claude-autopilot runs show <id>` reflects a `review` phase entry when',
97
+ 'the pipeline includes one. PR-side comment posting lives in',
98
+ '`claude-autopilot pr --inline-comments` / `--post-comments`, which is',
99
+ 'a separate verb.',
100
+ '-->',
101
+ '',
102
+ ];
103
+ fs.writeFileSync(outputPath, lines.join('\n'), 'utf8');
104
+ return {
105
+ reviewLogPath: outputPath,
106
+ context,
107
+ };
108
+ }
109
+ // ---------------------------------------------------------------------------
110
+ // Render — translate ReviewOutput back to a stdout summary + exit code.
111
+ // Lives outside the wrapped phase because it's pure presentation.
112
+ // ---------------------------------------------------------------------------
113
+ function renderReviewOutput(output, input) {
114
+ const { reviewLogPath, context } = output;
115
+ const { cwd } = input;
116
+ console.log('');
117
+ console.log(fmt('bold', '[review]') + ' ' + fmt('dim', context ? `context: ${context}` : 'no context provided'));
118
+ console.log(fmt('dim', ` → ${path.relative(cwd, reviewLogPath)}`));
119
+ console.log('');
120
+ console.log(fmt('cyan', 'Note:') + fmt('dim', ' the LLM-driven reviewer lives in Claude Code (superpowers:requesting-code-review,'));
121
+ console.log(fmt('dim', ' /review, /review-2pass, pr-review-toolkit:review-pr).'));
122
+ console.log(fmt('dim', ' PR comment posting lives in `claude-autopilot pr` (--inline-comments / --post-comments).'));
123
+ console.log('');
124
+ return 0;
125
+ }
126
+ //# sourceMappingURL=review.js.map
@@ -0,0 +1,45 @@
1
+ import type { BudgetConfig } from '../core/run-state/budget.ts';
2
+ import type { RunEvent, RunState } from '../core/run-state/types.ts';
3
+ type Color = 'red' | 'green' | 'yellow' | 'blue' | 'magenta' | 'cyan' | 'bold-red' | 'bold-green' | 'bold-yellow' | 'dim';
4
+ /** Wrap `text` in `color` if ansi is enabled, else return as-is. */
5
+ export declare function colorize(text: string, color: Color, ansi: boolean): string;
6
+ /** Strip ANSI escape sequences. Public so tests can assert against plain
7
+ * text without worrying about whether the renderer was called with
8
+ * ansi=true. */
9
+ export declare function stripAnsi(s: string): string;
10
+ /** Format a USD amount with two decimals + leading $. Used in cost lines and
11
+ * budget bars. Negative amounts (over-budget) are shown as `-$X.YZ`. */
12
+ export declare function fmtUSD(amount: number): string;
13
+ export interface RenderOptions {
14
+ /** When false, no ANSI escape codes are emitted. Default true. The verb
15
+ * forces this off under --json or when stdout is not a TTY. */
16
+ ansi: boolean;
17
+ /** When true, the renderer emits compact two-column output (timestamp +
18
+ * one-line event) without the header. Used for live tailing. Default
19
+ * is true; --no-follow snapshot mode flips it off + adds the header. */
20
+ compact?: boolean;
21
+ }
22
+ /** Render the running cost vs. configured per-run cap as a single line.
23
+ * Color thresholds: <50% green, 50-90% yellow, >90% red. When budget is
24
+ * null (no BudgetConfig recorded), shows just the running total. */
25
+ export declare function renderBudgetBar(totalCostUSD: number, budget: BudgetConfig | null, opts: RenderOptions): string;
26
+ /** Render the run header — the play arrow + run id, the phase plan, and the
27
+ * initial budget line. Returns an array of lines (no trailing newlines).
28
+ * The verb prints these at startup. */
29
+ export declare function renderHeader(state: RunState, budget: BudgetConfig | null, opts: RenderOptions): string[];
30
+ /** Render one event as a single output line. `runningTotal` is the running
31
+ * cost AFTER this event has been folded in (for `phase.cost`) — the verb
32
+ * is responsible for accumulating; this function only formats. */
33
+ export declare function renderEventLine(ev: RunEvent, runningTotal: number, opts: RenderOptions): string;
34
+ export interface FinalSummary {
35
+ runId: string;
36
+ status: 'success' | 'failed' | 'aborted' | 'paused' | 'running' | 'pending' | 'interrupted';
37
+ totalCostUSD: number;
38
+ /** Wall clock from run start to now, in milliseconds. */
39
+ durationMs: number;
40
+ }
41
+ /** One- or two-line goodbye block. Engine-on runs produce a real summary
42
+ * here; Ctrl-C interrupts get the same shape with status='interrupted'. */
43
+ export declare function renderFinalSummary(s: FinalSummary, opts: RenderOptions): string[];
44
+ export {};
45
+ //# sourceMappingURL=runs-watch-renderer.d.ts.map
@@ -0,0 +1,275 @@
1
+ // src/cli/runs-watch-renderer.ts
2
+ //
3
+ // Pure renderer for `runs watch <id>`. Every function here is referentially
4
+ // transparent — no file I/O, no clock reads, no subprocess calls — so the
5
+ // demo-grade live cost meter is testable as a pile of string assertions.
6
+ //
7
+ // The verb in `runs-watch.ts` reads events.ndjson, accumulates a running
8
+ // total, and calls `renderEventLine` for each new event. Headers are rendered
9
+ // once via `renderHeader`. Final summary is rendered via `renderFinalSummary`.
10
+ //
11
+ // Spec: tasks/v6.1-runs-watch.md "Pretty rendering" + "YC-demo polish".
12
+ // ----------------------------------------------------------------------------
13
+ // ANSI helpers. Single source of truth so tests can flip ansi=false and
14
+ // assert plain text trivially.
15
+ // ----------------------------------------------------------------------------
16
+ const ANSI_RESET = '\x1b[0m';
17
+ const ANSI_BOLD = '\x1b[1m';
18
+ const ANSI_DIM = '\x1b[2m';
19
+ const ANSI_RED = '\x1b[31m';
20
+ const ANSI_GREEN = '\x1b[32m';
21
+ const ANSI_YELLOW = '\x1b[33m';
22
+ const ANSI_BLUE = '\x1b[34m';
23
+ const ANSI_MAGENTA = '\x1b[35m';
24
+ const ANSI_CYAN = '\x1b[36m';
25
+ function colorCode(c) {
26
+ switch (c) {
27
+ case 'red': return ANSI_RED;
28
+ case 'green': return ANSI_GREEN;
29
+ case 'yellow': return ANSI_YELLOW;
30
+ case 'blue': return ANSI_BLUE;
31
+ case 'magenta': return ANSI_MAGENTA;
32
+ case 'cyan': return ANSI_CYAN;
33
+ case 'bold-red': return ANSI_BOLD + ANSI_RED;
34
+ case 'bold-green': return ANSI_BOLD + ANSI_GREEN;
35
+ case 'bold-yellow': return ANSI_BOLD + ANSI_YELLOW;
36
+ case 'dim': return ANSI_DIM;
37
+ }
38
+ }
39
+ /** Wrap `text` in `color` if ansi is enabled, else return as-is. */
40
+ export function colorize(text, color, ansi) {
41
+ if (!ansi)
42
+ return text;
43
+ return colorCode(color) + text + ANSI_RESET;
44
+ }
45
+ /** Strip ANSI escape sequences. Public so tests can assert against plain
46
+ * text without worrying about whether the renderer was called with
47
+ * ansi=true. */
48
+ export function stripAnsi(s) {
49
+ // eslint-disable-next-line no-control-regex
50
+ return s.replace(/\x1b\[[0-9;]*m/g, '');
51
+ }
52
+ // ----------------------------------------------------------------------------
53
+ // Money + duration formatting.
54
+ // ----------------------------------------------------------------------------
55
+ /** Format a USD amount with two decimals + leading $. Used in cost lines and
56
+ * budget bars. Negative amounts (over-budget) are shown as `-$X.YZ`. */
57
+ export function fmtUSD(amount) {
58
+ if (amount < 0)
59
+ return `-$${(-amount).toFixed(2)}`;
60
+ return `$${amount.toFixed(2)}`;
61
+ }
62
+ /** Format a token count with k/M suffixes when the number is large. Keeps
63
+ * cost lines readable on narrow terminals — `123.4k` instead of `123412`. */
64
+ function fmtTokens(n) {
65
+ if (n >= 1_000_000)
66
+ return `${(n / 1_000_000).toFixed(1)}M`;
67
+ if (n >= 1_000)
68
+ return `${(n / 1_000).toFixed(1)}k`;
69
+ return String(n);
70
+ }
71
+ function fmtDurationMs(ms) {
72
+ if (ms < 1000)
73
+ return `${ms}ms`;
74
+ if (ms < 60_000)
75
+ return `${(ms / 1000).toFixed(1)}s`;
76
+ const minutes = Math.floor(ms / 60_000);
77
+ const seconds = Math.floor((ms % 60_000) / 1000);
78
+ return `${minutes}m${seconds.toString().padStart(2, '0')}s`;
79
+ }
80
+ /** Pull the time portion out of an ISO timestamp — `12:00:42` from
81
+ * `2026-05-04T12:00:42.123Z`. Returns the original string on parse
82
+ * failure so the renderer never throws on malformed events. */
83
+ function fmtTimestamp(iso) {
84
+ const m = /T(\d{2}:\d{2}:\d{2})/.exec(iso);
85
+ return m ? m[1] : iso;
86
+ }
87
+ // ----------------------------------------------------------------------------
88
+ // Budget bar.
89
+ // ----------------------------------------------------------------------------
90
+ /** Render the running cost vs. configured per-run cap as a single line.
91
+ * Color thresholds: <50% green, 50-90% yellow, >90% red. When budget is
92
+ * null (no BudgetConfig recorded), shows just the running total. */
93
+ export function renderBudgetBar(totalCostUSD, budget, opts) {
94
+ if (budget === null) {
95
+ return ` budget: ${fmtUSD(totalCostUSD)} (no cap configured)`;
96
+ }
97
+ const cap = budget.perRunUSD;
98
+ const pctRaw = cap > 0 ? (totalCostUSD / cap) * 100 : 0;
99
+ // Clamp the percentage label to [0, 999] so absurd over-budget runs still
100
+ // fit a fixed-width column. The underlying number stays untruncated for
101
+ // the cost figure itself.
102
+ const pctLabel = Math.min(999, Math.max(0, Math.round(pctRaw)));
103
+ let color;
104
+ if (pctRaw > 90)
105
+ color = 'red';
106
+ else if (pctRaw >= 50)
107
+ color = 'yellow';
108
+ else
109
+ color = 'green';
110
+ const body = `${fmtUSD(totalCostUSD)} / ${fmtUSD(cap)} (${pctLabel}%)`;
111
+ return ` budget: ${colorize(body, color, opts.ansi)}`;
112
+ }
113
+ // ----------------------------------------------------------------------------
114
+ // Header.
115
+ // ----------------------------------------------------------------------------
116
+ /** Render the run header — the play arrow + run id, the phase plan, and the
117
+ * initial budget line. Returns an array of lines (no trailing newlines).
118
+ * The verb prints these at startup. */
119
+ export function renderHeader(state, budget, opts) {
120
+ const lines = [];
121
+ // Bullet glyph: ▶ in TTY, "*" in plain mode for screen-reader friendliness.
122
+ const bullet = opts.ansi ? '▶' : '*';
123
+ lines.push(`${colorize(bullet, 'cyan', opts.ansi)} run ${state.runId}`);
124
+ if (state.phases.length > 0) {
125
+ const phaseList = state.phases
126
+ .map(p => colorPhase(p.name, p.status, opts))
127
+ .join(opts.ansi ? ' → ' : ' -> ');
128
+ lines.push(` phases: ${phaseList}`);
129
+ }
130
+ lines.push(renderBudgetBar(state.totalCostUSD, budget, opts));
131
+ return lines;
132
+ }
133
+ function colorPhase(name, status, opts) {
134
+ switch (status) {
135
+ case 'succeeded':
136
+ return colorize(name, 'green', opts.ansi);
137
+ case 'running':
138
+ return colorize(name, 'cyan', opts.ansi);
139
+ case 'failed':
140
+ return colorize(name, 'red', opts.ansi);
141
+ case 'aborted':
142
+ return colorize(name, 'red', opts.ansi);
143
+ case 'skipped':
144
+ return colorize(name, 'dim', opts.ansi);
145
+ case 'pending':
146
+ default:
147
+ return colorize(name, 'dim', opts.ansi);
148
+ }
149
+ }
150
+ // ----------------------------------------------------------------------------
151
+ // Per-event line. The core of the live tail.
152
+ // ----------------------------------------------------------------------------
153
+ /** Total-column width — pads the running total so it scans visually as a
154
+ * fixed right-most column. Picked to fit `total: $9999.99` cleanly. */
155
+ const TOTAL_COL_WIDTH = 18;
156
+ /** Render one event as a single output line. `runningTotal` is the running
157
+ * cost AFTER this event has been folded in (for `phase.cost`) — the verb
158
+ * is responsible for accumulating; this function only formats. */
159
+ export function renderEventLine(ev, runningTotal, opts) {
160
+ const ts = `[${fmtTimestamp(ev.ts)}]`;
161
+ // The "verb" column. Padded so the body of the line aligns visually.
162
+ const verb = padRight(ev.event, 20);
163
+ switch (ev.event) {
164
+ case 'run.start': {
165
+ const phaseList = ev.phases.join(', ');
166
+ return `${ts} ${colorize(verb, 'cyan', opts.ansi)} phases=[${phaseList}]`;
167
+ }
168
+ case 'phase.start': {
169
+ const body = `${ev.phase}${ev.attempt > 1 ? ` (attempt ${ev.attempt})` : ''}`;
170
+ return `${ts} ${colorize(verb, 'cyan', opts.ansi)} ${body}`;
171
+ }
172
+ case 'phase.cost': {
173
+ const delta = colorize(`+${fmtUSD(ev.costUSD)}`, 'yellow', opts.ansi);
174
+ const tokens = `(in: ${fmtTokens(ev.inputTokens)}, out: ${fmtTokens(ev.outputTokens)})`;
175
+ const total = padLeft(`total: ${fmtUSD(runningTotal)}`, TOTAL_COL_WIDTH);
176
+ return `${ts} ${colorize(verb, 'yellow', opts.ansi)} ${padRight(ev.phase, 14)} ${delta} ${tokens} ${total}`;
177
+ }
178
+ case 'phase.success': {
179
+ const dur = fmtDurationMs(ev.durationMs);
180
+ // Box-drawing checkmark in TTY; plain "OK" in plain mode (per the
181
+ // ANSI-on-non-TTY rule above).
182
+ const glyph = opts.ansi ? '✓' : 'OK';
183
+ return `${ts} ${colorize(verb, 'green', opts.ansi)} ${padRight(ev.phase, 14)} ${colorize(glyph, 'green', opts.ansi)} ${dur}`;
184
+ }
185
+ case 'phase.failed': {
186
+ const dur = fmtDurationMs(ev.durationMs);
187
+ const errMsg = ev.error.length > 80 ? `${ev.error.slice(0, 77)}...` : ev.error;
188
+ const glyph = opts.ansi ? '✗' : 'FAIL';
189
+ return `${ts} ${colorize(verb, 'red', opts.ansi)} ${padRight(ev.phase, 14)} ${colorize(glyph, 'red', opts.ansi)} ${dur} ${errMsg}`;
190
+ }
191
+ case 'phase.aborted': {
192
+ const glyph = opts.ansi ? '✗' : 'ABORT';
193
+ return `${ts} ${colorize(verb, 'red', opts.ansi)} ${padRight(ev.phase, 14)} ${colorize(glyph, 'red', opts.ansi)} reason=${ev.reason}`;
194
+ }
195
+ case 'phase.externalRef': {
196
+ // YC-demo polish — surface the kind+id inline so observers see the
197
+ // breadcrumb materialize as the phase runs (e.g. "→ github-pr#42").
198
+ const arrow = opts.ansi ? '→' : '->';
199
+ const refLabel = `${arrow} ${ev.ref.kind}#${ev.ref.id}`;
200
+ return `${ts} ${colorize(verb, 'magenta', opts.ansi)} ${padRight(ev.phase, 14)} ${colorize(refLabel, 'magenta', opts.ansi)}`;
201
+ }
202
+ case 'phase.needs-human': {
203
+ return `${ts} ${colorize(verb, 'yellow', opts.ansi)} ${padRight(ev.phase, 14)} reason=${ev.reason}`;
204
+ }
205
+ case 'budget.check': {
206
+ const decisionColor = ev.decision === 'hard-fail' ? 'red'
207
+ : ev.decision === 'pause' ? 'yellow'
208
+ : 'dim';
209
+ const body = `${ev.phase} decision=${ev.decision} capRemaining=${fmtUSD(ev.capRemaining)}`;
210
+ return `${ts} ${colorize(verb, decisionColor, opts.ansi)} ${body}`;
211
+ }
212
+ case 'run.complete': {
213
+ const dur = fmtDurationMs(ev.durationMs);
214
+ const statusColor = ev.status === 'success' ? 'bold-green'
215
+ : ev.status === 'failed' ? 'bold-red'
216
+ : 'bold-red';
217
+ const body = `status=${ev.status} totalCostUSD=${fmtUSD(ev.totalCostUSD)} duration=${dur}`;
218
+ return `${ts} ${colorize(verb, statusColor, opts.ansi)} ${body}`;
219
+ }
220
+ case 'run.warning': {
221
+ return `${ts} ${colorize(verb, 'yellow', opts.ansi)} ${ev.message}`;
222
+ }
223
+ case 'run.recovery': {
224
+ return `${ts} ${colorize(verb, 'yellow', opts.ansi)} reason=${ev.reason}`;
225
+ }
226
+ case 'lock.takeover': {
227
+ return `${ts} ${colorize(verb, 'magenta', opts.ansi)} reason=${ev.reason}`;
228
+ }
229
+ case 'index.rebuilt': {
230
+ return `${ts} ${colorize(verb, 'dim', opts.ansi)} cause=${ev.cause}`;
231
+ }
232
+ case 'replay.override': {
233
+ return `${ts} ${colorize(verb, 'magenta', opts.ansi)} ${ev.phase} reason=${ev.reason}`;
234
+ }
235
+ default: {
236
+ // Exhaustiveness guard. New event variants must be added here so a
237
+ // future RunEvent extension forces a compile error rather than
238
+ // silently rendering an opaque event.
239
+ const _exhaustive = ev;
240
+ void _exhaustive;
241
+ return `${ts} ${verb}`;
242
+ }
243
+ }
244
+ }
245
+ /** One- or two-line goodbye block. Engine-on runs produce a real summary
246
+ * here; Ctrl-C interrupts get the same shape with status='interrupted'. */
247
+ export function renderFinalSummary(s, opts) {
248
+ const status = s.status;
249
+ const color = status === 'success' ? 'bold-green'
250
+ : status === 'failed' ? 'bold-red'
251
+ : status === 'aborted' ? 'bold-red'
252
+ : status === 'interrupted' ? 'bold-yellow'
253
+ : 'dim';
254
+ const dur = fmtDurationMs(s.durationMs);
255
+ const body = `status=${status} totalCostUSD=${fmtUSD(s.totalCostUSD)} duration=${dur}`;
256
+ return [
257
+ '',
258
+ `${colorize('done', color, opts.ansi)} run ${s.runId}`,
259
+ ` ${body}`,
260
+ ];
261
+ }
262
+ // ----------------------------------------------------------------------------
263
+ // Tiny string helpers (kept private to the renderer module).
264
+ // ----------------------------------------------------------------------------
265
+ function padRight(s, width) {
266
+ if (s.length >= width)
267
+ return s;
268
+ return s + ' '.repeat(width - s.length);
269
+ }
270
+ function padLeft(s, width) {
271
+ if (s.length >= width)
272
+ return s;
273
+ return ' '.repeat(width - s.length) + s;
274
+ }
275
+ //# sourceMappingURL=runs-watch-renderer.js.map
@@ -0,0 +1,41 @@
1
+ export interface RunsWatchCliResult {
2
+ exit: number;
3
+ stdout: string[];
4
+ stderr: string[];
5
+ }
6
+ export interface RunRunsWatchOptions {
7
+ runId: string;
8
+ cwd?: string;
9
+ /** Replay forward from this seq (1-based, matching the events.ndjson seq
10
+ * field). Useful for resuming a watch after a disconnect. */
11
+ since?: number;
12
+ /** When true, render snapshot once and exit. No file watcher, no Ctrl-C
13
+ * handler. Useful for one-shot status pulls. */
14
+ noFollow?: boolean;
15
+ /** When true, emit raw NDJSON (one event per line) to stdout instead of the
16
+ * pretty rendering. ANSI is forced off. */
17
+ json?: boolean;
18
+ /** When true, force ANSI off regardless of TTY detection. Honored even
19
+ * in default mode (e.g. `claude-autopilot runs watch <id> --no-color`). */
20
+ noColor?: boolean;
21
+ /** Override TTY detection. Tests pass `false` to assert ANSI-stripped
22
+ * output. Production callers leave undefined; the verb consults
23
+ * `process.stdout.isTTY`. */
24
+ __testIsTTY?: boolean;
25
+ /** Override `process.stdout.write`. Tests pass a buffer-collector so
26
+ * they can assert on emitted lines without spawning. */
27
+ __testWriteStdout?: (chunk: string) => void;
28
+ /** Override `process.stderr.write`. Same shape as above. */
29
+ __testWriteStderr?: (chunk: string) => void;
30
+ /** Override the polling interval (ms). Default 1000; tests pass a much
31
+ * smaller value to make live-tail tests run quickly. */
32
+ __testPollIntervalMs?: number;
33
+ /** When set, the verb resolves with this status the moment the watcher
34
+ * observes a `run.complete` (or matching terminal event). Tests use this
35
+ * to assert the auto-exit-on-completion behavior without a real signal. */
36
+ __testStopAfterTerminal?: boolean;
37
+ }
38
+ /** Public entry point — exported so the dispatcher in runs.ts can call us
39
+ * uniformly with the other verbs. */
40
+ export declare function runRunsWatch(opts: RunRunsWatchOptions): Promise<RunsWatchCliResult>;
41
+ //# sourceMappingURL=runs-watch.d.ts.map