@tuned-tensor/cli 0.13.1-beta.1 → 0.13.1-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,11 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  TRAINING_MODELS,
4
+ assertFoundationSpecReady,
4
5
  assertLocalRunInputReady,
6
+ buildFoundationPythonCommand,
5
7
  loadLocalRunInput,
6
8
  loadLocalRunnerConfig,
7
- runLocalPipeline
8
- } from "./chunk-YJYCGSHJ.js";
9
+ runLocalPipeline,
10
+ runLoggedProcess,
11
+ withFoundationPythonEnvironment
12
+ } from "./chunk-2P2RL23O.js";
9
13
 
10
14
  // src/cli.ts
11
15
  import { spawn as spawn2 } from "child_process";
@@ -38,16 +42,18 @@ function validateSpec(spec) {
38
42
  passed: Boolean(spec.system_prompt?.trim()),
39
43
  message: spec.system_prompt?.trim() ? void 0 : "Spec is missing a system_prompt"
40
44
  });
45
+ const isFoundation = spec.engine === "foundation";
41
46
  checks.push({
42
- name: "Has base model",
43
- passed: Boolean(spec.base_model?.trim()),
44
- message: spec.base_model?.trim() ? void 0 : "Spec is missing a base_model"
47
+ name: isFoundation ? "Has foundation engine" : "Has base model",
48
+ passed: isFoundation ? spec.foundation != null && typeof spec.foundation === "object" : Boolean(spec.base_model?.trim()),
49
+ message: isFoundation ? spec.foundation != null ? void 0 : "Foundation spec is missing a foundation block" : spec.base_model?.trim() ? void 0 : "Spec is missing a base_model"
45
50
  });
46
51
  const exCount = spec.examples?.length ?? 0;
52
+ const minExamples = isFoundation ? 2 : 1;
47
53
  checks.push({
48
54
  name: "Has examples",
49
- passed: exCount > 0,
50
- message: exCount > 0 ? `${exCount} example(s)` : "Add at least one example"
55
+ passed: exCount >= minExamples,
56
+ message: exCount >= minExamples ? `${exCount} example(s)` : isFoundation ? "Add at least two chat examples" : "Add at least one example"
51
57
  });
52
58
  checks.push({
53
59
  name: "Has guidelines",
@@ -666,14 +672,13 @@ function missingProviderAuthMessage() {
666
672
  function unknownAgentProviderMessage(provider) {
667
673
  return `Unknown provider "${provider}". Use /login <id> or /model <id>.`;
668
674
  }
669
- var MIN_REDACTED_SECRET_LENGTH = 8;
670
675
  function isRecord(value) {
671
676
  return value !== null && typeof value === "object" && !Array.isArray(value);
672
677
  }
673
678
  function pushSecret(secrets, value) {
674
679
  if (typeof value !== "string") return;
675
680
  const secret = value.trim();
676
- if (secret.length < MIN_REDACTED_SECRET_LENGTH) return;
681
+ if (!secret) return;
677
682
  if (!secrets.includes(secret)) secrets.push(secret);
678
683
  }
679
684
  function persistProviderApiKey(provider, apiKey, authPath = getAgentAuthPath()) {
@@ -914,7 +919,9 @@ import {
914
919
  PIPELINE_STEP_CAPABILITIES,
915
920
  PIPELINE_STEP_USES,
916
921
  PIPELINE_TARGETS,
922
+ canonicalFoundationPipeline as canonicalFoundationPipeline2,
917
923
  canonicalJson,
924
+ isFoundationPipeline,
918
925
  parsePipeline as parsePipeline2,
919
926
  pipelineDocumentSchema as pipelineDocumentSchema2,
920
927
  pipelineHash
@@ -922,8 +929,84 @@ import {
922
929
  function stepReferences(step) {
923
930
  if (step.uses === "evaluate") return step.with.model === "base" ? [] : [step.with.model];
924
931
  if (step.uses === "compare") return [step.with.before, step.with.after];
932
+ if (step.uses === "pretrain") return [step.with.tokenizer];
933
+ if (step.uses === "finetune" || step.uses === "rl") return [step.with.model];
925
934
  return [];
926
935
  }
936
+ function pipelineFromFoundationHyperparameters(name, hp) {
937
+ const lastTrainId = hp.rl_steps > 0 ? "rl" : "sft";
938
+ const steps = [
939
+ {
940
+ id: "tokenize",
941
+ uses: "tokenize",
942
+ target: "local",
943
+ with: { vocabSize: hp.vocab_size, maxChars: hp.max_chars }
944
+ },
945
+ {
946
+ id: "pretrain",
947
+ uses: "pretrain",
948
+ target: "local",
949
+ with: {
950
+ tokenizer: { from: "tokenize.tokenizer" },
951
+ depth: hp.depth,
952
+ steps: hp.pretrain_steps,
953
+ batchSize: hp.batch_size,
954
+ sequenceLength: hp.sequence_length,
955
+ nprocPerNode: hp.nproc_per_node
956
+ }
957
+ },
958
+ {
959
+ id: "bpb",
960
+ uses: "evaluate",
961
+ target: "local",
962
+ with: { model: { from: "pretrain.model" }, evaluator: "bpb" }
963
+ },
964
+ {
965
+ id: "sft",
966
+ uses: "finetune",
967
+ target: "local",
968
+ with: {
969
+ model: { from: "pretrain.model" },
970
+ steps: hp.finetune_steps,
971
+ batchSize: hp.batch_size
972
+ }
973
+ },
974
+ {
975
+ id: "chat",
976
+ uses: "evaluate",
977
+ target: "local",
978
+ with: { model: { from: "sft.model" }, evaluator: "chat" }
979
+ }
980
+ ];
981
+ if (hp.rl_steps > 0) {
982
+ steps.push(
983
+ {
984
+ id: "rl",
985
+ uses: "rl",
986
+ target: "local",
987
+ with: { model: { from: "sft.model" }, steps: hp.rl_steps }
988
+ },
989
+ {
990
+ id: "chat_rl",
991
+ uses: "evaluate",
992
+ target: "local",
993
+ with: { model: { from: "rl.model" }, evaluator: "chat" }
994
+ }
995
+ );
996
+ }
997
+ steps.push({
998
+ id: "infer",
999
+ uses: "evaluate",
1000
+ target: "local",
1001
+ with: { model: { from: `${lastTrainId}.model` }, evaluator: "inference" }
1002
+ });
1003
+ return {
1004
+ version: 1,
1005
+ name,
1006
+ runtime: { engine: "foundation" },
1007
+ steps
1008
+ };
1009
+ }
927
1010
  function producerId(reference) {
928
1011
  return reference.from.slice(0, reference.from.lastIndexOf("."));
929
1012
  }
@@ -3617,7 +3700,9 @@ var CLOUD_SPEC_KEYS = [
3617
3700
  ];
3618
3701
  var LOCAL_ONLY_SPEC_KEYS = [
3619
3702
  "hyperparameters",
3620
- "dataset_prebuilt"
3703
+ "dataset_prebuilt",
3704
+ "engine",
3705
+ "foundation"
3621
3706
  ];
3622
3707
  var LOCAL_SPEC_KEYS = [
3623
3708
  "id",
@@ -4739,16 +4824,342 @@ function registerAgentCommands(parent, options) {
4739
4824
 
4740
4825
  // src/commands/pipeline.ts
4741
4826
  import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
4742
- import { resolve as resolve4 } from "path";
4827
+ import { resolve as resolve5 } from "path";
4828
+
4829
+ // src/local-runtime/foundation-runner.ts
4830
+ import { createHash as createHash2, randomUUID as randomUUID5 } from "crypto";
4831
+ import { createReadStream } from "fs";
4832
+ import { chmod as chmod2, lstat as lstat3, mkdir as mkdir3, open as open2, readdir as readdir3, readFile as readFile4 } from "fs/promises";
4833
+ import { dirname as dirname4, join as join7, resolve as resolve4 } from "path";
4834
+ function isRef(value) {
4835
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value) && typeof value.from === "string";
4836
+ }
4837
+ function parseRef(value) {
4838
+ const index = value.from.lastIndexOf(".");
4839
+ if (index <= 0) throw new Error(`Invalid artifact reference: ${value.from}`);
4840
+ return { stepId: value.from.slice(0, index), kind: value.from.slice(index + 1) };
4841
+ }
4842
+ async function hashPath(path) {
4843
+ const info = await lstat3(path);
4844
+ if (info.isSymbolicLink()) {
4845
+ throw new Error(`Foundation artifacts must not contain symbolic links: ${path}`);
4846
+ }
4847
+ const hash = createHash2("sha256");
4848
+ if (info.isDirectory()) {
4849
+ for (const name of (await readdir3(path)).sort()) {
4850
+ hash.update(name);
4851
+ hash.update(await hashPath(join7(path, name)));
4852
+ }
4853
+ return hash.digest("hex");
4854
+ }
4855
+ if (!info.isFile()) {
4856
+ throw new Error(`Foundation artifacts must contain only regular files and directories: ${path}`);
4857
+ }
4858
+ await new Promise((done, reject) => {
4859
+ const stream = createReadStream(path);
4860
+ stream.on("data", (chunk) => hash.update(chunk));
4861
+ stream.on("error", reject);
4862
+ stream.on("end", () => done());
4863
+ });
4864
+ return hash.digest("hex");
4865
+ }
4866
+ async function makePrivateTree(path) {
4867
+ const info = await lstat3(path);
4868
+ if (info.isSymbolicLink()) {
4869
+ throw new Error(`Foundation artifacts must not contain symbolic links: ${path}`);
4870
+ }
4871
+ if (info.isDirectory()) {
4872
+ await chmod2(path, 448);
4873
+ for (const name of await readdir3(path)) {
4874
+ await makePrivateTree(join7(path, name));
4875
+ }
4876
+ return;
4877
+ }
4878
+ if (info.isFile()) {
4879
+ await chmod2(path, 384);
4880
+ return;
4881
+ }
4882
+ throw new Error(`Foundation artifacts must contain only regular files and directories: ${path}`);
4883
+ }
4884
+ async function readRequiredJsonObject(path, description) {
4885
+ let content;
4886
+ try {
4887
+ content = await readFile4(path, "utf8");
4888
+ } catch (error) {
4889
+ throw new Error(`${description} is missing or unreadable: ${path}`, { cause: error });
4890
+ }
4891
+ let parsed;
4892
+ try {
4893
+ parsed = JSON.parse(content);
4894
+ } catch (error) {
4895
+ throw new Error(`${description} is not valid JSON: ${path}`, { cause: error });
4896
+ }
4897
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
4898
+ throw new Error(`${description} must be a JSON object: ${path}`);
4899
+ }
4900
+ return parsed;
4901
+ }
4902
+ async function writePrivateText(path, content) {
4903
+ const file = await open2(path, "w", 384);
4904
+ try {
4905
+ await file.chmod(384);
4906
+ await file.writeFile(content, "utf8");
4907
+ } finally {
4908
+ await file.close();
4909
+ }
4910
+ }
4911
+ async function artifactFor(path) {
4912
+ return { path, sha256: await hashPath(path) };
4913
+ }
4914
+ async function requireRegularNonemptyFile(path, description) {
4915
+ let info;
4916
+ try {
4917
+ info = await lstat3(path);
4918
+ } catch (error) {
4919
+ throw new Error(`${description} is missing: ${path}`, { cause: error });
4920
+ }
4921
+ if (info.isSymbolicLink() || !info.isFile() || info.size === 0) {
4922
+ throw new Error(`${description} must be a non-empty regular file: ${path}`);
4923
+ }
4924
+ }
4925
+ function requirePositiveIntegerFields(value, fields, description) {
4926
+ for (const field of fields) {
4927
+ if (!Number.isInteger(value[field]) || Number(value[field]) <= 0) {
4928
+ throw new Error(`${description}.${field} must be a positive integer.`);
4929
+ }
4930
+ }
4931
+ }
4932
+ function stepConfig(step) {
4933
+ if (step.uses === "train") return {};
4934
+ if (step.uses === "tokenize") return { ...step.with ?? {} };
4935
+ return { ...step.with };
4936
+ }
4937
+ function assertFoundationPlanSupported(spec, plan) {
4938
+ const supported = /* @__PURE__ */ new Set(["tokenize", "pretrain", "finetune", "rl", "evaluate"]);
4939
+ const unsupported = plan.steps.find((step) => !supported.has(step.uses));
4940
+ if (unsupported) {
4941
+ throw new Error(
4942
+ `Foundation runner does not implement ${unsupported.uses} (step ${unsupported.id}).`
4943
+ );
4944
+ }
4945
+ if (plan.steps.some((step) => step.uses === "rl")) {
4946
+ const invalidReward = spec.examples.findIndex(
4947
+ (example) => !/-?\d+(?:\.\d+)?/.test(example.output.replaceAll(",", ""))
4948
+ );
4949
+ if (invalidReward >= 0) {
4950
+ throw new Error(
4951
+ `Foundation RL requires a numeric expected output for every example; examples[${invalidReward}].output has none.`
4952
+ );
4953
+ }
4954
+ }
4955
+ for (const step of plan.steps) {
4956
+ if (step.uses !== "pretrain") continue;
4957
+ const processCount = step.with.nprocPerNode ?? spec.foundation.nproc_per_node;
4958
+ if (processCount !== 1) {
4959
+ throw new Error(
4960
+ `Foundation nproc_per_node is ${processCount}; only 1 is supported by the current single-process runtime.`
4961
+ );
4962
+ }
4963
+ }
4964
+ }
4965
+ async function defaultFoundationSpawn(args2) {
4966
+ const launched = buildFoundationPythonCommand(args2.entrypoint, ["--config", args2.configPath]);
4967
+ const result2 = await runLoggedProcess({
4968
+ command: launched.command,
4969
+ commandArgs: launched.commandArgs,
4970
+ env: withFoundationPythonEnvironment(process.env),
4971
+ logPath: args2.logPath,
4972
+ stage: args2.stepId
4973
+ });
4974
+ if (result2.exitCode !== 0) {
4975
+ throw new Error(
4976
+ `Foundation step "${args2.stepId}" failed (exit ${result2.exitCode}).${result2.stderr ? `
4977
+ ${result2.stderr}` : ""}`
4978
+ );
4979
+ }
4980
+ }
4981
+ async function runFoundationPipeline(args2) {
4982
+ assertFoundationPlanSupported(args2.spec, args2.plan);
4983
+ const outputDir = resolve4(
4984
+ args2.outputDir ?? join7(dirname4(resolve4(args2.specPath)), ".tt-foundation-runs", randomUUID5())
4985
+ );
4986
+ await mkdir3(outputDir, { recursive: true, mode: 448 });
4987
+ await chmod2(outputDir, 448);
4988
+ const spawnStep = args2.spawnStep ?? defaultFoundationSpawn;
4989
+ const produced = /* @__PURE__ */ new Map();
4990
+ const steps = [];
4991
+ const examples = args2.spec.examples.map((example) => ({ input: example.input, output: example.output }));
4992
+ const hp = args2.spec.foundation;
4993
+ let tokenizerDir;
4994
+ let modelDir;
4995
+ const resolveKind = (stepId, kind) => {
4996
+ const path = produced.get(stepId)?.[kind];
4997
+ if (!path) throw new Error(`Step ${stepId} did not produce ${kind}.`);
4998
+ return path;
4999
+ };
5000
+ const resolveFrom = (value, kind, fallback) => {
5001
+ if (isRef(value)) return resolveKind(parseRef(value).stepId, parseRef(value).kind);
5002
+ if (fallback) return fallback;
5003
+ throw new Error(`Missing ${kind} artifact.`);
5004
+ };
5005
+ for (const step of args2.plan.steps) {
5006
+ const stepDir = join7(outputDir, step.id);
5007
+ const output = join7(stepDir, "output");
5008
+ await mkdir3(stepDir, { recursive: true, mode: 448 });
5009
+ await chmod2(stepDir, 448);
5010
+ await mkdir3(output, { recursive: true, mode: 448 });
5011
+ await chmod2(output, 448);
5012
+ const configPath2 = join7(stepDir, "config.json");
5013
+ const logPath = join7(stepDir, "step.log");
5014
+ const fields = stepConfig(step);
5015
+ let entrypoint;
5016
+ let config;
5017
+ if (step.uses === "tokenize") {
5018
+ entrypoint = "train_tokenizer.py";
5019
+ config = {
5020
+ output_dir: output,
5021
+ vocab_size: fields.vocabSize ?? hp.vocab_size,
5022
+ max_chars: fields.maxChars ?? hp.max_chars,
5023
+ system_prompt: args2.spec.system_prompt,
5024
+ examples
5025
+ };
5026
+ } else if (step.uses === "pretrain") {
5027
+ tokenizerDir = resolveFrom(fields.tokenizer, "tokenizer", tokenizerDir);
5028
+ entrypoint = "pretrain.py";
5029
+ config = {
5030
+ tokenizer_dir: tokenizerDir,
5031
+ output_dir: output,
5032
+ depth: fields.depth ?? hp.depth,
5033
+ steps: fields.steps ?? hp.pretrain_steps,
5034
+ batch_size: fields.batchSize ?? hp.batch_size,
5035
+ sequence_length: fields.sequenceLength ?? hp.sequence_length,
5036
+ nproc_per_node: fields.nprocPerNode ?? hp.nproc_per_node,
5037
+ max_chars: hp.max_chars,
5038
+ system_prompt: args2.spec.system_prompt,
5039
+ examples
5040
+ };
5041
+ } else if (step.uses === "finetune") {
5042
+ modelDir = resolveFrom(fields.model, "model", modelDir);
5043
+ entrypoint = "finetune.py";
5044
+ config = {
5045
+ model_dir: modelDir,
5046
+ tokenizer_dir: tokenizerDir,
5047
+ output_dir: output,
5048
+ steps: fields.steps ?? hp.finetune_steps,
5049
+ batch_size: fields.batchSize ?? hp.batch_size,
5050
+ sequence_length: hp.sequence_length,
5051
+ system_prompt: args2.spec.system_prompt,
5052
+ examples
5053
+ };
5054
+ } else if (step.uses === "rl") {
5055
+ modelDir = resolveFrom(fields.model, "model", modelDir);
5056
+ entrypoint = "rl.py";
5057
+ config = {
5058
+ model_dir: modelDir,
5059
+ tokenizer_dir: tokenizerDir,
5060
+ output_dir: output,
5061
+ steps: fields.steps ?? hp.rl_steps,
5062
+ system_prompt: args2.spec.system_prompt,
5063
+ examples
5064
+ };
5065
+ } else if (step.uses === "evaluate") {
5066
+ modelDir = resolveFrom(fields.model, "model", modelDir);
5067
+ entrypoint = "evaluate.py";
5068
+ config = {
5069
+ evaluator: fields.evaluator ?? "bpb",
5070
+ model_dir: modelDir,
5071
+ tokenizer_dir: tokenizerDir,
5072
+ output_dir: output,
5073
+ sequence_length: hp.sequence_length,
5074
+ max_chars: hp.max_chars,
5075
+ system_prompt: args2.spec.system_prompt,
5076
+ examples
5077
+ };
5078
+ } else {
5079
+ throw new Error(`Foundation runner does not implement ${step.uses} (step ${step.id}).`);
5080
+ }
5081
+ if ((step.uses === "finetune" || step.uses === "rl" || step.uses === "evaluate") && !tokenizerDir) {
5082
+ throw new Error(`Step ${step.id} needs a tokenizer from an earlier tokenize step.`);
5083
+ }
5084
+ await writePrivateText(configPath2, `${JSON.stringify(config, null, 2)}
5085
+ `);
5086
+ await spawnStep({ entrypoint, configPath: configPath2, logPath, stepId: step.id });
5087
+ await makePrivateTree(stepDir);
5088
+ const metrics = await readRequiredJsonObject(
5089
+ join7(output, "metrics.json"),
5090
+ `Foundation step "${step.id}" metrics`
5091
+ );
5092
+ if (metrics.ok !== true) {
5093
+ throw new Error(`Foundation step "${step.id}" metrics must report ok: true.`);
5094
+ }
5095
+ const artifacts = {};
5096
+ const record4 = {};
5097
+ if (step.uses === "tokenize") {
5098
+ const tokenizerPath = join7(output, "tokenizer.json");
5099
+ await requireRegularNonemptyFile(tokenizerPath, `Foundation step "${step.id}" tokenizer.json`);
5100
+ await readRequiredJsonObject(tokenizerPath, `Foundation step "${step.id}" tokenizer.json`);
5101
+ record4.tokenizer = output;
5102
+ tokenizerDir = output;
5103
+ artifacts.tokenizer = await artifactFor(output);
5104
+ } else if (step.uses === "pretrain" || step.uses === "finetune" || step.uses === "rl") {
5105
+ const modelPath = join7(output, "model.safetensors");
5106
+ const modelConfigPath = join7(output, "config.json");
5107
+ await requireRegularNonemptyFile(modelPath, `Foundation step "${step.id}" model.safetensors`);
5108
+ const modelConfig = await readRequiredJsonObject(
5109
+ modelConfigPath,
5110
+ `Foundation step "${step.id}" config.json`
5111
+ );
5112
+ requirePositiveIntegerFields(
5113
+ modelConfig,
5114
+ ["depth", "width", "heads", "vocab_size", "sequence_length"],
5115
+ `Foundation step "${step.id}" config.json`
5116
+ );
5117
+ record4.model = output;
5118
+ modelDir = output;
5119
+ artifacts.model = await artifactFor(output);
5120
+ } else if (step.uses === "evaluate") {
5121
+ const reportPath2 = join7(output, "report.json");
5122
+ const stepReport = await readRequiredJsonObject(
5123
+ reportPath2,
5124
+ `Foundation step "${step.id}" report.json`
5125
+ );
5126
+ if (stepReport.ok !== true) {
5127
+ throw new Error(`Foundation step "${step.id}" report.json must report ok: true.`);
5128
+ }
5129
+ record4.report = reportPath2;
5130
+ artifacts.report = await artifactFor(reportPath2);
5131
+ }
5132
+ produced.set(step.id, record4);
5133
+ steps.push({ id: step.id, uses: step.uses, metrics, artifacts });
5134
+ }
5135
+ const report = {
5136
+ status: "succeeded",
5137
+ ...args2.plan.name ? { name: args2.plan.name } : {},
5138
+ spec: args2.specPath,
5139
+ steps
5140
+ };
5141
+ const reportPath = join7(outputDir, "report.json");
5142
+ await writePrivateText(reportPath, `${JSON.stringify(report, null, 2)}
5143
+ `);
5144
+ return {
5145
+ status: "succeeded",
5146
+ ...args2.plan.name ? { name: args2.plan.name } : {},
5147
+ report_path: reportPath,
5148
+ steps
5149
+ };
5150
+ }
5151
+
5152
+ // src/commands/pipeline.ts
4743
5153
  var DEFAULT_PIPELINE_FILE = "tunedtensor.pipeline.json";
5154
+ var DEFAULT_SPEC_FILE = "tunedtensor.json";
4744
5155
  function parseList(value) {
4745
5156
  if (!value) return void 0;
4746
5157
  const ids = value.split(",").map((id) => id.trim()).filter(Boolean);
4747
5158
  if (!ids.length) throw new Error("Step selection must name at least one step.");
4748
5159
  return ids;
4749
5160
  }
4750
- function loadPipeline(path) {
4751
- const resolved = resolve4(path);
5161
+ function loadPipelineFile(path) {
5162
+ const resolved = resolve5(path);
4752
5163
  if (!existsSync4(resolved)) throw new Error(`Pipeline file not found: ${path}. Run \`tt pipeline init\` to create one.`);
4753
5164
  try {
4754
5165
  return JSON.parse(readFileSync4(resolved, "utf8"));
@@ -4756,6 +5167,45 @@ function loadPipeline(path) {
4756
5167
  throw new Error(`Pipeline file must be JSON: ${error.message}`);
4757
5168
  }
4758
5169
  }
5170
+ function pipelineFromSpec(input) {
5171
+ return pipelineFromFoundationHyperparameters(input.spec.name, input.spec.foundation);
5172
+ }
5173
+ function isParsedFoundationPipeline(document) {
5174
+ try {
5175
+ return isFoundationPipeline(parsePipeline2(document));
5176
+ } catch {
5177
+ return false;
5178
+ }
5179
+ }
5180
+ async function resolvePipelineDocument(options) {
5181
+ const filePath = resolve5(options.file);
5182
+ const fileExists = existsSync4(filePath);
5183
+ const specPath = options.spec ? resolve5(options.spec) : void 0;
5184
+ const specInput = specPath && existsSync4(specPath) ? await loadLocalRunInput(specPath) : void 0;
5185
+ if (specInput?.kind === "foundation-spec") {
5186
+ if (fileExists) {
5187
+ const document = loadPipelineFile(options.file);
5188
+ if (isParsedFoundationPipeline(document)) return document;
5189
+ throw new Error(
5190
+ `Pipeline file ${options.file} is an adapter recipe, but ${options.spec} is a foundation spec. Use a foundation pipeline, omit --file, or pass an adapter spec.`
5191
+ );
5192
+ }
5193
+ return pipelineFromSpec(specInput);
5194
+ }
5195
+ if (fileExists) {
5196
+ const document = loadPipelineFile(options.file);
5197
+ if (specInput && isParsedFoundationPipeline(document)) {
5198
+ throw new Error(
5199
+ `Pipeline file ${options.file} is a foundation recipe, but ${options.spec} is an adapter spec. Use a foundation spec, omit --file, or pass an adapter pipeline.`
5200
+ );
5201
+ }
5202
+ return document;
5203
+ }
5204
+ if (specPath && !existsSync4(specPath)) await loadLocalRunInput(specPath);
5205
+ throw new Error(
5206
+ `Pipeline file not found: ${options.file}. Run \`tt pipeline init\` or pass a foundation --spec.`
5207
+ );
5208
+ }
4759
5209
  function outputPlan(plan) {
4760
5210
  if (isJsonMode()) return printJson(plan);
4761
5211
  const typed = plan;
@@ -4766,28 +5216,49 @@ function outputPlan(plan) {
4766
5216
  }
4767
5217
  function registerPipelineCommands(parent) {
4768
5218
  const pipeline = parent.command("pipeline").description("Create, validate, inspect, and safely dry-run ordered pipeline recipes");
4769
- pipeline.command("init").description("Write a canonical v1 pipeline recipe").option("-f, --file <path>", "Output file", DEFAULT_PIPELINE_FILE).action((options) => {
4770
- const path = resolve4(options.file);
5219
+ pipeline.command("init").description("Write a canonical v1 pipeline recipe").option("-f, --file <path>", "Output file", DEFAULT_PIPELINE_FILE).option("--engine <engine>", "adapter (default) or foundation").option("--spec <path>", "Foundation spec whose hyperparameters stamp the DAG").action(async (options) => {
5220
+ const engine = options.engine ?? "adapter";
5221
+ if (engine !== "adapter" && engine !== "foundation") {
5222
+ throw new Error(`--engine must be adapter or foundation, got: ${engine}`);
5223
+ }
5224
+ if (engine !== "foundation" && options.spec) {
5225
+ throw new Error("--spec is only valid with --engine foundation.");
5226
+ }
5227
+ const path = resolve5(options.file);
4771
5228
  if (existsSync4(path)) throw new Error(`Pipeline file already exists: ${options.file}.`);
4772
- const recipe = canonicalPipeline("local");
5229
+ let recipe;
5230
+ if (engine === "foundation") {
5231
+ if (options.spec) {
5232
+ const input = await loadLocalRunInput(resolve5(options.spec));
5233
+ if (input.kind !== "foundation-spec") {
5234
+ throw new Error("--spec must be a foundation tunedtensor.json.");
5235
+ }
5236
+ recipe = pipelineFromSpec(input);
5237
+ } else {
5238
+ recipe = canonicalFoundationPipeline2();
5239
+ }
5240
+ } else {
5241
+ recipe = canonicalPipeline("local");
5242
+ }
4773
5243
  writeFileSync3(path, `${JSON.stringify(recipe, null, 2)}
4774
5244
  `);
4775
5245
  if (isJsonMode()) return printJson({ created: true, path, pipeline: recipe });
4776
5246
  printSuccess(`Created ${options.file}`);
4777
5247
  });
4778
- pipeline.command("validate").description("Validate a pipeline without any execution or transfer").option("-f, --file <path>", "Pipeline file", DEFAULT_PIPELINE_FILE).action((options) => {
4779
- const errors = validatePipeline(loadPipeline(options.file));
5248
+ pipeline.command("validate").description("Validate a pipeline without any execution or transfer").option("-f, --file <path>", "Pipeline file", DEFAULT_PIPELINE_FILE).option("--spec <path>", "Local behavior spec used to detect leftover engine mismatches", DEFAULT_SPEC_FILE).action(async (options) => {
5249
+ const errors = validatePipeline(await resolvePipelineDocument(options));
4780
5250
  const result2 = { valid: errors.length === 0, errors };
4781
5251
  if (isJsonMode()) return printJson(result2);
4782
5252
  if (errors.length) throw new Error(`Invalid pipeline:
4783
5253
  - ${errors.join("\n- ")}`);
4784
5254
  printSuccess("Pipeline is valid.");
4785
5255
  });
4786
- pipeline.command("plan").description("Resolve step targets and required artifact transfers").option("-f, --file <path>", "Pipeline file", DEFAULT_PIPELINE_FILE).option("--only <ids>", "Comma-separated step IDs to include").option("--skip <ids>", "Comma-separated step IDs to omit").action((options) => {
4787
- outputPlan(createExecutionPlan(loadPipeline(options.file), { only: parseList(options.only), skip: parseList(options.skip) }));
5256
+ pipeline.command("plan").description("Resolve step targets and required artifact transfers").option("-f, --file <path>", "Pipeline file", DEFAULT_PIPELINE_FILE).option("--spec <path>", "Local behavior spec used to detect leftover engine mismatches", DEFAULT_SPEC_FILE).option("--only <ids>", "Comma-separated step IDs to include").option("--skip <ids>", "Comma-separated step IDs to omit").action(async (options) => {
5257
+ outputPlan(createExecutionPlan(await resolvePipelineDocument(options), { only: parseList(options.only), skip: parseList(options.skip) }));
4788
5258
  });
4789
- pipeline.command("run").description("Run an ordered local pipeline, or safely preview any pipeline").option("--dry-run", "Resolve and display only; never execute, transfer, or reserve credits").option("-f, --file <path>", "Pipeline file", DEFAULT_PIPELINE_FILE).option("--spec <path>", "Local behavior spec", "tunedtensor.json").option("--config <path>", "Local runtime config").option("--only <ids>", "Comma-separated step IDs to include").option("--skip <ids>", "Comma-separated step IDs to omit").action(async (options) => {
4790
- const plan = createExecutionPlan(loadPipeline(options.file), { only: parseList(options.only), skip: parseList(options.skip) });
5259
+ pipeline.command("run").description("Run an ordered local pipeline, or safely preview any pipeline").option("--dry-run", "Resolve and display only; never execute, transfer, or reserve credits").option("-f, --file <path>", "Pipeline file", DEFAULT_PIPELINE_FILE).option("--spec <path>", "Local behavior spec", DEFAULT_SPEC_FILE).option("--config <path>", "Local runtime config").option("--only <ids>", "Comma-separated step IDs to include").option("--skip <ids>", "Comma-separated step IDs to omit").action(async (options) => {
5260
+ const document = await resolvePipelineDocument(options);
5261
+ const plan = createExecutionPlan(document, { only: parseList(options.only), skip: parseList(options.skip) });
4791
5262
  if (options.dryRun) {
4792
5263
  if (isJsonMode()) return printJson({ dry_run: true, ...plan });
4793
5264
  console.log("Dry run only \u2014 no execution, artifact transfer, or credit reservation will occur.");
@@ -4797,9 +5268,23 @@ function registerPipelineCommands(parent) {
4797
5268
  if (remote) {
4798
5269
  throw new Error(`Step "${remote.id}" targets cloud execution. This CLI is local-only; rewrite that step to local or use --dry-run.`);
4799
5270
  }
4800
- const input = await loadLocalRunInput(resolve4(options.spec));
5271
+ const input = await loadLocalRunInput(resolve5(options.spec));
5272
+ if (isParsedFoundationPipeline(document) || input.kind === "foundation-spec") {
5273
+ if (input.kind !== "foundation-spec") {
5274
+ throw new Error("Foundation pipelines require a foundation tunedtensor.json --spec.");
5275
+ }
5276
+ assertFoundationSpecReady(input.spec);
5277
+ const result3 = await runFoundationPipeline({
5278
+ spec: input.spec,
5279
+ plan,
5280
+ specPath: resolve5(options.spec)
5281
+ });
5282
+ if (isJsonMode()) return printJson(result3);
5283
+ printSuccess(`Foundation pipeline completed. Report: ${result3.report_path}`);
5284
+ return;
5285
+ }
4801
5286
  assertLocalRunInputReady(input.request);
4802
- const config = await loadLocalRunnerConfig(options.config ? resolve4(options.config) : void 0);
5287
+ const config = await loadLocalRunnerConfig(options.config ? resolve5(options.config) : void 0);
4803
5288
  const localPipeline = {
4804
5289
  version: 1,
4805
5290
  ...plan.name ? { name: plan.name } : {},
@@ -4881,10 +5366,10 @@ async function checkForCliUpdate(currentVersion, options = {}) {
4881
5366
  return null;
4882
5367
  }
4883
5368
  })();
4884
- const deadline = new Promise((resolve5) => {
5369
+ const deadline = new Promise((resolve6) => {
4885
5370
  timeout = setTimeout(() => {
4886
5371
  controller.abort();
4887
- resolve5(null);
5372
+ resolve6(null);
4888
5373
  }, timeoutMs);
4889
5374
  });
4890
5375
  try {
@@ -4982,7 +5467,7 @@ async function runSelfCommand(args2, options = {}) {
4982
5467
  if (!entrypoint) {
4983
5468
  throw new Error("Cannot locate the tt CLI entrypoint.");
4984
5469
  }
4985
- return await new Promise((resolve5, reject) => {
5470
+ return await new Promise((resolve6, reject) => {
4986
5471
  const child = spawn2(process.execPath, [entrypoint, ...args2], {
4987
5472
  cwd: options.cwd,
4988
5473
  env: options.env ?? process.env,
@@ -5018,7 +5503,7 @@ async function runSelfCommand(args2, options = {}) {
5018
5503
  child.once("close", (exitCode, signal) => {
5019
5504
  cleanup();
5020
5505
  const signalNumber = signal ? osConstants2.signals[signal] : void 0;
5021
- resolve5({
5506
+ resolve6({
5022
5507
  exitCode: exitCode ?? (typeof signalNumber === "number" ? 128 + signalNumber : 1),
5023
5508
  signal
5024
5509
  });
@@ -5206,7 +5691,7 @@ async function runCli(version, runtime = {}) {
5206
5691
  }
5207
5692
 
5208
5693
  // src/index.ts
5209
- runCli("0.13.1-beta.1").catch((err) => {
5694
+ runCli("0.13.1-beta.2").catch((err) => {
5210
5695
  reportError(err);
5211
5696
  process.exit(1);
5212
5697
  });