pai-zero 0.13.1 → 0.13.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/bin/pai.js CHANGED
@@ -4706,6 +4706,200 @@ var init_environment = __esm({
4706
4706
  }
4707
4707
  });
4708
4708
 
4709
+ // src/stages/validation/runner.ts
4710
+ import { join as join7 } from "path";
4711
+ import fs15 from "fs-extra";
4712
+ async function runTests(cwd) {
4713
+ const start = Date.now();
4714
+ const gstackPath = join7(cwd, ".pai", "gstack.json");
4715
+ let runner = "npm test";
4716
+ if (await fs15.pathExists(gstackPath)) {
4717
+ try {
4718
+ const config = await fs15.readJson(gstackPath);
4719
+ if (config.testRunner === "vitest") runner = "npx vitest run";
4720
+ else if (config.testRunner === "jest") runner = "npx jest";
4721
+ else if (config.testRunner === "mocha") runner = "npx mocha";
4722
+ } catch {
4723
+ }
4724
+ }
4725
+ const pkgPath = join7(cwd, "package.json");
4726
+ if (await fs15.pathExists(pkgPath)) {
4727
+ try {
4728
+ const pkg5 = await fs15.readJson(pkgPath);
4729
+ if (!pkg5.scripts?.test || pkg5.scripts.test.includes("no test specified")) {
4730
+ return {
4731
+ runner,
4732
+ passed: false,
4733
+ output: "\uD14C\uC2A4\uD2B8 \uC2A4\uD06C\uB9BD\uD2B8\uAC00 \uC815\uC758\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. package.json\uC758 scripts.test\uB97C \uC124\uC815\uD558\uC138\uC694.",
4734
+ duration: Date.now() - start
4735
+ };
4736
+ }
4737
+ } catch {
4738
+ }
4739
+ }
4740
+ try {
4741
+ const { execa } = await import("execa");
4742
+ const { stdout, stderr } = await execa("npm", ["test"], {
4743
+ cwd,
4744
+ timeout: 12e4,
4745
+ env: { ...process.env, CI: "true" }
4746
+ });
4747
+ return {
4748
+ runner,
4749
+ passed: true,
4750
+ output: stdout || stderr,
4751
+ duration: Date.now() - start
4752
+ };
4753
+ } catch (err) {
4754
+ const output = err instanceof Error ? err.message : String(err);
4755
+ return {
4756
+ runner,
4757
+ passed: false,
4758
+ output,
4759
+ duration: Date.now() - start
4760
+ };
4761
+ }
4762
+ }
4763
+ var init_runner = __esm({
4764
+ "src/stages/validation/runner.ts"() {
4765
+ "use strict";
4766
+ }
4767
+ });
4768
+
4769
+ // src/stages/validation/harness.ts
4770
+ import { join as join8 } from "path";
4771
+ import fs16 from "fs-extra";
4772
+ async function runHarnessCheck(cwd) {
4773
+ const harnessPath = join8(cwd, ".pai", "harness.json");
4774
+ if (!await fs16.pathExists(harnessPath)) {
4775
+ return { enabled: false, specFile: null, rules: [], checks: [] };
4776
+ }
4777
+ let config;
4778
+ try {
4779
+ config = await fs16.readJson(harnessPath);
4780
+ } catch {
4781
+ return { enabled: false, specFile: null, rules: [], checks: [] };
4782
+ }
4783
+ const specFile = config.specFile ?? "docs/openspec.md";
4784
+ const rules = config.rules ?? [];
4785
+ const checks = [];
4786
+ if (rules.includes("spec-implementation-match")) {
4787
+ const specExists = await fs16.pathExists(join8(cwd, specFile));
4788
+ const srcExists = await fs16.pathExists(join8(cwd, "src"));
4789
+ checks.push({
4790
+ rule: "spec-implementation-match",
4791
+ passed: specExists && srcExists,
4792
+ detail: specExists && srcExists ? "\uC124\uACC4 \uBB38\uC11C\uC640 \uC18C\uC2A4 \uB514\uB809\uD1A0\uB9AC \uC874\uC7AC" : `${!specExists ? specFile + " \uC5C6\uC74C" : ""} ${!srcExists ? "src/ \uC5C6\uC74C" : ""}`.trim()
4793
+ });
4794
+ }
4795
+ if (rules.includes("api-contract-test")) {
4796
+ const testDir = await fs16.pathExists(join8(cwd, "tests"));
4797
+ const testDir2 = await fs16.pathExists(join8(cwd, "test"));
4798
+ checks.push({
4799
+ rule: "api-contract-test",
4800
+ passed: testDir || testDir2,
4801
+ detail: testDir || testDir2 ? "\uD14C\uC2A4\uD2B8 \uB514\uB809\uD1A0\uB9AC \uC874\uC7AC" : "\uD14C\uC2A4\uD2B8 \uB514\uB809\uD1A0\uB9AC(tests/ \uB610\uB294 test/) \uC5C6\uC74C"
4802
+ });
4803
+ }
4804
+ return { enabled: true, specFile, rules, checks };
4805
+ }
4806
+ var init_harness = __esm({
4807
+ "src/stages/validation/harness.ts"() {
4808
+ "use strict";
4809
+ }
4810
+ });
4811
+
4812
+ // src/cli/commands/validate.cmd.ts
4813
+ var validate_cmd_exports = {};
4814
+ __export(validate_cmd_exports, {
4815
+ handleRobocoCliError: () => handleRobocoError,
4816
+ validateCommand: () => validateCommand
4817
+ });
4818
+ async function validateCommand(cwd, options = {}) {
4819
+ try {
4820
+ await withRoboco(
4821
+ cwd,
4822
+ "validation",
4823
+ "pai test",
4824
+ { skipGates: options.skipGates, force: options.force, gate: STAGE_GATES.validation },
4825
+ async () => {
4826
+ section("\uD14C\uC2A4\uD2B8 \uC2E4\uD589");
4827
+ const testResult = await runTests(cwd);
4828
+ if (testResult.passed) {
4829
+ success(`\uD14C\uC2A4\uD2B8 \uD1B5\uACFC (${testResult.runner}, ${testResult.duration}ms)`);
4830
+ } else {
4831
+ error("\uD14C\uC2A4\uD2B8 \uC2E4\uD328");
4832
+ info(testResult.output.slice(0, 300));
4833
+ }
4834
+ section("\uD558\uB124\uC2A4 \uAC80\uC99D");
4835
+ const harness = await runHarnessCheck(cwd);
4836
+ if (!harness.enabled) {
4837
+ info("Harness \uC124\uC815 \uC5C6\uC74C \u2014 \uAC74\uB108\uB700");
4838
+ info("\uC124\uC815 \uCD94\uAC00: `pai add` \uC5D0\uC11C Harness Engineering \uC120\uD0DD");
4839
+ } else {
4840
+ for (const check of harness.checks) {
4841
+ if (check.passed) {
4842
+ success(`${check.rule}: ${check.detail}`);
4843
+ } else {
4844
+ warn(`${check.rule}: ${check.detail}`);
4845
+ }
4846
+ }
4847
+ }
4848
+ const allPassed = testResult.passed && harness.checks.every((c2) => c2.passed);
4849
+ console.log("");
4850
+ if (allPassed) {
4851
+ success("\uAC80\uC99D \uD1B5\uACFC!");
4852
+ } else if (!testResult.passed) {
4853
+ error("\uAC80\uC99D \uC2E4\uD328 \u2014 \uD14C\uC2A4\uD2B8\uB97C \uC218\uC815\uD558\uC138\uC694.");
4854
+ throw new Error("tests-failed");
4855
+ } else {
4856
+ warn("\uBD80\uBD84 \uD1B5\uACFC \u2014 \uD558\uB124\uC2A4 \uAC80\uC99D \uD56D\uBAA9\uC744 \uD655\uC778\uD558\uC138\uC694.");
4857
+ }
4858
+ }
4859
+ );
4860
+ await syncHandoff(cwd).catch(() => {
4861
+ });
4862
+ } catch (err) {
4863
+ handleRobocoError(err);
4864
+ }
4865
+ }
4866
+ function handleRobocoError(err) {
4867
+ if (err instanceof RobocoLockError) {
4868
+ error(err.message);
4869
+ process.exitCode = 1;
4870
+ return;
4871
+ }
4872
+ if (err instanceof GateFailedError) {
4873
+ error(`\uB2E8\uACC4 \uC9C4\uC785 \uC2E4\uD328 (${err.result.name})`);
4874
+ for (const v of err.result.violations) {
4875
+ warn(` \xB7 ${v.message}${v.location ? ` [${v.location}]` : ""}`);
4876
+ }
4877
+ hint("\uC6B0\uD68C \uC635\uC158: --skip-gates (\uACBD\uACE0\uB9CC) / --force (\uAC8C\uC774\uD2B8 \uBB34\uC2DC)");
4878
+ process.exitCode = 1;
4879
+ return;
4880
+ }
4881
+ if (err instanceof Error) {
4882
+ if (err.message === "tests-failed") {
4883
+ process.exitCode = 1;
4884
+ return;
4885
+ }
4886
+ error(err.message);
4887
+ process.exitCode = 1;
4888
+ return;
4889
+ }
4890
+ throw err;
4891
+ }
4892
+ var init_validate_cmd = __esm({
4893
+ "src/cli/commands/validate.cmd.ts"() {
4894
+ "use strict";
4895
+ init_ui();
4896
+ init_runner();
4897
+ init_harness();
4898
+ init_roboco();
4899
+ init_gates();
4900
+ }
4901
+ });
4902
+
4709
4903
  // src/stages/evaluation/prompts/analyze.ts
4710
4904
  var analyze_exports = {};
4711
4905
  __export(analyze_exports, {
@@ -4762,8 +4956,8 @@ var analyzer_exports2 = {};
4762
4956
  __export(analyzer_exports2, {
4763
4957
  analyzeRepository: () => analyzeRepository
4764
4958
  });
4765
- import { join as join7 } from "path";
4766
- import fs15 from "fs-extra";
4959
+ import { join as join9 } from "path";
4960
+ import fs17 from "fs-extra";
4767
4961
  async function analyzeRepository(repoPath) {
4768
4962
  try {
4769
4963
  return await aiAnalysis(repoPath);
@@ -4824,14 +5018,14 @@ async function checkTestCoverage(repoPath) {
4824
5018
  ".nycrc"
4825
5019
  ];
4826
5020
  for (const f of testConfigs) {
4827
- const found = await fs15.pathExists(join7(repoPath, f));
5021
+ const found = await fs17.pathExists(join9(repoPath, f));
4828
5022
  findings.push({ item: f, found, details: found ? "\uC874\uC7AC" : "\uC5C6\uC74C" });
4829
5023
  if (found) score += 20;
4830
5024
  }
4831
5025
  const testDirs = ["tests", "test", "__tests__", "spec"];
4832
5026
  let hasTestDir = false;
4833
5027
  for (const d of testDirs) {
4834
- if (await fs15.pathExists(join7(repoPath, d))) {
5028
+ if (await fs17.pathExists(join9(repoPath, d))) {
4835
5029
  findings.push({ item: d, found: true, details: "\uD14C\uC2A4\uD2B8 \uB514\uB809\uD1A0\uB9AC \uC874\uC7AC" });
4836
5030
  hasTestDir = true;
4837
5031
  score += 30;
@@ -4855,7 +5049,7 @@ async function checkCiCd(repoPath) {
4855
5049
  { path: ".circleci", label: "CircleCI" }
4856
5050
  ];
4857
5051
  for (const { path: path10, label } of ciConfigs) {
4858
- const found = await fs15.pathExists(join7(repoPath, path10));
5052
+ const found = await fs17.pathExists(join9(repoPath, path10));
4859
5053
  findings.push({ item: label, found, details: found ? "\uC874\uC7AC" : "\uC5C6\uC74C" });
4860
5054
  if (found) score += 40;
4861
5055
  }
@@ -4874,7 +5068,7 @@ async function checkHooks(repoPath) {
4874
5068
  { path: ".claude/settings.json", label: "Claude Code settings" }
4875
5069
  ];
4876
5070
  for (const { path: path10, label } of hookConfigs) {
4877
- const found = await fs15.pathExists(join7(repoPath, path10));
5071
+ const found = await fs17.pathExists(join9(repoPath, path10));
4878
5072
  findings.push({ item: label, found, details: found ? "\uC874\uC7AC" : "\uC5C6\uC74C" });
4879
5073
  if (found) score += 20;
4880
5074
  }
@@ -4892,7 +5086,7 @@ async function checkRepoStructure(repoPath) {
4892
5086
  { path: ".gitignore", label: ".gitignore" }
4893
5087
  ];
4894
5088
  for (const { path: path10, label } of structureChecks) {
4895
- const found = await fs15.pathExists(join7(repoPath, path10));
5089
+ const found = await fs17.pathExists(join9(repoPath, path10));
4896
5090
  findings.push({ item: label, found, details: found ? "\uC874\uC7AC" : "\uC5C6\uC74C" });
4897
5091
  if (found) score += 25;
4898
5092
  }
@@ -4909,7 +5103,7 @@ async function checkDocumentation(repoPath) {
4909
5103
  { path: "docs/openspec.md", label: "OpenSpec PRD", points: 25 }
4910
5104
  ];
4911
5105
  for (const { path: path10, label, points } of docChecks) {
4912
- const found = await fs15.pathExists(join7(repoPath, path10));
5106
+ const found = await fs17.pathExists(join9(repoPath, path10));
4913
5107
  findings.push({ item: label, found, details: found ? "\uC874\uC7AC" : "\uC5C6\uC74C" });
4914
5108
  if (found) score += points;
4915
5109
  }
@@ -4928,7 +5122,7 @@ async function checkHarnessEngineering(repoPath) {
4928
5122
  { path: ".pai/config.json", label: "PAI config", points: 10 }
4929
5123
  ];
4930
5124
  for (const { path: path10, label, points } of harnessChecks) {
4931
- const found = await fs15.pathExists(join7(repoPath, path10));
5125
+ const found = await fs17.pathExists(join9(repoPath, path10));
4932
5126
  findings.push({ item: label, found, details: found ? "\uC874\uC7AC" : "\uC5C6\uC74C" });
4933
5127
  if (found) score += points;
4934
5128
  }
@@ -5311,56 +5505,56 @@ __export(shell_cd_exports, {
5311
5505
  installShellHelper: () => installShellHelper,
5312
5506
  requestCdAfter: () => requestCdAfter
5313
5507
  });
5314
- import { join as join8 } from "path";
5508
+ import { join as join10 } from "path";
5315
5509
  import { homedir as homedir2 } from "os";
5316
- import fs16 from "fs-extra";
5510
+ import fs18 from "fs-extra";
5317
5511
  async function requestCdAfter(targetDir) {
5318
- await fs16.ensureDir(PAI_DIR);
5319
- await fs16.writeFile(CD_FILE, targetDir);
5512
+ await fs18.ensureDir(PAI_DIR);
5513
+ await fs18.writeFile(CD_FILE, targetDir);
5320
5514
  }
5321
5515
  async function installShellHelper() {
5322
- await fs16.ensureDir(PAI_DIR);
5516
+ await fs18.ensureDir(PAI_DIR);
5323
5517
  if (isWindows) {
5324
5518
  return installPowerShellHelper();
5325
5519
  }
5326
5520
  return installBashHelper();
5327
5521
  }
5328
5522
  async function installBashHelper() {
5329
- await fs16.writeFile(HELPER_FILE_SH, BASH_HELPER);
5523
+ await fs18.writeFile(HELPER_FILE_SH, BASH_HELPER);
5330
5524
  const rcFile = getShellRcPath();
5331
5525
  const sourceLine = 'source "$HOME/.pai/shell-helper.sh"';
5332
- if (await fs16.pathExists(rcFile)) {
5333
- const content = await fs16.readFile(rcFile, "utf8");
5526
+ if (await fs18.pathExists(rcFile)) {
5527
+ const content = await fs18.readFile(rcFile, "utf8");
5334
5528
  if (content.includes("shell-helper.sh")) {
5335
5529
  return true;
5336
5530
  }
5337
- await fs16.appendFile(rcFile, `
5531
+ await fs18.appendFile(rcFile, `
5338
5532
  # PAI \u2014 \uC790\uB3D9 \uB514\uB809\uD1A0\uB9AC \uC774\uB3D9
5339
5533
  ${sourceLine}
5340
5534
  `);
5341
5535
  return false;
5342
5536
  }
5343
- await fs16.writeFile(rcFile, `${sourceLine}
5537
+ await fs18.writeFile(rcFile, `${sourceLine}
5344
5538
  `);
5345
5539
  return false;
5346
5540
  }
5347
5541
  async function installPowerShellHelper() {
5348
- await fs16.writeFile(HELPER_FILE_PS1, POWERSHELL_HELPER);
5542
+ await fs18.writeFile(HELPER_FILE_PS1, POWERSHELL_HELPER);
5349
5543
  const rcFile = getShellRcPath();
5350
5544
  const sourceLine = '. "$env:USERPROFILE\\.pai\\shell-helper.ps1"';
5351
- await fs16.ensureDir(join8(rcFile, ".."));
5352
- if (await fs16.pathExists(rcFile)) {
5353
- const content = await fs16.readFile(rcFile, "utf8");
5545
+ await fs18.ensureDir(join10(rcFile, ".."));
5546
+ if (await fs18.pathExists(rcFile)) {
5547
+ const content = await fs18.readFile(rcFile, "utf8");
5354
5548
  if (content.includes("shell-helper.ps1")) {
5355
5549
  return true;
5356
5550
  }
5357
- await fs16.appendFile(rcFile, `
5551
+ await fs18.appendFile(rcFile, `
5358
5552
  # PAI \u2014 \uC790\uB3D9 \uB514\uB809\uD1A0\uB9AC \uC774\uB3D9
5359
5553
  ${sourceLine}
5360
5554
  `);
5361
5555
  return false;
5362
5556
  }
5363
- await fs16.writeFile(rcFile, `${sourceLine}
5557
+ await fs18.writeFile(rcFile, `${sourceLine}
5364
5558
  `);
5365
5559
  return false;
5366
5560
  }
@@ -5369,10 +5563,10 @@ var init_shell_cd = __esm({
5369
5563
  "src/utils/shell-cd.ts"() {
5370
5564
  "use strict";
5371
5565
  init_platform();
5372
- PAI_DIR = join8(homedir2(), ".pai");
5373
- CD_FILE = join8(PAI_DIR, ".cd-after");
5374
- HELPER_FILE_SH = join8(PAI_DIR, "shell-helper.sh");
5375
- HELPER_FILE_PS1 = join8(PAI_DIR, "shell-helper.ps1");
5566
+ PAI_DIR = join10(homedir2(), ".pai");
5567
+ CD_FILE = join10(PAI_DIR, ".cd-after");
5568
+ HELPER_FILE_SH = join10(PAI_DIR, "shell-helper.sh");
5569
+ HELPER_FILE_PS1 = join10(PAI_DIR, "shell-helper.ps1");
5376
5570
  BASH_HELPER = `# PAI shell helper \u2014 \uC790\uB3D9 \uB514\uB809\uD1A0\uB9AC \uC774\uB3D9 \uC9C0\uC6D0
5377
5571
  pai() {
5378
5572
  local cd_target="$HOME/.pai/.cd-after"
@@ -5424,7 +5618,7 @@ __export(claude_settings_exports, {
5424
5618
  });
5425
5619
  import os3 from "os";
5426
5620
  import path9 from "path";
5427
- import fs17 from "fs-extra";
5621
+ import fs19 from "fs-extra";
5428
5622
  function getClaudeSettingsPath(homeDir = os3.homedir()) {
5429
5623
  return path9.join(homeDir, ".claude", "settings.json");
5430
5624
  }
@@ -5441,13 +5635,13 @@ async function enableOmcPlugin(options = {}) {
5441
5635
  const pluginId = options.pluginId ?? DEFAULT_PLUGIN_ID;
5442
5636
  const wantBackup = options.backup ?? true;
5443
5637
  const settingsPath = getClaudeSettingsPath();
5444
- await fs17.ensureDir(path9.dirname(settingsPath));
5445
- if (!await fs17.pathExists(settingsPath)) {
5638
+ await fs19.ensureDir(path9.dirname(settingsPath));
5639
+ if (!await fs19.pathExists(settingsPath)) {
5446
5640
  const skeleton = buildSkeleton(marketplaceId, marketplaceUrl, pluginId);
5447
- await fs17.writeFile(settingsPath, JSON.stringify(skeleton, null, 2) + "\n", "utf8");
5641
+ await fs19.writeFile(settingsPath, JSON.stringify(skeleton, null, 2) + "\n", "utf8");
5448
5642
  return { action: "created", settingsPath };
5449
5643
  }
5450
- const raw = await fs17.readFile(settingsPath, "utf8");
5644
+ const raw = await fs19.readFile(settingsPath, "utf8");
5451
5645
  let parsed;
5452
5646
  try {
5453
5647
  const value = parseJsonWithBom(raw);
@@ -5457,7 +5651,7 @@ async function enableOmcPlugin(options = {}) {
5457
5651
  parsed = value;
5458
5652
  } catch (err) {
5459
5653
  const backupPath2 = `${settingsPath}.backup-${timestampSuffix()}`;
5460
- await fs17.copy(settingsPath, backupPath2);
5654
+ await fs19.copy(settingsPath, backupPath2);
5461
5655
  throw new ClaudeSettingsError(
5462
5656
  `settings.json \uD30C\uC2F1 \uC2E4\uD328: ${err.message}. \uBC31\uC5C5: ${backupPath2}`,
5463
5657
  backupPath2
@@ -5469,10 +5663,10 @@ async function enableOmcPlugin(options = {}) {
5469
5663
  let backupPath;
5470
5664
  if (wantBackup) {
5471
5665
  backupPath = `${settingsPath}.backup-${timestampSuffix()}`;
5472
- await fs17.copy(settingsPath, backupPath);
5666
+ await fs19.copy(settingsPath, backupPath);
5473
5667
  }
5474
5668
  const merged = mergeOmcIntoSettings(parsed, marketplaceId, marketplaceUrl, pluginId);
5475
- await fs17.writeFile(settingsPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
5669
+ await fs19.writeFile(settingsPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
5476
5670
  return { action: "added", settingsPath, backupPath };
5477
5671
  }
5478
5672
  function buildSkeleton(marketplaceId, marketplaceUrl, pluginId) {
@@ -5527,12 +5721,12 @@ var init_claude_settings = __esm({
5527
5721
 
5528
5722
  // src/stages/evaluation/cache.ts
5529
5723
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
5530
- import { join as join9 } from "path";
5724
+ import { join as join11 } from "path";
5531
5725
  import { createHash } from "crypto";
5532
5726
  function computeRepoHash(repoPath) {
5533
5727
  const hash = createHash("sha256");
5534
5728
  for (const file of FILES_TO_HASH) {
5535
- const fullPath = join9(repoPath, file);
5729
+ const fullPath = join11(repoPath, file);
5536
5730
  try {
5537
5731
  const content = readFileSync(fullPath);
5538
5732
  hash.update(`${file}:${content.length}`);
@@ -5543,7 +5737,7 @@ function computeRepoHash(repoPath) {
5543
5737
  return hash.digest("hex").slice(0, 16);
5544
5738
  }
5545
5739
  function getCachePath(repoPath) {
5546
- return join9(repoPath, CACHE_DIR, CACHE_FILE);
5740
+ return join11(repoPath, CACHE_DIR, CACHE_FILE);
5547
5741
  }
5548
5742
  function loadCache(repoPath) {
5549
5743
  try {
@@ -5554,7 +5748,7 @@ function loadCache(repoPath) {
5554
5748
  }
5555
5749
  }
5556
5750
  function saveCache(repoPath, store) {
5557
- const cacheDir = join9(repoPath, CACHE_DIR, "cache");
5751
+ const cacheDir = join11(repoPath, CACHE_DIR, "cache");
5558
5752
  if (!existsSync(cacheDir)) {
5559
5753
  mkdirSync(cacheDir, { recursive: true });
5560
5754
  }
@@ -5608,200 +5802,6 @@ var init_cache = __esm({
5608
5802
  }
5609
5803
  });
5610
5804
 
5611
- // src/stages/validation/runner.ts
5612
- import { join as join10 } from "path";
5613
- import fs18 from "fs-extra";
5614
- async function runTests(cwd) {
5615
- const start = Date.now();
5616
- const gstackPath = join10(cwd, ".pai", "gstack.json");
5617
- let runner = "npm test";
5618
- if (await fs18.pathExists(gstackPath)) {
5619
- try {
5620
- const config = await fs18.readJson(gstackPath);
5621
- if (config.testRunner === "vitest") runner = "npx vitest run";
5622
- else if (config.testRunner === "jest") runner = "npx jest";
5623
- else if (config.testRunner === "mocha") runner = "npx mocha";
5624
- } catch {
5625
- }
5626
- }
5627
- const pkgPath = join10(cwd, "package.json");
5628
- if (await fs18.pathExists(pkgPath)) {
5629
- try {
5630
- const pkg5 = await fs18.readJson(pkgPath);
5631
- if (!pkg5.scripts?.test || pkg5.scripts.test.includes("no test specified")) {
5632
- return {
5633
- runner,
5634
- passed: false,
5635
- output: "\uD14C\uC2A4\uD2B8 \uC2A4\uD06C\uB9BD\uD2B8\uAC00 \uC815\uC758\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. package.json\uC758 scripts.test\uB97C \uC124\uC815\uD558\uC138\uC694.",
5636
- duration: Date.now() - start
5637
- };
5638
- }
5639
- } catch {
5640
- }
5641
- }
5642
- try {
5643
- const { execa } = await import("execa");
5644
- const { stdout, stderr } = await execa("npm", ["test"], {
5645
- cwd,
5646
- timeout: 12e4,
5647
- env: { ...process.env, CI: "true" }
5648
- });
5649
- return {
5650
- runner,
5651
- passed: true,
5652
- output: stdout || stderr,
5653
- duration: Date.now() - start
5654
- };
5655
- } catch (err) {
5656
- const output = err instanceof Error ? err.message : String(err);
5657
- return {
5658
- runner,
5659
- passed: false,
5660
- output,
5661
- duration: Date.now() - start
5662
- };
5663
- }
5664
- }
5665
- var init_runner = __esm({
5666
- "src/stages/validation/runner.ts"() {
5667
- "use strict";
5668
- }
5669
- });
5670
-
5671
- // src/stages/validation/harness.ts
5672
- import { join as join11 } from "path";
5673
- import fs19 from "fs-extra";
5674
- async function runHarnessCheck(cwd) {
5675
- const harnessPath = join11(cwd, ".pai", "harness.json");
5676
- if (!await fs19.pathExists(harnessPath)) {
5677
- return { enabled: false, specFile: null, rules: [], checks: [] };
5678
- }
5679
- let config;
5680
- try {
5681
- config = await fs19.readJson(harnessPath);
5682
- } catch {
5683
- return { enabled: false, specFile: null, rules: [], checks: [] };
5684
- }
5685
- const specFile = config.specFile ?? "docs/openspec.md";
5686
- const rules = config.rules ?? [];
5687
- const checks = [];
5688
- if (rules.includes("spec-implementation-match")) {
5689
- const specExists = await fs19.pathExists(join11(cwd, specFile));
5690
- const srcExists = await fs19.pathExists(join11(cwd, "src"));
5691
- checks.push({
5692
- rule: "spec-implementation-match",
5693
- passed: specExists && srcExists,
5694
- detail: specExists && srcExists ? "\uC124\uACC4 \uBB38\uC11C\uC640 \uC18C\uC2A4 \uB514\uB809\uD1A0\uB9AC \uC874\uC7AC" : `${!specExists ? specFile + " \uC5C6\uC74C" : ""} ${!srcExists ? "src/ \uC5C6\uC74C" : ""}`.trim()
5695
- });
5696
- }
5697
- if (rules.includes("api-contract-test")) {
5698
- const testDir = await fs19.pathExists(join11(cwd, "tests"));
5699
- const testDir2 = await fs19.pathExists(join11(cwd, "test"));
5700
- checks.push({
5701
- rule: "api-contract-test",
5702
- passed: testDir || testDir2,
5703
- detail: testDir || testDir2 ? "\uD14C\uC2A4\uD2B8 \uB514\uB809\uD1A0\uB9AC \uC874\uC7AC" : "\uD14C\uC2A4\uD2B8 \uB514\uB809\uD1A0\uB9AC(tests/ \uB610\uB294 test/) \uC5C6\uC74C"
5704
- });
5705
- }
5706
- return { enabled: true, specFile, rules, checks };
5707
- }
5708
- var init_harness = __esm({
5709
- "src/stages/validation/harness.ts"() {
5710
- "use strict";
5711
- }
5712
- });
5713
-
5714
- // src/cli/commands/validate.cmd.ts
5715
- var validate_cmd_exports = {};
5716
- __export(validate_cmd_exports, {
5717
- handleRobocoCliError: () => handleRobocoError,
5718
- validateCommand: () => validateCommand
5719
- });
5720
- async function validateCommand(cwd, options = {}) {
5721
- try {
5722
- await withRoboco(
5723
- cwd,
5724
- "validation",
5725
- "pai test",
5726
- { skipGates: options.skipGates, force: options.force, gate: STAGE_GATES.validation },
5727
- async () => {
5728
- section("\uD14C\uC2A4\uD2B8 \uC2E4\uD589");
5729
- const testResult = await runTests(cwd);
5730
- if (testResult.passed) {
5731
- success(`\uD14C\uC2A4\uD2B8 \uD1B5\uACFC (${testResult.runner}, ${testResult.duration}ms)`);
5732
- } else {
5733
- error("\uD14C\uC2A4\uD2B8 \uC2E4\uD328");
5734
- info(testResult.output.slice(0, 300));
5735
- }
5736
- section("\uD558\uB124\uC2A4 \uAC80\uC99D");
5737
- const harness = await runHarnessCheck(cwd);
5738
- if (!harness.enabled) {
5739
- info("Harness \uC124\uC815 \uC5C6\uC74C \u2014 \uAC74\uB108\uB700");
5740
- info("\uC124\uC815 \uCD94\uAC00: `pai add` \uC5D0\uC11C Harness Engineering \uC120\uD0DD");
5741
- } else {
5742
- for (const check of harness.checks) {
5743
- if (check.passed) {
5744
- success(`${check.rule}: ${check.detail}`);
5745
- } else {
5746
- warn(`${check.rule}: ${check.detail}`);
5747
- }
5748
- }
5749
- }
5750
- const allPassed = testResult.passed && harness.checks.every((c2) => c2.passed);
5751
- console.log("");
5752
- if (allPassed) {
5753
- success("\uAC80\uC99D \uD1B5\uACFC!");
5754
- } else if (!testResult.passed) {
5755
- error("\uAC80\uC99D \uC2E4\uD328 \u2014 \uD14C\uC2A4\uD2B8\uB97C \uC218\uC815\uD558\uC138\uC694.");
5756
- throw new Error("tests-failed");
5757
- } else {
5758
- warn("\uBD80\uBD84 \uD1B5\uACFC \u2014 \uD558\uB124\uC2A4 \uAC80\uC99D \uD56D\uBAA9\uC744 \uD655\uC778\uD558\uC138\uC694.");
5759
- }
5760
- }
5761
- );
5762
- await syncHandoff(cwd).catch(() => {
5763
- });
5764
- } catch (err) {
5765
- handleRobocoError(err);
5766
- }
5767
- }
5768
- function handleRobocoError(err) {
5769
- if (err instanceof RobocoLockError) {
5770
- error(err.message);
5771
- process.exitCode = 1;
5772
- return;
5773
- }
5774
- if (err instanceof GateFailedError) {
5775
- error(`\uB2E8\uACC4 \uC9C4\uC785 \uC2E4\uD328 (${err.result.name})`);
5776
- for (const v of err.result.violations) {
5777
- warn(` \xB7 ${v.message}${v.location ? ` [${v.location}]` : ""}`);
5778
- }
5779
- hint("\uC6B0\uD68C \uC635\uC158: --skip-gates (\uACBD\uACE0\uB9CC) / --force (\uAC8C\uC774\uD2B8 \uBB34\uC2DC)");
5780
- process.exitCode = 1;
5781
- return;
5782
- }
5783
- if (err instanceof Error) {
5784
- if (err.message === "tests-failed") {
5785
- process.exitCode = 1;
5786
- return;
5787
- }
5788
- error(err.message);
5789
- process.exitCode = 1;
5790
- return;
5791
- }
5792
- throw err;
5793
- }
5794
- var init_validate_cmd = __esm({
5795
- "src/cli/commands/validate.cmd.ts"() {
5796
- "use strict";
5797
- init_ui();
5798
- init_runner();
5799
- init_harness();
5800
- init_roboco();
5801
- init_gates();
5802
- }
5803
- });
5804
-
5805
5805
  // src/cli/commands/evaluate.cmd.ts
5806
5806
  var evaluate_cmd_exports = {};
5807
5807
  __export(evaluate_cmd_exports, {
@@ -6234,7 +6234,24 @@ async function setupInDirectory(projectDir, projectName) {
6234
6234
  config,
6235
6235
  previousResults: /* @__PURE__ */ new Map()
6236
6236
  };
6237
- const result = await environmentStage.run(input);
6237
+ let stageResult;
6238
+ try {
6239
+ await withRoboco(
6240
+ projectDir,
6241
+ "environment",
6242
+ "pai init",
6243
+ {},
6244
+ async () => {
6245
+ stageResult = await environmentStage.run(input);
6246
+ }
6247
+ );
6248
+ await syncHandoff(projectDir).catch(() => {
6249
+ });
6250
+ } catch (err) {
6251
+ handleRobocoError(err);
6252
+ return;
6253
+ }
6254
+ const result = stageResult;
6238
6255
  if (result.status === "success") {
6239
6256
  try {
6240
6257
  const { requestCdAfter: requestCdAfter2 } = await Promise.resolve().then(() => (init_shell_cd(), shell_cd_exports));
@@ -6657,6 +6674,8 @@ var init_init_cmd = __esm({
6657
6674
  init_environment();
6658
6675
  init_config();
6659
6676
  init_detector();
6677
+ init_roboco();
6678
+ init_validate_cmd();
6660
6679
  init_ui();
6661
6680
  init_logger();
6662
6681
  }
@@ -7260,6 +7279,29 @@ async function runPipeline(cwd, config, options = {}) {
7260
7279
  config,
7261
7280
  previousResults: ctx.getResults()
7262
7281
  };
7282
+ if (!options.force) {
7283
+ const gateResult = await STAGE_GATES[stageName](cwd).catch(() => null);
7284
+ if (gateResult) {
7285
+ await saveGateResult(cwd, `${stageName}.entry`, gateResult).catch(() => {
7286
+ });
7287
+ if (!gateResult.passed && !options.skipGates) {
7288
+ error(`${stageName} \uAC8C\uC774\uD2B8 \uC2E4\uD328 \u2014 \uD30C\uC774\uD504\uB77C\uC778 \uC911\uB2E8`);
7289
+ for (const v of gateResult.violations) {
7290
+ warn(` \xB7 ${v.message}${v.location ? ` [${v.location}]` : ""}`);
7291
+ }
7292
+ hint("\uC6B0\uD68C: pai run --skip-gates (\uACBD\uACE0) / --force (\uBB34\uC2DC)");
7293
+ await markStageEnd(cwd, stageName, false, `\uAC8C\uC774\uD2B8 \uC2E4\uD328: ${gateResult.name}`).catch(() => {
7294
+ });
7295
+ shouldStop = true;
7296
+ break;
7297
+ }
7298
+ if (!gateResult.passed && options.skipGates) {
7299
+ for (const v of gateResult.violations) {
7300
+ warn(` [\uAC8C\uC774\uD2B8 \uACBD\uACE0] ${v.message}`);
7301
+ }
7302
+ }
7303
+ }
7304
+ }
7263
7305
  if (stage.canSkip(input)) {
7264
7306
  info(`${stageName} \u2014 \uAC74\uB108\uB700 (\uC774\uBBF8 \uC644\uB8CC)`);
7265
7307
  ctx.setResult(stageName, {
@@ -7272,8 +7314,14 @@ async function runPipeline(cwd, config, options = {}) {
7272
7314
  });
7273
7315
  continue;
7274
7316
  }
7317
+ await markStageStart(cwd, stageName, "pai run").catch(() => {
7318
+ });
7275
7319
  const result = await stage.run(input);
7276
7320
  ctx.setResult(stageName, result);
7321
+ const stageSuccess = result.status !== "failed";
7322
+ const failReason = result.errors[0]?.message;
7323
+ await markStageEnd(cwd, stageName, stageSuccess, failReason).catch(() => {
7324
+ });
7277
7325
  switch (result.status) {
7278
7326
  case "success":
7279
7327
  success(`${stageName} \uC644\uB8CC (${result.duration}ms)`);
@@ -7321,6 +7369,8 @@ var init_pipeline = __esm({
7321
7369
  init_execution();
7322
7370
  init_validation();
7323
7371
  init_evaluation();
7372
+ init_roboco();
7373
+ init_gates();
7324
7374
  init_ui();
7325
7375
  STAGES = {
7326
7376
  environment: environmentStage,
@@ -7340,23 +7390,17 @@ __export(pipeline_cmd_exports, {
7340
7390
  async function pipelineCommand(cwd, options) {
7341
7391
  printBanner();
7342
7392
  try {
7343
- await withRoboco(
7344
- cwd,
7345
- "environment",
7346
- "pai run",
7347
- {
7348
- skipGates: options.skipGates,
7349
- force: options.force,
7350
- ttlMs: PIPELINE_TTL_MS
7351
- },
7352
- async () => {
7353
- const config = await loadConfig(cwd) ?? createDefaultConfig("my-project", "prototype");
7354
- const pipelineOpts = {};
7355
- if (options.from) pipelineOpts.from = options.from;
7356
- if (options.only) pipelineOpts.only = options.only.split(",").map((s) => s.trim());
7357
- await runPipeline(cwd, config, pipelineOpts);
7358
- }
7359
- );
7393
+ await acquireLock(cwd, "environment", "pai run", PIPELINE_TTL_MS);
7394
+ try {
7395
+ const config = await loadConfig(cwd) ?? createDefaultConfig("my-project", "prototype");
7396
+ const pipelineOpts = { skipGates: options.skipGates, force: options.force };
7397
+ if (options.from) pipelineOpts.from = options.from;
7398
+ if (options.only) pipelineOpts.only = options.only.split(",").map((s) => s.trim());
7399
+ await runPipeline(cwd, config, pipelineOpts);
7400
+ } finally {
7401
+ await releaseLock(cwd).catch(() => {
7402
+ });
7403
+ }
7360
7404
  await syncHandoff(cwd).catch(() => {
7361
7405
  });
7362
7406
  } catch (err) {