jioogen 0.2.4 → 0.2.5

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
@@ -363,9 +363,9 @@ var require_utils = __commonJS({
363
363
  fpath.split(self.sep).forEach(function(name) {
364
364
  if (!name || name.substr(-1, 1) === ":") return;
365
365
  resolvedPath += self.sep + name;
366
- var stat3;
366
+ var stat5;
367
367
  try {
368
- stat3 = self.fs.statSync(resolvedPath);
368
+ stat5 = self.fs.statSync(resolvedPath);
369
369
  } catch (e) {
370
370
  if (e.message && e.message.startsWith("ENOENT")) {
371
371
  self.fs.mkdirSync(resolvedPath);
@@ -373,7 +373,7 @@ var require_utils = __commonJS({
373
373
  throw e;
374
374
  }
375
375
  }
376
- if (stat3 && stat3.isFile()) throw Errors.FILE_IN_THE_WAY(`"${resolvedPath}"`);
376
+ if (stat5 && stat5.isFile()) throw Errors.FILE_IN_THE_WAY(`"${resolvedPath}"`);
377
377
  });
378
378
  }
379
379
  mkdirSync(folder);
@@ -382,8 +382,8 @@ var require_utils = __commonJS({
382
382
  const self = this;
383
383
  if (self.fs.existsSync(path)) {
384
384
  if (!overwrite) return false;
385
- var stat3 = self.fs.statSync(path);
386
- if (stat3.isDirectory()) {
385
+ var stat5 = self.fs.statSync(path);
386
+ if (stat5.isDirectory()) {
387
387
  return false;
388
388
  }
389
389
  }
@@ -416,8 +416,8 @@ var require_utils = __commonJS({
416
416
  const self = this;
417
417
  self.fs.exists(path, function(exist) {
418
418
  if (exist && !overwrite) return callback(false);
419
- self.fs.stat(path, function(err, stat3) {
420
- if (exist && stat3 && stat3.isDirectory()) {
419
+ self.fs.stat(path, function(err, stat5) {
420
+ if (exist && stat5 && stat5.isDirectory()) {
421
421
  return callback(false);
422
422
  }
423
423
  var folder = pth.dirname(path);
@@ -469,13 +469,13 @@ var require_utils = __commonJS({
469
469
  for (const part of rel.split(pth.sep)) {
470
470
  if (!part || part === ".") continue;
471
471
  cur = pth.join(cur, part);
472
- let stat3;
472
+ let stat5;
473
473
  try {
474
- stat3 = self.fs.lstatSync(cur);
474
+ stat5 = self.fs.lstatSync(cur);
475
475
  } catch (e) {
476
476
  break;
477
477
  }
478
- if (stat3.isSymbolicLink()) throw Errors.FILE_IN_THE_WAY(`"${cur}"`);
478
+ if (stat5.isSymbolicLink()) throw Errors.FILE_IN_THE_WAY(`"${cur}"`);
479
479
  }
480
480
  };
481
481
  Utils.prototype.findFiles = function(path) {
@@ -502,11 +502,11 @@ var require_utils = __commonJS({
502
502
  self.fs.readdirSync(dir).forEach(function(file) {
503
503
  const path2 = pth.join(dir, file);
504
504
  if (escapesRoot(path2)) return;
505
- const stat3 = self.fs.statSync(path2);
505
+ const stat5 = self.fs.statSync(path2);
506
506
  if (!pattern || pattern.test(path2)) {
507
- files.push(pth.normalize(path2) + (stat3.isDirectory() ? self.sep : ""));
507
+ files.push(pth.normalize(path2) + (stat5.isDirectory() ? self.sep : ""));
508
508
  }
509
- if (stat3.isDirectory() && recursive) {
509
+ if (stat5.isDirectory() && recursive) {
510
510
  const realDir = self.fs.realpathSync(path2);
511
511
  if (!visited.has(realDir)) {
512
512
  visited.add(realDir);
@@ -553,14 +553,14 @@ var require_utils = __commonJS({
553
553
  if (!--pending) done();
554
554
  return;
555
555
  }
556
- self.fs.stat(file, function(err3, stat3) {
556
+ self.fs.stat(file, function(err3, stat5) {
557
557
  if (err3) return done(err3);
558
- if (!stat3) {
558
+ if (!stat5) {
559
559
  if (!--pending) done();
560
560
  return;
561
561
  }
562
- results.push(pth.normalize(file) + (stat3.isDirectory() ? self.sep : ""));
563
- if (!stat3.isDirectory()) {
562
+ results.push(pth.normalize(file) + (stat5.isDirectory() ? self.sep : ""));
563
+ if (!stat5.isDirectory()) {
564
564
  if (!--pending) done();
565
565
  return;
566
566
  }
@@ -2124,8 +2124,8 @@ var require_adm_zip = __commonJS({
2124
2124
  return null;
2125
2125
  }
2126
2126
  function fixPath(zipPath) {
2127
- const { join: join7, normalize, sep: sep2 } = pth.posix;
2128
- return join7(pth.isAbsolute(zipPath) ? "/" : ".", normalize(sep2 + zipPath.split("\\").join(sep2) + sep2));
2127
+ const { join: join8, normalize, sep: sep2 } = pth.posix;
2128
+ return join8(pth.isAbsolute(zipPath) ? "/" : ".", normalize(sep2 + zipPath.split("\\").join(sep2) + sep2));
2129
2129
  }
2130
2130
  function filenameFilter(filterfn) {
2131
2131
  if (filterfn instanceof RegExp) {
@@ -2521,10 +2521,10 @@ var require_adm_zip = __commonJS({
2521
2521
  * @param {function|string} [props.namefix] - optional function to help fix filename
2522
2522
  */
2523
2523
  addLocalFolderPromise: function(localPath, props) {
2524
- return new Promise((resolve5, reject) => {
2524
+ return new Promise((resolve6, reject) => {
2525
2525
  this.addLocalFolderAsync2(Object.assign({ localPath }, props), (err, done) => {
2526
2526
  if (err) return reject(err);
2527
- if (done) resolve5(this);
2527
+ if (done) resolve6(this);
2528
2528
  });
2529
2529
  });
2530
2530
  },
@@ -2716,12 +2716,12 @@ var require_adm_zip = __commonJS({
2716
2716
  keepOriginalPermission = get_Bool(false, keepOriginalPermission);
2717
2717
  overwrite = get_Bool(false, overwrite);
2718
2718
  if (!callback) {
2719
- return new Promise((resolve5, reject) => {
2719
+ return new Promise((resolve6, reject) => {
2720
2720
  this.extractAllToAsync(targetPath, overwrite, keepOriginalPermission, function(err) {
2721
2721
  if (err) {
2722
2722
  reject(err);
2723
2723
  } else {
2724
- resolve5(this);
2724
+ resolve6(this);
2725
2725
  }
2726
2726
  });
2727
2727
  });
@@ -2836,11 +2836,11 @@ var require_adm_zip = __commonJS({
2836
2836
  */
2837
2837
  writeZipPromise: function(targetFileName, props) {
2838
2838
  const { overwrite, perm } = Object.assign({ overwrite: true }, props);
2839
- return new Promise((resolve5, reject) => {
2839
+ return new Promise((resolve6, reject) => {
2840
2840
  if (!targetFileName && opts.filename) targetFileName = opts.filename;
2841
2841
  if (!targetFileName) reject("ADM-ZIP: ZIP File Name Missing");
2842
2842
  this.toBufferPromise().then((zipData) => {
2843
- const ret = (done) => done ? resolve5(done) : reject("ADM-ZIP: Wasn't able to write zip file");
2843
+ const ret = (done) => done ? resolve6(done) : reject("ADM-ZIP: Wasn't able to write zip file");
2844
2844
  filetools.writeFileToAsync(targetFileName, zipData, overwrite, perm, ret);
2845
2845
  }, reject);
2846
2846
  });
@@ -2849,8 +2849,8 @@ var require_adm_zip = __commonJS({
2849
2849
  * @returns {Promise<Buffer>} A promise to the Buffer.
2850
2850
  */
2851
2851
  toBufferPromise: function() {
2852
- return new Promise((resolve5, reject) => {
2853
- _zip.toAsyncBuffer(resolve5, reject);
2852
+ return new Promise((resolve6, reject) => {
2853
+ _zip.toAsyncBuffer(resolve6, reject);
2854
2854
  });
2855
2855
  },
2856
2856
  /**
@@ -2978,8 +2978,8 @@ var ApiClient = class {
2978
2978
  };
2979
2979
 
2980
2980
  // src/commands.ts
2981
- import { readdir as readdir4, readFile as readFile6 } from "node:fs/promises";
2982
- import { join as join6, resolve as resolve4 } from "node:path";
2981
+ import { readdir as readdir5, readFile as readFile7 } from "node:fs/promises";
2982
+ import { join as join7, resolve as resolve5 } from "node:path";
2983
2983
 
2984
2984
  // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
2985
2985
  var external_exports = {};
@@ -3472,15 +3472,15 @@ var makeIssue = (params) => {
3472
3472
  message: issueData.message
3473
3473
  };
3474
3474
  }
3475
- let errorMessage = "";
3475
+ let errorMessage2 = "";
3476
3476
  const maps = errorMaps.filter((m) => !!m).slice().reverse();
3477
3477
  for (const map of maps) {
3478
- errorMessage = map(fullIssue, { data, defaultError: errorMessage }).message;
3478
+ errorMessage2 = map(fullIssue, { data, defaultError: errorMessage2 }).message;
3479
3479
  }
3480
3480
  return {
3481
3481
  ...issueData,
3482
3482
  path: fullPath,
3483
- message: errorMessage
3483
+ message: errorMessage2
3484
3484
  };
3485
3485
  };
3486
3486
  var EMPTY_PATH = [];
@@ -8562,19 +8562,9 @@ function isMissingFile(error) {
8562
8562
  return isRecord(error) && error.code === "ENOENT";
8563
8563
  }
8564
8564
 
8565
- // src/errors.ts
8566
- var UsageError = class extends Error {
8567
- };
8568
-
8569
- // src/output.ts
8570
- function formatTable(headers, rows) {
8571
- const widths = headers.map(
8572
- (header, index) => Math.max(header.length, ...rows.map((row) => (row[index] ?? "").length))
8573
- );
8574
- const render = (cells) => cells.map((cell, index) => cell.padEnd(widths[index] ?? 0)).join(" ").trimEnd();
8575
- const divider = widths.map((width) => "-".repeat(width)).join(" ");
8576
- return [render(headers), divider, ...rows.map(render)].join("\n");
8577
- }
8565
+ // src/doctor.ts
8566
+ import { readdir as readdir3, readFile as readFile5, rm as rm4, stat as stat3 } from "node:fs/promises";
8567
+ import { join as join5 } from "node:path";
8578
8568
 
8579
8569
  // src/sidecar.ts
8580
8570
  import { randomUUID } from "node:crypto";
@@ -8762,7 +8752,7 @@ async function removeStaleLock(target) {
8762
8752
  }
8763
8753
  }
8764
8754
  function delay(ms) {
8765
- return new Promise((resolve5) => setTimeout(resolve5, ms));
8755
+ return new Promise((resolve6) => setTimeout(resolve6, ms));
8766
8756
  }
8767
8757
 
8768
8758
  // src/skills.ts
@@ -8837,8 +8827,11 @@ var JIOOGEN_AGENTS_MD = `${MARKER}
8837
8827
 
8838
8828
  ## 5. Checkpoint
8839
8829
  Berhenti dan minta konfirmasi user bila task berikutnya berpindah layer, nomor
8840
- \`phase\` naik, atau task bertanda \`checkpoint: true\`. Jangan lanjut sebelum user
8841
- mengetik "lanjut".
8830
+ \`phase\` naik, task bertanda \`checkpoint: true\`, atau task tergolong berisiko
8831
+ (migrasi/schema/auth/permission/payment/webhook/secret/deploy/infra). Jangan lanjut
8832
+ sebelum user memutuskan. Setelah user memutuskan, jalankan salah satu:
8833
+ \`npx jioogen review --approve\`, \`npx jioogen review --request-changes "<teks>"\`,
8834
+ atau \`npx jioogen review --stop\`, lalu \`npx jioogen sync\`.
8842
8835
 
8843
8836
  ## 6. Skill eksekusi
8844
8837
  Baca skill \`${EXECUTION_SKILL_NAME}\` di awal prompt
@@ -8916,10 +8909,13 @@ atas ide, refactor, perbaikan, atau fitur lain.
8916
8909
  Berhenti dan minta konfirmasi user bila:
8917
8910
  - task berikutnya berpindah \`layer\` (mis. \`frontend_mock\` \u2192 \`backend_api\`/\`database\`),
8918
8911
  - nomor \`phase\` naik,
8919
- - task bertanda \`checkpoint: true\`.
8912
+ - task bertanda \`checkpoint: true\`,
8913
+ - task tergolong berisiko (migrasi/schema/auth/permission/payment/webhook/secret/deploy/infra).
8920
8914
 
8921
- Jangan lanjut sebelum user mengetik "lanjut". Set \`current.awaitingCheckpoint = true\`
8922
- di sidecar saat berhenti.
8915
+ Jangan lanjut sebelum user memutuskan. Set \`current.awaitingCheckpoint = true\` di
8916
+ sidecar saat berhenti, lalu setelah user memutuskan jalankan
8917
+ \`npx jioogen review --approve\` / \`--request-changes "<teks>"\` / \`--stop\`,
8918
+ diikuti \`npx jioogen sync\`.
8923
8919
 
8924
8920
  ## 6. Larangan ringkas
8925
8921
 
@@ -9278,9 +9274,511 @@ function skillsDisabled(options) {
9278
9274
  return value === "1" || value === "true" || value === "yes";
9279
9275
  }
9280
9276
 
9277
+ // src/doctor.ts
9278
+ var REQUIRED_NODE_MAJOR = 22;
9279
+ var LOCK_STALE_MS2 = 1e4;
9280
+ var AGENTS_MARKER = "<!-- jioogen-execution-skill:";
9281
+ var BUNDLE_SCAFFOLD_CHECKS = /* @__PURE__ */ new Set(["agents", "execution-skill", "skill-packs"]);
9282
+ function makeCheck(id, status, detail, fixable) {
9283
+ return fixable === true ? { id, status, detail, fixable: true } : { id, status, detail };
9284
+ }
9285
+ function errorMessage(error) {
9286
+ return error instanceof Error ? error.message : String(error);
9287
+ }
9288
+ function isMissingFile4(error) {
9289
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
9290
+ }
9291
+ function resolveDeps(deps) {
9292
+ return {
9293
+ readCredentials: deps?.readCredentials ?? readCredentials,
9294
+ fetchMe: deps?.fetchMe ?? (async (apiUrl, cookie) => {
9295
+ const client = new ApiClient(apiUrl, cookie);
9296
+ return client.getJson(ENDPOINTS.me);
9297
+ }),
9298
+ fetchExecution: deps?.fetchExecution ?? (async (apiUrl, cookie, projectId) => {
9299
+ const client = new ApiClient(apiUrl, cookie);
9300
+ return client.getJson(projectPath(projectId, "/execution"));
9301
+ }),
9302
+ installSkills: deps?.installSkills ?? ((bundleDir) => installAgentSkills(bundleDir))
9303
+ };
9304
+ }
9305
+ function checkNode() {
9306
+ const raw = process.versions.node;
9307
+ const major = Number.parseInt(raw.split(".")[0] ?? "0", 10);
9308
+ if (Number.isFinite(major) && major >= REQUIRED_NODE_MAJOR) {
9309
+ return makeCheck("node", "pass", `Node.js v${raw} memenuhi syarat >=${REQUIRED_NODE_MAJOR}.`);
9310
+ }
9311
+ return makeCheck(
9312
+ "node",
9313
+ "fail",
9314
+ `Node.js v${raw} terlalu lama; butuh >=${REQUIRED_NODE_MAJOR}. Perbarui Node.js lalu ulangi.`
9315
+ );
9316
+ }
9317
+ function checkCredentials(credentials, error) {
9318
+ if (error !== null) {
9319
+ return makeCheck(
9320
+ "credentials",
9321
+ "fail",
9322
+ `Credential tidak bisa dibaca: ${error}. Periksa izin file credential.`
9323
+ );
9324
+ }
9325
+ if (credentials === null) {
9326
+ return makeCheck(
9327
+ "credentials",
9328
+ "fail",
9329
+ "Belum ada credential. Jalankan `jioogen login --email ... --password-stdin`."
9330
+ );
9331
+ }
9332
+ return makeCheck("credentials", "pass", `Credential tersimpan untuk ${credentials.apiUrl}.`);
9333
+ }
9334
+ function checkApi(credentials, email, error) {
9335
+ if (credentials === null) {
9336
+ return makeCheck(
9337
+ "api",
9338
+ "warn",
9339
+ "Dilewati: credential belum ada. Jalankan `jioogen login` lalu ulangi."
9340
+ );
9341
+ }
9342
+ if (error !== null) {
9343
+ const message = errorMessage(error);
9344
+ if (error instanceof ApiError && (error.status === 401 || error.status === 403)) {
9345
+ return makeCheck(
9346
+ "api",
9347
+ "fail",
9348
+ `Sesi ditolak (${error.status}). Jalankan \`jioogen login\` lagi.`
9349
+ );
9350
+ }
9351
+ return makeCheck(
9352
+ "api",
9353
+ "warn",
9354
+ `Tidak dapat menghubungi ${credentials.apiUrl}: ${message}. Cek koneksi lalu ulangi.`
9355
+ );
9356
+ }
9357
+ return makeCheck(
9358
+ "api",
9359
+ "pass",
9360
+ `API ${credentials.apiUrl} terjangkau; login sebagai ${email ?? "-"}.`
9361
+ );
9362
+ }
9363
+ function checkBinding(requestedProjectId, link, error) {
9364
+ if (error !== null) {
9365
+ return makeCheck(
9366
+ "binding",
9367
+ "fail",
9368
+ `Link project tidak valid: ${error}. Perbaiki atau hapus .jioogen/project.json.`
9369
+ );
9370
+ }
9371
+ if (link === null) {
9372
+ return makeCheck(
9373
+ "binding",
9374
+ "fail",
9375
+ "Belum terikat ke project. Jalankan `jioogen pull <projectId>` di folder bundle."
9376
+ );
9377
+ }
9378
+ if (requestedProjectId !== void 0 && link.projectId !== requestedProjectId) {
9379
+ return makeCheck(
9380
+ "binding",
9381
+ "fail",
9382
+ `Bundle terikat ke project ${link.projectId}, bukan ${requestedProjectId}. Jalankan \`jioogen pull ${requestedProjectId}\`.`
9383
+ );
9384
+ }
9385
+ return makeCheck("binding", "pass", `Bundle terikat ke project ${link.projectId}.`);
9386
+ }
9387
+ async function checkProject(deps, credentials, projectId) {
9388
+ if (credentials === null) {
9389
+ return makeCheck("project", "warn", "Dilewati: credential belum ada.");
9390
+ }
9391
+ if (projectId === void 0 || projectId.length === 0) {
9392
+ return makeCheck(
9393
+ "project",
9394
+ "warn",
9395
+ "Dilewati: project belum diketahui. Jalankan `jioogen pull <projectId>`."
9396
+ );
9397
+ }
9398
+ try {
9399
+ await deps.fetchExecution(credentials.apiUrl, credentials.cookie, projectId);
9400
+ return makeCheck("project", "pass", `Project ${projectId} dapat diakses akun ini.`);
9401
+ } catch (error) {
9402
+ if (error instanceof ApiError) {
9403
+ if (error.status === 404) {
9404
+ return makeCheck(
9405
+ "project",
9406
+ "fail",
9407
+ `Project ${projectId} tidak ditemukan atau bukan milik akun ini.`
9408
+ );
9409
+ }
9410
+ if (error.status === 401 || error.status === 403) {
9411
+ return makeCheck(
9412
+ "project",
9413
+ "fail",
9414
+ `Akses project ditolak (${error.status}). Jalankan \`jioogen login\` lagi.`
9415
+ );
9416
+ }
9417
+ }
9418
+ return makeCheck(
9419
+ "project",
9420
+ "warn",
9421
+ `Gagal memeriksa project ${projectId}: ${errorMessage(error)}. Coba lagi saat API stabil.`
9422
+ );
9423
+ }
9424
+ }
9425
+ async function isFile2(path) {
9426
+ try {
9427
+ return (await stat3(path)).isFile();
9428
+ } catch {
9429
+ return false;
9430
+ }
9431
+ }
9432
+ async function isDirectory2(path) {
9433
+ try {
9434
+ return (await stat3(path)).isDirectory();
9435
+ } catch {
9436
+ return false;
9437
+ }
9438
+ }
9439
+ async function listMarkdownFiles(directory) {
9440
+ try {
9441
+ const entries = await readdir3(directory, { withFileTypes: true });
9442
+ return entries.filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".md")).map((entry) => entry.name);
9443
+ } catch {
9444
+ return [];
9445
+ }
9446
+ }
9447
+ async function checkBundle(bundleDir) {
9448
+ const missing = [];
9449
+ if (!await isFile2(join5(bundleDir, "PRD.md"))) missing.push("PRD.md");
9450
+ const specsDir = join5(bundleDir, "specs");
9451
+ if (!await isDirectory2(specsDir)) {
9452
+ missing.push("specs/");
9453
+ } else {
9454
+ const content = (await listMarkdownFiles(specsDir)).filter(
9455
+ (name) => name.toLowerCase() !== "readme.md"
9456
+ );
9457
+ if (content.length === 0) missing.push("specs/*.md (konten)");
9458
+ }
9459
+ const tasksDir = join5(bundleDir, "tasks");
9460
+ if (!await isDirectory2(tasksDir)) {
9461
+ missing.push("tasks/");
9462
+ } else {
9463
+ const content = (await listMarkdownFiles(tasksDir)).filter(
9464
+ (name) => name.toLowerCase() !== "readme.md"
9465
+ );
9466
+ if (content.length === 0) missing.push("tasks/*.md (task)");
9467
+ }
9468
+ if (missing.length > 0) {
9469
+ return makeCheck(
9470
+ "bundle",
9471
+ "fail",
9472
+ `Bundle tidak lengkap: ${missing.join(", ")}. Jalankan \`jioogen pull <projectId>\`.`
9473
+ );
9474
+ }
9475
+ try {
9476
+ const report = await inspectBundle(bundleDir);
9477
+ if (!report.ok) {
9478
+ const codes = Object.entries(report.issueCounts).map(([code, count]) => `${code}\xD7${count}`).join(", ");
9479
+ return makeCheck(
9480
+ "bundle",
9481
+ "fail",
9482
+ `Bundle gagal gate (${report.issueCount} isu: ${codes}). Perbaiki PRD/specs/tasks.`
9483
+ );
9484
+ }
9485
+ return makeCheck("bundle", "pass", `Bundle lengkap dan lulus gate (${report.files} file).`);
9486
+ } catch (error) {
9487
+ return makeCheck("bundle", "fail", `Gagal memeriksa bundle: ${errorMessage(error)}.`);
9488
+ }
9489
+ }
9490
+ async function checkSidecar(bundleDir) {
9491
+ try {
9492
+ const state = await readState(bundleDir);
9493
+ if (state === null) {
9494
+ return makeCheck("sidecar", "warn", "Sidecar belum ada. Jalankan `jioogen task next`.");
9495
+ }
9496
+ return makeCheck("sidecar", "pass", `Sidecar valid dengan ${state.tasks.length} task.`);
9497
+ } catch (error) {
9498
+ return makeCheck(
9499
+ "sidecar",
9500
+ "fail",
9501
+ `Sidecar tidak valid: ${errorMessage(error)}. Hapus tasks/.execution/state.json lalu jalankan \`jioogen task next\`.`
9502
+ );
9503
+ }
9504
+ }
9505
+ async function checkLock(bundleDir) {
9506
+ const target = lockPath(bundleDir);
9507
+ let info;
9508
+ try {
9509
+ info = await stat3(target);
9510
+ } catch (error) {
9511
+ if (isMissingFile4(error)) {
9512
+ return makeCheck("lock", "pass", "Tidak ada lock; sidecar bebas ditulis.");
9513
+ }
9514
+ return makeCheck("lock", "warn", `Tidak bisa memeriksa lock: ${errorMessage(error)}.`);
9515
+ }
9516
+ const ageMs = Date.now() - info.mtimeMs;
9517
+ const ageSeconds = Math.max(0, Math.round(ageMs / 1e3));
9518
+ if (ageMs > LOCK_STALE_MS2) {
9519
+ return makeCheck(
9520
+ "lock",
9521
+ "warn",
9522
+ `Lock basi (${ageSeconds} detik) tertinggal. Jalankan \`jioogen doctor --fix\` untuk menghapus.`,
9523
+ true
9524
+ );
9525
+ }
9526
+ return makeCheck(
9527
+ "lock",
9528
+ "warn",
9529
+ `Lock masih aktif (${ageSeconds} detik); proses lain mungkin menulis. Tunggu atau hapus bila proses sudah mati.`
9530
+ );
9531
+ }
9532
+ async function checkAgents(bundleDir) {
9533
+ let content;
9534
+ try {
9535
+ content = await readFile5(join5(bundleDir, "AGENTS.md"), "utf8");
9536
+ } catch (error) {
9537
+ if (isMissingFile4(error)) {
9538
+ return makeCheck(
9539
+ "agents",
9540
+ "fail",
9541
+ "AGENTS.md belum ada. Jalankan `jioogen doctor --fix` untuk memasang kontrak eksekusi.",
9542
+ true
9543
+ );
9544
+ }
9545
+ return makeCheck(
9546
+ "agents",
9547
+ "fail",
9548
+ `AGENTS.md tidak bisa dibaca: ${errorMessage(error)}.`,
9549
+ true
9550
+ );
9551
+ }
9552
+ if (!content.includes(AGENTS_MARKER)) {
9553
+ return makeCheck(
9554
+ "agents",
9555
+ "fail",
9556
+ "AGENTS.md ada tetapi tanpa marker JiooGen. Jalankan `jioogen doctor --fix` untuk memasang ulang.",
9557
+ true
9558
+ );
9559
+ }
9560
+ return makeCheck("agents", "pass", "AGENTS.md terpasang dengan marker eksekusi JiooGen.");
9561
+ }
9562
+ async function checkExecutionSkill(bundleDir) {
9563
+ const missing = [];
9564
+ for (const dir of AGENT_SKILL_DIRS) {
9565
+ const target = join5(bundleDir, dir, EXECUTION_SKILL_NAME, "SKILL.md");
9566
+ if (!await isFile2(target)) missing.push(dir);
9567
+ }
9568
+ if (missing.length > 0) {
9569
+ return makeCheck(
9570
+ "execution-skill",
9571
+ "fail",
9572
+ `Skill ${EXECUTION_SKILL_NAME} hilang di: ${missing.join(", ")}. Jalankan \`jioogen doctor --fix\`.`,
9573
+ true
9574
+ );
9575
+ }
9576
+ return makeCheck(
9577
+ "execution-skill",
9578
+ "pass",
9579
+ `Skill ${EXECUTION_SKILL_NAME} ada di ${AGENT_SKILL_DIRS.length} direktori harness.`
9580
+ );
9581
+ }
9582
+ async function checkSkillPacks(bundleDir) {
9583
+ let manifest;
9584
+ try {
9585
+ manifest = await readHarnessManifest(bundleDir);
9586
+ } catch (error) {
9587
+ return makeCheck(
9588
+ "skill-packs",
9589
+ "warn",
9590
+ `harness.json tidak bisa dibaca: ${errorMessage(error)}. Jalankan \`jioogen doctor --fix\`.`,
9591
+ true
9592
+ );
9593
+ }
9594
+ if (manifest === null) {
9595
+ return makeCheck(
9596
+ "skill-packs",
9597
+ "warn",
9598
+ ".jioogen/harness.json belum ada atau tidak valid. Jalankan `jioogen doctor --fix` untuk menulis ulang.",
9599
+ true
9600
+ );
9601
+ }
9602
+ if (manifest.packs.length === 0) {
9603
+ return makeCheck("skill-packs", "pass", "Tidak ada skill pack yang dikelola.");
9604
+ }
9605
+ const issues = [];
9606
+ for (const pack of manifest.packs) {
9607
+ for (const name of pack.missing) {
9608
+ issues.push(`${pack.name}: ${name} tidak tersedia saat instalasi`);
9609
+ }
9610
+ for (const skill of pack.skills) {
9611
+ let matched = false;
9612
+ for (const dir of AGENT_SKILL_DIRS) {
9613
+ const target = join5(bundleDir, dir, skill.name);
9614
+ if (!await isFile2(join5(target, "SKILL.md"))) continue;
9615
+ try {
9616
+ if (await hashSkillDirectory(target) === skill.sha256) {
9617
+ matched = true;
9618
+ break;
9619
+ }
9620
+ } catch {
9621
+ }
9622
+ }
9623
+ if (!matched) issues.push(`${pack.name}: ${skill.name} hilang atau hash tidak cocok`);
9624
+ }
9625
+ }
9626
+ if (issues.length > 0) {
9627
+ return makeCheck(
9628
+ "skill-packs",
9629
+ "warn",
9630
+ `Skill pack bermasalah: ${issues.join("; ")}. Jalankan \`jioogen doctor --fix\`.`,
9631
+ true
9632
+ );
9633
+ }
9634
+ return makeCheck(
9635
+ "skill-packs",
9636
+ "pass",
9637
+ `Semua skill pack cocok (${manifest.packs.length} pack).`
9638
+ );
9639
+ }
9640
+ async function checkDuplicates(bundleDir) {
9641
+ let manifest = null;
9642
+ try {
9643
+ manifest = await readHarnessManifest(bundleDir);
9644
+ } catch {
9645
+ }
9646
+ const managed = /* @__PURE__ */ new Set([EXECUTION_SKILL_NAME]);
9647
+ for (const pack of manifest?.packs ?? []) {
9648
+ for (const skill of pack.skills) managed.add(skill.name);
9649
+ }
9650
+ const dirsByName = /* @__PURE__ */ new Map();
9651
+ for (const dir of AGENT_SKILL_DIRS) {
9652
+ const root = join5(bundleDir, dir);
9653
+ if (!await isDirectory2(root)) continue;
9654
+ let entries;
9655
+ try {
9656
+ entries = await readdir3(root, { withFileTypes: true });
9657
+ } catch {
9658
+ continue;
9659
+ }
9660
+ for (const entry of entries) {
9661
+ if (!entry.isDirectory()) continue;
9662
+ if (managed.has(entry.name)) continue;
9663
+ if (!await isFile2(join5(root, entry.name, "SKILL.md"))) continue;
9664
+ const owners = dirsByName.get(entry.name) ?? [];
9665
+ owners.push(dir);
9666
+ dirsByName.set(entry.name, owners);
9667
+ }
9668
+ }
9669
+ const duplicates = [...dirsByName.entries()].filter(([, dirs]) => dirs.length > 1);
9670
+ if (duplicates.length === 0) {
9671
+ return makeCheck("duplicates", "pass", "Tidak ada skill duplikat antar-harness.");
9672
+ }
9673
+ const detail = duplicates.map(([name, dirs]) => `${name} (${dirs.join(", ")})`).join("; ");
9674
+ return makeCheck(
9675
+ "duplicates",
9676
+ "warn",
9677
+ `Skill yang sama ada di beberapa harness: ${detail}. Pastikan itu disengaja.`
9678
+ );
9679
+ }
9680
+ async function checkHarness(bundleDir) {
9681
+ const detected = [];
9682
+ for (const dir of AGENT_SKILL_DIRS) {
9683
+ if (await isDirectory2(join5(bundleDir, dir))) detected.push(dir);
9684
+ }
9685
+ if (detected.length === 0) {
9686
+ return makeCheck("harness", "pass", "Tidak ada direktori harness terdeteksi.");
9687
+ }
9688
+ return makeCheck("harness", "pass", `Harness terdeteksi: ${detected.join(", ")}.`);
9689
+ }
9690
+ async function runChecks(options, deps) {
9691
+ const checks = [checkNode()];
9692
+ let credentials = null;
9693
+ let credentialsError = null;
9694
+ try {
9695
+ credentials = await deps.readCredentials();
9696
+ } catch (error) {
9697
+ credentialsError = errorMessage(error);
9698
+ }
9699
+ checks.push(checkCredentials(credentials, credentialsError));
9700
+ let email = null;
9701
+ let apiError2 = null;
9702
+ if (credentials !== null) {
9703
+ try {
9704
+ email = (await deps.fetchMe(credentials.apiUrl, credentials.cookie)).email;
9705
+ } catch (error) {
9706
+ apiError2 = error;
9707
+ }
9708
+ }
9709
+ checks.push(checkApi(credentials, email, apiError2));
9710
+ let link = null;
9711
+ let linkError = null;
9712
+ try {
9713
+ link = await findProjectLink(options.bundleDir);
9714
+ } catch (error) {
9715
+ linkError = errorMessage(error);
9716
+ }
9717
+ checks.push(checkBinding(options.projectId, link, linkError));
9718
+ const effectiveProjectId = options.projectId ?? link?.projectId;
9719
+ checks.push(await checkProject(deps, credentials, effectiveProjectId));
9720
+ checks.push(await checkBundle(options.bundleDir));
9721
+ checks.push(await checkSidecar(options.bundleDir));
9722
+ checks.push(await checkLock(options.bundleDir));
9723
+ checks.push(await checkAgents(options.bundleDir));
9724
+ checks.push(await checkExecutionSkill(options.bundleDir));
9725
+ checks.push(await checkSkillPacks(options.bundleDir));
9726
+ checks.push(await checkDuplicates(options.bundleDir));
9727
+ checks.push(await checkHarness(options.bundleDir));
9728
+ return checks;
9729
+ }
9730
+ async function applyFixes(options, deps, checks) {
9731
+ let changed = false;
9732
+ if (checks.some((check) => check.id === "lock" && check.fixable === true)) {
9733
+ try {
9734
+ await rm4(lockPath(options.bundleDir), { force: true });
9735
+ changed = true;
9736
+ } catch {
9737
+ }
9738
+ }
9739
+ const needsScaffold = checks.some(
9740
+ (check) => check.fixable === true && BUNDLE_SCAFFOLD_CHECKS.has(check.id)
9741
+ );
9742
+ if (needsScaffold) {
9743
+ try {
9744
+ await deps.installSkills(options.bundleDir);
9745
+ changed = true;
9746
+ } catch {
9747
+ }
9748
+ }
9749
+ return changed;
9750
+ }
9751
+ async function runDoctor(options) {
9752
+ const deps = resolveDeps(options.deps);
9753
+ let checks = await runChecks(options, deps);
9754
+ if (options.fix) {
9755
+ const changed = await applyFixes(options, deps, checks);
9756
+ if (changed) checks = await runChecks(options, deps);
9757
+ }
9758
+ return { ok: !checks.some((check) => check.status === "fail"), checks };
9759
+ }
9760
+
9761
+ // src/errors.ts
9762
+ var UsageError = class extends Error {
9763
+ };
9764
+
9765
+ // src/output.ts
9766
+ function formatTable(headers, rows) {
9767
+ const widths = headers.map(
9768
+ (header, index) => Math.max(header.length, ...rows.map((row) => (row[index] ?? "").length))
9769
+ );
9770
+ const render = (cells) => cells.map((cell, index) => cell.padEnd(widths[index] ?? 0)).join(" ").trimEnd();
9771
+ const divider = widths.map((width) => "-".repeat(width)).join(" ");
9772
+ return [render(headers), divider, ...rows.map(render)].join("\n");
9773
+ }
9774
+
9775
+ // src/session.ts
9776
+ import { rm as rm5, stat as stat4 } from "node:fs/promises";
9777
+ import { resolve as resolve3 } from "node:path";
9778
+
9281
9779
  // src/tasks.ts
9282
- import { readdir as readdir3, readFile as readFile5 } from "node:fs/promises";
9283
- import { join as join5 } from "node:path";
9780
+ import { readdir as readdir4, readFile as readFile6 } from "node:fs/promises";
9781
+ import { join as join6 } from "node:path";
9284
9782
  var TASK_ID_RE2 = /\b(?:TASK|T)-\d{3,}\b/i;
9285
9783
  var HEADING_RE = /^#{1,4}\s+(.+)$/m;
9286
9784
  var LAYER_RANK = {
@@ -9330,12 +9828,12 @@ var LAYER_CHECKLISTS = {
9330
9828
  ]
9331
9829
  };
9332
9830
  async function loadTasks(bundleDir) {
9333
- const tasksDir = join5(bundleDir, "tasks");
9831
+ const tasksDir = join6(bundleDir, "tasks");
9334
9832
  let entries;
9335
9833
  try {
9336
- entries = await readdir3(tasksDir, { withFileTypes: true });
9834
+ entries = await readdir4(tasksDir, { withFileTypes: true });
9337
9835
  } catch (error) {
9338
- if (isMissingFile4(error)) throw new Error(`Direktori tasks/ tidak ditemukan di ${bundleDir}`);
9836
+ if (isMissingFile5(error)) throw new Error(`Direktori tasks/ tidak ditemukan di ${bundleDir}`);
9339
9837
  throw error;
9340
9838
  }
9341
9839
  const names = entries.filter(
@@ -9344,16 +9842,16 @@ async function loadTasks(bundleDir) {
9344
9842
  const tasks = [];
9345
9843
  if (names.length > 0) {
9346
9844
  for (const name of names) {
9347
- const content = await readFile5(join5(tasksDir, name), "utf8");
9845
+ const content = await readFile6(join6(tasksDir, name), "utf8");
9348
9846
  tasks.push(parseSingleTask(name, content));
9349
9847
  }
9350
9848
  } else {
9351
- const singlePath = join5(tasksDir, "TASKS.md");
9849
+ const singlePath = join6(tasksDir, "TASKS.md");
9352
9850
  let content;
9353
9851
  try {
9354
- content = await readFile5(singlePath, "utf8");
9852
+ content = await readFile6(singlePath, "utf8");
9355
9853
  } catch (error) {
9356
- if (isMissingFile4(error)) throw new Error(`Tidak ada berkas task di ${tasksDir}`);
9854
+ if (isMissingFile5(error)) throw new Error(`Tidak ada berkas task di ${tasksDir}`);
9357
9855
  throw error;
9358
9856
  }
9359
9857
  tasks.push(...parseSingleFile(content));
@@ -9523,16 +10021,194 @@ function isBoundary(previous, next) {
9523
10021
  }
9524
10022
  return previous.phase !== void 0 && next.phase !== previous.phase;
9525
10023
  }
9526
- function isMissingFile4(error) {
10024
+ var RISKY_TASK_RE = /\b(migrat(?:e|ion|ions)?|migrasi|prisma|schema|rls|auth|permission|payment|checkout|webhook|secret|deploy|drop)\b/i;
10025
+ var TARGET_FILES_LABEL_RE = /^\s*(?:[-*]\s*)?\*{0,2}targetFiles\*{0,2}\s*:\s*(.*)$/i;
10026
+ function isRiskyTask(task) {
10027
+ const targetFiles = task.targetFiles ?? targetFilesFromContent(task.content);
10028
+ return RISKY_TASK_RE.test([task.id, task.title, ...targetFiles].join("\n"));
10029
+ }
10030
+ function targetFilesFromContent(content) {
10031
+ const lines = content.split(/\r?\n/);
10032
+ const index = lines.findIndex((line) => TARGET_FILES_LABEL_RE.test(line));
10033
+ if (index === -1) return [];
10034
+ const inline = (lines[index]?.match(TARGET_FILES_LABEL_RE)?.[1] ?? "").trim();
10035
+ if (inline.length > 0) return splitTargetFiles(inline);
10036
+ const items = [];
10037
+ for (let cursor = index + 1; cursor < lines.length; cursor += 1) {
10038
+ const line = lines[cursor] ?? "";
10039
+ if (line.trim().length === 0) continue;
10040
+ const item = /^\s*-\s+(.+?)\s*$/.exec(line);
10041
+ if (item === null) break;
10042
+ const cleaned = cleanTargetFile(item[1] ?? "");
10043
+ if (cleaned.length > 0) items.push(cleaned);
10044
+ }
10045
+ return items;
10046
+ }
10047
+ function splitTargetFiles(value) {
10048
+ return value.replace(/[[\]`]/g, "").split(",").map(cleanTargetFile).filter((part) => part.length > 0 && !/^(?:\(?none\)?|n\/a|-+)$/i.test(part));
10049
+ }
10050
+ function cleanTargetFile(value) {
10051
+ return value.replace(/^[-*\s]+/, "").trim();
10052
+ }
10053
+ function isMissingFile5(error) {
10054
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
10055
+ }
10056
+
10057
+ // src/session.ts
10058
+ var LOCK_STALE_MS3 = 1e4;
10059
+ var CHANGE_REQUEST_PREFIX = "Perubahan diminta:";
10060
+ async function resumeSession(options) {
10061
+ const context = await resolveBundleContext(options);
10062
+ await removeStaleLock2(context.bundleDir);
10063
+ const initial = await readState(context.bundleDir);
10064
+ if (initial === null) {
10065
+ return {
10066
+ projectId: context.projectId ?? "",
10067
+ bundleDir: context.bundleDir,
10068
+ hasState: false,
10069
+ current: null,
10070
+ nextRef: null,
10071
+ awaitingCheckpoint: false
10072
+ };
10073
+ }
10074
+ return withStateLock(context.bundleDir, async () => {
10075
+ const state = await readState(context.bundleDir) ?? initial;
10076
+ const definitions = await loadTasks(context.bundleDir);
10077
+ const merged = mergeTasks(state, definitions);
10078
+ const written = await writeState(context.bundleDir, merged);
10079
+ const current = written.current ?? null;
10080
+ const next = selectNext(definitions, written.tasks);
10081
+ return {
10082
+ projectId: written.projectId ?? context.projectId ?? "",
10083
+ bundleDir: context.bundleDir,
10084
+ hasState: true,
10085
+ current,
10086
+ nextRef: next?.task.id ?? null,
10087
+ awaitingCheckpoint: current?.awaitingCheckpoint === true
10088
+ };
10089
+ });
10090
+ }
10091
+ async function reviewSession(options, review) {
10092
+ const context = await resolveBundleContext(options);
10093
+ return withStateLock(context.bundleDir, async () => {
10094
+ const state = await readState(context.bundleDir);
10095
+ if (state === null) return unchanged(review.action, null);
10096
+ const current = state.current ?? null;
10097
+ if (review.action === "stop") {
10098
+ return stopReview(context.bundleDir, state, current, review.note);
10099
+ }
10100
+ if (current === null || current.awaitingCheckpoint !== true) {
10101
+ return unchanged(review.action, current?.ref ?? null);
10102
+ }
10103
+ if (review.action === "approve") {
10104
+ const cleared = { ...current, awaitingCheckpoint: false };
10105
+ const written2 = await writeState(context.bundleDir, setCurrent(state, cleared));
10106
+ const ref2 = written2.current?.ref ?? current.ref ?? null;
10107
+ return { action: review.action, ref: ref2, clearedCheckpoint: true, nextRef: ref2 };
10108
+ }
10109
+ const ref = review.ref ?? latestDoneRef(state);
10110
+ if (ref === null) {
10111
+ return {
10112
+ action: review.action,
10113
+ ref: null,
10114
+ clearedCheckpoint: false,
10115
+ nextRef: current.ref ?? null
10116
+ };
10117
+ }
10118
+ const target = findTask(state, ref);
10119
+ if (target === void 0) {
10120
+ throw new Error(
10121
+ `Task ${ref} tidak ada di sidecar. Jalankan \`jioogen task next\` lebih dulu.`
10122
+ );
10123
+ }
10124
+ const note = `${CHANGE_REQUEST_PREFIX} ${review.note ?? ""}`.trim();
10125
+ const reopened = withStatus(state, target.ref, "in_progress", { note, awaitingCheckpoint: false });
10126
+ const reopenedTarget = findTask(reopened, target.ref);
10127
+ const nextCurrent = {
10128
+ ref: reopenedTarget?.ref ?? target.ref,
10129
+ ...reopenedTarget?.layer !== void 0 ? { layer: reopenedTarget.layer } : {},
10130
+ ...reopenedTarget?.phase !== void 0 ? { phase: reopenedTarget.phase } : {},
10131
+ awaitingCheckpoint: false
10132
+ };
10133
+ const written = await writeState(context.bundleDir, setCurrent(reopened, nextCurrent));
10134
+ const finalRef = written.current?.ref ?? nextCurrent.ref ?? null;
10135
+ return { action: review.action, ref: finalRef, clearedCheckpoint: true, nextRef: finalRef };
10136
+ });
10137
+ }
10138
+ async function stopReview(bundleDir, state, current, note) {
10139
+ const ref = current?.ref ?? null;
10140
+ if (note === void 0 || note.length === 0 || current?.ref === void 0) {
10141
+ return { action: "stop", ref, clearedCheckpoint: false, nextRef: ref };
10142
+ }
10143
+ const key = current.ref.toUpperCase();
10144
+ const now = (/* @__PURE__ */ new Date()).toISOString();
10145
+ const tasks = state.tasks.map(
10146
+ (task) => task.ref.toUpperCase() === key ? { ...task, note, updatedAt: now } : task
10147
+ );
10148
+ const written = await writeState(bundleDir, { ...state, tasks });
10149
+ return {
10150
+ action: "stop",
10151
+ ref,
10152
+ clearedCheckpoint: false,
10153
+ nextRef: written.current?.ref ?? ref
10154
+ };
10155
+ }
10156
+ function unchanged(action, ref) {
10157
+ return { action, ref, clearedCheckpoint: false, nextRef: ref };
10158
+ }
10159
+ function findTask(state, ref) {
10160
+ const key = ref.toUpperCase();
10161
+ return state.tasks.find((task) => task.ref.toUpperCase() === key);
10162
+ }
10163
+ function latestDoneRef(state) {
10164
+ let latest = null;
10165
+ for (const task of state.tasks) {
10166
+ if (task.status !== "done") continue;
10167
+ if (latest === null || updatedAtMs(task) >= updatedAtMs(latest)) latest = task;
10168
+ }
10169
+ return latest?.ref ?? null;
10170
+ }
10171
+ function updatedAtMs(task) {
10172
+ if (task.updatedAt === void 0) return Number.NEGATIVE_INFINITY;
10173
+ const value = Date.parse(task.updatedAt);
10174
+ return Number.isNaN(value) ? Number.NEGATIVE_INFINITY : value;
10175
+ }
10176
+ async function resolveBundleContext(options) {
10177
+ const link = await findProjectLink(options.dir ?? process.cwd());
10178
+ const projectId = options.project ?? envFirst("JIOOGEN_PROJECT", "JIOOCODE_PROJECT") ?? link?.projectId;
10179
+ return {
10180
+ projectId,
10181
+ bundleDir: resolve3(link?.bundleDir ?? options.dir ?? process.cwd())
10182
+ };
10183
+ }
10184
+ async function removeStaleLock2(bundleDir) {
10185
+ const target = lockPath(bundleDir);
10186
+ let info;
10187
+ try {
10188
+ info = await stat4(target);
10189
+ } catch (error) {
10190
+ if (isMissingFile6(error)) return;
10191
+ throw error;
10192
+ }
10193
+ if (Date.now() - info.mtimeMs <= LOCK_STALE_MS3) return;
10194
+ await rm5(target, { force: true });
10195
+ }
10196
+ function envFirst(primary, legacy) {
10197
+ const value = process.env[primary]?.trim();
10198
+ if (value !== void 0 && value.length > 0) return value;
10199
+ const fallback = process.env[legacy]?.trim();
10200
+ return fallback !== void 0 && fallback.length > 0 ? fallback : void 0;
10201
+ }
10202
+ function isMissingFile6(error) {
9527
10203
  return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
9528
10204
  }
9529
10205
 
9530
10206
  // src/zip.ts
9531
10207
  var import_adm_zip = __toESM(require_adm_zip(), 1);
9532
10208
  import { mkdir as mkdir4, writeFile as writeFile4 } from "node:fs/promises";
9533
- import { dirname as dirname4, resolve as resolve3, sep } from "node:path";
10209
+ import { dirname as dirname4, resolve as resolve4, sep } from "node:path";
9534
10210
  async function extractBundle(bytes, targetDir) {
9535
- const root = resolve3(targetDir);
10211
+ const root = resolve4(targetDir);
9536
10212
  const zip = new import_adm_zip.default(Buffer.from(bytes));
9537
10213
  const written = [];
9538
10214
  for (const entry of zip.getEntries()) {
@@ -9542,7 +10218,7 @@ async function extractBundle(bytes, targetDir) {
9542
10218
  if (segments.some((segment) => segment.length === 0 || segment === "." || segment === "..")) {
9543
10219
  throw new Error(`Path bundle tidak aman: ${entry.entryName}`);
9544
10220
  }
9545
- const destination = resolve3(root, relative2);
10221
+ const destination = resolve4(root, relative2);
9546
10222
  if (!destination.startsWith(`${root}${sep}`)) {
9547
10223
  throw new Error(`Path bundle keluar dari direktori target: ${entry.entryName}`);
9548
10224
  }
@@ -9559,10 +10235,10 @@ function isValidProjectId(projectId) {
9559
10235
  return PROJECT_ID_RE.test(projectId.trim());
9560
10236
  }
9561
10237
  async function loginCommand(options) {
9562
- const token = options.token ?? envFirst("JIOOGEN_TOKEN", "JIOOCODE_TOKEN");
10238
+ const token = options.token ?? envFirst2("JIOOGEN_TOKEN", "JIOOCODE_TOKEN");
9563
10239
  if (token !== void 0 && token.length > 0) {
9564
10240
  const apiUrl = normalizeBaseUrl(
9565
- options.api ?? envFirst("JIOOGEN_API", "JIOOCODE_API") ?? DEFAULT_API_URL
10241
+ options.api ?? envFirst2("JIOOGEN_API", "JIOOCODE_API") ?? DEFAULT_API_URL
9566
10242
  );
9567
10243
  const client = new ApiClient(apiUrl, `${SESSION_COOKIE}=${token}`);
9568
10244
  const body = await client.getJson(ENDPOINTS.me);
@@ -9577,17 +10253,17 @@ async function loginCommand(options) {
9577
10253
  console.log(`Credential disimpan di ${credentialsPath()} (mode 0600).`);
9578
10254
  return;
9579
10255
  }
9580
- const email = options.email ?? envFirst("JIOOGEN_EMAIL", "JIOOCODE_EMAIL");
10256
+ const email = options.email ?? envFirst2("JIOOGEN_EMAIL", "JIOOCODE_EMAIL");
9581
10257
  if (email === void 0 || email.length === 0) {
9582
10258
  throw new Error("--email wajib diisi (atau set JIOOGEN_EMAIL).");
9583
10259
  }
9584
- let password = options.password ?? envFirst("JIOOGEN_PASSWORD", "JIOOCODE_PASSWORD");
10260
+ let password = options.password ?? envFirst2("JIOOGEN_PASSWORD", "JIOOCODE_PASSWORD");
9585
10261
  if (options.passwordStdin === true) password = (await readStdin()).trim();
9586
10262
  if (password === void 0 || password.length === 0) {
9587
10263
  throw new Error("--password atau --password-stdin wajib diisi.");
9588
10264
  }
9589
10265
  const result = await apiLogin(
9590
- options.api ?? envFirst("JIOOGEN_API", "JIOOCODE_API") ?? DEFAULT_API_URL,
10266
+ options.api ?? envFirst2("JIOOGEN_API", "JIOOCODE_API") ?? DEFAULT_API_URL,
9591
10267
  email,
9592
10268
  password
9593
10269
  );
@@ -9645,7 +10321,7 @@ async function pullCommand(projectId, options) {
9645
10321
  }
9646
10322
  await assertPullDefaultDir(process.cwd(), options.dir);
9647
10323
  const { apiUrl, client } = await resolveApi(options);
9648
- const target = resolve4(options.dir ?? projectId);
10324
+ const target = resolve5(options.dir ?? projectId);
9649
10325
  await assertPullTarget(target, projectId, options.force === true);
9650
10326
  const bytes = await client.getBytes(projectPath(projectId, "/bundle"));
9651
10327
  const written = await extractBundle(bytes, target);
@@ -9662,12 +10338,12 @@ async function pullCommand(projectId, options) {
9662
10338
  }
9663
10339
  async function linkCommand(projectId, options) {
9664
10340
  const { apiUrl, client } = await resolveApi(options);
9665
- const target = resolve4(options.dir ?? process.cwd());
10341
+ const target = resolve5(options.dir ?? process.cwd());
9666
10342
  await assertLinkTarget(target, projectId);
9667
10343
  await verifyProjectAccess(client, projectId);
9668
10344
  await writeProjectLink(target, { projectId, apiUrl });
9669
10345
  const skills = await installAgentSkills(target, { noSkills: skillsDisabled(options) });
9670
- const linkedFile = join6(target, ".jioogen", "project.json");
10346
+ const linkedFile = join7(target, ".jioogen", "project.json");
9671
10347
  if (options.json === true) {
9672
10348
  console.log(JSON.stringify({ projectId, target, apiUrl, linkedFile, skills }, null, 2));
9673
10349
  return;
@@ -9762,7 +10438,7 @@ async function skillsRemoveCommand(packArgument, options) {
9762
10438
  }
9763
10439
  async function resolveSkillsBundleDir(options) {
9764
10440
  const link = await findProjectLink(options.dir ?? process.cwd());
9765
- return resolve4(link?.bundleDir ?? options.dir ?? process.cwd());
10441
+ return resolve5(link?.bundleDir ?? options.dir ?? process.cwd());
9766
10442
  }
9767
10443
  function printSkillPacks(packs, missing) {
9768
10444
  if (packs.length === 0 && missing.length === 0) return;
@@ -9774,7 +10450,7 @@ function printSkillPacks(packs, missing) {
9774
10450
  }
9775
10451
  async function checkCommand(options) {
9776
10452
  const link = await findProjectLink(options.dir ?? process.cwd());
9777
- const bundleDir = resolve4(link?.bundleDir ?? options.dir ?? process.cwd());
10453
+ const bundleDir = resolve5(link?.bundleDir ?? options.dir ?? process.cwd());
9778
10454
  const result = await inspectBundle(bundleDir);
9779
10455
  if (options.json === true) {
9780
10456
  console.log(JSON.stringify(result, null, 2));
@@ -9783,6 +10459,90 @@ async function checkCommand(options) {
9783
10459
  }
9784
10460
  if (!result.ok) process.exitCode = 1;
9785
10461
  }
10462
+ async function doctorCommand(options) {
10463
+ const link = await findProjectLink(options.dir ?? process.cwd());
10464
+ const bundleDir = resolve5(link?.bundleDir ?? options.dir ?? process.cwd());
10465
+ const projectId = options.project ?? link?.projectId;
10466
+ const report = await runDoctor({
10467
+ bundleDir,
10468
+ ...projectId === void 0 ? {} : { projectId },
10469
+ fix: options.fix === true
10470
+ });
10471
+ if (options.json === true) {
10472
+ console.log(JSON.stringify(report, null, 2));
10473
+ } else {
10474
+ for (const check of report.checks) {
10475
+ console.log(`${check.status.toUpperCase().padEnd(4)} ${check.id} \u2014 ${check.detail}`);
10476
+ }
10477
+ const fails = report.checks.filter((check) => check.status === "fail").length;
10478
+ const warns = report.checks.filter((check) => check.status === "warn").length;
10479
+ console.log(
10480
+ `
10481
+ ${report.ok ? "Doctor OK" : "Doctor GAGAL"} (${fails} fail, ${warns} warn).` + (options.fix === true ? "" : " Jalankan `jioogen doctor --fix` untuk perbaikan wiring.")
10482
+ );
10483
+ }
10484
+ if (!report.ok) process.exitCode = 1;
10485
+ }
10486
+ async function resumeCommand(options) {
10487
+ const result = await resumeSession(options);
10488
+ if (options.json === true) {
10489
+ console.log(JSON.stringify(result, null, 2));
10490
+ return;
10491
+ }
10492
+ if (!result.hasState) {
10493
+ console.log("Belum ada eksekusi tersimpan. Mulai dengan `jioogen task next`.");
10494
+ return;
10495
+ }
10496
+ const current = result.current?.ref ?? "-";
10497
+ console.log(`Resume project ${result.projectId} di ${result.bundleDir}.`);
10498
+ console.log(`Current: ${current}${result.awaitingCheckpoint ? " (checkpoint)" : ""}`);
10499
+ console.log(`Task berikutnya: ${result.nextRef ?? "selesai"}.`);
10500
+ if (result.awaitingCheckpoint) {
10501
+ console.log(
10502
+ 'Putuskan: `jioogen review --approve`, `--request-changes "<teks>"`, atau `--stop`.'
10503
+ );
10504
+ }
10505
+ }
10506
+ async function reviewCommand(options) {
10507
+ const actions = [
10508
+ options.approve === true ? "approve" : void 0,
10509
+ options.requestChanges !== void 0 ? "request-changes" : void 0,
10510
+ options.stop === true ? "stop" : void 0
10511
+ ].filter((action2) => action2 !== void 0);
10512
+ if (actions.length !== 1) {
10513
+ throw new UsageError(
10514
+ '`jioogen review` butuh tepat satu: --approve, --request-changes "<teks>", atau --stop.'
10515
+ );
10516
+ }
10517
+ const action = actions[0];
10518
+ if (action === void 0) {
10519
+ throw new UsageError("`jioogen review` butuh tepat satu aksi.");
10520
+ }
10521
+ const result = await reviewSession(options, {
10522
+ action,
10523
+ ...options.requestChanges === void 0 ? {} : { note: options.requestChanges },
10524
+ ...options.ref === void 0 ? {} : { ref: options.ref }
10525
+ });
10526
+ if (options.json === true) {
10527
+ console.log(JSON.stringify(result, null, 2));
10528
+ return;
10529
+ }
10530
+ if (action === "stop") {
10531
+ console.log("Review: stop. Checkpoint dipertahankan; lanjutkan kapan saja dengan `resume`.");
10532
+ return;
10533
+ }
10534
+ if (!result.clearedCheckpoint) {
10535
+ console.log("Tidak ada checkpoint aktif. Jalankan `jioogen resume` untuk status.");
10536
+ return;
10537
+ }
10538
+ if (action === "approve") {
10539
+ console.log("Review: disetujui. Checkpoint dibuka.");
10540
+ } else {
10541
+ console.log(`Review: perubahan diminta untuk ${result.ref ?? "task terakhir"}.`);
10542
+ }
10543
+ if (result.nextRef !== null) console.log(`Task berikutnya: ${result.nextRef}.`);
10544
+ console.log("Jalankan `jioogen sync` agar panel monitoring ikut diperbarui.");
10545
+ }
9786
10546
  async function handoffCommand(options) {
9787
10547
  const { client, projectId } = await resolveBundle(options);
9788
10548
  const payload = await client.postJson(
@@ -9887,11 +10647,17 @@ async function taskCompleteCommand(ref, options) {
9887
10647
  (entry) => entry.ref.toUpperCase() === ref.toUpperCase()
9888
10648
  );
9889
10649
  const next2 = selectNext(local.definitions, completed.tasks);
9890
- if (completedTask2 !== void 0 && next2 !== null && isBoundary(completedTask2, next2.task)) {
10650
+ const definition = local.definitions.find(
10651
+ (entry) => entry.id.toUpperCase() === ref.toUpperCase()
10652
+ );
10653
+ const risky = definition !== void 0 && isRiskyTask(definition);
10654
+ const boundary = completedTask2 !== void 0 && (next2 !== null && isBoundary(completedTask2, next2.task) || risky);
10655
+ if (boundary && completedTask2 !== void 0) {
10656
+ const target = next2?.task;
9891
10657
  const withCheckpoint = setCurrent(completed, {
9892
- ref: next2.task.id,
9893
- ...next2.task.layer !== void 0 ? { layer: next2.task.layer } : {},
9894
- phase: next2.task.phase,
10658
+ ref: target?.id ?? completedTask2.ref,
10659
+ layer: target?.layer ?? completedTask2.layer,
10660
+ phase: target?.phase ?? completedTask2.phase ?? 1,
9895
10661
  awaitingCheckpoint: true
9896
10662
  });
9897
10663
  await writeState(local.bundleDir, withCheckpoint);
@@ -9901,11 +10667,17 @@ async function taskCompleteCommand(ref, options) {
9901
10667
  return { completedTask: completedTask2, next: next2, checkpoint: false };
9902
10668
  });
9903
10669
  console.log(`Done ${completedTask?.ref ?? ref}.`);
9904
- if (checkpoint && next !== null) {
10670
+ if (checkpoint) {
10671
+ if (next === null) {
10672
+ console.log("Semua task selesai; menunggu review terakhir.");
10673
+ } else {
10674
+ console.log(
10675
+ `Checkpoint berikutnya: ${next.task.id} (${next.task.layer ?? "-"} / phase ${next.task.phase}).`
10676
+ );
10677
+ }
9905
10678
  console.log(
9906
- `Checkpoint berikutnya: ${next.task.id} (${next.task.layer ?? "-"} / phase ${next.task.phase}).`
10679
+ 'Berhenti. Setelah user memutuskan: `jioogen review --approve`, `--request-changes "<teks>"`, atau `--stop`; lalu `jioogen sync`.'
9907
10680
  );
9908
- console.log("Berhenti dan minta konfirmasi user sebelum task start berikutnya.");
9909
10681
  return;
9910
10682
  }
9911
10683
  if (next !== null) console.log(`Berikutnya: ${next.task.id} \u2014 ${next.task.title}`);
@@ -10007,7 +10779,7 @@ async function syncCommand(options) {
10007
10779
  assertBundleRelative(relative2);
10008
10780
  files.push({
10009
10781
  path: relative2,
10010
- content: await readFile6(join6(bundleDir, relative2), "utf8")
10782
+ content: await readFile7(join7(bundleDir, relative2), "utf8")
10011
10783
  });
10012
10784
  }
10013
10785
  }
@@ -10029,9 +10801,9 @@ async function syncCommand(options) {
10029
10801
  async function resolveApi(options) {
10030
10802
  const stored = await readCredentials();
10031
10803
  const apiUrl = normalizeBaseUrl(
10032
- options.api ?? envFirst("JIOOGEN_API", "JIOOCODE_API") ?? stored?.apiUrl ?? DEFAULT_API_URL
10804
+ options.api ?? envFirst2("JIOOGEN_API", "JIOOCODE_API") ?? stored?.apiUrl ?? DEFAULT_API_URL
10033
10805
  );
10034
- const explicitCookie = envFirst("JIOOGEN_COOKIE", "JIOOCODE_COOKIE");
10806
+ const explicitCookie = envFirst2("JIOOGEN_COOKIE", "JIOOCODE_COOKIE");
10035
10807
  const storedCookie = stored !== null && normalizeBaseUrl(stored.apiUrl) === apiUrl ? stored.cookie : void 0;
10036
10808
  const cookie = explicitCookie || storedCookie;
10037
10809
  if (cookie === void 0 || cookie.length === 0) {
@@ -10046,7 +10818,7 @@ async function resolveApi(options) {
10046
10818
  }
10047
10819
  async function resolveLocalBundle(options) {
10048
10820
  const link = await findProjectLink(options.dir ?? process.cwd());
10049
- const projectId = options.project ?? envFirst("JIOOGEN_PROJECT", "JIOOCODE_PROJECT") ?? link?.projectId;
10821
+ const projectId = options.project ?? envFirst2("JIOOGEN_PROJECT", "JIOOCODE_PROJECT") ?? link?.projectId;
10050
10822
  if (projectId === void 0 || projectId.length === 0) {
10051
10823
  throw new Error(
10052
10824
  "Project belum di-bind. Jalankan `jioogen pull <projectId>` atau pakai --project."
@@ -10054,7 +10826,7 @@ async function resolveLocalBundle(options) {
10054
10826
  }
10055
10827
  return {
10056
10828
  projectId,
10057
- bundleDir: resolve4(link?.bundleDir ?? options.dir ?? process.cwd()),
10829
+ bundleDir: resolve5(link?.bundleDir ?? options.dir ?? process.cwd()),
10058
10830
  linkedApiUrl: link?.apiUrl
10059
10831
  };
10060
10832
  }
@@ -10107,9 +10879,9 @@ async function assertPullDefaultDir(cwd, dirOption) {
10107
10879
  async function assertPullTarget(target, projectId, force = false) {
10108
10880
  let entries;
10109
10881
  try {
10110
- entries = await readdir4(target);
10882
+ entries = await readdir5(target);
10111
10883
  } catch (error) {
10112
- if (isMissingFile5(error)) return;
10884
+ if (isMissingFile7(error)) return;
10113
10885
  throw error;
10114
10886
  }
10115
10887
  if (entries.length === 0) return;
@@ -10141,19 +10913,19 @@ async function assertLinkTarget(target, projectId) {
10141
10913
  }
10142
10914
  async function hasBundleMarkers(target) {
10143
10915
  try {
10144
- await readFile6(join6(target, "PRD.md"));
10916
+ await readFile7(join7(target, "PRD.md"));
10145
10917
  return true;
10146
10918
  } catch (error) {
10147
- if (!isMissingFile5(error)) throw error;
10919
+ if (!isMissingFile7(error)) throw error;
10148
10920
  }
10149
- return hasTaskMarkdown(join6(target, "tasks"));
10921
+ return hasTaskMarkdown(join7(target, "tasks"));
10150
10922
  }
10151
10923
  async function hasTaskMarkdown(directory) {
10152
10924
  let names;
10153
10925
  try {
10154
- names = await readdir4(directory);
10926
+ names = await readdir5(directory);
10155
10927
  } catch (error) {
10156
- if (isMissingFile5(error)) return false;
10928
+ if (isMissingFile7(error)) return false;
10157
10929
  throw error;
10158
10930
  }
10159
10931
  return names.some((name) => name.toLowerCase().endsWith(".md"));
@@ -10209,18 +10981,18 @@ async function readStdin() {
10209
10981
  for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
10210
10982
  return Buffer.concat(chunks).toString("utf8");
10211
10983
  }
10212
- function envFirst(primary, legacy) {
10984
+ function envFirst2(primary, legacy) {
10213
10985
  const value = process.env[primary]?.trim();
10214
10986
  if (value !== void 0 && value.length > 0) return value;
10215
10987
  const fallback = process.env[legacy]?.trim();
10216
10988
  return fallback !== void 0 && fallback.length > 0 ? fallback : void 0;
10217
10989
  }
10218
- function isMissingFile5(error) {
10990
+ function isMissingFile7(error) {
10219
10991
  return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
10220
10992
  }
10221
10993
 
10222
10994
  // src/index.ts
10223
- var VERSION = "0.2.4";
10995
+ var VERSION = "0.2.5";
10224
10996
  var USAGE = `jioogen \u2014 CLI JiooGen (task selection lokal, server read-only)
10225
10997
 
10226
10998
  Usage:
@@ -10237,6 +11009,9 @@ Usage:
10237
11009
  jioogen skills update [--dir <dir>]
10238
11010
  jioogen skills remove <baseline|ui-quality|backend-rules> [--dir <dir>]
10239
11011
  jioogen check [--dir <dir>] [--json]
11012
+ jioogen doctor [--fix] [--dir <dir>] [--json]
11013
+ jioogen resume [--dir <dir>] [--json]
11014
+ jioogen review (--approve | --request-changes <teks> | --stop) [--ref <TASK-XXX>] [--json]
10240
11015
  jioogen handoff [--project <id>] [--json]
10241
11016
  jioogen task next [--json]
10242
11017
  jioogen task start <ref>
@@ -10277,6 +11052,11 @@ async function main() {
10277
11052
  "no-skills": { type: "boolean" },
10278
11053
  files: { type: "string" },
10279
11054
  note: { type: "string" },
11055
+ fix: { type: "boolean" },
11056
+ approve: { type: "boolean" },
11057
+ stop: { type: "boolean" },
11058
+ "request-changes": { type: "string" },
11059
+ ref: { type: "string" },
10280
11060
  json: { type: "boolean" },
10281
11061
  help: { type: "boolean", short: "h" },
10282
11062
  version: { type: "boolean", short: "v" }
@@ -10304,6 +11084,11 @@ async function main() {
10304
11084
  noSkills: flag(raw["no-skills"]),
10305
11085
  files: text(raw.files),
10306
11086
  note: text(raw.note),
11087
+ fix: flag(raw.fix),
11088
+ approve: flag(raw.approve),
11089
+ stop: flag(raw.stop),
11090
+ requestChanges: text(raw["request-changes"]),
11091
+ ref: text(raw.ref),
10307
11092
  json: flag(raw.json)
10308
11093
  };
10309
11094
  if (flag(raw.version)) {
@@ -10366,6 +11151,15 @@ async function dispatch(command, rest, options) {
10366
11151
  case "check":
10367
11152
  await checkCommand(options);
10368
11153
  return;
11154
+ case "doctor":
11155
+ await doctorCommand(options);
11156
+ return;
11157
+ case "resume":
11158
+ await resumeCommand(options);
11159
+ return;
11160
+ case "review":
11161
+ await reviewCommand(options);
11162
+ return;
10369
11163
  case "handoff":
10370
11164
  await handoffCommand(options);
10371
11165
  return;