@sanity/ailf 3.4.1 → 3.5.1

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 (35) hide show
  1. package/bin/ailf.js +16 -1
  2. package/config/airbyte/ai_literacy_framework.connector.yaml +114 -0
  3. package/config/bigquery/README.md +44 -8
  4. package/config/bigquery/views/official_area_scores.sql +20 -0
  5. package/config/bigquery/views/official_runs.sql +31 -0
  6. package/config/bigquery/views/reports.sql +19 -0
  7. package/config/bigquery/views/team_runs_template.sql +17 -0
  8. package/dist/_vendor/ailf-core/examples/index.d.ts +1 -1
  9. package/dist/_vendor/ailf-core/examples/index.js +1 -1
  10. package/dist/_vendor/ailf-core/ports/context.d.ts +25 -0
  11. package/dist/_vendor/ailf-core/schemas/pipeline-request.d.ts +23 -0
  12. package/dist/_vendor/ailf-core/schemas/pipeline-request.js +59 -1
  13. package/dist/_vendor/ailf-shared/index.d.ts +2 -0
  14. package/dist/_vendor/ailf-shared/index.js +2 -0
  15. package/dist/_vendor/ailf-shared/owner-teams.d.ts +26 -0
  16. package/dist/_vendor/ailf-shared/owner-teams.js +52 -0
  17. package/dist/_vendor/ailf-shared/run-classification.d.ts +100 -0
  18. package/dist/_vendor/ailf-shared/run-classification.js +28 -0
  19. package/dist/_vendor/ailf-shared/run-context.d.ts +23 -0
  20. package/dist/adapters/api-client/build-request.d.ts +42 -0
  21. package/dist/adapters/api-client/build-request.js +188 -10
  22. package/dist/adapters/api-client/index.d.ts +1 -1
  23. package/dist/adapters/api-client/index.js +1 -1
  24. package/dist/commands/explain-handler.js +5 -0
  25. package/dist/commands/pipeline-action.d.ts +6 -0
  26. package/dist/commands/pipeline-action.js +13 -1
  27. package/dist/commands/pipeline.d.ts +5 -0
  28. package/dist/commands/pipeline.js +16 -2
  29. package/dist/commands/remote-pipeline.js +13 -1
  30. package/dist/orchestration/steps/finalize-run-step.js +1 -0
  31. package/dist/orchestration/steps/publish-report-step.js +1 -0
  32. package/dist/pipeline/map-request-to-config.js +18 -0
  33. package/dist/pipeline/run-context.d.ts +63 -0
  34. package/dist/pipeline/run-context.js +166 -0
  35. package/package.json +1 -1
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Known owner-team slugs and soft-normalization helper.
3
+ *
4
+ * `RunOwner.team` is free-form by design (external teams name themselves
5
+ * and internal names drift). This module provides two things to keep UX
6
+ * polished without blocking new entrants:
7
+ *
8
+ * - `KNOWN_OWNER_TEAMS` — a seed list of canonical slugs that populates
9
+ * Studio filter comboboxes as suggestions. Not a closed enum.
10
+ * - `normalizeOwnerTeam()` — maps a handful of common aliases to
11
+ * canonical slugs. Warn-only: returns the original string when no
12
+ * mapping applies. Adding an alias here is a one-liner.
13
+ *
14
+ * @see docs/decisions/D0037-run-classification-and-ownership-taxonomy.md
15
+ */
16
+ export const KNOWN_OWNER_TEAMS = [
17
+ "content-lake",
18
+ "core-docs",
19
+ "growth",
20
+ "media",
21
+ "platform",
22
+ "studio",
23
+ ];
24
+ /**
25
+ * Lowercase alias → canonical slug. Lightweight — only entries where
26
+ * drift has been observed belong here. Unknown values pass through.
27
+ */
28
+ const OWNER_TEAM_ALIASES = {
29
+ coredocs: "core-docs",
30
+ docs: "core-docs",
31
+ studio_team: "studio",
32
+ "studio-team": "studio",
33
+ };
34
+ /**
35
+ * Normalize a free-form team slug to its canonical form.
36
+ *
37
+ * - Trims and lowercases.
38
+ * - Maps known aliases to canonical slugs.
39
+ * - Passes unknown values through unchanged (warn-only at the UI layer).
40
+ * - Returns `"unknown"` for empty input.
41
+ */
42
+ export function normalizeOwnerTeam(value) {
43
+ if (!value)
44
+ return "unknown";
45
+ const trimmed = value.trim().toLowerCase();
46
+ if (!trimmed)
47
+ return "unknown";
48
+ return OWNER_TEAM_ALIASES[trimmed] ?? trimmed;
49
+ }
50
+ export function isKnownOwnerTeam(value) {
51
+ return KNOWN_OWNER_TEAMS.includes(value);
52
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Run classification, ownership, executor, and environment metadata.
3
+ *
4
+ * These fields extend `RunContext` to capture run *intent*, *attribution*,
5
+ * and *reproducibility* — orthogonal to the *mechanism* captured by
6
+ * `RunTrigger`. A scheduled run can be experimental; a manual run can be
7
+ * official; a PR-triggered run is executed by GH Actions but attributable
8
+ * to the PR author.
9
+ *
10
+ * @see docs/decisions/D0037-run-classification-and-ownership-taxonomy.md
11
+ * @see docs/design-docs/run-classification-and-ownership.md
12
+ */
13
+ /**
14
+ * How a run should be treated for reporting and trend tracking.
15
+ *
16
+ * Orthogonal to `RunTrigger` (mechanism). Defaults to `"ad-hoc"` when
17
+ * unannotated so pre-taxonomy runs never leak into the canonical series.
18
+ */
19
+ export type RunClassification = "official" | "ad-hoc" | "experimental" | "test" | "external";
20
+ export declare const RUN_CLASSIFICATIONS: readonly RunClassification[];
21
+ export declare function isRunClassification(value: unknown): value is RunClassification;
22
+ /**
23
+ * Attribution — which team and (optionally) individual the run *belongs to*.
24
+ *
25
+ * `team` is a free-form slug, not a closed enum: external teams name
26
+ * themselves and internal names drift. A soft-normalization layer under
27
+ * `config/owners.ts` maps aliases to canonical slugs (warn-only).
28
+ */
29
+ export interface RunOwner {
30
+ team: string;
31
+ individual?: string;
32
+ }
33
+ /**
34
+ * Who or what actually invoked the run.
35
+ *
36
+ * Separate from `RunOwner` because they diverge for automated surfaces:
37
+ * a PR gate is *executed by* GH Actions but *attributable to* the PR
38
+ * author. Both variants expose a `name` field so consumers can format
39
+ * them with one template.
40
+ *
41
+ * Every detectable identity field is optional — a misconfigured shell,
42
+ * a container without `git`, or a CI provider that doesn't expose actor
43
+ * metadata can all still produce a valid run with thin provenance.
44
+ */
45
+ export type RunExecutor = RunExecutorUser | RunExecutorSystem;
46
+ export interface RunExecutorUser {
47
+ type: "user";
48
+ /** Detected from `git config user.name`, `os.userInfo().username`, or GH actor. */
49
+ name?: string;
50
+ /** From `git config user.email`. Subject to the `AILF_CAPTURE_EMAIL` opt-out. */
51
+ email?: string;
52
+ /** Where the invocation originated. Always knowable. */
53
+ surface: RunExecutorSurface;
54
+ /** GH actor when the user invoked via a GH surface (PR, manual dispatch). */
55
+ githubActor?: string;
56
+ }
57
+ export interface RunExecutorSystem {
58
+ type: "system";
59
+ /** e.g. `"github-actions"`, `"vercel-cron"`, `"sanity-webhook"`. */
60
+ name: string;
61
+ workflow?: string;
62
+ runId?: string;
63
+ }
64
+ export type RunExecutorSurface = "cli" | "studio" | "api";
65
+ export declare const RUN_EXECUTOR_SURFACES: readonly RunExecutorSurface[];
66
+ /**
67
+ * Links to related runs. Fills the gap where the Studio report schema
68
+ * already carried these fields but `RunContext` did not.
69
+ */
70
+ export interface RunLineage {
71
+ /** Prior `RunId` this run re-executes. */
72
+ rerunOf?: string;
73
+ /** Sibling `RunId` this run is intentionally compared against. */
74
+ comparedAgainst?: string;
75
+ /** API-gateway job ID that dispatched this run. */
76
+ parentJobId?: string;
77
+ }
78
+ /**
79
+ * Reproducibility metadata — which AILF/Node ran the eval.
80
+ *
81
+ * Required on every new run so cross-version trend comparisons can
82
+ * isolate framework changes from doc changes.
83
+ */
84
+ export interface RunTool {
85
+ ailfVersion: string;
86
+ nodeVersion: string;
87
+ }
88
+ /**
89
+ * Platform + CI-provider metadata for debugging flakes. Hostname is
90
+ * intentionally excluded — it leaks machine/user identity without
91
+ * filtering benefit.
92
+ */
93
+ export interface RunHost {
94
+ /** `os.platform()` — `"darwin"` | `"linux"` | `"win32"`. */
95
+ platform: string;
96
+ /** `os.arch()` — `"x64"` | `"arm64"`. */
97
+ arch: string;
98
+ /** CI provider when running under one, e.g. `"github-actions"`. */
99
+ ci?: string;
100
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Run classification, ownership, executor, and environment metadata.
3
+ *
4
+ * These fields extend `RunContext` to capture run *intent*, *attribution*,
5
+ * and *reproducibility* — orthogonal to the *mechanism* captured by
6
+ * `RunTrigger`. A scheduled run can be experimental; a manual run can be
7
+ * official; a PR-triggered run is executed by GH Actions but attributable
8
+ * to the PR author.
9
+ *
10
+ * @see docs/decisions/D0037-run-classification-and-ownership-taxonomy.md
11
+ * @see docs/design-docs/run-classification-and-ownership.md
12
+ */
13
+ export const RUN_CLASSIFICATIONS = [
14
+ "official",
15
+ "ad-hoc",
16
+ "experimental",
17
+ "test",
18
+ "external",
19
+ ];
20
+ export function isRunClassification(value) {
21
+ return (typeof value === "string" &&
22
+ RUN_CLASSIFICATIONS.includes(value));
23
+ }
24
+ export const RUN_EXECUTOR_SURFACES = [
25
+ "cli",
26
+ "studio",
27
+ "api",
28
+ ];
@@ -15,15 +15,26 @@
15
15
  * @see docs/design-docs/run-artifact-store.md (§ Drift Prevention)
16
16
  */
17
17
  import type { EvalMode } from "./eval-modes.js";
18
+ import type { RunClassification, RunExecutor, RunHost, RunLineage, RunOwner, RunTool } from "./run-classification.js";
18
19
  import type { RunTrigger } from "./run-trigger.js";
19
20
  export interface RunContext {
20
21
  /** Which feature areas were evaluated */
21
22
  areas: string[];
23
+ /**
24
+ * How this run should be treated for reporting and trend tracking.
25
+ * Orthogonal to `trigger` (mechanism). Defaults to `"ad-hoc"` when
26
+ * unannotated — only the scheduled workflow mints `"official"`.
27
+ *
28
+ * @see docs/decisions/D0037-run-classification-and-ownership-taxonomy.md
29
+ */
30
+ classification: RunClassification;
22
31
  /**
23
32
  * Evaluation fingerprint — SHA-256 of all inputs that affect eval output.
24
33
  * Used for cross-environment cache lookup (CI → Content Lake).
25
34
  */
26
35
  evalFingerprint?: string;
36
+ /** Who/what actually invoked the run. May or may not match `owner`. */
37
+ executor: RunExecutor;
27
38
  /** Git metadata (when run from CI) */
28
39
  git?: {
29
40
  branch: string;
@@ -33,6 +44,12 @@ export interface RunContext {
33
44
  };
34
45
  /** Grader model used for scoring */
35
46
  graderModel: string;
47
+ /** Platform/CI metadata for debugging flakes. */
48
+ host?: RunHost;
49
+ /** Free-form searchable tags — release IDs, regression hunts, experiments. */
50
+ labels?: string[];
51
+ /** Links to related runs (re-runs, comparison partners, API parent job). */
52
+ lineage?: RunLineage;
36
53
  /** Evaluation mode */
37
54
  mode: EvalMode;
38
55
  /** Models under evaluation */
@@ -40,6 +57,10 @@ export interface RunContext {
40
57
  id: string;
41
58
  label: string;
42
59
  }[];
60
+ /** Which team (and optionally individual) this run is attributable to. */
61
+ owner: RunOwner;
62
+ /** Human-authored "why I ran this" — useful for Content Lake archaeology. */
63
+ purpose?: string;
43
64
  /** Documentation source configuration */
44
65
  source: {
45
66
  baseUrl: string;
@@ -50,6 +71,8 @@ export interface RunContext {
50
71
  };
51
72
  /** Specific task IDs evaluated when scoped to a subset */
52
73
  taskIds?: string[];
74
+ /** Which AILF/Node ran the eval — for cross-version trend compatibility. */
75
+ tool?: RunTool;
53
76
  /** What initiated this run */
54
77
  trigger: RunTrigger;
55
78
  }
@@ -13,6 +13,16 @@
13
13
  * @see packages/eval/src/adapters/task-sources/repo-task-source.ts
14
14
  */
15
15
  import { type PipelineRequest } from "../../_vendor/ailf-core/index.d.ts";
16
+ /**
17
+ * Thrown when `buildRemoteRequest` can't find any runnable tasks.
18
+ *
19
+ * The CLI catches this separately from ZodError so it can print the
20
+ * message without an accompanying stack trace — the message is already
21
+ * the whole story for the user.
22
+ */
23
+ export declare class NoRunnableTasksError extends Error {
24
+ readonly name = "NoRunnableTasksError";
25
+ }
16
26
  /** Options for building a remote pipeline request. */
17
27
  export interface BuildRequestOptions {
18
28
  /** Path to .ailf/tasks/ directory. */
@@ -27,6 +37,7 @@ export interface BuildRequestOptions {
27
37
  */
28
38
  export interface RemoteConfigSlice {
29
39
  mode?: string;
40
+ variant?: string;
30
41
  debug?: {
31
42
  enabled?: boolean;
32
43
  firstN?: number;
@@ -51,6 +62,18 @@ export interface RemoteConfigSlice {
51
62
  readinessEnabled?: boolean;
52
63
  discoveryReportEnabled?: boolean;
53
64
  noRemoteCache?: boolean;
65
+ /**
66
+ * D0037 / W0069 — CLI-flag overrides for the caller envelope. These
67
+ * take precedence over the equivalent env vars when set. When both a
68
+ * flag and its env var are unset the field is omitted from the
69
+ * request (server applies its own defaults).
70
+ */
71
+ classificationOption?: string;
72
+ ownerTeamOption?: string;
73
+ ownerIndividualOption?: string;
74
+ purposeOption?: string;
75
+ /** Repeatable --label values; appended to AILF_LABELS env values. */
76
+ labelOptions?: string[];
54
77
  }
55
78
  /**
56
79
  * Build a PipelineRequest from local tasks and config.
@@ -75,3 +98,22 @@ export declare function buildRemoteRequest(options: BuildRequestOptions): Promis
75
98
  * Returns the resolved path or throws if not found.
76
99
  */
77
100
  export declare function resolveTasksDir(rootDir: string, explicitPath?: string): string;
101
+ /**
102
+ * Build the D0037 caller envelope payload from CLI flags + env vars.
103
+ *
104
+ * Precedence, highest first:
105
+ * 1. Explicit CLI flag (--classification, --owner-team, --purpose, …)
106
+ * 2. Env var (AILF_CLASSIFICATION, AILF_OWNER_TEAM, AILF_PURPOSE, …)
107
+ * 3. Omit — server applies its own defaults (ad-hoc / unknown).
108
+ *
109
+ * Labels are additive: --label values concatenate with AILF_LABELS.
110
+ *
111
+ * `executor` is always set on remote submissions because we know the
112
+ * invocation is a user-driven CLI call. Surface defaults to `"cli"`
113
+ * unless AILF_EXECUTOR_SURFACE explicitly overrides; name falls back to
114
+ * GITHUB_ACTOR when available.
115
+ *
116
+ * Returns partial `PipelineRequest` fields only. Omits any key whose
117
+ * source (flag + env) was unset.
118
+ */
119
+ export declare function buildCallerEnvelope(config: RemoteConfigSlice): Partial<PipelineRequest>;
@@ -15,8 +15,7 @@
15
15
  import { existsSync } from "fs";
16
16
  import { resolve } from "path";
17
17
  import { PipelineRequestSchema, } from "../../_vendor/ailf-core/index.js";
18
- import { LEGACY_EVAL_MODE_ALIASES } from "../../_vendor/ailf-shared/index.js";
19
- import { LiteracyVariant } from "../../pipeline/normalize-mode.js";
18
+ import { LEGACY_EVAL_MODE_ALIASES, isRunClassification, } from "../../_vendor/ailf-shared/index.js";
20
19
  import { RepoTaskSource } from "../task-sources/repo-task-source.js";
21
20
  const LEGACY_LITERACY_VARIANT_SET = new Set(LEGACY_EVAL_MODE_ALIASES);
22
21
  /**
@@ -27,6 +26,16 @@ const LEGACY_LITERACY_VARIANT_SET = new Set(LEGACY_EVAL_MODE_ALIASES);
27
26
  function resolveCanonicalTaskMode(configMode) {
28
27
  return LEGACY_LITERACY_VARIANT_SET.has(configMode) ? "literacy" : configMode;
29
28
  }
29
+ /**
30
+ * Thrown when `buildRemoteRequest` can't find any runnable tasks.
31
+ *
32
+ * The CLI catches this separately from ZodError so it can print the
33
+ * message without an accompanying stack trace — the message is already
34
+ * the whole story for the user.
35
+ */
36
+ export class NoRunnableTasksError extends Error {
37
+ name = "NoRunnableTasksError";
38
+ }
30
39
  // ---------------------------------------------------------------------------
31
40
  // Public API
32
41
  // ---------------------------------------------------------------------------
@@ -56,11 +65,13 @@ export async function buildRemoteRequest(options) {
56
65
  ? allTasks.filter((t) => t.mode === taskModeFilter)
57
66
  : allTasks;
58
67
  if (tasks.length === 0) {
59
- throw new Error("No tasks found after applying filters.\n" +
60
- ` Tasks directory: ${tasksDir}\n` +
61
- (config.areas ? ` Area filter: ${config.areas.join(", ")}\n` : "") +
62
- (config.tasks ? ` Task filter: ${config.tasks.join(", ")}\n` : "") +
63
- " Check that your .ailf/tasks/ YAML files define tasks matching these filters.");
68
+ throw await emptyTasksError({
69
+ taskSource,
70
+ tasksDir,
71
+ config,
72
+ filterOptions,
73
+ taskModeFilter,
74
+ });
64
75
  }
65
76
  // 2. Convert tasks to inline format
66
77
  const inlineTasks = tasks.map(taskToInlineFormat);
@@ -69,10 +80,14 @@ export async function buildRemoteRequest(options) {
69
80
  taskMode: "inline",
70
81
  inlineTasks,
71
82
  };
72
- // Mode
73
- if (config.mode && config.mode !== LiteracyVariant.FULL) {
83
+ // Mode + variant — send both when set so the server sees the caller's
84
+ // canonical intent. Legacy aliases ("full", "baseline", …) are accepted
85
+ // by `PipelineRequestSchema.mode` for back-compat but the CLI now emits
86
+ // the canonical form (`mode: "literacy"` + explicit `variant`).
87
+ if (config.mode)
74
88
  raw.mode = config.mode;
75
- }
89
+ if (config.variant)
90
+ raw.variant = config.variant;
76
91
  // Debug
77
92
  if (config.debug?.enabled) {
78
93
  raw.debug = config.debug;
@@ -127,6 +142,10 @@ export async function buildRemoteRequest(options) {
127
142
  const callerGit = detectCallerGit();
128
143
  if (callerGit)
129
144
  raw.callerGit = callerGit;
145
+ // D0037 caller envelope — merge CLI flags + env vars and attach each
146
+ // populated field. Flags override env. Skipped fields are omitted so
147
+ // the server applies its own defaults.
148
+ Object.assign(raw, buildCallerEnvelope(config));
130
149
  // 4. Validate the assembled request
131
150
  const parsed = PipelineRequestSchema.parse(raw);
132
151
  return { request: parsed, taskCount: tasks.length };
@@ -202,6 +221,88 @@ function taskToInlineFormat(task) {
202
221
  }
203
222
  return inline;
204
223
  }
224
+ /**
225
+ * Build a descriptive error when the task list is empty after filtering.
226
+ *
227
+ * Loads the full task list a second time with `includeDrafts: true` so we
228
+ * can distinguish the two common failure modes:
229
+ *
230
+ * 1. Every discovered task is non-active (`status: "draft"` from
231
+ * `ailf init` scaffolding, or `status: "paused"`). Tell the user how
232
+ * to opt a task in.
233
+ * 2. The tasks directory is genuinely empty for this filter combination.
234
+ * Echo the filters back so the mismatch is obvious.
235
+ *
236
+ * The directory-missing and file-missing cases are already surfaced
237
+ * earlier by `RepoTaskSource.loadTasks()`, so we never reach this helper
238
+ * for those.
239
+ */
240
+ async function emptyTasksError(args) {
241
+ const { taskSource, tasksDir, config, filterOptions, taskModeFilter } = args;
242
+ // Re-load without the status gate to categorize what got filtered.
243
+ let relaxed = [];
244
+ try {
245
+ relaxed = await taskSource.loadTasks({
246
+ ...(filterOptions ?? {}),
247
+ includeDrafts: true,
248
+ });
249
+ }
250
+ catch {
251
+ // Fall through to the generic message if re-loading fails for any
252
+ // reason (e.g. directory removed mid-run).
253
+ }
254
+ const modeMatched = taskModeFilter
255
+ ? relaxed.filter((t) => t.mode === taskModeFilter)
256
+ : relaxed;
257
+ const drafts = modeMatched.filter((t) => (t.status ?? "active") === "draft");
258
+ const paused = modeMatched.filter((t) => t.status === "paused");
259
+ const filtersBlock = (config.areas?.length
260
+ ? ` Area filter: ${config.areas.join(", ")}\n`
261
+ : "") +
262
+ (config.tasks?.length
263
+ ? ` Task filter: ${config.tasks.join(", ")}\n`
264
+ : "") +
265
+ (config.tags?.length ? ` Tag filter: ${config.tags.join(", ")}\n` : "") +
266
+ (taskModeFilter ? ` Mode filter: ${taskModeFilter}\n` : "");
267
+ if (modeMatched.length === 0) {
268
+ return new NoRunnableTasksError("No tasks matched your filters.\n" +
269
+ ` Tasks directory: ${tasksDir}\n` +
270
+ filtersBlock +
271
+ " Check that your .ailf/tasks/ YAML or .task.ts files define tasks\n" +
272
+ " matching these filters.");
273
+ }
274
+ // All matched tasks were excluded by the status gate.
275
+ const draftIds = drafts.map((t) => t.id);
276
+ const pausedIds = paused.map((t) => t.id);
277
+ const draftSample = draftIds.slice(0, 3).join(", ");
278
+ const draftMore = draftIds.length > 3 ? `, +${draftIds.length - 3} more` : "";
279
+ const pausedSample = pausedIds.slice(0, 3).join(", ");
280
+ const pausedMore = pausedIds.length > 3 ? `, +${pausedIds.length - 3} more` : "";
281
+ const lines = [];
282
+ lines.push("No runnable tasks after applying filters.");
283
+ lines.push(` Tasks directory: ${tasksDir}`);
284
+ if (filtersBlock)
285
+ lines.push(filtersBlock.trimEnd());
286
+ if (drafts.length > 0) {
287
+ lines.push(` ${drafts.length} task(s) skipped because status: "draft": ${draftSample}${draftMore}`);
288
+ }
289
+ if (paused.length > 0) {
290
+ lines.push(` ${paused.length} task(s) skipped because status: "paused": ${pausedSample}${pausedMore}`);
291
+ }
292
+ lines.push("");
293
+ lines.push(" To run one of these anyway, either:");
294
+ if (drafts.length > 0) {
295
+ lines.push(` • Change the task's status field from "draft" to "active", or`);
296
+ lines.push(` • Target it explicitly: --task ${drafts[0]?.id ?? "<id>"}`);
297
+ }
298
+ else if (paused.length > 0) {
299
+ lines.push(` • Target it explicitly by id: --task ${paused[0]?.id ?? "<id>"}, or`);
300
+ lines.push(` • Flip its status from "paused" to "active"`);
301
+ }
302
+ lines.push(" Tasks scaffolded by `ailf init` ship as drafts so you can edit");
303
+ lines.push(" them before they start contributing to your literacy score.");
304
+ return new NoRunnableTasksError(lines.join("\n"));
305
+ }
205
306
  function buildFilterOptions(config) {
206
307
  const areas = config.areas?.length ? config.areas : undefined;
207
308
  const taskIds = config.tasks?.length ? config.tasks : undefined;
@@ -210,6 +311,83 @@ function buildFilterOptions(config) {
210
311
  return undefined;
211
312
  return { areas, taskIds, tags };
212
313
  }
314
+ /**
315
+ * Build the D0037 caller envelope payload from CLI flags + env vars.
316
+ *
317
+ * Precedence, highest first:
318
+ * 1. Explicit CLI flag (--classification, --owner-team, --purpose, …)
319
+ * 2. Env var (AILF_CLASSIFICATION, AILF_OWNER_TEAM, AILF_PURPOSE, …)
320
+ * 3. Omit — server applies its own defaults (ad-hoc / unknown).
321
+ *
322
+ * Labels are additive: --label values concatenate with AILF_LABELS.
323
+ *
324
+ * `executor` is always set on remote submissions because we know the
325
+ * invocation is a user-driven CLI call. Surface defaults to `"cli"`
326
+ * unless AILF_EXECUTOR_SURFACE explicitly overrides; name falls back to
327
+ * GITHUB_ACTOR when available.
328
+ *
329
+ * Returns partial `PipelineRequest` fields only. Omits any key whose
330
+ * source (flag + env) was unset.
331
+ */
332
+ export function buildCallerEnvelope(config) {
333
+ const out = {};
334
+ // Classification: flag > env. Validated against the closed enum.
335
+ const rawClassification = config.classificationOption ??
336
+ process.env.AILF_CLASSIFICATION?.trim() ??
337
+ undefined;
338
+ if (rawClassification) {
339
+ if (isRunClassification(rawClassification)) {
340
+ out.classification = rawClassification;
341
+ }
342
+ else {
343
+ // Surface the invalid value so downstream Zod validation gives a
344
+ // clear error message pointing at the flag, not the inner enum.
345
+ out.classification =
346
+ rawClassification;
347
+ }
348
+ }
349
+ // Owner: flag > env. Team required, individual optional.
350
+ const team = config.ownerTeamOption ?? process.env.AILF_OWNER_TEAM?.trim() ?? undefined;
351
+ const individual = config.ownerIndividualOption ??
352
+ process.env.AILF_OWNER_INDIVIDUAL?.trim() ??
353
+ process.env.GITHUB_ACTOR?.trim() ??
354
+ undefined;
355
+ if (team) {
356
+ out.owner = individual ? { team, individual } : { team };
357
+ }
358
+ // Purpose: flag > env.
359
+ const purpose = config.purposeOption ?? process.env.AILF_PURPOSE?.trim() ?? undefined;
360
+ if (purpose)
361
+ out.purpose = purpose;
362
+ // Labels: flag AND env are additive (dedup + trim).
363
+ const flagLabels = config.labelOptions ?? [];
364
+ const envLabels = (process.env.AILF_LABELS ?? "")
365
+ .split(",")
366
+ .map((s) => s.trim())
367
+ .filter(Boolean);
368
+ const mergedLabels = Array.from(new Set([...envLabels, ...flagLabels]));
369
+ if (mergedLabels.length > 0)
370
+ out.labels = mergedLabels;
371
+ // Executor: always set on remote submissions — we know this is a CLI
372
+ // user. Only omit when absolutely nothing identifying is available.
373
+ const surfaceEnv = process.env.AILF_EXECUTOR_SURFACE?.trim();
374
+ const surface = surfaceEnv === "studio" || surfaceEnv === "api" ? surfaceEnv : "cli";
375
+ const githubActor = process.env.GITHUB_ACTOR?.trim() || undefined;
376
+ const nameFromIndividual = config.ownerIndividualOption ??
377
+ process.env.AILF_OWNER_INDIVIDUAL?.trim() ??
378
+ undefined;
379
+ const executorName = githubActor ?? nameFromIndividual;
380
+ const executor = {
381
+ type: "user",
382
+ surface,
383
+ };
384
+ if (executorName)
385
+ executor.name = executorName;
386
+ if (githubActor)
387
+ executor.githubActor = githubActor;
388
+ out.executor = executor;
389
+ return out;
390
+ }
213
391
  /**
214
392
  * Auto-detect caller git metadata from GitHub Actions environment variables.
215
393
  *
@@ -5,7 +5,7 @@
5
5
  * import { ApiClient, buildRemoteRequest, resolveTasksDir } from "./adapters/api-client/index.js"
6
6
  */
7
7
  export { ApiClient } from "./api-client.js";
8
- export { buildRemoteRequest, resolveTasksDir, type BuildRequestOptions, type RemoteConfigSlice, } from "./build-request.js";
8
+ export { buildCallerEnvelope, buildRemoteRequest, NoRunnableTasksError, resolveTasksDir, type BuildRequestOptions, type RemoteConfigSlice, } from "./build-request.js";
9
9
  export { ApiAuthError, ApiConnectionError, ApiError, ApiTimeoutError, } from "./errors.js";
10
10
  export { formatJobError } from "./format-error.js";
11
11
  export { createProgressDisplay } from "./progress.js";
@@ -5,7 +5,7 @@
5
5
  * import { ApiClient, buildRemoteRequest, resolveTasksDir } from "./adapters/api-client/index.js"
6
6
  */
7
7
  export { ApiClient } from "./api-client.js";
8
- export { buildRemoteRequest, resolveTasksDir, } from "./build-request.js";
8
+ export { buildCallerEnvelope, buildRemoteRequest, NoRunnableTasksError, resolveTasksDir, } from "./build-request.js";
9
9
  export { ApiAuthError, ApiConnectionError, ApiError, ApiTimeoutError, } from "./errors.js";
10
10
  export { formatJobError } from "./format-error.js";
11
11
  export { createProgressDisplay } from "./progress.js";
@@ -727,6 +727,11 @@ async function buildPipelineExplainPlan(actionCommand, rootDir) {
727
727
  artifactsDir: raw.artifactsDir,
728
728
  artifactsDryRun: raw.artifactsDryRun ?? false,
729
729
  artifactsExclude: raw.artifactsExclude,
730
+ classification: raw.classification,
731
+ ownerTeam: raw.ownerTeam,
732
+ ownerIndividual: raw.ownerIndividual,
733
+ purpose: raw.purpose,
734
+ label: raw.label ?? [],
730
735
  };
731
736
  const resolved = computeResolvedOptions(withDefaults);
732
737
  const planOpts = {
@@ -68,6 +68,12 @@ export interface ResolvedOptions {
68
68
  artifactsDir?: string;
69
69
  artifactsDryRun: boolean;
70
70
  artifactsExclude?: readonly string[];
71
+ /** D0037 / W0069 caller envelope — surfaces only on --remote today. */
72
+ classificationOption?: string;
73
+ ownerTeamOption?: string;
74
+ ownerIndividualOption?: string;
75
+ purposeOption?: string;
76
+ labelOptions: string[];
71
77
  }
72
78
  /**
73
79
  * Pure option resolution — computes ResolvedOptions from CLI flags without
@@ -14,7 +14,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
14
14
  import { dirname, resolve } from "path";
15
15
  import { fileURLToPath } from "url";
16
16
  import { classifyUrls } from "../pipeline/classify-url.js";
17
- import { normalizeMode } from "../pipeline/normalize-mode.js";
17
+ import { LiteracyVariant, normalizeMode } from "../pipeline/normalize-mode.js";
18
18
  import { assessImpact, buildReverseMapping, } from "../pipeline/reverse-mapping.js";
19
19
  import { buildAppContext, parseArtifactUploadEnv, } from "../orchestration/build-app-context.js";
20
20
  import { buildStepSequence } from "../orchestration/build-step-sequence.js";
@@ -47,6 +47,13 @@ export function computeResolvedOptions(opts) {
47
47
  mode = normalized.mode;
48
48
  // Explicit --variant flag takes precedence over what normalizeMode inferred
49
49
  variant = opts.variant ?? normalized.variant;
50
+ // Canonical mode "literacy" with no variant defaults to the full variant
51
+ // (standard + agentic). This preserves the pre-canonical CLI behavior
52
+ // where `--mode full` was the default, without emitting the legacy alias
53
+ // deprecation warning for users who pass no flags at all.
54
+ if (mode === "literacy" && !variant) {
55
+ variant = LiteracyVariant.FULL;
56
+ }
50
57
  }
51
58
  catch (err) {
52
59
  console.error(`❌ ${err instanceof Error ? err.message : String(err)}`);
@@ -269,6 +276,11 @@ export function computeResolvedOptions(opts) {
269
276
  artifactsDir: resolveArtifactsDir(opts),
270
277
  artifactsDryRun: opts.artifactsDryRun,
271
278
  artifactsExclude: parseArtifactsExcludeList(opts.artifactsExclude),
279
+ classificationOption: opts.classification?.trim() || undefined,
280
+ ownerTeamOption: opts.ownerTeam?.trim() || undefined,
281
+ ownerIndividualOption: opts.ownerIndividual?.trim() || undefined,
282
+ purposeOption: opts.purpose?.trim() || undefined,
283
+ labelOptions: opts.label ?? [],
272
284
  };
273
285
  }
274
286
  /**
@@ -68,5 +68,10 @@ export interface PipelineCliOptions {
68
68
  artifactsDir?: string;
69
69
  artifactsDryRun: boolean;
70
70
  artifactsExclude?: string;
71
+ classification?: string;
72
+ ownerTeam?: string;
73
+ ownerIndividual?: string;
74
+ purpose?: string;
75
+ label: string[];
71
76
  }
72
77
  export declare function createPipelineCommand(): Command;