@syndicalt/snow-cli 2.0.0 → 2.0.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.
- package/README.md +62 -1
- package/dist/index.js +230 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,6 +28,10 @@ A portable CLI for ServiceNow. Query tables, inspect schemas, edit and search sc
|
|
|
28
28
|
|
|
29
29
|
---
|
|
30
30
|
|
|
31
|
+
## Full Documentation
|
|
32
|
+
|
|
33
|
+
The full documentation is available on the [documentation page](https://syndicalt.github.io/snow_cli/). This README provides a quick overview and examples to get started.
|
|
34
|
+
|
|
31
35
|
## Installation
|
|
32
36
|
|
|
33
37
|
**From npm (when published):**
|
|
@@ -89,6 +93,9 @@ snow diff all --against test --scripts --scope x_myco_myapp
|
|
|
89
93
|
# 11. Run the full factory pipeline: plan → build → test → promote
|
|
90
94
|
snow factory "Build a hardware asset request app with approval workflow" --envs test,prod
|
|
91
95
|
|
|
96
|
+
# 11b. Run tests and auto-fix failures with the LLM optimization loop
|
|
97
|
+
snow factory "Create a Script Include named SLACalculator with risk level logic" --run-tests --optimize
|
|
98
|
+
|
|
92
99
|
# 12. Start an interactive session to build iteratively
|
|
93
100
|
snow ai chat
|
|
94
101
|
|
|
@@ -845,6 +852,12 @@ snow factory "Build an incident escalation app" --dry-run
|
|
|
845
852
|
# Generate and immediately run ATF tests
|
|
846
853
|
snow factory "Build a change approval workflow" --run-tests
|
|
847
854
|
|
|
855
|
+
# Generate ATF tests, run them, and auto-fix failures with the LLM (up to 3 retries)
|
|
856
|
+
snow factory "Create a priority calculator script include" --run-tests --optimize
|
|
857
|
+
|
|
858
|
+
# Same, but allow up to 5 fix iterations
|
|
859
|
+
snow factory "Create a priority calculator script include" --run-tests --optimize --max-retries 5
|
|
860
|
+
|
|
848
861
|
# Skip ATF test generation (faster)
|
|
849
862
|
snow factory "Add a business rule to auto-assign P1 incidents" --skip-tests
|
|
850
863
|
|
|
@@ -863,6 +876,8 @@ snow factory "" --list
|
|
|
863
876
|
| `--scope <prefix>` | Override the application scope prefix for all artifacts (e.g. `x_myco_myapp`) |
|
|
864
877
|
| `--skip-tests` | Skip ATF test generation |
|
|
865
878
|
| `--run-tests` | Execute the generated ATF test suite immediately after pushing |
|
|
879
|
+
| `--optimize` | After running tests, auto-fix failures via LLM feedback loop (implies `--run-tests`) |
|
|
880
|
+
| `--max-retries <n>` | Max optimization iterations when using `--optimize` (default: `3`) |
|
|
866
881
|
| `--dry-run` | Show the generated plan only — no builds, no deployments |
|
|
867
882
|
| `--resume <id>` | Resume a previous run from its checkpoint (see `--list`) |
|
|
868
883
|
| `--list` | Show recent factory runs and their component completion status |
|
|
@@ -886,7 +901,13 @@ snow factory "" --list
|
|
|
886
901
|
[4/N] Generating ATF tests (unless --skip-tests)
|
|
887
902
|
LLM generates a test suite with server-side script assertions
|
|
888
903
|
Tests pushed to the instance as sys_atf_test + sys_atf_step records
|
|
889
|
-
|
|
904
|
+
If --run-tests or --optimize: suite is executed via the CICD API
|
|
905
|
+
Pass/fail counts and per-test results are displayed
|
|
906
|
+
|
|
907
|
+
If --optimize and tests failed:
|
|
908
|
+
→ LLM receives the failing test names + error messages + artifact source
|
|
909
|
+
→ Generates corrected artifacts, re-pushes them, re-runs the suite
|
|
910
|
+
→ Repeats until all pass or --max-retries is exhausted
|
|
890
911
|
|
|
891
912
|
[5/N] Promoting to <env> (once per additional env in --envs)
|
|
892
913
|
Checks for pre-existing artifacts on the target
|
|
@@ -906,6 +927,40 @@ snow factory "" --resume abc12345 # resume from last successful phase
|
|
|
906
927
|
|
|
907
928
|
The resume prompt argument is ignored when `--resume` is provided — the original prompt and plan are loaded from the checkpoint.
|
|
908
929
|
|
|
930
|
+
#### Auto-optimization loop
|
|
931
|
+
|
|
932
|
+
When `--optimize` is set, the factory runs an automated fix cycle after the initial ATF test run. For each iteration:
|
|
933
|
+
|
|
934
|
+
1. Failing test names and error messages are collected from the `sys_atf_result` table
|
|
935
|
+
2. The current source code of every script-bearing artifact is extracted from the local manifest
|
|
936
|
+
3. A structured fix prompt is sent to the LLM — no questions, just a corrected build
|
|
937
|
+
4. Fixed artifacts are merged into the build (replaced by `type + name`), re-pushed to the instance
|
|
938
|
+
5. The existing ATF suite is re-run; if all tests pass, the loop exits early
|
|
939
|
+
|
|
940
|
+
```
|
|
941
|
+
[Optimize 1/3] 2 test(s) failing — asking LLM to fix...
|
|
942
|
+
────────────────────────────────────────────────
|
|
943
|
+
Patching:
|
|
944
|
+
~ script_include SLACalculator
|
|
945
|
+
Re-pushing fixed artifacts to instance...
|
|
946
|
+
Re-running ATF tests...
|
|
947
|
+
3/3 tests passed
|
|
948
|
+
✓ Test low risk level
|
|
949
|
+
✓ Test medium risk level
|
|
950
|
+
✓ Test high risk level
|
|
951
|
+
|
|
952
|
+
✔ All tests passing after optimization!
|
|
953
|
+
```
|
|
954
|
+
|
|
955
|
+
If failures persist after all retries, a warning is shown and the best-effort build is kept on disk.
|
|
956
|
+
|
|
957
|
+
**Requirements for ATF test execution:**
|
|
958
|
+
- The `sn_cicd` plugin must be active on the instance
|
|
959
|
+
- The authenticated user must have the `sn_cicd` role
|
|
960
|
+
- ATF must be enabled (`System ATF` → `Properties` → `Enable ATF`)
|
|
961
|
+
|
|
962
|
+
The optimization loop uses the same LLM provider as the build. Each iteration costs one LLM call, so `--max-retries 3` (default) means at most 3 additional calls beyond the initial build.
|
|
963
|
+
|
|
909
964
|
#### Example output
|
|
910
965
|
|
|
911
966
|
```
|
|
@@ -968,6 +1023,12 @@ The resume prompt argument is ignored when `--resume` is provided — the origin
|
|
|
968
1023
|
• Test manager notification trigger (2 steps)
|
|
969
1024
|
✓ Test suite created
|
|
970
1025
|
https://dev12345.service-now.com/nav_to.do?uri=sys_atf_test_suite.do?sys_id=...
|
|
1026
|
+
Running ATF tests...
|
|
1027
|
+
4/4 tests passed
|
|
1028
|
+
✓ Test employee record creation and field defaults
|
|
1029
|
+
✓ Test OnboardingUtils task generation
|
|
1030
|
+
✓ Test business rule fires on insert
|
|
1031
|
+
✓ Test manager notification trigger
|
|
971
1032
|
```
|
|
972
1033
|
|
|
973
1034
|
#### Permission-denied errors
|
package/dist/index.js
CHANGED
|
@@ -1075,18 +1075,83 @@ async function pushATFSuite(client, suite, stepConfigSysId) {
|
|
|
1075
1075
|
return { suiteSysId, suiteUrl, testCount: suite.tests.length, stepCount };
|
|
1076
1076
|
}
|
|
1077
1077
|
async function runATFSuite(client, suiteSysId) {
|
|
1078
|
-
const
|
|
1079
|
-
|
|
1078
|
+
const startRes = await client.post("/api/sn_cicd/testsuite/run", void 0, {
|
|
1079
|
+
params: { test_suite_sys_id: suiteSysId }
|
|
1080
|
+
});
|
|
1081
|
+
const resData = startRes;
|
|
1082
|
+
const inner = resData["result"] ?? resData;
|
|
1083
|
+
const links = inner["links"];
|
|
1084
|
+
const progressId = links?.["progress"]?.["id"] ?? links?.["results"]?.["id"];
|
|
1085
|
+
if (!progressId) {
|
|
1086
|
+
throw new Error(
|
|
1087
|
+
"CICD API did not return a progress link.\nRaw response: " + JSON.stringify(startRes, null, 2) + "\nEnsure the sn_cicd plugin is active and your user has the sn_cicd role."
|
|
1088
|
+
);
|
|
1089
|
+
}
|
|
1090
|
+
const resultsId = links?.["results"]?.["id"] ?? progressId;
|
|
1091
|
+
const TERMINAL = /* @__PURE__ */ new Set(["2", "3", "4"]);
|
|
1092
|
+
const POLL_MS = 5e3;
|
|
1093
|
+
const MAX_POLLS = 120;
|
|
1094
|
+
let done = false;
|
|
1095
|
+
let lastProgressRes = {};
|
|
1096
|
+
for (let i = 0; i < MAX_POLLS; i++) {
|
|
1097
|
+
await new Promise((resolve2) => setTimeout(resolve2, POLL_MS));
|
|
1098
|
+
const progress = await client.get(
|
|
1099
|
+
`/api/sn_cicd/progress/${progressId}`
|
|
1100
|
+
);
|
|
1101
|
+
lastProgressRes = progress["result"] ?? progress;
|
|
1102
|
+
if (TERMINAL.has(String(lastProgressRes["status"] ?? ""))) {
|
|
1103
|
+
done = true;
|
|
1104
|
+
break;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
if (!done) {
|
|
1108
|
+
throw new Error("ATF test suite timed out after 10 minutes");
|
|
1109
|
+
}
|
|
1110
|
+
const progressLinks = lastProgressRes["links"];
|
|
1111
|
+
const finalResultsId = progressLinks?.["results"]?.["id"] ?? resultsId;
|
|
1112
|
+
const resultsRes = await client.get(
|
|
1113
|
+
`/api/sn_cicd/testsuite/results/${finalResultsId}`
|
|
1114
|
+
);
|
|
1115
|
+
const resultData = resultsRes["result"] ?? resultsRes;
|
|
1116
|
+
const successCount = parseInt(String(resultData["rolledup_test_success_count"] ?? 0), 10);
|
|
1117
|
+
const failureCount = parseInt(String(resultData["rolledup_test_failure_count"] ?? 0), 10) + parseInt(String(resultData["rolledup_test_error_count"] ?? 0), 10);
|
|
1118
|
+
const skipCount = parseInt(String(resultData["rolledup_test_skip_count"] ?? 0), 10);
|
|
1119
|
+
const total = successCount + failureCount + skipCount;
|
|
1120
|
+
const suiteResultLinks = resultData["links"];
|
|
1121
|
+
const suiteResultSysId = suiteResultLinks?.["results"]?.["id"] ?? finalResultsId;
|
|
1122
|
+
let testResults = [];
|
|
1123
|
+
try {
|
|
1124
|
+
const rows = await client.queryTable("sys_atf_result", {
|
|
1125
|
+
sysparmQuery: `test_suite_result=${suiteResultSysId}`,
|
|
1126
|
+
sysparmFields: "test,status,output",
|
|
1127
|
+
sysparmLimit: 200,
|
|
1128
|
+
sysparmDisplayValue: true
|
|
1129
|
+
});
|
|
1130
|
+
testResults = rows.map((r) => {
|
|
1131
|
+
const testField = r["test"];
|
|
1132
|
+
const name = testField && typeof testField === "object" ? String(testField["display_value"] ?? "(unknown)") : String(testField ?? "(unknown)");
|
|
1133
|
+
const statusStr = String(r["status"] ?? "");
|
|
1134
|
+
return {
|
|
1135
|
+
name,
|
|
1136
|
+
status: statusStr === "success" || statusStr === "pass" ? "success" : "failure",
|
|
1137
|
+
message: r["output"] ? String(r["output"]) : void 0
|
|
1138
|
+
};
|
|
1139
|
+
});
|
|
1140
|
+
} catch {
|
|
1141
|
+
testResults = Array.from({ length: successCount }, (_, i) => ({
|
|
1142
|
+
name: `Test ${i + 1}`,
|
|
1143
|
+
status: "success"
|
|
1144
|
+
}));
|
|
1145
|
+
for (let i = 0; i < failureCount; i++) {
|
|
1146
|
+
testResults.push({ name: `Test ${successCount + i + 1}`, status: "failure" });
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1080
1149
|
return {
|
|
1081
|
-
status:
|
|
1082
|
-
passed:
|
|
1083
|
-
failed:
|
|
1084
|
-
total
|
|
1085
|
-
testResults
|
|
1086
|
-
name: t.test_name ?? "(unknown)",
|
|
1087
|
-
status: t.status ?? "unknown",
|
|
1088
|
-
message: t.message
|
|
1089
|
-
}))
|
|
1150
|
+
status: String(resultData["test_suite_status"] ?? resultData["status"] ?? "unknown"),
|
|
1151
|
+
passed: successCount,
|
|
1152
|
+
failed: failureCount,
|
|
1153
|
+
total,
|
|
1154
|
+
testResults
|
|
1090
1155
|
};
|
|
1091
1156
|
}
|
|
1092
1157
|
var ATF_SYSTEM_PROMPT;
|
|
@@ -5627,8 +5692,148 @@ Scope prefix for all custom names: ${plan.scope.prefix}`;
|
|
|
5627
5692
|
writeFileSync8(join7(compDir, `${base}.manifest.json`), JSON.stringify(build, null, 2), "utf-8");
|
|
5628
5693
|
return build;
|
|
5629
5694
|
}
|
|
5695
|
+
async function runOptimizationLoop(opts) {
|
|
5696
|
+
const { provider, client, atfSuiteSysId, maxRetries, runDir } = opts;
|
|
5697
|
+
let currentBuild = opts.build;
|
|
5698
|
+
let runResult = opts.initialRunResult;
|
|
5699
|
+
const { SN_SYSTEM_PROMPT: SN_SYSTEM_PROMPT2 } = await Promise.resolve().then(() => (init_sn_context(), sn_context_exports));
|
|
5700
|
+
for (let iteration = 1; iteration <= maxRetries; iteration++) {
|
|
5701
|
+
const failures = runResult.testResults.filter((t) => t.status !== "success");
|
|
5702
|
+
if (failures.length === 0) break;
|
|
5703
|
+
console.log();
|
|
5704
|
+
printDivider();
|
|
5705
|
+
console.log(` ${chalk13.bold.magenta(`[Optimize ${iteration}/${maxRetries}]`)} ${chalk13.red(`${failures.length} test(s) failing`)} \u2014 asking LLM to fix...`);
|
|
5706
|
+
printDivider();
|
|
5707
|
+
const failureLines = failures.map((t) => ` - "${t.name}": ${t.message ?? "no details"}`).join("\n");
|
|
5708
|
+
const artifactLines = currentBuild.artifacts.filter((a) => a.fields["script"] ?? a.fields["condition"]).map((a) => {
|
|
5709
|
+
const name = String(a.fields["name"] ?? "");
|
|
5710
|
+
const script = String(a.fields["script"] ?? "");
|
|
5711
|
+
const condition = String(a.fields["condition"] ?? "");
|
|
5712
|
+
let out = `[${a.type}: ${name}]`;
|
|
5713
|
+
if (script) out += `
|
|
5714
|
+
script:
|
|
5715
|
+
${script}`;
|
|
5716
|
+
if (condition) out += `
|
|
5717
|
+
condition:
|
|
5718
|
+
${condition}`;
|
|
5719
|
+
return out;
|
|
5720
|
+
}).join("\n\n---\n\n");
|
|
5721
|
+
const fixPrompt = `OPTIMIZATION LOOP \u2014 Iteration ${iteration} of ${maxRetries}
|
|
5722
|
+
|
|
5723
|
+
The following ATF tests failed after deploying the generated artifacts:
|
|
5724
|
+
|
|
5725
|
+
FAILING TESTS:
|
|
5726
|
+
${failureLines}
|
|
5727
|
+
|
|
5728
|
+
CURRENT ARTIFACT CODE:
|
|
5729
|
+
${artifactLines}
|
|
5730
|
+
|
|
5731
|
+
Your task: analyze the failures, identify root causes, and return corrected artifacts.
|
|
5732
|
+
Rules:
|
|
5733
|
+
- Return ONLY a JSON object wrapped in a \`\`\`json code fence
|
|
5734
|
+
- Use the same schema: { "name": "...", "description": "...", "artifacts": [...] }
|
|
5735
|
+
- Only include artifacts that need changes
|
|
5736
|
+
- All scripts must be ES5 (var only, no arrow functions, no const/let)
|
|
5737
|
+
- Do NOT ask clarifying questions \u2014 provide the fix immediately`;
|
|
5738
|
+
const messages = [
|
|
5739
|
+
{ role: "system", content: SN_SYSTEM_PROMPT2 },
|
|
5740
|
+
{ role: "user", content: fixPrompt }
|
|
5741
|
+
];
|
|
5742
|
+
const llmSpinner = ora12(` Calling LLM for fix...`).start();
|
|
5743
|
+
let fixedArtifacts;
|
|
5744
|
+
try {
|
|
5745
|
+
const raw = await provider.complete(messages);
|
|
5746
|
+
llmSpinner.stop();
|
|
5747
|
+
const json = extractJSON(raw);
|
|
5748
|
+
const parsed = JSON.parse(json);
|
|
5749
|
+
const rawArtifacts = Array.isArray(parsed["artifacts"]) ? parsed["artifacts"] : [];
|
|
5750
|
+
fixedArtifacts = rawArtifacts.map((a) => {
|
|
5751
|
+
const art = a;
|
|
5752
|
+
const type = String(art["type"] ?? "");
|
|
5753
|
+
let fields;
|
|
5754
|
+
if (art["fields"] && typeof art["fields"] === "object" && !Array.isArray(art["fields"])) {
|
|
5755
|
+
fields = art["fields"];
|
|
5756
|
+
} else {
|
|
5757
|
+
const { type: _t, ...rest } = art;
|
|
5758
|
+
fields = rest;
|
|
5759
|
+
}
|
|
5760
|
+
return { type, fields };
|
|
5761
|
+
});
|
|
5762
|
+
} catch (err) {
|
|
5763
|
+
llmSpinner.fail(chalk13.yellow(` LLM call failed on iteration ${iteration} \u2014 stopping optimization`));
|
|
5764
|
+
console.error(chalk13.dim(` ${err instanceof Error ? err.message : String(err)}`));
|
|
5765
|
+
break;
|
|
5766
|
+
}
|
|
5767
|
+
const mergedArtifacts = currentBuild.artifacts.map((orig) => {
|
|
5768
|
+
const fix = fixedArtifacts.find(
|
|
5769
|
+
(f) => f.type === orig.type && String(f.fields["name"]) === String(orig.fields["name"])
|
|
5770
|
+
);
|
|
5771
|
+
return fix ?? orig;
|
|
5772
|
+
});
|
|
5773
|
+
const addedArtifacts = fixedArtifacts.filter(
|
|
5774
|
+
(f) => !currentBuild.artifacts.some(
|
|
5775
|
+
(orig) => orig.type === f.type && String(orig.fields["name"]) === String(f.fields["name"])
|
|
5776
|
+
)
|
|
5777
|
+
);
|
|
5778
|
+
const updatedBuild = {
|
|
5779
|
+
...currentBuild,
|
|
5780
|
+
artifacts: [...mergedArtifacts, ...addedArtifacts]
|
|
5781
|
+
};
|
|
5782
|
+
const changed = fixedArtifacts.filter(
|
|
5783
|
+
(f) => currentBuild.artifacts.some(
|
|
5784
|
+
(orig) => orig.type === f.type && String(orig.fields["name"]) === String(f.fields["name"])
|
|
5785
|
+
)
|
|
5786
|
+
);
|
|
5787
|
+
if (changed.length > 0) {
|
|
5788
|
+
console.log(` ${chalk13.dim("Patching:")}`);
|
|
5789
|
+
for (const a of changed) {
|
|
5790
|
+
console.log(` ${chalk13.yellow("~")} ${chalk13.cyan(a.type.padEnd(16))} ${String(a.fields["name"] ?? "")}`);
|
|
5791
|
+
}
|
|
5792
|
+
}
|
|
5793
|
+
const pushSpinner = ora12(` Re-pushing fixed artifacts to instance...`).start();
|
|
5794
|
+
try {
|
|
5795
|
+
await pushArtifacts(client, updatedBuild);
|
|
5796
|
+
pushSpinner.stop();
|
|
5797
|
+
} catch (err) {
|
|
5798
|
+
pushSpinner.fail(chalk13.yellow("Re-push failed \u2014 stopping optimization"));
|
|
5799
|
+
console.error(chalk13.dim(` ${err instanceof Error ? err.message : String(err)}`));
|
|
5800
|
+
break;
|
|
5801
|
+
}
|
|
5802
|
+
const base = slugify2(updatedBuild.name);
|
|
5803
|
+
writeFileSync8(join7(runDir, `${base}.xml`), generateUpdateSetXML(updatedBuild), "utf-8");
|
|
5804
|
+
writeFileSync8(join7(runDir, `${base}.manifest.json`), JSON.stringify(updatedBuild, null, 2), "utf-8");
|
|
5805
|
+
const testSpinner = ora12(` Re-running ATF tests...`).start();
|
|
5806
|
+
try {
|
|
5807
|
+
runResult = await runATFSuite(client, atfSuiteSysId);
|
|
5808
|
+
testSpinner.stop();
|
|
5809
|
+
} catch (err) {
|
|
5810
|
+
testSpinner.fail(chalk13.yellow("ATF re-run failed \u2014 stopping optimization"));
|
|
5811
|
+
console.error(chalk13.dim(` ${err instanceof Error ? err.message : String(err)}`));
|
|
5812
|
+
break;
|
|
5813
|
+
}
|
|
5814
|
+
const passColor = runResult.failed === 0 ? chalk13.green : chalk13.yellow;
|
|
5815
|
+
console.log(` ${passColor(`${runResult.passed}/${runResult.total} tests passed`)}`);
|
|
5816
|
+
for (const t of runResult.testResults) {
|
|
5817
|
+
const icon = t.status === "success" ? chalk13.green("\u2713") : chalk13.red("\u2717");
|
|
5818
|
+
const msg = t.message ? chalk13.dim(` (${t.message})`) : "";
|
|
5819
|
+
console.log(` ${icon} ${t.name}${msg}`);
|
|
5820
|
+
}
|
|
5821
|
+
currentBuild = updatedBuild;
|
|
5822
|
+
if (runResult.failed === 0) {
|
|
5823
|
+
console.log();
|
|
5824
|
+
console.log(chalk13.green(" \u2714 All tests passing after optimization!"));
|
|
5825
|
+
break;
|
|
5826
|
+
}
|
|
5827
|
+
}
|
|
5828
|
+
if (runResult.failed > 0) {
|
|
5829
|
+
console.log();
|
|
5830
|
+
console.log(chalk13.yellow(` \u26A0 ${runResult.failed} test(s) still failing after ${maxRetries} optimization iteration(s).`));
|
|
5831
|
+
console.log(chalk13.dim(" Review the ATF suite in ServiceNow for details."));
|
|
5832
|
+
}
|
|
5833
|
+
return currentBuild;
|
|
5834
|
+
}
|
|
5630
5835
|
function factoryCommand() {
|
|
5631
|
-
const cmd = new Command13("factory").description("AI-orchestrated multi-component ServiceNow app pipeline: plan \u2192 build \u2192 test \u2192 promote").argument("<prompt>", "Natural language description of the application to build").option("--envs <aliases>", "Comma-separated instance aliases to deploy to in order (default: active instance only)", "").option("--scope <prefix>", "Override the application scope prefix (e.g. x_myco_myapp)").option("--skip-tests", "Skip ATF test generation").option("--run-tests", "Execute ATF tests on the source instance after generating them").option("--dry-run", "Show the plan only \u2014 do not build or deploy").option("--resume <id>", "Resume a previous factory run from its checkpoint").option("--list", "List recent factory runs and their status").action(async (prompt, opts) => {
|
|
5836
|
+
const cmd = new Command13("factory").description("AI-orchestrated multi-component ServiceNow app pipeline: plan \u2192 build \u2192 test \u2192 promote").argument("<prompt>", "Natural language description of the application to build").option("--envs <aliases>", "Comma-separated instance aliases to deploy to in order (default: active instance only)", "").option("--scope <prefix>", "Override the application scope prefix (e.g. x_myco_myapp)").option("--skip-tests", "Skip ATF test generation").option("--run-tests", "Execute ATF tests on the source instance after generating them").option("--optimize", "Auto-fix failing ATF tests via LLM feedback loop (implies --run-tests)").option("--max-retries <n>", "Max optimization iterations (used with --optimize)", "3").option("--dry-run", "Show the plan only \u2014 do not build or deploy").option("--resume <id>", "Resume a previous factory run from its checkpoint").option("--list", "List recent factory runs and their status").action(async (prompt, opts) => {
|
|
5632
5837
|
if (opts.list) {
|
|
5633
5838
|
const runs = listCheckpoints();
|
|
5634
5839
|
if (runs.length === 0) {
|
|
@@ -5819,7 +6024,7 @@ function factoryCommand() {
|
|
|
5819
6024
|
console.log(` ${chalk13.green("+")} ${chalk13.cyan(a.type.padEnd(16))} ${name}`);
|
|
5820
6025
|
}
|
|
5821
6026
|
}
|
|
5822
|
-
|
|
6027
|
+
let combinedBuild = {
|
|
5823
6028
|
name: plan.name,
|
|
5824
6029
|
description: plan.description,
|
|
5825
6030
|
scope: plan.scope,
|
|
@@ -5906,7 +6111,7 @@ function factoryCommand() {
|
|
|
5906
6111
|
console.error(chalk13.dim(` ${err instanceof Error ? err.message : String(err)}`));
|
|
5907
6112
|
atfResult = null;
|
|
5908
6113
|
}
|
|
5909
|
-
if (opts.runTests && atfResult) {
|
|
6114
|
+
if ((opts.runTests || opts.optimize) && atfResult) {
|
|
5910
6115
|
const runSpinner = ora12(" Running ATF tests...").start();
|
|
5911
6116
|
try {
|
|
5912
6117
|
const runResult = await runATFSuite(sourceClient, atfResult.suiteSysId);
|
|
@@ -5918,6 +6123,17 @@ function factoryCommand() {
|
|
|
5918
6123
|
const msg = t.message ? chalk13.dim(` (${t.message})`) : "";
|
|
5919
6124
|
console.log(` ${icon} ${t.name}${msg}`);
|
|
5920
6125
|
}
|
|
6126
|
+
if (opts.optimize && runResult.failed > 0) {
|
|
6127
|
+
combinedBuild = await runOptimizationLoop({
|
|
6128
|
+
provider,
|
|
6129
|
+
client: sourceClient,
|
|
6130
|
+
build: combinedBuild,
|
|
6131
|
+
atfSuiteSysId: atfResult.suiteSysId,
|
|
6132
|
+
initialRunResult: runResult,
|
|
6133
|
+
maxRetries: parseInt(opts.maxRetries ?? "3", 10),
|
|
6134
|
+
runDir
|
|
6135
|
+
});
|
|
6136
|
+
}
|
|
5921
6137
|
} catch (err) {
|
|
5922
6138
|
runSpinner.fail(chalk13.yellow("ATF run failed (non-fatal)"));
|
|
5923
6139
|
console.error(chalk13.dim(` ${err instanceof Error ? err.message : String(err)}`));
|