lastlight-shared 0.1.7 → 0.3.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.
- package/dist/config-types.d.ts +136 -0
- package/dist/config-types.js +118 -1
- package/dist/config-types.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/repo-config-schema.d.ts +328 -0
- package/dist/repo-config-schema.js +957 -0
- package/dist/repo-config-schema.js.map +1 -0
- package/dist/workflow-loader.d.ts +104 -16
- package/dist/workflow-loader.js +244 -77
- package/dist/workflow-loader.js.map +1 -1
- package/package.json +6 -2
package/dist/config-types.d.ts
CHANGED
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
* `config/config.ts` so `shared` never depends back on core (locked decision
|
|
4
4
|
* 11). Core re-exports these from `lastlight-shared` so its own
|
|
5
5
|
* `config/config.js` import surface is unchanged.
|
|
6
|
+
*
|
|
7
|
+
* The `fix:` / `dependencies:` / `review:` policy blocks below live here for the
|
|
8
|
+
* same reason plus one more: they are **repo-settable** (issues #251/#252), so
|
|
9
|
+
* `repo-config-schema.ts` — which bounds a repo's `.lastlight/` and is compiled
|
|
10
|
+
* into the CLI as well as core — has to name their shape and their shipped
|
|
11
|
+
* defaults. Core's normaliser and the repo-layer sanitizer therefore agree by
|
|
12
|
+
* construction rather than by two hand-maintained copies.
|
|
6
13
|
*/
|
|
7
14
|
export interface DisabledConfig {
|
|
8
15
|
workflows: string[];
|
|
@@ -15,3 +22,132 @@ export interface RouteConfig {
|
|
|
15
22
|
github: Record<string, string>;
|
|
16
23
|
slack: Record<string, string>;
|
|
17
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* The five classes a `diagnose` phase may return (09 → S1,
|
|
27
|
+
* `apps/server/skills/fixing/SKILL.md`), which is also the vocabulary
|
|
28
|
+
* {@link FixConfig.retryableClasses} names members of.
|
|
29
|
+
*
|
|
30
|
+
* It lives here rather than beside the marker parser in core because BOTH
|
|
31
|
+
* validators of that leaf need it: core's boot normaliser, and the repo-layer
|
|
32
|
+
* clamp in `./repo-config-schema.ts` — which is compiled into the CLI and may
|
|
33
|
+
* never reach core. `apps/server/src/engine/fix-markers.ts` re-exports it, so
|
|
34
|
+
* every reader of the marker grammar still finds it where it expects to.
|
|
35
|
+
*/
|
|
36
|
+
export declare const DIAGNOSIS_CLASSES: readonly ["reproducible", "env-mismatch", "flaky", "infra-dependent", "upstream-broken"];
|
|
37
|
+
export type DiagnosisClass = (typeof DIAGNOSIS_CLASSES)[number];
|
|
38
|
+
/** True when `value` is one of {@link DIAGNOSIS_CLASSES}. */
|
|
39
|
+
export declare function isDiagnosisClass(value: unknown): value is DiagnosisClass;
|
|
40
|
+
/**
|
|
41
|
+
* Retry/escalation policy for every PR_FIX_SHAPED workflow (`pr-fix`,
|
|
42
|
+
* `dependabot-ci-fix`).
|
|
43
|
+
*
|
|
44
|
+
* Repo-settable subset (bounded in `repo-config-schema.ts`): `maxAttempts`,
|
|
45
|
+
* `localIterations`, `maxCostUsd`, `maxFlakyDeferrals` and `retryableClasses`,
|
|
46
|
+
* each clamped so a repo can only ever be MORE conservative than the operator.
|
|
47
|
+
* `escalateModelAfterAttempt` (spend control) and `gateTimeoutSeconds` (resource
|
|
48
|
+
* control) are operator-only.
|
|
49
|
+
*/
|
|
50
|
+
export interface FixConfig {
|
|
51
|
+
/** Cross-run attempts per (repo, PR) before the PR is escalated to a human. */
|
|
52
|
+
maxAttempts: number;
|
|
53
|
+
/**
|
|
54
|
+
* Within-run gate-loop iterations inside ONE attempt.
|
|
55
|
+
*
|
|
56
|
+
* Read by the fix phase's `generic_loop.max_iterations:
|
|
57
|
+
* { from: fix.localIterations, default: 2 }` in `pr-fix.yaml` /
|
|
58
|
+
* `dependabot-ci-fix.yaml` — the effective block is seeded on the run's
|
|
59
|
+
* template context, so the repo-clamped value is the operative bound.
|
|
60
|
+
*/
|
|
61
|
+
localIterations: number;
|
|
62
|
+
/**
|
|
63
|
+
* `until_bash` budget, in seconds, for the repo's build/test gate. Read by
|
|
64
|
+
* the same phase's `timeout_seconds: { from: fix.gateTimeoutSeconds, … }`.
|
|
65
|
+
*/
|
|
66
|
+
gateTimeoutSeconds: number;
|
|
67
|
+
/** Attempts ABOVE this number use `models["pr-fix-retry"]` when one is set. */
|
|
68
|
+
escalateModelAfterAttempt: number;
|
|
69
|
+
/** Cumulative cost ceiling across attempts for one PR. `null` = unbounded. */
|
|
70
|
+
maxCostUsd: number | null;
|
|
71
|
+
/** How many times a `flaky` diagnosis may defer before it is treated as reproducible. */
|
|
72
|
+
maxFlakyDeferrals: number;
|
|
73
|
+
/**
|
|
74
|
+
* Diagnosis classes another attempt may help with; every other class escalates
|
|
75
|
+
* immediately. Members must be {@link DIAGNOSIS_CLASSES}.
|
|
76
|
+
*
|
|
77
|
+
* Typed `string[]` rather than `DiagnosisClass[]` because it is parsed from
|
|
78
|
+
* untrusted YAML and a bad member must NARROW the retry set with a warning
|
|
79
|
+
* rather than fail the boot — but it is validated against the enum on both
|
|
80
|
+
* paths now. It was not: a typo (`reproducable`) silently made every
|
|
81
|
+
* diagnosis escalate `not-retryable` on the second dispatch, with nothing
|
|
82
|
+
* said anywhere (#256).
|
|
83
|
+
*/
|
|
84
|
+
retryableClasses: string[];
|
|
85
|
+
}
|
|
86
|
+
/** The shipped `fix:` block. Mirrors `fix:` in `apps/server/config/default.yaml`. */
|
|
87
|
+
export declare function defaultFixConfig(): FixConfig;
|
|
88
|
+
/** How much blast radius a major dependency bump carries, ascending. */
|
|
89
|
+
export declare const DEPENDENCY_IMPACT_LEVELS: readonly ["none", "low", "medium", "high"];
|
|
90
|
+
export type DependencyImpact = (typeof DEPENDENCY_IMPACT_LEVELS)[number];
|
|
91
|
+
/** True when `value` is one of {@link DEPENDENCY_IMPACT_LEVELS}. */
|
|
92
|
+
export declare function isDependencyImpact(value: unknown): value is DependencyImpact;
|
|
93
|
+
/** Position of an impact tier on the `none < low < medium < high` scale. */
|
|
94
|
+
export declare function dependencyImpactRank(impact: DependencyImpact): number;
|
|
95
|
+
/**
|
|
96
|
+
* Policy for merging dependency PRs — specifically, how far up the impact scale
|
|
97
|
+
* a MAJOR bump may be auto-merged instead of escalated to a human.
|
|
98
|
+
*
|
|
99
|
+
* Repo-settable subset: `autoMergeMaxImpact` (clamped to the lower tier), and
|
|
100
|
+
* `requireSettledChecks` + `auditComment` (both add-only `true`).
|
|
101
|
+
* `minSettledChecks` is **operator-only**: the §6.2 `max(repo, operator)` clamp
|
|
102
|
+
* would weld the escape hatch shut for a repo with no CI at all (09 locked
|
|
103
|
+
* decision 18).
|
|
104
|
+
*/
|
|
105
|
+
export interface DependenciesConfig {
|
|
106
|
+
/** Ceiling for auto-merging a MAJOR bump. `none` = never auto-merge a major. */
|
|
107
|
+
autoMergeMaxImpact: DependencyImpact;
|
|
108
|
+
/** Enforce settled-"passing" checks on ALL routes (webhook, cron, comment). */
|
|
109
|
+
requireSettledChecks: boolean;
|
|
110
|
+
/** An auto-merge decision needs >= N settled checks; `0` = today's behaviour. */
|
|
111
|
+
minSettledChecks: number;
|
|
112
|
+
/** Post the evidence comment when auto-merging a major. */
|
|
113
|
+
auditComment: boolean;
|
|
114
|
+
}
|
|
115
|
+
/** The shipped `dependencies:` block. Mirrors `dependencies:` in `config/default.yaml`. */
|
|
116
|
+
export declare function defaultDependenciesConfig(): DependenciesConfig;
|
|
117
|
+
/**
|
|
118
|
+
* When a `pr-review` run is triggered.
|
|
119
|
+
*
|
|
120
|
+
* - `eager` — dispatch on `pr.opened` / `synchronize` / `reopened`, in parallel
|
|
121
|
+
* with CI (the historical behaviour).
|
|
122
|
+
* - `after-checks` — dispatch once the head SHA's checks SETTLE, either colour.
|
|
123
|
+
* - `on-request` — never automatically; only when explicitly asked for.
|
|
124
|
+
*
|
|
125
|
+
* (`review.afterChecks` — the settled/passing sub-mode — was deleted by 09
|
|
126
|
+
* locked decision 14: a PR whose CI never goes green would never be reviewed.)
|
|
127
|
+
*/
|
|
128
|
+
export declare const REVIEW_TRIGGERS: readonly ["eager", "after-checks", "on-request"];
|
|
129
|
+
export type ReviewTrigger = (typeof REVIEW_TRIGGERS)[number];
|
|
130
|
+
/** True when `value` is one of {@link REVIEW_TRIGGERS}. */
|
|
131
|
+
export declare function isReviewTrigger(value: unknown): value is ReviewTrigger;
|
|
132
|
+
/** Position of a trigger mode on the automation scale. */
|
|
133
|
+
export declare function reviewTriggerRank(trigger: ReviewTrigger): number;
|
|
134
|
+
/**
|
|
135
|
+
* The `review:` block. Every key is repo-settable, and every one is CLAMPED
|
|
136
|
+
* towards less automation: `postsCheck` and `skipDraft` are add-only `true` (a
|
|
137
|
+
* repo may ask for the check and may skip drafts; it may not suppress an
|
|
138
|
+
* operator's check or force reviews onto drafts), `trigger` takes the lower
|
|
139
|
+
* {@link reviewTriggerRank} of repo and operator, and `requestLabel` is free —
|
|
140
|
+
* naming a label only ever adds an explicit, human-initiated route.
|
|
141
|
+
*/
|
|
142
|
+
export interface ReviewConfig {
|
|
143
|
+
/** Post the `last-light/review` Check Run. */
|
|
144
|
+
postsCheck: boolean;
|
|
145
|
+
/** Which trigger mode this deployment/repo uses. */
|
|
146
|
+
trigger: ReviewTrigger;
|
|
147
|
+
/** Label that requests a review in `on-request` mode. `null` = no label route. */
|
|
148
|
+
requestLabel: string | null;
|
|
149
|
+
/** Skip draft PRs (matching what the review cron has always done). */
|
|
150
|
+
skipDraft: boolean;
|
|
151
|
+
}
|
|
152
|
+
/** The shipped `review:` block. Mirrors `review:` in `config/default.yaml`. */
|
|
153
|
+
export declare function defaultReviewConfig(): ReviewConfig;
|
package/dist/config-types.js
CHANGED
|
@@ -3,6 +3,123 @@
|
|
|
3
3
|
* `config/config.ts` so `shared` never depends back on core (locked decision
|
|
4
4
|
* 11). Core re-exports these from `lastlight-shared` so its own
|
|
5
5
|
* `config/config.js` import surface is unchanged.
|
|
6
|
+
*
|
|
7
|
+
* The `fix:` / `dependencies:` / `review:` policy blocks below live here for the
|
|
8
|
+
* same reason plus one more: they are **repo-settable** (issues #251/#252), so
|
|
9
|
+
* `repo-config-schema.ts` — which bounds a repo's `.lastlight/` and is compiled
|
|
10
|
+
* into the CLI as well as core — has to name their shape and their shipped
|
|
11
|
+
* defaults. Core's normaliser and the repo-layer sanitizer therefore agree by
|
|
12
|
+
* construction rather than by two hand-maintained copies.
|
|
6
13
|
*/
|
|
7
|
-
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// fix: — the PR_FIX_SHAPED retry policy (issue #251)
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
/**
|
|
18
|
+
* The five classes a `diagnose` phase may return (09 → S1,
|
|
19
|
+
* `apps/server/skills/fixing/SKILL.md`), which is also the vocabulary
|
|
20
|
+
* {@link FixConfig.retryableClasses} names members of.
|
|
21
|
+
*
|
|
22
|
+
* It lives here rather than beside the marker parser in core because BOTH
|
|
23
|
+
* validators of that leaf need it: core's boot normaliser, and the repo-layer
|
|
24
|
+
* clamp in `./repo-config-schema.ts` — which is compiled into the CLI and may
|
|
25
|
+
* never reach core. `apps/server/src/engine/fix-markers.ts` re-exports it, so
|
|
26
|
+
* every reader of the marker grammar still finds it where it expects to.
|
|
27
|
+
*/
|
|
28
|
+
export const DIAGNOSIS_CLASSES = [
|
|
29
|
+
"reproducible",
|
|
30
|
+
"env-mismatch",
|
|
31
|
+
"flaky",
|
|
32
|
+
"infra-dependent",
|
|
33
|
+
"upstream-broken",
|
|
34
|
+
];
|
|
35
|
+
/** True when `value` is one of {@link DIAGNOSIS_CLASSES}. */
|
|
36
|
+
export function isDiagnosisClass(value) {
|
|
37
|
+
return typeof value === "string" && DIAGNOSIS_CLASSES.includes(value);
|
|
38
|
+
}
|
|
39
|
+
/** The shipped `fix:` block. Mirrors `fix:` in `apps/server/config/default.yaml`. */
|
|
40
|
+
export function defaultFixConfig() {
|
|
41
|
+
return {
|
|
42
|
+
maxAttempts: 3,
|
|
43
|
+
localIterations: 2,
|
|
44
|
+
gateTimeoutSeconds: 900,
|
|
45
|
+
escalateModelAfterAttempt: 1,
|
|
46
|
+
maxCostUsd: 5.0,
|
|
47
|
+
maxFlakyDeferrals: 2,
|
|
48
|
+
retryableClasses: ["reproducible", "env-mismatch"],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// dependencies: — major-bump auto-merge policy (issue #252)
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
/** How much blast radius a major dependency bump carries, ascending. */
|
|
55
|
+
export const DEPENDENCY_IMPACT_LEVELS = ["none", "low", "medium", "high"];
|
|
56
|
+
/** True when `value` is one of {@link DEPENDENCY_IMPACT_LEVELS}. */
|
|
57
|
+
export function isDependencyImpact(value) {
|
|
58
|
+
return typeof value === "string" && DEPENDENCY_IMPACT_LEVELS.includes(value);
|
|
59
|
+
}
|
|
60
|
+
/** Position of an impact tier on the `none < low < medium < high` scale. */
|
|
61
|
+
export function dependencyImpactRank(impact) {
|
|
62
|
+
return DEPENDENCY_IMPACT_LEVELS.indexOf(impact);
|
|
63
|
+
}
|
|
64
|
+
/** The shipped `dependencies:` block. Mirrors `dependencies:` in `config/default.yaml`. */
|
|
65
|
+
export function defaultDependenciesConfig() {
|
|
66
|
+
return {
|
|
67
|
+
autoMergeMaxImpact: "medium",
|
|
68
|
+
requireSettledChecks: true,
|
|
69
|
+
minSettledChecks: 1,
|
|
70
|
+
auditComment: true,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// review: — when `pr-review` runs
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
/**
|
|
77
|
+
* When a `pr-review` run is triggered.
|
|
78
|
+
*
|
|
79
|
+
* - `eager` — dispatch on `pr.opened` / `synchronize` / `reopened`, in parallel
|
|
80
|
+
* with CI (the historical behaviour).
|
|
81
|
+
* - `after-checks` — dispatch once the head SHA's checks SETTLE, either colour.
|
|
82
|
+
* - `on-request` — never automatically; only when explicitly asked for.
|
|
83
|
+
*
|
|
84
|
+
* (`review.afterChecks` — the settled/passing sub-mode — was deleted by 09
|
|
85
|
+
* locked decision 14: a PR whose CI never goes green would never be reviewed.)
|
|
86
|
+
*/
|
|
87
|
+
export const REVIEW_TRIGGERS = ["eager", "after-checks", "on-request"];
|
|
88
|
+
/** True when `value` is one of {@link REVIEW_TRIGGERS}. */
|
|
89
|
+
export function isReviewTrigger(value) {
|
|
90
|
+
return typeof value === "string" && REVIEW_TRIGGERS.includes(value);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* How much automation a trigger mode buys, ascending — the scale the repo-layer
|
|
94
|
+
* clamp takes the minimum on.
|
|
95
|
+
*
|
|
96
|
+
* Not derived from {@link REVIEW_TRIGGERS}' index, which runs the other way: the
|
|
97
|
+
* list is ordered most-automatic first for readability, and silently inverting
|
|
98
|
+
* it is exactly the kind of coupling that breaks when someone reorders the
|
|
99
|
+
* literal. Stated explicitly instead.
|
|
100
|
+
*
|
|
101
|
+
* `eager` runs a full agent review on every push; `after-checks` runs one per
|
|
102
|
+
* settled head; `on-request` runs none unless asked. So a repo that commits
|
|
103
|
+
* `eager` against an `on-request` deployment is buying itself an agent run per
|
|
104
|
+
* push at the operator's expense (#256) — the same direction `fix.maxAttempts`
|
|
105
|
+
* is clamped in, and the same clamp applies.
|
|
106
|
+
*/
|
|
107
|
+
const REVIEW_TRIGGER_AUTOMATION = {
|
|
108
|
+
"on-request": 0,
|
|
109
|
+
"after-checks": 1,
|
|
110
|
+
eager: 2,
|
|
111
|
+
};
|
|
112
|
+
/** Position of a trigger mode on the automation scale. */
|
|
113
|
+
export function reviewTriggerRank(trigger) {
|
|
114
|
+
return REVIEW_TRIGGER_AUTOMATION[trigger];
|
|
115
|
+
}
|
|
116
|
+
/** The shipped `review:` block. Mirrors `review:` in `config/default.yaml`. */
|
|
117
|
+
export function defaultReviewConfig() {
|
|
118
|
+
return {
|
|
119
|
+
postsCheck: false,
|
|
120
|
+
trigger: "after-checks",
|
|
121
|
+
requestLabel: null,
|
|
122
|
+
skipDraft: true,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
8
125
|
//# sourceMappingURL=config-types.js.map
|
package/dist/config-types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config-types.js","sourceRoot":"","sources":["../src/config-types.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"config-types.js","sourceRoot":"","sources":["../src/config-types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAeH,8EAA8E;AAC9E,qDAAqD;AACrD,8EAA8E;AAE9E;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAC/B,cAAc;IACd,cAAc;IACd,OAAO;IACP,iBAAiB;IACjB,iBAAiB;CACT,CAAC;AAIX,6DAA6D;AAC7D,MAAM,UAAU,gBAAgB,CAAC,KAAc;IAC7C,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAK,iBAAuC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AAC/F,CAAC;AAiDD,qFAAqF;AACrF,MAAM,UAAU,gBAAgB;IAC9B,OAAO;QACL,WAAW,EAAE,CAAC;QACd,eAAe,EAAE,CAAC;QAClB,kBAAkB,EAAE,GAAG;QACvB,yBAAyB,EAAE,CAAC;QAC5B,UAAU,EAAE,GAAG;QACf,iBAAiB,EAAE,CAAC;QACpB,gBAAgB,EAAE,CAAC,cAAc,EAAE,cAAc,CAAC;KACnD,CAAC;AACJ,CAAC;AAED,8EAA8E;AAC9E,4DAA4D;AAC5D,8EAA8E;AAE9E,wEAAwE;AACxE,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,CAAU,CAAC;AAInF,oEAAoE;AACpE,MAAM,UAAU,kBAAkB,CAAC,KAAc;IAC/C,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAK,wBAA8C,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AACtG,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,oBAAoB,CAAC,MAAwB;IAC3D,OAAO,wBAAwB,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;AAClD,CAAC;AAuBD,2FAA2F;AAC3F,MAAM,UAAU,yBAAyB;IACvC,OAAO;QACL,kBAAkB,EAAE,QAAQ;QAC5B,oBAAoB,EAAE,IAAI;QAC1B,gBAAgB,EAAE,CAAC;QACnB,YAAY,EAAE,IAAI;KACnB,CAAC;AACJ,CAAC;AAED,8EAA8E;AAC9E,kCAAkC;AAClC,8EAA8E;AAE9E;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,OAAO,EAAE,cAAc,EAAE,YAAY,CAAU,CAAC;AAIhF,2DAA2D;AAC3D,MAAM,UAAU,eAAe,CAAC,KAAc;IAC5C,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAK,eAAqC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AAC7F,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,yBAAyB,GAAkC;IAC/D,YAAY,EAAE,CAAC;IACf,cAAc,EAAE,CAAC;IACjB,KAAK,EAAE,CAAC;CACT,CAAC;AAEF,0DAA0D;AAC1D,MAAM,UAAU,iBAAiB,CAAC,OAAsB;IACtD,OAAO,yBAAyB,CAAC,OAAO,CAAC,CAAC;AAC5C,CAAC;AAqBD,+EAA+E;AAC/E,MAAM,UAAU,mBAAmB;IACjC,OAAO;QACL,UAAU,EAAE,KAAK;QACjB,OAAO,EAAE,cAAc;QACvB,YAAY,EAAE,IAAI;QAClB,SAAS,EAAE,IAAI;KAChB,CAAC;AACJ,CAAC"}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,cAAc,gBAAgB,CAAC;AAC/B,cAAc,YAAY,CAAC;AAC3B,cAAc,wBAAwB,CAAC;AACvC,cAAc,qBAAqB,CAAC;AACpC,cAAc,eAAe,CAAC;AAC9B,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,cAAc,gBAAgB,CAAC;AAC/B,cAAc,YAAY,CAAC;AAC3B,cAAc,wBAAwB,CAAC;AACvC,cAAc,qBAAqB,CAAC;AACpC,cAAc,eAAe,CAAC;AAC9B,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,yBAAyB,CAAC"}
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The PURE half of the per-repository config layer (issue #180) — the schema,
|
|
3
|
+
* the operator bounds, and the validators/merger that enforce them.
|
|
4
|
+
*
|
|
5
|
+
* A managed repo may commit a `.lastlight/` directory that overrides a BOUNDED
|
|
6
|
+
* subset of Last Light's config for runs against that repo. The directory
|
|
7
|
+
* mirrors a deployment overlay's on-disk shape exactly:
|
|
8
|
+
*
|
|
9
|
+
* .lastlight/
|
|
10
|
+
* lastlight.yml # the config override
|
|
11
|
+
* workflows/prompts/*.md # prompt overrides
|
|
12
|
+
* skills/<name>/SKILL.md # skill overrides
|
|
13
|
+
* agent-context/*.md # persona/rules additions
|
|
14
|
+
*
|
|
15
|
+
* ── Why this lives in `lastlight-shared` ──────────────────────────────────
|
|
16
|
+
* Two consumers need exactly the same answers about a `.lastlight/` tree:
|
|
17
|
+
* - `lastlight-core` at runtime, after fetching the layer from GitHub
|
|
18
|
+
* (`apps/server/src/config/repo-config.ts`, which owns the impure half —
|
|
19
|
+
* the fetch, the TTL cache, the on-disk unpack — and re-exports everything
|
|
20
|
+
* here so its import surface is unchanged); and
|
|
21
|
+
* - the `lastlight` CLI, offline, inside a user's own code repo
|
|
22
|
+
* (`lastlight repo config validate`).
|
|
23
|
+
* The CLI must never gain a dependency edge to core, so the bounds logic sits
|
|
24
|
+
* here — the one package both already depend on. Nothing in this file touches
|
|
25
|
+
* the filesystem, the network, or runtime config: it is a function of its
|
|
26
|
+
* arguments, which is also what makes it directly unit-testable.
|
|
27
|
+
*
|
|
28
|
+
* ── The trust rule ────────────────────────────────────────────────────────
|
|
29
|
+
* The layer is ALWAYS read from the repo's **default branch**. Never a PR head.
|
|
30
|
+
* Never the sandbox checkout. That rule is enforced by the fetcher in core;
|
|
31
|
+
* this module only describes what may appear in the layer once it arrives.
|
|
32
|
+
*
|
|
33
|
+
* ── The failure rule ──────────────────────────────────────────────────────
|
|
34
|
+
* Warn, drop the bad bits, run anyway. A repo's config file must never fail a
|
|
35
|
+
* run. Invalid YAML drops the whole file; an unknown or out-of-bounds key drops
|
|
36
|
+
* just that key. Every rejection becomes a structured {@link RepoConfigWarning}
|
|
37
|
+
* so the dashboard/CLI can report it back to the repo's owners.
|
|
38
|
+
*/
|
|
39
|
+
import { type DependenciesConfig, type DisabledConfig, type FixConfig, type ReviewConfig } from "./config-types.js";
|
|
40
|
+
/** Hard cap on the unpacked `.lastlight/` layer, in bytes. */
|
|
41
|
+
export declare const REPO_CONFIG_MAX_BYTES: number;
|
|
42
|
+
/** Hard cap on the number of files in the unpacked layer. */
|
|
43
|
+
export declare const REPO_CONFIG_MAX_FILES = 200;
|
|
44
|
+
/** The config file inside `.lastlight/`. Exactly this name — no `.yaml` variant. */
|
|
45
|
+
export declare const REPO_CONFIG_FILE = "lastlight.yml";
|
|
46
|
+
/**
|
|
47
|
+
* The operator's bounds on the per-repo config layer (issue #180). A repo may
|
|
48
|
+
* narrow its own behaviour within these bounds; it can never widen them, and
|
|
49
|
+
* violating them drops the offending key with a warning rather than failing
|
|
50
|
+
* the run.
|
|
51
|
+
*
|
|
52
|
+
* Trust note: the layer this policy bounds is always fetched from the repo's
|
|
53
|
+
* DEFAULT BRANCH. A PR head can't reach it, so a PR can't reconfigure the agent
|
|
54
|
+
* that reviews it. That rule lives in `apps/server/src/config/repo-config.ts`;
|
|
55
|
+
* this type only describes the bounds.
|
|
56
|
+
*/
|
|
57
|
+
export interface RepoConfigPolicy {
|
|
58
|
+
/** Master switch. `false` ignores every repo's `.lastlight/` entirely (no fetch). */
|
|
59
|
+
enabled: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Config keys a repo may set, as dotted paths (`models`, `disabled.workflows`,
|
|
62
|
+
* …). A repo leaf is kept when some entry is that leaf's path or a prefix of
|
|
63
|
+
* it — so `models` admits `models.architect`, while `disabled.workflows` does
|
|
64
|
+
* NOT admit `disabled.prompts`. Everything else is dropped with a warning.
|
|
65
|
+
*/
|
|
66
|
+
allowKeys: string[];
|
|
67
|
+
/**
|
|
68
|
+
* Model specs a repo may select. `null` (the default) means "any model whose
|
|
69
|
+
* `provider/` prefix is a provider Last Light knows how to wire" — the repo
|
|
70
|
+
* still can't invent a provider. A list restricts to exactly those specs.
|
|
71
|
+
*/
|
|
72
|
+
allowedModels: string[] | null;
|
|
73
|
+
/**
|
|
74
|
+
* Whether the repo's asset overrides (`workflows/prompts/*.md`,
|
|
75
|
+
* `skills/<name>/SKILL.md`, `agent-context/*.md`) are unpacked and used.
|
|
76
|
+
* `false` keeps `lastlight.yml` only.
|
|
77
|
+
*/
|
|
78
|
+
allowAssets: boolean;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The allow-list a deployment gets when it says nothing. Kept as a constant so
|
|
82
|
+
* the normaliser, the docs, `config/default.yaml` and the CLI's offline
|
|
83
|
+
* validator can't drift apart.
|
|
84
|
+
*
|
|
85
|
+
* It MUST stay identical to `repoConfig.allowKeys` in
|
|
86
|
+
* `apps/server/config/default.yaml`: this list is what a deployment falls back
|
|
87
|
+
* to when config isn't in reach (`repoConfigPolicy()`'s no-config path, and the
|
|
88
|
+
* CLI's offline `lastlight repo config validate`), so a divergence tells repo
|
|
89
|
+
* owners their file is out of bounds when it isn't. Pinned by the
|
|
90
|
+
* `default allow-list` block in `apps/server/tests/config/repo-config-shared.test.ts`
|
|
91
|
+
* — the two drifted apart once already, silently.
|
|
92
|
+
*/
|
|
93
|
+
export declare const DEFAULT_REPO_CONFIG_ALLOW_KEYS: readonly string[];
|
|
94
|
+
/**
|
|
95
|
+
* The bounds to assume when no deployment config is in reach — the shipped
|
|
96
|
+
* defaults. The offline CLI validator (`lastlight repo config validate`) uses
|
|
97
|
+
* this: it can't know the operator's narrowing, so it validates against the
|
|
98
|
+
* widest shipped policy and says so.
|
|
99
|
+
*/
|
|
100
|
+
export declare function defaultRepoConfigPolicy(): RepoConfigPolicy;
|
|
101
|
+
/** Which config layer supplied a resolved leaf. Mirrors core's `ConfigSource`. */
|
|
102
|
+
export type ConfigSource = "default" | "overlay" | "env" | "repo";
|
|
103
|
+
/** Why a piece of a repo's `.lastlight/` was dropped. */
|
|
104
|
+
export type RepoConfigWarningCode =
|
|
105
|
+
/** `lastlight.yml` did not parse as YAML — the whole file is ignored. */
|
|
106
|
+
"invalid-yaml"
|
|
107
|
+
/** `lastlight.yml` parsed to something that isn't a mapping. */
|
|
108
|
+
| "not-a-mapping"
|
|
109
|
+
/** The key isn't in the operator's `repoConfig.allowKeys`. */
|
|
110
|
+
| "key-not-allowed"
|
|
111
|
+
/** The key is allowed but the value has the wrong type/shape. */
|
|
112
|
+
| "invalid-value"
|
|
113
|
+
/** A model spec outside `repoConfig.allowedModels`. */
|
|
114
|
+
| "model-not-allowed"
|
|
115
|
+
/** A model spec whose `provider/` prefix isn't a provider we can wire. */
|
|
116
|
+
| "unknown-provider"
|
|
117
|
+
/** An `approval` entry that would clear a gate — the layer is add-only. */
|
|
118
|
+
| "approval-downgrade"
|
|
119
|
+
/**
|
|
120
|
+
* A `fix` / `dependencies` / `review` entry that would make the repo LESS
|
|
121
|
+
* conservative than the operator (issues #251/#252). Clamped to the operator's
|
|
122
|
+
* value — the repo keeps running, it just doesn't get the looser setting.
|
|
123
|
+
*/
|
|
124
|
+
| "policy-downgrade"
|
|
125
|
+
/** A file path that escapes the layer root. */
|
|
126
|
+
| "path-escape"
|
|
127
|
+
/** A symlink (or other non-regular blob) in the layer. */
|
|
128
|
+
| "symlink"
|
|
129
|
+
/** The layer exceeded {@link REPO_CONFIG_MAX_BYTES}. */
|
|
130
|
+
| "size-cap"
|
|
131
|
+
/** The layer exceeded {@link REPO_CONFIG_MAX_FILES}. */
|
|
132
|
+
| "file-count-cap"
|
|
133
|
+
/** A workflow YAML under `workflows/` — repos may contribute prompts, not workflows. */
|
|
134
|
+
| "workflow-not-allowed"
|
|
135
|
+
/** Asset files present but `repoConfig.allowAssets` is false. */
|
|
136
|
+
| "assets-not-allowed"
|
|
137
|
+
/** Files in a layer directory that don't match its expected shape. */
|
|
138
|
+
| "unrecognised-asset"
|
|
139
|
+
/** The GitHub fetch failed; the previous cached layer (if any) still stands. */
|
|
140
|
+
| "fetch-failed";
|
|
141
|
+
/**
|
|
142
|
+
* One structured, reportable rejection. Deliberately a plain data object (not
|
|
143
|
+
* an Error): these are collected, persisted in the cache sidecar and rendered
|
|
144
|
+
* by the dashboard/CLI, never thrown.
|
|
145
|
+
*/
|
|
146
|
+
export interface RepoConfigWarning {
|
|
147
|
+
code: RepoConfigWarningCode;
|
|
148
|
+
/** `owner/repo` this warning belongs to, when known. */
|
|
149
|
+
repo?: string;
|
|
150
|
+
/** The config path (`models.architect`) or file path (`workflows/x.yaml`) at fault. */
|
|
151
|
+
path: string;
|
|
152
|
+
/** One-line human-readable explanation, safe to post back to the repo. */
|
|
153
|
+
message: string;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* One blob of a `.lastlight/` tree, as handed to {@link sanitizeRepoFiles}.
|
|
157
|
+
* Structurally the same shape core's GitHub client produces (`RepoConfigFile`
|
|
158
|
+
* in `apps/server/src/engine/github/github.ts`) and the CLI reads off disk —
|
|
159
|
+
* declared here so the bounds logic needs no GitHub types.
|
|
160
|
+
*/
|
|
161
|
+
export interface RepoLayerFile {
|
|
162
|
+
/** Path relative to `.lastlight/`. */
|
|
163
|
+
path: string;
|
|
164
|
+
/** Git filemode (`100644`, `100755`, `120000`, …). */
|
|
165
|
+
mode: string;
|
|
166
|
+
size: number;
|
|
167
|
+
content: Buffer;
|
|
168
|
+
}
|
|
169
|
+
/** A repo's fetched-and-unpacked `.lastlight/` layer. */
|
|
170
|
+
export interface RepoLayer {
|
|
171
|
+
/** `owner/repo`. */
|
|
172
|
+
repo: string;
|
|
173
|
+
/** The ref this layer was read from — always the repo's default branch. */
|
|
174
|
+
defaultBranch: string;
|
|
175
|
+
/** Git tree SHA of `.lastlight/` — the content identity used for conditional refetch. */
|
|
176
|
+
treeSha: string;
|
|
177
|
+
/** ETag of the default branch's root tree, for the cheap 304 path. */
|
|
178
|
+
etag?: string;
|
|
179
|
+
/** ISO timestamp of the last successful download (not of the last check). */
|
|
180
|
+
fetchedAt: string;
|
|
181
|
+
/**
|
|
182
|
+
* Absolute path of the unpacked tree. Mirrors an overlay root
|
|
183
|
+
* (`workflows/`, `skills/`, `agent-context/`), so it can be handed to the
|
|
184
|
+
* layer-aware asset loader directly.
|
|
185
|
+
*/
|
|
186
|
+
root: string;
|
|
187
|
+
/** Parsed `lastlight.yml` — raw and UNVALIDATED; bounds are applied at resolve time. */
|
|
188
|
+
config?: Record<string, unknown>;
|
|
189
|
+
/** Accepted asset paths relative to {@link root}. Empty when `allowAssets` is false. */
|
|
190
|
+
assets: string[];
|
|
191
|
+
/** Everything dropped while fetching/unpacking this layer. */
|
|
192
|
+
warnings: RepoConfigWarning[];
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* The boot-resolved config the repo layer is applied on top of: the merged
|
|
196
|
+
* (default→overlay→env) values plus the matching provenance tree from
|
|
197
|
+
* `resolveConfigLayers`. Core builds one with `repoConfigBaseFromRuntime`.
|
|
198
|
+
*/
|
|
199
|
+
export interface RepoConfigBase {
|
|
200
|
+
value: Record<string, unknown>;
|
|
201
|
+
sources: Record<string, unknown>;
|
|
202
|
+
}
|
|
203
|
+
/** The effective, repo-specific values for the keys a repo is allowed to touch. */
|
|
204
|
+
export interface RepoMergedConfig {
|
|
205
|
+
models: Record<string, string>;
|
|
206
|
+
variants: Record<string, string>;
|
|
207
|
+
/**
|
|
208
|
+
* Full disabled shape. Only `workflows` and `crons` are repo-settable by
|
|
209
|
+
* default; the rest always come from the operator's layers.
|
|
210
|
+
*/
|
|
211
|
+
disabled: DisabledConfig;
|
|
212
|
+
approval: Record<string, boolean>;
|
|
213
|
+
/**
|
|
214
|
+
* Retry policy for the PR_FIX_SHAPED workflows (issue #251) and major-bump
|
|
215
|
+
* auto-merge policy (issue #252), each already clamped so the repo is never
|
|
216
|
+
* looser than the operator. Always present: a base that carries no `fix:` /
|
|
217
|
+
* `dependencies:` / `review:` node (an older boot config, or the CLI's offline
|
|
218
|
+
* validator) falls back leaf-by-leaf to the shipped defaults, so consumers
|
|
219
|
+
* never have to reason about a partially-populated block.
|
|
220
|
+
*/
|
|
221
|
+
fix: FixConfig;
|
|
222
|
+
dependencies: DependenciesConfig;
|
|
223
|
+
review: ReviewConfig;
|
|
224
|
+
}
|
|
225
|
+
/** Provenance mirror of {@link RepoMergedConfig} — each leaf tagged with its winning layer. */
|
|
226
|
+
export interface RepoConfigSources {
|
|
227
|
+
models: Record<string, ConfigSource>;
|
|
228
|
+
variants: Record<string, ConfigSource>;
|
|
229
|
+
disabled: Record<keyof DisabledConfig, ConfigSource>;
|
|
230
|
+
approval: Record<string, ConfigSource>;
|
|
231
|
+
fix: Record<string, ConfigSource>;
|
|
232
|
+
dependencies: Record<string, ConfigSource>;
|
|
233
|
+
review: Record<string, ConfigSource>;
|
|
234
|
+
}
|
|
235
|
+
/** Result of {@link resolveRepoConfig}. */
|
|
236
|
+
export interface ResolvedRepoConfig {
|
|
237
|
+
merged: RepoMergedConfig;
|
|
238
|
+
sources: RepoConfigSources;
|
|
239
|
+
/** Fetch/unpack warnings from the layer PLUS everything this resolve dropped. */
|
|
240
|
+
warnings: RepoConfigWarning[];
|
|
241
|
+
}
|
|
242
|
+
/** What role a path inside `.lastlight/` plays in the repo layer. */
|
|
243
|
+
export type RepoLayerPathKind = "config" | "prompt" | "skill" | "agent-context";
|
|
244
|
+
/**
|
|
245
|
+
* Classify a path relative to `.lastlight/`, or `null` when it is not part of
|
|
246
|
+
* the layer at all.
|
|
247
|
+
*
|
|
248
|
+
* `null` is a routine answer, not an error: `.lastlight/` is shared real
|
|
249
|
+
* estate. With `buildAssets.location: repo` the build workflow commits its
|
|
250
|
+
* handoff docs to `.lastlight/<issueKey>/*.md`, and repos are free to keep
|
|
251
|
+
* other things there. Those are simply outside the layer — the warning path is
|
|
252
|
+
* reserved for files that LOOK like layer assets but have the wrong shape
|
|
253
|
+
* (see {@link sanitizeRepoFiles}).
|
|
254
|
+
*/
|
|
255
|
+
export declare function repoLayerPathKind(path: string): RepoLayerPathKind | null;
|
|
256
|
+
/** True when a path is a workflow DEFINITION — the one thing a repo may never contribute. */
|
|
257
|
+
export declare function isRepoWorkflowPath(path: string): boolean;
|
|
258
|
+
/**
|
|
259
|
+
* Apply every file-level bound to a `.lastlight/` subtree.
|
|
260
|
+
*
|
|
261
|
+
* Pure — takes the blobs, returns the ones that may be written to disk plus a
|
|
262
|
+
* warning per rejection. The caps are enforced here as well as at fetch time
|
|
263
|
+
* (the client stops downloading at its own limits) because this function is the
|
|
264
|
+
* last gate before anything touches the filesystem, and defence in depth is
|
|
265
|
+
* cheap.
|
|
266
|
+
*
|
|
267
|
+
* Generic in the file type so a caller carrying a richer blob shape (core's
|
|
268
|
+
* `RepoConfigFile`) gets its own type back rather than a widened one.
|
|
269
|
+
*/
|
|
270
|
+
export declare function sanitizeRepoFiles<T extends RepoLayerFile>(files: readonly T[], policy: RepoConfigPolicy, repo?: string): {
|
|
271
|
+
accepted: T[];
|
|
272
|
+
warnings: RepoConfigWarning[];
|
|
273
|
+
};
|
|
274
|
+
/**
|
|
275
|
+
* Parse a repo's `lastlight.yml`. Malformed YAML, or YAML that isn't a mapping,
|
|
276
|
+
* drops the WHOLE file — a half-understood config file is more dangerous than
|
|
277
|
+
* none, and the repo gets a warning either way.
|
|
278
|
+
*/
|
|
279
|
+
export declare function parseRepoConfigYaml(raw: string, repo?: string): {
|
|
280
|
+
config?: Record<string, unknown>;
|
|
281
|
+
warnings: RepoConfigWarning[];
|
|
282
|
+
};
|
|
283
|
+
/**
|
|
284
|
+
* Reduce a repo's raw `lastlight.yml` to the sub-tree it is actually allowed to
|
|
285
|
+
* contribute. Pure. Everything dropped produces a warning.
|
|
286
|
+
*
|
|
287
|
+
* `base` supplies the operator's current values, which the `approval` add-only
|
|
288
|
+
* rule needs: a repo may raise a gate, never lower one.
|
|
289
|
+
*/
|
|
290
|
+
export declare function sanitizeRepoConfigLayer(raw: Record<string, unknown> | undefined, policy: RepoConfigPolicy, base: RepoConfigBase, repo?: string): {
|
|
291
|
+
layer: Record<string, unknown>;
|
|
292
|
+
warnings: RepoConfigWarning[];
|
|
293
|
+
};
|
|
294
|
+
/**
|
|
295
|
+
* Apply a repo's layer on top of the boot config and report what happened.
|
|
296
|
+
*
|
|
297
|
+
* PURE — no fs, no network, no runtime-config reads. Plain objects deep-merge
|
|
298
|
+
* key-by-key while arrays and scalars replace wholesale — byte-for-byte the
|
|
299
|
+
* semantics of the boot layers (see {@link mergeLayer}), so the repo layer can
|
|
300
|
+
* never acquire semantics the operator's layers don't have.
|
|
301
|
+
*
|
|
302
|
+
* Note on `disabled.*`: those are arrays, so a repo's list REPLACES the
|
|
303
|
+
* operator's rather than adding to it (locked precedence). Operators who don't
|
|
304
|
+
* want that remove `disabled.workflows` / `disabled.crons` from
|
|
305
|
+
* `repoConfig.allowKeys`.
|
|
306
|
+
*
|
|
307
|
+
* Passing `undefined` for `repoLayer` (no `.lastlight/`, fetch failed, feature
|
|
308
|
+
* disabled) returns the base unchanged with no warnings — the inert path.
|
|
309
|
+
*/
|
|
310
|
+
export declare function resolveRepoConfig(base: RepoConfigBase, policy: RepoConfigPolicy, repoLayer?: RepoLayer): ResolvedRepoConfig;
|
|
311
|
+
/**
|
|
312
|
+
* Merge one layer INTO `value`/`sources` in place, tagging every leaf it
|
|
313
|
+
* supplies with `source`.
|
|
314
|
+
*
|
|
315
|
+
* THE single definition of Last Light's config-merge semantics: plain objects
|
|
316
|
+
* deep-merge key-by-key so each leaf resolves (and is attributed) on its own;
|
|
317
|
+
* arrays and scalars replace wholesale. Core's boot-layer resolver
|
|
318
|
+
* (`apps/server/src/config/config-resolve.ts`) re-exports this rather than
|
|
319
|
+
* carrying its own — the repo layer must merge exactly the way default/overlay/
|
|
320
|
+
* env do, or a repo could acquire precedence the operator's own layers don't
|
|
321
|
+
* have, and two implementations is exactly how that drift starts.
|
|
322
|
+
*
|
|
323
|
+
* It lives HERE, in the leaf package, because the direction of the dependency
|
|
324
|
+
* edge only permits it here: `lastlight-shared` may never depend on core.
|
|
325
|
+
*/
|
|
326
|
+
export declare function mergeLayer(value: Record<string, unknown>, sources: Record<string, unknown>, layer: Record<string, unknown>, source: ConfigSource): void;
|
|
327
|
+
/** Narrow an unknown provenance leaf to a {@link ConfigSource}. */
|
|
328
|
+
export declare function isConfigSource(value: unknown): value is ConfigSource;
|