@delegance/claude-autopilot 5.5.2 → 7.2.0

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 (150) hide show
  1. package/CHANGELOG.md +1776 -6
  2. package/README.md +65 -1
  3. package/bin/_launcher.js +38 -23
  4. package/dist/src/adapters/council/openai.js +12 -6
  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/index.d.ts +2 -0
  10. package/dist/src/adapters/deploy/index.js +33 -0
  11. package/dist/src/adapters/deploy/render.d.ts +181 -0
  12. package/dist/src/adapters/deploy/render.js +550 -0
  13. package/dist/src/adapters/deploy/types.d.ts +67 -3
  14. package/dist/src/adapters/deploy/vercel.d.ts +17 -1
  15. package/dist/src/adapters/deploy/vercel.js +29 -49
  16. package/dist/src/adapters/pricing.d.ts +36 -0
  17. package/dist/src/adapters/pricing.js +40 -0
  18. package/dist/src/adapters/review-engine/codex.js +10 -7
  19. package/dist/src/cli/autopilot.d.ts +75 -0
  20. package/dist/src/cli/autopilot.js +750 -0
  21. package/dist/src/cli/brainstorm.d.ts +23 -0
  22. package/dist/src/cli/brainstorm.js +131 -0
  23. package/dist/src/cli/costs.d.ts +15 -1
  24. package/dist/src/cli/costs.js +99 -10
  25. package/dist/src/cli/dashboard/index.d.ts +5 -0
  26. package/dist/src/cli/dashboard/index.js +49 -0
  27. package/dist/src/cli/dashboard/login.d.ts +22 -0
  28. package/dist/src/cli/dashboard/login.js +260 -0
  29. package/dist/src/cli/dashboard/logout.d.ts +12 -0
  30. package/dist/src/cli/dashboard/logout.js +45 -0
  31. package/dist/src/cli/dashboard/status.d.ts +30 -0
  32. package/dist/src/cli/dashboard/status.js +65 -0
  33. package/dist/src/cli/dashboard/upload.d.ts +16 -0
  34. package/dist/src/cli/dashboard/upload.js +48 -0
  35. package/dist/src/cli/deploy.d.ts +3 -3
  36. package/dist/src/cli/deploy.js +34 -9
  37. package/dist/src/cli/engine-flag-deprecation.d.ts +14 -0
  38. package/dist/src/cli/engine-flag-deprecation.js +20 -0
  39. package/dist/src/cli/fix.d.ts +18 -0
  40. package/dist/src/cli/fix.js +105 -11
  41. package/dist/src/cli/help-text.d.ts +52 -0
  42. package/dist/src/cli/help-text.js +416 -0
  43. package/dist/src/cli/implement.d.ts +91 -0
  44. package/dist/src/cli/implement.js +196 -0
  45. package/dist/src/cli/index.d.ts +2 -1
  46. package/dist/src/cli/index.js +774 -245
  47. package/dist/src/cli/json-envelope.d.ts +187 -0
  48. package/dist/src/cli/json-envelope.js +270 -0
  49. package/dist/src/cli/json-mode.d.ts +33 -0
  50. package/dist/src/cli/json-mode.js +201 -0
  51. package/dist/src/cli/migrate.d.ts +111 -0
  52. package/dist/src/cli/migrate.js +305 -0
  53. package/dist/src/cli/plan.d.ts +81 -0
  54. package/dist/src/cli/plan.js +149 -0
  55. package/dist/src/cli/pr.d.ts +106 -0
  56. package/dist/src/cli/pr.js +191 -19
  57. package/dist/src/cli/preflight.js +26 -0
  58. package/dist/src/cli/review.d.ts +27 -0
  59. package/dist/src/cli/review.js +126 -0
  60. package/dist/src/cli/runs-watch-renderer.d.ts +45 -0
  61. package/dist/src/cli/runs-watch-renderer.js +275 -0
  62. package/dist/src/cli/runs-watch.d.ts +41 -0
  63. package/dist/src/cli/runs-watch.js +395 -0
  64. package/dist/src/cli/runs.d.ts +122 -0
  65. package/dist/src/cli/runs.js +902 -0
  66. package/dist/src/cli/scaffold.d.ts +39 -0
  67. package/dist/src/cli/scaffold.js +287 -0
  68. package/dist/src/cli/scan.d.ts +93 -0
  69. package/dist/src/cli/scan.js +166 -40
  70. package/dist/src/cli/setup.d.ts +30 -0
  71. package/dist/src/cli/setup.js +137 -0
  72. package/dist/src/cli/spec.d.ts +66 -0
  73. package/dist/src/cli/spec.js +132 -0
  74. package/dist/src/cli/validate.d.ts +29 -0
  75. package/dist/src/cli/validate.js +131 -0
  76. package/dist/src/core/config/schema.d.ts +9 -0
  77. package/dist/src/core/config/schema.js +7 -0
  78. package/dist/src/core/config/types.d.ts +11 -0
  79. package/dist/src/core/council/runner.d.ts +10 -1
  80. package/dist/src/core/council/runner.js +25 -3
  81. package/dist/src/core/council/types.d.ts +7 -0
  82. package/dist/src/core/errors.d.ts +1 -1
  83. package/dist/src/core/errors.js +11 -0
  84. package/dist/src/core/logging/redaction.d.ts +13 -0
  85. package/dist/src/core/logging/redaction.js +20 -0
  86. package/dist/src/core/migrate/schema-validator.js +15 -1
  87. package/dist/src/core/phases/static-rules.d.ts +5 -1
  88. package/dist/src/core/phases/static-rules.js +2 -5
  89. package/dist/src/core/run-state/budget.d.ts +88 -0
  90. package/dist/src/core/run-state/budget.js +141 -0
  91. package/dist/src/core/run-state/cli-internal.d.ts +21 -0
  92. package/dist/src/core/run-state/cli-internal.js +174 -0
  93. package/dist/src/core/run-state/events.d.ts +59 -0
  94. package/dist/src/core/run-state/events.js +512 -0
  95. package/dist/src/core/run-state/lock.d.ts +61 -0
  96. package/dist/src/core/run-state/lock.js +206 -0
  97. package/dist/src/core/run-state/phase-context.d.ts +60 -0
  98. package/dist/src/core/run-state/phase-context.js +108 -0
  99. package/dist/src/core/run-state/phase-registry.d.ts +137 -0
  100. package/dist/src/core/run-state/phase-registry.js +162 -0
  101. package/dist/src/core/run-state/phase-runner.d.ts +80 -0
  102. package/dist/src/core/run-state/phase-runner.js +447 -0
  103. package/dist/src/core/run-state/provider-readback.d.ts +130 -0
  104. package/dist/src/core/run-state/provider-readback.js +426 -0
  105. package/dist/src/core/run-state/replay-decision.d.ts +69 -0
  106. package/dist/src/core/run-state/replay-decision.js +144 -0
  107. package/dist/src/core/run-state/resolve-engine.d.ts +45 -0
  108. package/dist/src/core/run-state/resolve-engine.js +74 -0
  109. package/dist/src/core/run-state/resume-preflight.d.ts +66 -0
  110. package/dist/src/core/run-state/resume-preflight.js +116 -0
  111. package/dist/src/core/run-state/run-phase-with-lifecycle.d.ts +69 -0
  112. package/dist/src/core/run-state/run-phase-with-lifecycle.js +193 -0
  113. package/dist/src/core/run-state/runs.d.ts +57 -0
  114. package/dist/src/core/run-state/runs.js +288 -0
  115. package/dist/src/core/run-state/snapshot.d.ts +14 -0
  116. package/dist/src/core/run-state/snapshot.js +114 -0
  117. package/dist/src/core/run-state/state.d.ts +40 -0
  118. package/dist/src/core/run-state/state.js +164 -0
  119. package/dist/src/core/run-state/types.d.ts +284 -0
  120. package/dist/src/core/run-state/types.js +19 -0
  121. package/dist/src/core/run-state/ulid.d.ts +11 -0
  122. package/dist/src/core/run-state/ulid.js +95 -0
  123. package/dist/src/core/schema-alignment/extractor/index.d.ts +1 -1
  124. package/dist/src/core/schema-alignment/extractor/index.js +2 -2
  125. package/dist/src/core/schema-alignment/extractor/prisma.d.ts +13 -1
  126. package/dist/src/core/schema-alignment/extractor/prisma.js +65 -10
  127. package/dist/src/core/schema-alignment/git-history.d.ts +19 -0
  128. package/dist/src/core/schema-alignment/git-history.js +53 -0
  129. package/dist/src/core/static-rules/rules/brand-tokens.js +2 -2
  130. package/dist/src/core/static-rules/rules/schema-alignment.js +14 -4
  131. package/dist/src/dashboard/auto-upload.d.ts +26 -0
  132. package/dist/src/dashboard/auto-upload.js +107 -0
  133. package/dist/src/dashboard/config.d.ts +22 -0
  134. package/dist/src/dashboard/config.js +109 -0
  135. package/dist/src/dashboard/upload/canonical.d.ts +3 -0
  136. package/dist/src/dashboard/upload/canonical.js +16 -0
  137. package/dist/src/dashboard/upload/chain.d.ts +9 -0
  138. package/dist/src/dashboard/upload/chain.js +27 -0
  139. package/dist/src/dashboard/upload/snapshot.d.ts +23 -0
  140. package/dist/src/dashboard/upload/snapshot.js +66 -0
  141. package/dist/src/dashboard/upload/uploader.d.ts +54 -0
  142. package/dist/src/dashboard/upload/uploader.js +330 -0
  143. package/package.json +19 -3
  144. package/scripts/autoregress.ts +1 -1
  145. package/scripts/test-runner.mjs +4 -0
  146. package/skills/claude-autopilot.md +1 -1
  147. package/skills/make-interfaces-feel-better/SKILL.md +104 -0
  148. package/skills/simplify-ui/SKILL.md +103 -0
  149. package/skills/ui/SKILL.md +117 -0
  150. package/skills/ui-ux-pro-max/SKILL.md +90 -0
@@ -0,0 +1,284 @@
1
+ /** Schema version for everything written by this engine. Bump on breaking
2
+ * changes to RunState / RunEvent / PhaseSnapshot shape.
3
+ *
4
+ * v7.0 — bumped from 1 to 2 to signal the v7 cycle on every newly-written
5
+ * state.json. v6.x runs (schema_version=1) remain readable by v7 binaries
6
+ * because `RUN_STATE_MIN_SUPPORTED_SCHEMA_VERSION` stays at 1. v6 binaries
7
+ * cannot read v7-written runs; the corrupted_state error includes a
8
+ * "downgrade resume is not supported" hint so operators know why. */
9
+ export declare const RUN_STATE_SCHEMA_VERSION: 2;
10
+ export type SchemaVersion = typeof RUN_STATE_SCHEMA_VERSION;
11
+ /** Identifies a single OS-level writer. PID + a hash of the hostname (we
12
+ * don't persist the raw hostname to the lock metadata so co-tenant signal
13
+ * doesn't leak between users sharing a directory). */
14
+ export interface WriterId {
15
+ pid: number;
16
+ hostHash: string;
17
+ }
18
+ /** Top-level run status, mirroring the lifecycle diagram in the spec. */
19
+ export type RunStatus = 'pending' | 'running' | 'paused' | 'success' | 'failed' | 'aborted';
20
+ /** Per-phase status within a run. */
21
+ export type PhaseStatus = 'pending' | 'running' | 'succeeded' | 'failed' | 'skipped' | 'aborted';
22
+ /** External operation reference — the persisted breadcrumb that makes replay
23
+ * decisions deterministic. Used heavily in Phase 6 (idempotency contracts);
24
+ * typed now so events can carry it without later schema churn.
25
+ *
26
+ * v6.2.1 — `migration-batch` joins the union as the side-effect contract's
27
+ * PRE-effect breadcrumb for the `migrate` phase. Semantics: a deterministic
28
+ * id covers a planned migration batch and is emitted BEFORE the dispatcher
29
+ * is invoked, so a partial crash leaves a resume target visible to the
30
+ * orchestrator's preflight readback. The post-effect `migration-version`
31
+ * refs (one per actually-applied migration) remain authoritative for
32
+ * reconciliation; `migration-batch` exists purely so resume can tell
33
+ * "we started this batch but didn't finish" apart from "we never started." */
34
+ export type ExternalRefKind = 'github-pr' | 'github-comment' | 'git-remote-push' | 'deploy' | 'migration-batch' | 'migration-version' | 'rollback-target' | 'spec-file' | 'plan-file' | 'sarif-artifact' | 'review-comments';
35
+ export interface ExternalRef {
36
+ kind: ExternalRefKind;
37
+ /** Provider-specific identifier (PR number, commit SHA, deploy ID, …). */
38
+ id: string;
39
+ provider?: string;
40
+ /** Human-readable artifact link if the provider exposes one. */
41
+ url?: string;
42
+ /** ISO timestamp of the platform's confirmation. */
43
+ observedAt: string;
44
+ }
45
+ /** Per-phase artifact pointer recorded inside the snapshot. */
46
+ export interface PhaseArtifactRef {
47
+ /** Logical name (e.g. "spec", "plan", "impl-diff"). */
48
+ name: string;
49
+ /** Path inside `artifacts/` (relative to run dir). */
50
+ path: string;
51
+ sha256?: string;
52
+ size?: number;
53
+ copiedAt?: string;
54
+ }
55
+ /** Snapshot of a single phase, persisted under `phases/<name>.json` and
56
+ * reflected inside state.json's `phases[]`. */
57
+ export interface PhaseSnapshot {
58
+ schema_version: SchemaVersion;
59
+ name: string;
60
+ /** Order within the run (0-indexed). */
61
+ index: number;
62
+ status: PhaseStatus;
63
+ /** True iff `RunPhase.idempotent` was declared true at registration. */
64
+ idempotent: boolean;
65
+ /** True iff `RunPhase.hasSideEffects` was declared true at registration. */
66
+ hasSideEffects: boolean;
67
+ startedAt?: string;
68
+ endedAt?: string;
69
+ durationMs?: number;
70
+ /** Sum of `phase.cost` events for this phase. */
71
+ costUSD: number;
72
+ attempts: number;
73
+ /** Last failure message (string) if status === 'failed'. */
74
+ lastError?: string;
75
+ artifacts: PhaseArtifactRef[];
76
+ externalRefs: ExternalRef[];
77
+ /** Phase-specific metadata. Free-form; engine doesn't introspect. */
78
+ meta?: Record<string, unknown>;
79
+ /** Phase 6 — last successful output, persisted so a future
80
+ * `skip-already-applied` decision can return it without re-execution.
81
+ * The engine writes this on every `phase.success`; absent on failed
82
+ * / pre-Phase-6 snapshots. JSON-serializable values only. */
83
+ result?: unknown;
84
+ }
85
+ /** The state.json checkpoint. Authoritative answer is always
86
+ * events.ndjson; this is a derived snapshot for O(1) status queries. */
87
+ export interface RunState {
88
+ schema_version: SchemaVersion;
89
+ runId: string;
90
+ /** ULID generation time, ISO. */
91
+ startedAt: string;
92
+ endedAt?: string;
93
+ status: RunStatus;
94
+ /** Phase order is fixed at run creation. */
95
+ phases: PhaseSnapshot[];
96
+ /** Index into `phases[]` of the currently-running or last-attempted phase. */
97
+ currentPhaseIdx: number;
98
+ /** Sum of phase.cost events across the whole run. */
99
+ totalCostUSD: number;
100
+ /** Last seq written to events.ndjson at the time of the snapshot. */
101
+ lastEventSeq: number;
102
+ /** The writer that wrote this snapshot. */
103
+ writerId: WriterId;
104
+ /** Working directory the run was started in (absolute path). */
105
+ cwd: string;
106
+ /** Snapshot of the run config at creation (subset of guardrail.config.yaml).
107
+ * Free-form; engine doesn't introspect here, later phases do. */
108
+ config?: Record<string, unknown>;
109
+ }
110
+ /** Universal envelope on every event line. */
111
+ export interface RunEventBase {
112
+ schema_version: SchemaVersion;
113
+ /** ISO timestamp. */
114
+ ts: string;
115
+ runId: string;
116
+ /** Monotonic per-run sequence. Receivers MUST detect gaps. */
117
+ seq: number;
118
+ /** The writer that appended this event. */
119
+ writerId: WriterId;
120
+ }
121
+ export interface RunStartEvent extends RunEventBase {
122
+ event: 'run.start';
123
+ phases: string[];
124
+ config?: Record<string, unknown>;
125
+ }
126
+ export interface RunCompleteEvent extends RunEventBase {
127
+ event: 'run.complete';
128
+ status: 'success' | 'failed' | 'aborted';
129
+ totalCostUSD: number;
130
+ durationMs: number;
131
+ }
132
+ export interface RunWarningEvent extends RunEventBase {
133
+ event: 'run.warning';
134
+ message: string;
135
+ details?: Record<string, unknown>;
136
+ }
137
+ export interface RunRecoveryEvent extends RunEventBase {
138
+ event: 'run.recovery';
139
+ reason: 'recovered-from-partial-write' | 'recovered-from-corrupt-snapshot' | 'recovered-from-missing-snapshot';
140
+ details?: Record<string, unknown>;
141
+ }
142
+ export interface PhaseStartEvent extends RunEventBase {
143
+ event: 'phase.start';
144
+ phase: string;
145
+ phaseIdx: number;
146
+ idempotent: boolean;
147
+ hasSideEffects: boolean;
148
+ /** Attempt counter, 1-based. >1 implies a resume / retry. */
149
+ attempt: number;
150
+ }
151
+ export interface PhaseSuccessEvent extends RunEventBase {
152
+ event: 'phase.success';
153
+ phase: string;
154
+ phaseIdx: number;
155
+ durationMs: number;
156
+ artifacts: PhaseArtifactRef[];
157
+ }
158
+ export interface PhaseFailedEvent extends RunEventBase {
159
+ event: 'phase.failed';
160
+ phase: string;
161
+ phaseIdx: number;
162
+ durationMs: number;
163
+ /** Stringified error message. Stack traces stay out of the durable log. */
164
+ error: string;
165
+ /** Optional structured error code (matches GuardrailError.code if thrown). */
166
+ errorCode?: string;
167
+ }
168
+ export interface PhaseAbortedEvent extends RunEventBase {
169
+ event: 'phase.aborted';
170
+ phase: string;
171
+ phaseIdx: number;
172
+ reason: 'user-interrupt' | 'budget-exceeded' | 'lock-takeover' | 'crash';
173
+ }
174
+ export interface PhaseCostEvent extends RunEventBase {
175
+ event: 'phase.cost';
176
+ phase: string;
177
+ phaseIdx: number;
178
+ provider: string;
179
+ inputTokens: number;
180
+ outputTokens: number;
181
+ costUSD: number;
182
+ }
183
+ export interface PhaseExternalRefEvent extends RunEventBase {
184
+ event: 'phase.externalRef';
185
+ phase: string;
186
+ phaseIdx: number;
187
+ ref: ExternalRef;
188
+ }
189
+ export interface PhaseNeedsHumanEvent extends RunEventBase {
190
+ event: 'phase.needs-human';
191
+ phase: string;
192
+ phaseIdx: number;
193
+ reason: string;
194
+ /** Hint surfaced to the user / CI consumer. */
195
+ nextActions?: string[];
196
+ }
197
+ export interface LockTakeoverEvent extends RunEventBase {
198
+ event: 'lock.takeover';
199
+ /** Identity of the writer who previously held the lock (best-effort —
200
+ * may be null if metadata was missing). */
201
+ previousWriter: WriterId | null;
202
+ reason: string;
203
+ }
204
+ export interface IndexRebuiltEvent extends RunEventBase {
205
+ event: 'index.rebuilt';
206
+ /** Why the rebuild was needed: "missing", "corrupt", or "force". */
207
+ cause: 'missing' | 'corrupt' | 'force';
208
+ }
209
+ /** Phase 4 — budget enforcement preflight. Emitted by `runPhase` BEFORE
210
+ * `phase.start` for every phase whose parent run carries a `BudgetConfig`.
211
+ * Carries the full `BudgetCheck` payload from `checkPhaseBudget` so
212
+ * consumers (cost dashboards, CI) can attribute spend and decisions
213
+ * without re-running the policy. Per the v6 spec "Budget enforcement"
214
+ * section + Codex CRITICAL #3 (two-layer guard, layer 2 always runs). */
215
+ export interface BudgetCheckEvent extends RunEventBase {
216
+ event: 'budget.check';
217
+ phase: string;
218
+ phaseIdx: number;
219
+ decision: 'proceed' | 'pause' | 'hard-fail';
220
+ /** `estimate.high` from `RunPhase.estimateCost` if it returned a value;
221
+ * null when the phase doesn't implement estimateCost. Layer 2 (the
222
+ * mandatory floor) ALWAYS runs regardless. */
223
+ estimatedHigh: number | null;
224
+ /** Sum of every prior `phase.cost` event in this run, in USD. */
225
+ actualSoFar: number;
226
+ /** The reserve the runner deducted against `perRunUSD` for this phase
227
+ * (Layer 2 floor, expressed in USD). */
228
+ reserveApplied: number;
229
+ /** USD remaining under `perRunUSD` after `actualSoFar` + the larger of
230
+ * `estimatedHigh` and `reserveApplied`. May be negative on hard-fail. */
231
+ capRemaining: number;
232
+ reason: string;
233
+ /** v6.2.0 — which scope produced the decision. `'phase'` (legacy
234
+ * default) is the single-phase wrapper path; `'run'` is the
235
+ * orchestrator's cross-phase mode. Optional only for back-compat
236
+ * with older events.ndjson files; events emitted on v6.2.0+ always
237
+ * carry a value. */
238
+ scope?: 'phase' | 'run';
239
+ }
240
+ /** Phase 6 — emitted when a `forceReplay` override flips a needs-human (or
241
+ * any other refusal) into a retry. Carries the phase and the refs that
242
+ * WERE consulted so the durable log shows exactly what was overridden.
243
+ * Spec: "a `--force-replay` override writes an explicit `replay.override`
244
+ * event with user-supplied reason." */
245
+ export interface ReplayOverrideEvent extends RunEventBase {
246
+ event: 'replay.override';
247
+ phase: string;
248
+ phaseIdx: number;
249
+ /** Free-form user / CI reason for the override. */
250
+ reason: string;
251
+ /** Refs the underlying refusal cited (echoed for triage). */
252
+ refsConsulted: ExternalRef[];
253
+ }
254
+ /** Discriminated union of every event variant. Add new variants here and
255
+ * the code that switches over `event` will type-error at compile time. */
256
+ export type RunEvent = RunStartEvent | RunCompleteEvent | RunWarningEvent | RunRecoveryEvent | PhaseStartEvent | PhaseSuccessEvent | PhaseFailedEvent | PhaseAbortedEvent | PhaseCostEvent | PhaseExternalRefEvent | PhaseNeedsHumanEvent | LockTakeoverEvent | IndexRebuiltEvent | BudgetCheckEvent | ReplayOverrideEvent;
257
+ /** Distributive Omit so the discriminated-union shape is preserved when we
258
+ * strip the fields the appender fills in. Plain `Omit<RunEvent, ...>`
259
+ * collapses the union into a single intersection and loses variant-specific
260
+ * fields — so a literal `{ event: 'phase.cost', costUSD: 1, ... }` would
261
+ * fail typecheck. */
262
+ type DistributiveOmit<T, K extends keyof T> = T extends unknown ? Omit<T, K> : never;
263
+ /** When appending we don't know `seq`, `ts`, `runId`, `schema_version`,
264
+ * or `writerId` yet — the appender supplies them. */
265
+ export type RunEventInput = DistributiveOmit<RunEvent, 'seq' | 'ts' | 'runId' | 'schema_version' | 'writerId'>;
266
+ export interface RunIndexEntry {
267
+ runId: string;
268
+ status: RunStatus;
269
+ startedAt: string;
270
+ endedAt?: string;
271
+ totalCostUSD: number;
272
+ /** Last completed phase name (for `runs list` summary). */
273
+ lastPhase?: string;
274
+ /** True if state.json was synthesized via replay because it was missing
275
+ * or corrupt at last open. Surfaces as a warning in the UI. */
276
+ recovered?: boolean;
277
+ }
278
+ export interface RunIndex {
279
+ schema_version: SchemaVersion;
280
+ /** Newest first. */
281
+ runs: RunIndexEntry[];
282
+ }
283
+ export {};
284
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,19 @@
1
+ // src/core/run-state/types.ts
2
+ //
3
+ // v6 Run State Engine — pure data layer types. Phase 1 (persistence) only.
4
+ // Behavior — phase wrapping, CLI verbs, budget enforcement, etc. — lands in
5
+ // later phases. The shapes here are versioned via `schema_version: 1` so a
6
+ // future migration can detect and migrate older runs.
7
+ //
8
+ // Spec: docs/specs/v6-run-state-engine.md ("State on disk", "Run lifecycle",
9
+ // "Idempotency rules + external operation ledger", "Persistence protocol").
10
+ /** Schema version for everything written by this engine. Bump on breaking
11
+ * changes to RunState / RunEvent / PhaseSnapshot shape.
12
+ *
13
+ * v7.0 — bumped from 1 to 2 to signal the v7 cycle on every newly-written
14
+ * state.json. v6.x runs (schema_version=1) remain readable by v7 binaries
15
+ * because `RUN_STATE_MIN_SUPPORTED_SCHEMA_VERSION` stays at 1. v6 binaries
16
+ * cannot read v7-written runs; the corrupted_state error includes a
17
+ * "downgrade resume is not supported" hint so operators know why. */
18
+ export const RUN_STATE_SCHEMA_VERSION = 2;
19
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,11 @@
1
+ /** Validate that a string matches the ULID shape (length + alphabet). */
2
+ export declare function isValidULID(s: string): boolean;
3
+ /** Generate a new ULID. Optionally pass a fixed `now` (ms epoch) for tests. */
4
+ export declare function ulid(now?: number): string;
5
+ /** Decode the timestamp-portion of a ULID back to ms epoch. Throws on
6
+ * malformed input. Useful for `runs list` ordering and for tests. */
7
+ export declare function decodeTime(id: string): number;
8
+ /** Exposed for tests that want to verify alphabet membership. */
9
+ export declare const ULID_ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
10
+ export declare const ULID_LENGTH: number;
11
+ //# sourceMappingURL=ulid.d.ts.map
@@ -0,0 +1,95 @@
1
+ // src/core/run-state/ulid.ts
2
+ //
3
+ // Tiny pure-TS ULID generator. We deliberately avoid pulling in the `ulid`
4
+ // npm package — the algorithm is short and the runtime dep budget for the
5
+ // engine is tight. Conforms to https://github.com/ulid/spec :
6
+ //
7
+ // - 26 characters, Crockford's Base32 (no I, L, O, U).
8
+ // - First 10 chars = 48-bit Unix-millisecond timestamp (ms since epoch).
9
+ // - Last 16 chars = 80 bits of randomness (crypto.randomBytes).
10
+ // - Lexicographic sort == chronological sort (within ms; tie-break is
11
+ // random within the same ms — Phase 1 does not implement the optional
12
+ // monotonic-overflow within-ms behavior, since runIDs are issued by a
13
+ // single writer and one-per-millisecond is unreachable in practice).
14
+ // - URL-safe (Crockford Base32 only emits [0-9A-Z]).
15
+ import { randomBytes } from 'node:crypto';
16
+ const ENCODING = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'; // Crockford Base32 alphabet (32 chars).
17
+ const ENCODING_LEN = ENCODING.length; // 32.
18
+ const TIME_LEN = 10;
19
+ const RANDOM_LEN = 16;
20
+ const ULID_LEN = TIME_LEN + RANDOM_LEN; // 26.
21
+ /** Max representable timestamp = 2^48 - 1 ms. Sanity-check, not a bug
22
+ * most callers will hit (it's the year 10889). */
23
+ const TIME_MAX = 281474976710655;
24
+ /** Validate that a string matches the ULID shape (length + alphabet). */
25
+ export function isValidULID(s) {
26
+ if (typeof s !== 'string')
27
+ return false;
28
+ if (s.length !== ULID_LEN)
29
+ return false;
30
+ for (let i = 0; i < ULID_LEN; i++) {
31
+ if (ENCODING.indexOf(s[i]) < 0)
32
+ return false;
33
+ }
34
+ return true;
35
+ }
36
+ function encodeTime(now) {
37
+ if (!Number.isFinite(now) || now < 0 || now > TIME_MAX) {
38
+ throw new RangeError(`ulid: timestamp out of range (got ${now})`);
39
+ }
40
+ let out = '';
41
+ let t = now;
42
+ for (let i = TIME_LEN - 1; i >= 0; i--) {
43
+ const mod = t % ENCODING_LEN;
44
+ out = ENCODING[mod] + out;
45
+ t = (t - mod) / ENCODING_LEN;
46
+ }
47
+ return out;
48
+ }
49
+ function encodeRandom() {
50
+ // 16 chars * 5 bits each = 80 bits. We draw 10 bytes (80 bits) of
51
+ // crypto-grade randomness and encode 5 bits at a time. Any extra fractional
52
+ // bits are discarded — this is the standard ULID approach.
53
+ const bytes = randomBytes(10);
54
+ // Pack the 10 bytes into a 80-bit unsigned integer view, 5-bit chunks.
55
+ // We do it manually rather than using BigInt for portability — the array
56
+ // is short enough that the explicit math is just as fast and avoids any
57
+ // dependency on BigInt-typed regression in older runtimes.
58
+ const bits = new Array(80);
59
+ for (let i = 0; i < 10; i++) {
60
+ const b = bytes[i];
61
+ for (let j = 0; j < 8; j++) {
62
+ bits[i * 8 + j] = (b >> (7 - j)) & 1;
63
+ }
64
+ }
65
+ let out = '';
66
+ for (let i = 0; i < RANDOM_LEN; i++) {
67
+ let v = 0;
68
+ for (let j = 0; j < 5; j++) {
69
+ v = (v << 1) | bits[i * 5 + j];
70
+ }
71
+ out += ENCODING[v];
72
+ }
73
+ return out;
74
+ }
75
+ /** Generate a new ULID. Optionally pass a fixed `now` (ms epoch) for tests. */
76
+ export function ulid(now = Date.now()) {
77
+ return encodeTime(now) + encodeRandom();
78
+ }
79
+ /** Decode the timestamp-portion of a ULID back to ms epoch. Throws on
80
+ * malformed input. Useful for `runs list` ordering and for tests. */
81
+ export function decodeTime(id) {
82
+ if (!isValidULID(id)) {
83
+ throw new Error(`ulid: not a valid ULID: ${String(id)}`);
84
+ }
85
+ let t = 0;
86
+ for (let i = 0; i < TIME_LEN; i++) {
87
+ const idx = ENCODING.indexOf(id[i]);
88
+ t = t * ENCODING_LEN + idx;
89
+ }
90
+ return t;
91
+ }
92
+ /** Exposed for tests that want to verify alphabet membership. */
93
+ export const ULID_ALPHABET = ENCODING;
94
+ export const ULID_LENGTH = ULID_LEN;
95
+ //# sourceMappingURL=ulid.js.map
@@ -1,3 +1,3 @@
1
1
  import type { SchemaEntity } from '../types.ts';
2
- export declare function extract(filePath: string): SchemaEntity[];
2
+ export declare function extract(filePath: string, previousContent?: string | null): SchemaEntity[];
3
3
  //# sourceMappingURL=index.d.ts.map
@@ -3,7 +3,7 @@ import * as fs from 'node:fs';
3
3
  import * as path from 'node:path';
4
4
  import { extractFromSql } from "./sql.js";
5
5
  import { extractFromPrisma } from "./prisma.js";
6
- export function extract(filePath) {
6
+ export function extract(filePath, previousContent) {
7
7
  let content;
8
8
  try {
9
9
  content = fs.readFileSync(filePath, 'utf8');
@@ -16,7 +16,7 @@ export function extract(filePath) {
16
16
  if (ext === '.sql')
17
17
  return extractFromSql(content);
18
18
  if (base === 'schema.prisma' || ext === '.prisma')
19
- return extractFromPrisma(content);
19
+ return extractFromPrisma(content, previousContent);
20
20
  process.stderr.write(`[schema-alignment] no extractor for ${ext} files — skipping ${path.basename(filePath)}\n`);
21
21
  return [];
22
22
  }
@@ -1,3 +1,15 @@
1
1
  import type { SchemaEntity } from '../types.ts';
2
- export declare function extractFromPrisma(content: string): SchemaEntity[];
2
+ /**
3
+ * Extract schema entities from a Prisma schema file.
4
+ *
5
+ * When `previousContent` is provided, only the diff (added/removed fields,
6
+ * new/dropped tables) is emitted — this avoids over-reporting when a user
7
+ * touches schema.prisma for any reason (adding one field, editing a comment)
8
+ * and every long-existing field gets re-checked against type/API/UI layers.
9
+ *
10
+ * When `previousContent` is null/undefined, every model and field is emitted
11
+ * — the original "all entities are new" behavior used as a fallback when git
12
+ * history isn't available.
13
+ */
14
+ export declare function extractFromPrisma(content: string, previousContent?: string | null): SchemaEntity[];
3
15
  //# sourceMappingURL=prisma.d.ts.map
@@ -1,18 +1,73 @@
1
- export function extractFromPrisma(content) {
2
- const entities = [];
3
- // Match model blocks: model Name { ... }
4
- const modelRe = /^model\s+(\w+)\s*\{([^}]+)\}/gm;
5
- for (const modelMatch of content.matchAll(modelRe)) {
1
+ const MODEL_RE = /^model\s+(\w+)\s*\{([^}]+)\}/gm;
2
+ const FIELD_RE = /^\s+(\w+)\s+\S/gm;
3
+ function parseModels(content) {
4
+ const models = new Map();
5
+ for (const modelMatch of content.matchAll(MODEL_RE)) {
6
6
  const table = modelMatch[1];
7
- entities.push({ table, operation: 'create_table' });
7
+ const fields = new Set();
8
8
  const body = modelMatch[2];
9
- // Match field lines: fieldName TypeName ...
10
- const fieldRe = /^\s+(\w+)\s+\S/gm;
11
- for (const fieldMatch of body.matchAll(fieldRe)) {
9
+ for (const fieldMatch of body.matchAll(FIELD_RE)) {
12
10
  const column = fieldMatch[1];
13
11
  if (column.startsWith('@') || column === 'id')
14
12
  continue;
15
- entities.push({ table, column, operation: 'add_column' });
13
+ fields.add(column);
14
+ }
15
+ models.set(table, fields);
16
+ }
17
+ return models;
18
+ }
19
+ /**
20
+ * Extract schema entities from a Prisma schema file.
21
+ *
22
+ * When `previousContent` is provided, only the diff (added/removed fields,
23
+ * new/dropped tables) is emitted — this avoids over-reporting when a user
24
+ * touches schema.prisma for any reason (adding one field, editing a comment)
25
+ * and every long-existing field gets re-checked against type/API/UI layers.
26
+ *
27
+ * When `previousContent` is null/undefined, every model and field is emitted
28
+ * — the original "all entities are new" behavior used as a fallback when git
29
+ * history isn't available.
30
+ */
31
+ export function extractFromPrisma(content, previousContent) {
32
+ const current = parseModels(content);
33
+ if (previousContent === undefined || previousContent === null) {
34
+ const entities = [];
35
+ for (const [table, fields] of current) {
36
+ entities.push({ table, operation: 'create_table' });
37
+ for (const column of fields)
38
+ entities.push({ table, column, operation: 'add_column' });
39
+ }
40
+ return entities;
41
+ }
42
+ const previous = parseModels(previousContent);
43
+ const entities = [];
44
+ for (const [table, currentFields] of current) {
45
+ const previousFields = previous.get(table);
46
+ if (!previousFields) {
47
+ // New table — emit create_table + add_column for every field
48
+ entities.push({ table, operation: 'create_table' });
49
+ for (const column of currentFields)
50
+ entities.push({ table, column, operation: 'add_column' });
51
+ continue;
52
+ }
53
+ for (const column of currentFields) {
54
+ if (!previousFields.has(column))
55
+ entities.push({ table, column, operation: 'add_column' });
56
+ }
57
+ for (const column of previousFields) {
58
+ if (!currentFields.has(column))
59
+ entities.push({ table, column, operation: 'drop_column' });
60
+ }
61
+ }
62
+ // Second pass — entirely dropped models. The first loop only iterates
63
+ // `current`, so a model present in `previous` but removed from `current`
64
+ // would never emit any entity, leaving stale references in type/API/UI
65
+ // layers undetected. Caught by Cursor Bugbot on PR #44 (MEDIUM).
66
+ for (const [table, previousFields] of previous) {
67
+ if (current.has(table))
68
+ continue;
69
+ for (const column of previousFields) {
70
+ entities.push({ table, column, operation: 'drop_column' });
16
71
  }
17
72
  }
18
73
  return entities;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Read the previous version of a file from git, comparing against `base`.
3
+ * Returns null if the file has no git history at that ref (untracked,
4
+ * brand-new, not in a repo, or the ref doesn't resolve) — callers should
5
+ * fall back to whole-file extraction in that case.
6
+ *
7
+ * When `base` is omitted, the default is CI-aware (see `resolveDefaultBase`):
8
+ * - GitHub Actions PR build → `origin/<GITHUB_BASE_REF>`
9
+ * - GitLab MR build → `origin/<CI_MERGE_REQUEST_TARGET_BRANCH_NAME>`
10
+ * - everything else → `HEAD~1`
11
+ *
12
+ * Reading from `HEAD` directly is wrong in CI (any post-commit context):
13
+ * `HEAD` IS the current commit, so the diff against the working-tree file
14
+ * is always empty and no schema entities are emitted. Caught by Cursor
15
+ * Bugbot on PR #44 (HIGH); the multi-commit-PR variant of the same bug
16
+ * caught as a MEDIUM follow-up on the rebased commit.
17
+ */
18
+ export declare function getPreviousFileContent(filePath: string, cwd?: string, base?: string): string | null;
19
+ //# sourceMappingURL=git-history.d.ts.map
@@ -0,0 +1,53 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import * as path from 'node:path';
3
+ /**
4
+ * Resolve the default base ref for git diffs in a CI-aware way.
5
+ *
6
+ * Priority: `GITHUB_BASE_REF` (GitHub Actions PR builds), then
7
+ * `CI_MERGE_REQUEST_TARGET_BRANCH_NAME` (GitLab MR builds), then `HEAD~1`
8
+ * for local / single-commit contexts. Branch-name env vars are prefixed
9
+ * with `origin/` so `git show <ref>:<file>` resolves to the
10
+ * remote-tracking-branch tip (the actual merge base).
11
+ *
12
+ * Caught + extended by Cursor Bugbot follow-up on PR #44 (MEDIUM): a static
13
+ * default of `HEAD~1` is wrong for multi-commit PRs — it points at the
14
+ * previous commit on the branch, not the merge base.
15
+ */
16
+ function resolveDefaultBase() {
17
+ const ghBase = process.env.GITHUB_BASE_REF;
18
+ if (ghBase && ghBase.length > 0)
19
+ return `origin/${ghBase}`;
20
+ const glBase = process.env.CI_MERGE_REQUEST_TARGET_BRANCH_NAME;
21
+ if (glBase && glBase.length > 0)
22
+ return `origin/${glBase}`;
23
+ return 'HEAD~1';
24
+ }
25
+ /**
26
+ * Read the previous version of a file from git, comparing against `base`.
27
+ * Returns null if the file has no git history at that ref (untracked,
28
+ * brand-new, not in a repo, or the ref doesn't resolve) — callers should
29
+ * fall back to whole-file extraction in that case.
30
+ *
31
+ * When `base` is omitted, the default is CI-aware (see `resolveDefaultBase`):
32
+ * - GitHub Actions PR build → `origin/<GITHUB_BASE_REF>`
33
+ * - GitLab MR build → `origin/<CI_MERGE_REQUEST_TARGET_BRANCH_NAME>`
34
+ * - everything else → `HEAD~1`
35
+ *
36
+ * Reading from `HEAD` directly is wrong in CI (any post-commit context):
37
+ * `HEAD` IS the current commit, so the diff against the working-tree file
38
+ * is always empty and no schema entities are emitted. Caught by Cursor
39
+ * Bugbot on PR #44 (HIGH); the multi-commit-PR variant of the same bug
40
+ * caught as a MEDIUM follow-up on the rebased commit.
41
+ */
42
+ export function getPreviousFileContent(filePath, cwd = process.cwd(), base = resolveDefaultBase()) {
43
+ const relPath = path.isAbsolute(filePath) ? path.relative(cwd, filePath) : filePath;
44
+ const result = spawnSync('git', ['show', `${base}:${relPath}`], {
45
+ cwd,
46
+ encoding: 'utf8',
47
+ timeout: 5000,
48
+ });
49
+ if (result.status !== 0)
50
+ return null;
51
+ return result.stdout;
52
+ }
53
+ //# sourceMappingURL=git-history.js.map
@@ -36,8 +36,8 @@ function buildPalette(brandCfg, cwd) {
36
36
  export const brandTokensRule = {
37
37
  name: 'brand-tokens',
38
38
  severity: 'warning',
39
- async check(touchedFiles, config = {}) {
40
- const brandCfg = config.brand;
39
+ async check(touchedFiles, ctx = {}) {
40
+ const brandCfg = ctx.config?.brand;
41
41
  if (!brandCfg)
42
42
  return [];
43
43
  const cwd = process.cwd();
@@ -1,5 +1,6 @@
1
1
  import { detect } from "../../schema-alignment/detector.js";
2
2
  import { extract } from "../../schema-alignment/extractor/index.js";
3
+ import { getPreviousFileContent } from "../../schema-alignment/git-history.js";
3
4
  import { scanLayers } from "../../schema-alignment/scanner.js";
4
5
  import { runLlmCheck } from "../../schema-alignment/llm-check.js";
5
6
  function isDestructive(entity) {
@@ -50,15 +51,24 @@ function structuralFinding(result, layer, defaultSev, sourceFile) {
50
51
  export const schemaAlignmentRule = {
51
52
  name: 'schema-alignment',
52
53
  severity: 'warning',
53
- async check(touchedFiles, config = {}) {
54
- const saConfig = config['schema-alignment'];
54
+ async check(touchedFiles, ctx = {}) {
55
+ const saConfig = ctx.config?.['schema-alignment'];
55
56
  if (saConfig?.enabled === false)
56
57
  return [];
57
58
  const cwd = process.cwd();
58
59
  const migrationFiles = detect(touchedFiles, saConfig);
59
60
  if (migrationFiles.length === 0)
60
61
  return [];
61
- const allEntities = migrationFiles.flatMap(f => extract(f).map(entity => ({ entity, sourceFile: f })));
62
+ // For Prisma schema files, fetch the previous version from git so we only
63
+ // emit entities for what actually changed in this diff. SQL migrations
64
+ // are inherently a diff already; the SQL extractor ignores
65
+ // `previousContent`, so skipping the `git show` spawn there avoids pure
66
+ // waste (Bugbot LOW on PR #44).
67
+ const allEntities = migrationFiles.flatMap(f => {
68
+ const isPrisma = f.endsWith('.prisma');
69
+ const previousContent = isPrisma ? getPreviousFileContent(f, cwd) : null;
70
+ return extract(f, previousContent).map(entity => ({ entity, sourceFile: f }));
71
+ });
62
72
  if (allEntities.length === 0)
63
73
  return [];
64
74
  const scanResults = scanLayers(allEntities.map(e => e.entity), cwd, saConfig);
@@ -75,7 +85,7 @@ export const schemaAlignmentRule = {
75
85
  return [];
76
86
  const defaultSev = saConfig?.severity ?? 'warning';
77
87
  const llmEnabled = saConfig?.llmCheck !== false;
78
- const engine = config['_engine'];
88
+ const engine = ctx.engine;
79
89
  // Structural mode — always compute these so we can fall back if LLM path yields nothing
80
90
  const structural = [];
81
91
  for (const { result: r, sourceFile } of gapResults) {