hanoman 0.1.43 → 0.1.45

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/server.js CHANGED
@@ -4151,7 +4151,7 @@ var init_enums = __esm({
4151
4151
  "use strict";
4152
4152
  init_zod();
4153
4153
  zStage = external_exports.enum(["brainstorming", "objective", "spec-ready", "planned", "executing", "done"]);
4154
- zSpecSource = external_exports.enum(["brief", "qa", "audit", "help", "goal"]);
4154
+ zSpecSource = external_exports.enum(["brief", "qa", "audit", "help", "goal", "no_effort"]);
4155
4155
  zDocStatus = external_exports.enum(["ok", "drift", "broken"]);
4156
4156
  zPriority = external_exports.enum(["tinggi", "sedang", "rendah"]);
4157
4157
  zProjectKind = external_exports.enum(["from-scratch", "existing"]);
@@ -4166,7 +4166,7 @@ var init_enums = __esm({
4166
4166
 
4167
4167
  // ../shared/src/spec-source.ts
4168
4168
  function payloadShapeFor(source) {
4169
- return source === "qa" ? "qa" : source === "goal" ? "goal" : "brief";
4169
+ return source === "qa" ? "qa" : GOAL_SHAPED_SOURCES.has(source) ? "goal" : "brief";
4170
4170
  }
4171
4171
  function shapeOfPayload(payload) {
4172
4172
  const p = payload ?? {};
@@ -4203,15 +4203,17 @@ function convertPayload(to, payload) {
4203
4203
  expected: str3("outcome"),
4204
4204
  actual: str3("context"),
4205
4205
  env: "",
4206
+ constraints: str3("constraints"),
4206
4207
  ...fromAudit ? { fromAudit } : {}
4207
- }, nonEmpty(["constraints"]));
4208
+ }, []);
4208
4209
  return done({
4209
4210
  severity: severityFromPriority(prio()),
4210
4211
  steps: "",
4211
4212
  expected: str3("goal"),
4212
4213
  actual: "",
4213
- env: ""
4214
- }, nonEmpty(["done", "constraints"]));
4214
+ env: "",
4215
+ constraints: str3("constraints")
4216
+ }, nonEmpty(["done"]));
4215
4217
  }
4216
4218
  if (toShape === "goal") {
4217
4219
  if (fromShape === "brief") {
@@ -4224,7 +4226,7 @@ function convertPayload(to, payload) {
4224
4226
  return done({
4225
4227
  goal: str3("expected"),
4226
4228
  done: "",
4227
- constraints: "",
4229
+ constraints: str3("constraints"),
4228
4230
  priority: priorityFromSeverity(p.severity)
4229
4231
  }, nonEmpty(["steps", "actual", "env", "fromAudit"]));
4230
4232
  }
@@ -4232,7 +4234,7 @@ function convertPayload(to, payload) {
4232
4234
  return done({
4233
4235
  context: str3("actual"),
4234
4236
  outcome: str3("expected"),
4235
- constraints: "",
4237
+ constraints: str3("constraints"),
4236
4238
  priority: priorityFromSeverity(p.severity),
4237
4239
  ...fromAudit ? { fromAudit } : {}
4238
4240
  }, nonEmpty(["steps", "env"]));
@@ -4243,11 +4245,12 @@ function convertPayload(to, payload) {
4243
4245
  priority: prio()
4244
4246
  }, nonEmpty(["done"]));
4245
4247
  }
4246
- var SHAPE_REQUIRED;
4248
+ var GOAL_SHAPED_SOURCES, SHAPE_REQUIRED;
4247
4249
  var init_spec_source = __esm({
4248
4250
  "../shared/src/spec-source.ts"() {
4249
4251
  "use strict";
4250
4252
  init_enums();
4253
+ GOAL_SHAPED_SOURCES = /* @__PURE__ */ new Set(["goal", "no_effort"]);
4251
4254
  SHAPE_REQUIRED = {
4252
4255
  brief: ["context", "outcome"],
4253
4256
  qa: ["steps", "expected", "actual", "env"],
@@ -4596,6 +4599,10 @@ var init_entities = __esm({
4596
4599
  expected: external_exports.string(),
4597
4600
  actual: external_exports.string(),
4598
4601
  env: external_exports.string(),
4602
+ // SPEC-826 · bentuk ketiga yang ikut punya batasan pengerjaan. `.default("")` bukan gaya:
4603
+ // payload qa yang SUDAH tersimpan tak punya field ini, dan `z.string()` polos membuat setiap
4604
+ // baris lama gagal validasi begitu ia lewat zSpec/zCreateSpec/zPatchSpec/zChangeSpecSource.
4605
+ constraints: external_exports.string().default(""),
4599
4606
  fromAudit: external_exports.string().optional()
4600
4607
  });
4601
4608
  zGoalPayload = external_exports.object({
@@ -5456,9 +5463,9 @@ var init_cron_expr = __esm({
5456
5463
 
5457
5464
  // ../shared/src/dto.ts
5458
5465
  function flowForSource(source) {
5459
- return source === "qa" ? "qa" : source === "audit" ? "audit" : source === "goal" ? "goal" : "feature";
5466
+ return source === "qa" ? "qa" : source === "audit" ? "audit" : source === "goal" ? "goal" : source === "no_effort" ? "no_effort" : "feature";
5460
5467
  }
5461
- var zIssueDeviceToken, zSessionResult, zSessionHistory, zCreateProject, zProjectId, zRenameProject, zUpdateProject, zCreateSpec, zPatchSpec, zChangeSpecSource, zMarkSpecDone, zIntegrate, zSessionSummary, zProjectView, zSchedulerQueueItem, zSchedulerSourceView, zSchedulerSessionView, zSchedulerQueueCounts, zSchedulerState, zLeadDecisionView, zLeadFlowView, zLeadAnswer, zLeadProjectStatus, zLeadStatusView, zFlow, zPrdBrief, zPrdDoc, zBreakdownItem, zBreakdownDoc, zBatchCreateSpec, zEscalationTarget, zEscalationPrefill, zAuditEscalation, zAuditEscalationView, zTerminalSession, zDocFileContent, zDocIndexCat, zDocIndex, HOST_RE, USER_RE, zCreateVps, zPatchVps, USER_ROLES, zUserRole, zLogin, zSignup, zSetup, zChangePassword, zCreateClientAccount, zUpdateClientAccount, zVpsCheck, zMarkNa, zMarkNaBulk, zAttest, zRemediate, UPDATE_RESTART_EXIT, zUpdateApplyBody, zTicketView, zTicketAttachmentView, zTicketDetail, zTicketEditInput, zHelpInfo, zPublicTicketStatus, zCronExpr, zCreateCron, zPatchCron, zSchedulerCron, zSchedulerCronRun;
5468
+ var zIssueDeviceToken, zSessionResult, zSessionHistory, zCreateProject, zProjectId, zRenameProject, zUpdateProject, zCreateSpec, zPatchSpec, zChangeSpecSource, zMarkSpecDone, zIntegrate, zSessionSummary, zProjectView, zSchedulerQueueItem, zSchedulerSourceView, zSchedulerSessionView, zSchedulerQueueCounts, zSchedulerState, zLeadDecisionView, zLeadFlowView, zLeadAnswer, zLeadProjectStatus, zLeadStatusView, zFlow, isGoalShapedFlow, zPrdBrief, zPrdDoc, zBreakdownItem, zBreakdownDoc, zBatchCreateSpec, zEscalationTarget, zEscalationPrefill, zAuditEscalation, zAuditEscalationView, zTerminalSession, zDocFileContent, zDocIndexCat, zDocIndex, HOST_RE, USER_RE, zCreateVps, zPatchVps, USER_ROLES, zUserRole, zLogin, zSignup, zSetup, zChangePassword, zCreateClientAccount, zUpdateClientAccount, zVpsCheck, zMarkNa, zMarkNaBulk, zAttest, zRemediate, UPDATE_RESTART_EXIT, zUpdateApplyBody, zTicketView, zTicketAttachmentView, zTicketDetail, zTicketEditInput, zHelpInfo, zPublicTicketStatus, zCronExpr, zCreateCron, zPatchCron, zSchedulerCron, zSchedulerCronRun;
5462
5469
  var init_dto = __esm({
5463
5470
  "../shared/src/dto.ts"() {
5464
5471
  "use strict";
@@ -5730,7 +5737,8 @@ var init_dto = __esm({
5730
5737
  capacity: external_exports.number()
5731
5738
  }).default({ inFlight: 0, queued: 0, capacity: 1 })
5732
5739
  });
5733
- zFlow = external_exports.enum(["feature", "qa", "scaffold", "reverse", "prd", "audit", "breakdown", "goal"]);
5740
+ zFlow = external_exports.enum(["feature", "qa", "scaffold", "reverse", "prd", "audit", "breakdown", "goal", "no_effort"]);
5741
+ isGoalShapedFlow = (flow) => flow === "goal" || flow === "no_effort";
5734
5742
  zPrdBrief = external_exports.object({
5735
5743
  title: external_exports.string().min(1),
5736
5744
  context: external_exports.string(),
@@ -6092,6 +6100,9 @@ var init_api = __esm({
6092
6100
  ideCompare: (id, from, to) => `${API}/projects/${id}/compare?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}`,
6093
6101
  ideCompareFile: (id, from, to, path2) => `${API}/projects/${id}/compare/file?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}&path=${encodeURIComponent(path2)}`,
6094
6102
  ideGit: (id) => `${API}/projects/${id}/git`,
6103
+ // ADR-0121 · operasi berkas Explorer: satu path untuk buat/rename/hapus, satu untuk unggah.
6104
+ ideEntry: (id, path2) => `${API}/projects/${id}/entry${path2 ? `?path=${encodeURIComponent(path2)}` : ""}`,
6105
+ ideUpload: (id) => `${API}/projects/${id}/upload`,
6095
6106
  ideGitMerge: (id) => `${API}/projects/${id}/git/merge`,
6096
6107
  // SPEC-229 · merge git graph isolasi
6097
6108
  // SPEC-234 · status working tree (staged/unstaged) + diff satu file working tree
@@ -6266,11 +6277,11 @@ function isExternalLink(target2) {
6266
6277
  return !target2 || /^(https?:|#|mailto:)/.test(target2);
6267
6278
  }
6268
6279
  function resolveLink(fromRel, target2) {
6269
- const clean = target2.trim().split(/\s+/)[0].split("#")[0].split("\\").join("/");
6270
- if (!clean) return "";
6271
- if (clean.startsWith("/")) return clean.slice(1).split("/").filter((p) => p && p !== ".").join("/");
6280
+ const clean2 = target2.trim().split(/\s+/)[0].split("#")[0].split("\\").join("/");
6281
+ if (!clean2) return "";
6282
+ if (clean2.startsWith("/")) return clean2.slice(1).split("/").filter((p) => p && p !== ".").join("/");
6272
6283
  const dir2 = fromRel.includes("/") ? fromRel.slice(0, fromRel.lastIndexOf("/")) : "";
6273
- const parts = (dir2 ? dir2.split("/") : []).concat(clean.replace(/^\.\//, "").split("/"));
6284
+ const parts = (dir2 ? dir2.split("/") : []).concat(clean2.replace(/^\.\//, "").split("/"));
6274
6285
  const out3 = [];
6275
6286
  for (const p of parts) {
6276
6287
  if (p === "" || p === ".") continue;
@@ -7144,12 +7155,13 @@ var init_mcp_schema = __esm({
7144
7155
  expected: str("Yang seharusnya terjadi."),
7145
7156
  actual: str("Yang sebenarnya terjadi. Dari sinilah `objective` backlog diturunkan server."),
7146
7157
  env: str("Lingkungan tempat temuan muncul: versi, browser, OS, instance."),
7158
+ constraints: str("Batasan yang mengikat pengerjaan: yang tak boleh berubah, yang wajib dipertahankan. Boleh string kosong \u2014 SENGAJA tak wajib (SPEC-826)."),
7147
7159
  fromAudit: str("Opsional. Id backlog audit asal (mis. `SPEC-371`).")
7148
7160
  },
7149
7161
  required: ["severity", "steps", "expected", "actual", "env"]
7150
7162
  });
7151
7163
  GOAL_PAYLOAD = obj({
7152
- description: "Bentuk payload untuk source `goal`. Sesi goal mengejar satu tujuan tanpa fase perencanaan (ADR-0089).",
7164
+ description: "Bentuk payload untuk source `goal` dan `no_effort`. Sesi goal mengejar satu tujuan tanpa fase perencanaan (ADR-0089); sesi no_effort mengerjakan satu task remeh dalam SATU fase lalu berhenti (ADR-0123).",
7153
7165
  properties: {
7154
7166
  goal: str("Satu tujuan yang dikejar sesi. Dari sinilah `objective` backlog diturunkan server."),
7155
7167
  done: str("Bukti berhenti yang dituntut. Kosong berarti goal itu sendiri buktinya."),
@@ -7159,12 +7171,12 @@ var init_mcp_schema = __esm({
7159
7171
  required: ["goal", "done", "constraints", "priority"]
7160
7172
  });
7161
7173
  SPEC_PAYLOAD_ONEOF = {
7162
- description: "Isi backlog. BENTUKNYA DITENTUKAN `source`: `qa` \u2192 {severity, steps, expected, actual, env}; `goal` \u2192 {goal, done, constraints, priority}; `brief`/`audit`/`help` \u2192 {context, outcome, constraints, priority}. Bentuk yang tak cocok ditolak sebelum dikirim.",
7174
+ description: "Isi backlog. BENTUKNYA DITENTUKAN `source`: `qa` \u2192 {severity, steps, expected, actual, env, constraints}; `goal`/`no_effort` \u2192 {goal, done, constraints, priority}; `brief`/`audit`/`help` \u2192 {context, outcome, constraints, priority}. `constraints` qa opsional (default string kosong); bentuk yang tak cocok ditolak sebelum dikirim.",
7163
7175
  oneOf: [BRIEF_PAYLOAD, QA_PAYLOAD, GOAL_PAYLOAD]
7164
7176
  };
7165
7177
  SOURCE_PAYLOAD_ALLOF = [
7166
7178
  { if: { properties: { source: { const: "qa" } }, required: ["source"] }, then: { properties: { payload: QA_PAYLOAD } } },
7167
- { if: { properties: { source: { const: "goal" } }, required: ["source"] }, then: { properties: { payload: GOAL_PAYLOAD } } },
7179
+ { if: { properties: { source: { enum: ["goal", "no_effort"] } }, required: ["source"] }, then: { properties: { payload: GOAL_PAYLOAD } } },
7168
7180
  { if: { properties: { source: { enum: ["brief", "audit", "help"] } }, required: ["source"] }, then: { properties: { payload: BRIEF_PAYLOAD } } }
7169
7181
  ];
7170
7182
  PAGE_PARAMS = {
@@ -8112,25 +8124,26 @@ Judul: ${spec.title}
8112
8124
  Objective: ${spec.objective}${detail}`
8113
8125
  ].filter(Boolean).join("\n\n");
8114
8126
  }
8115
- function startGoalPrompt(spec, branchTo, opts = {}) {
8127
+ function startGoalPrompt(flow, spec, branchTo, opts = {}) {
8116
8128
  const m = resolveMethod(opts.method);
8117
8129
  const g = readGoalPayload(spec.payload);
8130
+ const noEffort = flow === "no_effort";
8118
8131
  const detail = [
8119
8132
  `Goal: ${g?.goal ?? spec.objective}`,
8120
8133
  g?.done ? `Selesai bila: ${g.done}` : "",
8121
8134
  g?.constraints ? `Batasan: ${g.constraints}` : ""
8122
8135
  ].filter(Boolean).join("\n");
8123
8136
  return [
8124
- "hanoman goal \u2014 sesi ini mengejar SATU goal sampai tercapai. TIDAK ada fase Brainstorm, Objective, Spec, maupun Plan: jangan menulis design doc, jangan menulis plan berkotak, jangan memecah pekerjaan ini jadi backlog baru. Langsung kerjakan goal-nya. Tetap ikuti internal/docs sebagai Source of Truth; perbarui docs yang tersentuh dan link-nya di index, dalam commit yang sama.",
8137
+ noEffort ? "hanoman no-effort \u2014 sesi ini mengerjakan SATU pekerjaan remeh lalu berhenti. TIDAK ada fase Brainstorm, Objective, Spec, Plan, maupun fase pembuktian terpisah: jangan menulis design doc, jangan menulis plan berkotak, jangan memecah pekerjaan ini jadi backlog baru, dan jangan menambah fase sendiri. Langsung kerjakan, buktikan seperlunya di fase yang sama, lalu berhenti. Tetap ikuti internal/docs sebagai Source of Truth; perbarui docs yang tersentuh dan link-nya di index, dalam commit yang sama." : "hanoman goal \u2014 sesi ini mengejar SATU goal sampai tercapai. TIDAK ada fase Brainstorm, Objective, Spec, maupun Plan: jangan menulis design doc, jangan menulis plan berkotak, jangan memecah pekerjaan ini jadi backlog baru. Langsung kerjakan goal-nya. Tetap ikuti internal/docs sebagai Source of Truth; perbarui docs yang tersentuh dan link-nya di index, dalam commit yang sama.",
8125
8138
  opts.resume ? resumeClause(opts.resume, branchTo, m.planDir, false) : "",
8126
8139
  detail,
8127
- phaseInstruction(PIPELINES.goal, m),
8128
- "Fase Verifikasi bukan formalitas: jalankan perintah yang membuktikan goal-nya tercapai (test/typecheck/benchmark/perintah yang relevan) dan baca outputnya. Klaim tanpa output bukan bukti.",
8140
+ phaseInstruction(PIPELINES[flow], m),
8141
+ noEffort ? "" : "Fase Verifikasi bukan formalitas: jalankan perintah yang membuktikan goal-nya tercapai (test/typecheck/benchmark/perintah yang relevan) dan baca outputnya. Klaim tanpa output bukan bukti.",
8129
8142
  autonomyClause(opts.autonomy),
8130
- scopeClause("goal", opts.verifyScope),
8131
- codeStyleClause("goal"),
8143
+ scopeClause(flow, opts.verifyScope),
8144
+ codeStyleClause(flow),
8132
8145
  methodClause(m),
8133
- skillInstruction(PIPELINES.goal, m, writesCode("goal")),
8146
+ skillInstruction(PIPELINES[flow], m, writesCode(flow)),
8134
8147
  `Setelah fase terakhir: commit, lalu \`git push origin HEAD:refs/heads/${branchTo}\`. Worktree ini detached HEAD \u2014 itu memang disengaja.`,
8135
8148
  `Backlog item ${spec.id} \xB7 sumber ${spec.source} \xB7 prioritas ${spec.priority}
8136
8149
  Judul: ${spec.title}`
@@ -8216,7 +8229,7 @@ Stack: ${project.stack || "\u2014"}`,
8216
8229
  ${cron.prompt}`
8217
8230
  ].join("\n\n");
8218
8231
  }
8219
- var PIPELINES, AUTONOMY_CLAUSE, AUTONOMY_CLAUSE_FULL, autonomyClause, phaseInstruction, skillInstruction, auditDecisionInstruction, ESCALATION_CONTRACT, auditOnlyInstruction, auditContinuationInstruction, writesCode, scopeClause, codeStyleClause, methodClause, PROJECT_METHOD, resumeClause, RESUMED_WORKTREE_NOTE, REVERSE_PHASE_GUIDE, SCAFFOLD_PHASE_GUIDE;
8232
+ var PIPELINES, WORK_PHASES, AUTONOMY_CLAUSE, AUTONOMY_CLAUSE_FULL, autonomyClause, phaseInstruction, skillInstruction, auditDecisionInstruction, ESCALATION_CONTRACT, auditOnlyInstruction, auditContinuationInstruction, writesCode, scopeClause, codeStyleClause, methodClause, PROJECT_METHOD, resumeClause, RESUMED_WORKTREE_NOTE, REVERSE_PHASE_GUIDE, SCAFFOLD_PHASE_GUIDE;
8220
8233
  var init_prompt = __esm({
8221
8234
  "../runner/src/prompt.ts"() {
8222
8235
  "use strict";
@@ -8238,8 +8251,14 @@ var init_prompt = __esm({
8238
8251
  // SPEC-407 · ADR-0089 · backlog goal: tak ada fase perencanaan sama sekali. `Goal` = kerjakan,
8239
8252
  // `Verifikasi` = buktikan. Kedua nama unik lintas PIPELINES — syarat peta REACHED di server,
8240
8253
  // yang berkunci nama fase saja.
8241
- goal: ["Goal", "Verifikasi"]
8242
- };
8254
+ goal: ["Goal", "Verifikasi"],
8255
+ // SPEC-825 · ADR-0123 · task remeh: SATU fase. Fase `Verifikasi` milik flow goal menghabiskan
8256
+ // satu giliran agen untuk membuktikan sesuatu yang diff-nya sendiri sudah membuktikan; untuk
8257
+ // ganti copy / bump konstanta / typo docs itu murni biaya. Nama `Kerjakan` unik lintas
8258
+ // PIPELINES — syarat peta REACHED server, yang berkunci nama fase saja.
8259
+ no_effort: ["Kerjakan"]
8260
+ };
8261
+ WORK_PHASES = ["Execute", "Goal", "Kerjakan"];
8243
8262
  AUTONOMY_CLAUSE = 'Jalankan seluruh pipeline sampai tuntas tanpa berhenti di batas antar-fase. Checkpoint "review"/"approval"/"need review" milik skill superpowers BUKAN titik berhenti di sini \u2014 lanjut saja ke fase berikutnya. Berhenti HANYA saat butuh keputusan manusia sejati (percabangan yang mengubah bentuk kerja: data model, kontrak API, scope); saat itu tanyakan di terminal ini dan tunggu jawabannya. Selain itu, terus lanjut.';
8244
8263
  AUTONOMY_CLAUSE_FULL = "Kamu berjalan TANPA pengawas \u2014 tak ada manusia yang menonton terminal ini untuk menjawab. Putuskan sendiri di SETIAP percabangan (termasuk yang mengubah bentuk kerja: data model, kontrak API, scope) berdasarkan Source of Truth dan penilaian terbaikmu; JANGAN berhenti bertanya. Tembus seluruh pipeline sampai stage `done`, lalu commit & push. Jangan menunggu review/persetujuan siapa pun \u2014 catat asumsi & keputusan penting di pesan commit agar bisa di-review pasca-fakta. Merge ke branch utama tetap dilakukan manusia, bukan kamu.";
8245
8264
  autonomyClause = (mode) => mode === "full-control" ? AUTONOMY_CLAUSE_FULL : AUTONOMY_CLAUSE;
@@ -8293,7 +8312,7 @@ ${lines2.join("\n")}` : "";
8293
8312
  return `Backlog brief ini LANJUTAN dari audit ${fromAudit}. Worktree ini lahir dari branch audit itu, jadi dokumen audit sudah ada di ${doc}. BACA dokumen itu lebih dulu dan pakai sebagai bahan fase Brainstorm & Objective \u2014 temuannya sudah terbukti, jangan menginvestigasi ulang dari nol. Semua fase tetap dijalankan: dokumen audit memuat TEMUAN, bukan bentuk solusi, jadi perancangan fitur tetap pekerjaanmu.`;
8294
8313
  return `Backlog qa ini LANJUTAN dari audit ${fromAudit}. Worktree ini lahir dari branch audit itu, jadi dokumen audit sudah ada di ${doc}. JANGAN mengulang investigasi fase Audit dari nol \u2014 baca dokumen audit itu sebagai temuan, tandai fase Audit dilewati (\`echo "Audit skipped" >> "$HANOMAN_PHASE_FILE"\`), lalu ambil keputusan pasca-Audit: perbaikan jelas & kecil \u2192 langsung Execute (tandai \`Spec skipped\` dan \`Plan skipped\` bila sesuai); selain itu Spec \u2192 Plan \u2192 Execute penuh.`;
8295
8314
  };
8296
- writesCode = (flow) => PIPELINES[flow].includes("Execute") || PIPELINES[flow].includes("Goal");
8315
+ writesCode = (flow) => PIPELINES[flow].some((p) => WORK_PHASES.includes(p));
8297
8316
  scopeClause = (flow, scope) => scope && writesCode(flow) ? verifyScopeClause(scope) : "";
8298
8317
  codeStyleClause = (flow) => writesCode(flow) ? CODE_STYLE_CLAUSE : "";
8299
8318
  methodClause = (method) => method.extraClause ?? "";
@@ -8484,7 +8503,7 @@ var init_settings = __esm({
8484
8503
  });
8485
8504
 
8486
8505
  // ../runner/src/goal.ts
8487
- function goalFlowCondition(specId, branchTo, spec) {
8506
+ function goalFlowCondition(flow, specId, branchTo, spec) {
8488
8507
  const g = readGoalPayload(spec?.payload);
8489
8508
  const goal = g?.goal || (spec?.objective ?? "").trim() || "(goal tak tercatat di backlog item)";
8490
8509
  const bukti = g?.done || goal;
@@ -8492,13 +8511,13 @@ function goalFlowCondition(specId, branchTo, spec) {
8492
8511
  `Sesi goal hanoman ${specId}. GOAL: ${goal}`,
8493
8512
  "Sesi ini hanya boleh berhenti bila transkrip TERBARU memuat bukti langsung semua hal berikut:",
8494
8513
  `1. goal tercapai \u2014 ${bukti};`,
8495
- `2. output \`cat "$HANOMAN_PHASE_FILE"\` yang memuat satu baris untuk SETIAP fase ${PIPELINES.goal.join(" \u2192 ")}, masing-masing berakhiran \`done\` atau \`skipped\`;`,
8514
+ `2. output \`cat "$HANOMAN_PHASE_FILE"\` yang memuat satu baris untuk SETIAP fase ${PIPELINES[flow].join(" \u2192 ")}, masing-masing berakhiran \`done\` atau \`skipped\`;`,
8496
8515
  `3. output \`git push origin HEAD:refs/heads/${branchTo}\` yang SUKSES sesudah commit terakhir.`,
8497
8516
  "Bila salah satu bukti tak ada di transkrip terbaru, kondisi BELUM terpenuhi: jalankan perintah verifikasinya, tuntaskan yang masih kurang, lalu lanjutkan \u2014 jangan berhenti."
8498
8517
  ].join("\n");
8499
8518
  }
8500
8519
  function defaultGoalCondition({ flow, specId, branchTo, spec }) {
8501
- if (flow === "goal") return goalFlowCondition(specId, branchTo, spec);
8520
+ if (isGoalShapedFlow(flow)) return goalFlowCondition(flow, specId, branchTo, spec);
8502
8521
  const phases = PIPELINES[flow];
8503
8522
  const planGate = phases.includes("Plan") && phases.includes("Execute");
8504
8523
  const clauses = [
@@ -9293,7 +9312,7 @@ function readPhases(file, flow) {
9293
9312
  function stageFor(phases) {
9294
9313
  let best = -1;
9295
9314
  for (const p of phases) {
9296
- if ((p.name === "Execute" || p.name === "Goal") && p.state === "active")
9315
+ if (WORK_PHASES.includes(p.name) && p.state === "active")
9297
9316
  best = Math.max(best, STAGES.indexOf("executing"));
9298
9317
  if (p.state !== "done" && p.state !== "skipped") continue;
9299
9318
  const s2 = REACHED[p.name];
@@ -9350,7 +9369,10 @@ var init_session_phases = __esm({
9350
9369
  // SPEC-407 · ADR-0089 · flow goal (Goal → Verifikasi): fase kerja mencapai `executing`, fase
9351
9370
  // verifikasi yang mencapai `done`. Kedua nama unik lintas PIPELINES — peta ini berkunci nama.
9352
9371
  Goal: "executing",
9353
- Verifikasi: "done"
9372
+ Verifikasi: "done",
9373
+ // SPEC-825 · ADR-0123 · flow no_effort (Kerjakan): satu fase, jadi fase kerjanya sendiri yang
9374
+ // mencapai `done` — tak ada fase verifikasi untuk menutupnya.
9375
+ Kerjakan: "done"
9354
9376
  };
9355
9377
  phasesComplete = (phases) => phases.length > 0 && phases.every((p) => p.state === "done" || p.state === "skipped");
9356
9378
  }
@@ -11626,9 +11648,9 @@ var init_config3 = __esm({
11626
11648
  // src/services/telegram/protocol.ts
11627
11649
  import { createHash as createHash2 } from "node:crypto";
11628
11650
  function normalizeTelegramCommand(text) {
11629
- const clean = text.trim();
11630
- const match = clean.match(/^\/([a-z0-9_]+)(?:@[a-z0-9_]+)?(?:\s+([\s\S]*))?$/i);
11631
- if (!match) return clean;
11651
+ const clean2 = text.trim();
11652
+ const match = clean2.match(/^\/([a-z0-9_]+)(?:@[a-z0-9_]+)?(?:\s+([\s\S]*))?$/i);
11653
+ if (!match) return clean2;
11632
11654
  const args = match[2]?.trim().replace(/\s+/g, " ");
11633
11655
  return `/${match[1].toLowerCase()}${args ? ` ${args}` : ""}`;
11634
11656
  }
@@ -11694,19 +11716,19 @@ function inboundDigest(input) {
11694
11716
  return createHash2("sha256").update(`${input.updateId}\0${input.chatId}\0${input.userId}\0${input.kind}\0${input.text}`, "utf8").digest("hex");
11695
11717
  }
11696
11718
  function sanitizeTelegramOutput(text, exactSecrets = []) {
11697
- let clean = text.replace(ANSI, "").replace(CONTROLS, "");
11719
+ let clean2 = text.replace(ANSI, "").replace(CONTROLS, "");
11698
11720
  for (const secret of [...exactSecrets].filter(Boolean).sort((a, b) => b.length - a.length)) {
11699
- clean = clean.split(secret).join("[REDACTED]");
11721
+ clean2 = clean2.split(secret).join("[REDACTED]");
11700
11722
  }
11701
- for (const pattern of CREDENTIAL_PATTERNS) clean = clean.replace(pattern, "[REDACTED]");
11702
- return clean.trim();
11723
+ for (const pattern of CREDENTIAL_PATTERNS) clean2 = clean2.replace(pattern, "[REDACTED]");
11724
+ return clean2.trim();
11703
11725
  }
11704
11726
  function splitTelegramText(text, max = 4096, exactSecrets = []) {
11705
11727
  if (!Number.isInteger(max) || max < 1) throw new Error("max harus integer positif");
11706
- const clean = sanitizeTelegramOutput(text, exactSecrets);
11707
- if (!clean) return [];
11728
+ const clean2 = sanitizeTelegramOutput(text, exactSecrets);
11729
+ if (!clean2) return [];
11708
11730
  const chunks = [];
11709
- let rest = clean;
11731
+ let rest = clean2;
11710
11732
  while (rest.length > max) {
11711
11733
  let cut = max;
11712
11734
  const before2 = rest.charCodeAt(cut - 1);
@@ -18558,11 +18580,11 @@ var require_clean = __commonJS({
18558
18580
  "../node_modules/.pnpm/semver@7.8.5/node_modules/semver/functions/clean.js"(exports, module) {
18559
18581
  "use strict";
18560
18582
  var parse3 = require_parse();
18561
- var clean = (version2, options2) => {
18583
+ var clean2 = (version2, options2) => {
18562
18584
  const s2 = parse3(version2.trim().replace(/^[=v]+/, ""), options2);
18563
18585
  return s2 ? s2.version : null;
18564
18586
  };
18565
- module.exports = clean;
18587
+ module.exports = clean2;
18566
18588
  }
18567
18589
  });
18568
18590
 
@@ -19959,7 +19981,7 @@ var require_semver2 = __commonJS({
19959
19981
  var identifiers = require_identifiers();
19960
19982
  var parse3 = require_parse();
19961
19983
  var valid = require_valid();
19962
- var clean = require_clean();
19984
+ var clean2 = require_clean();
19963
19985
  var inc = require_inc();
19964
19986
  var diff = require_diff();
19965
19987
  var major = require_major();
@@ -19998,7 +20020,7 @@ var require_semver2 = __commonJS({
19998
20020
  module.exports = {
19999
20021
  parse: parse3,
20000
20022
  valid,
20001
- clean,
20023
+ clean: clean2,
20002
20024
  inc,
20003
20025
  diff,
20004
20026
  major,
@@ -24356,7 +24378,7 @@ var require_multipart2 = __commonJS({
24356
24378
  var Busboy = require_main();
24357
24379
  var os = __require("node:os");
24358
24380
  var fp = require_fastify_plugin();
24359
- var { createWriteStream } = __require("node:fs");
24381
+ var { createWriteStream: createWriteStream2 } = __require("node:fs");
24360
24382
  var { unlink: unlink6 } = __require("node:fs/promises");
24361
24383
  var path2 = __require("node:path");
24362
24384
  var { generateId } = require_generateId();
@@ -24736,7 +24758,7 @@ var require_multipart2 = __commonJS({
24736
24758
  continue;
24737
24759
  }
24738
24760
  const filepath = path2.join(tmpdir5, generateId() + path2.extname(part.filename || "file" + i++));
24739
- const target2 = createWriteStream(filepath);
24761
+ const target2 = createWriteStream2(filepath);
24740
24762
  try {
24741
24763
  this.tmpUploads.push(filepath);
24742
24764
  await pump2(part.file, target2);
@@ -24983,6 +25005,7 @@ async function resolveRepoEntry(root, rel, opts = {}) {
24983
25005
  for (let i = 0; i < parts.length; i++) {
24984
25006
  current = join6(current, parts[i]);
24985
25007
  const stat2 = await lstat(current).catch((error) => {
25008
+ if (error.code === "ENOENT" && opts.allowMissingTail) return null;
24986
25009
  if (error.code === "ENOENT" && opts.allowMissingFinal && i === parts.length - 1) return null;
24987
25010
  return denied("komponen tidak ada");
24988
25011
  });
@@ -25859,7 +25882,7 @@ async function nextSpecId(repoDir) {
25859
25882
  // src/services/spec-fields.ts
25860
25883
  init_src();
25861
25884
  function deriveSpecFields(source, payload, manualPriority) {
25862
- if (source === "goal") {
25885
+ if (payloadShapeFor(source) === "goal") {
25863
25886
  const pick2 = (v) => typeof v === "string" ? v.trim() : "";
25864
25887
  return {
25865
25888
  priority: manualPriority,
@@ -28731,7 +28754,7 @@ async function specs_default(app2) {
28731
28754
  source: b.source,
28732
28755
  stage: "brainstorming",
28733
28756
  priority,
28734
- author: isQa ? `QA \xB7 ${author}` : b.source === "audit" ? `Audit \xB7 ${author}` : b.source === "goal" ? `Goal \xB7 ${author}` : author,
28757
+ author: isQa ? `QA \xB7 ${author}` : b.source === "audit" ? `Audit \xB7 ${author}` : b.source === "goal" ? `Goal \xB7 ${author}` : b.source === "no_effort" ? `No effort \xB7 ${author}` : author,
28735
28758
  objective,
28736
28759
  payload: b.payload,
28737
28760
  branchFrom: b.branchFrom ?? null,
@@ -29373,10 +29396,10 @@ async function listGraph(repoDir, limit = 200, opts = {}) {
29373
29396
  const [sha, parents, author, at, subject, refs2] = line.split(US);
29374
29397
  const tags = [], branchRefs = [];
29375
29398
  for (const d of (refs2 ?? "").split(",").map((r) => r.trim()).filter(Boolean)) {
29376
- const clean = d.replace(/^HEAD -> /, "");
29377
- if (clean === "HEAD") continue;
29378
- if (clean.startsWith("tag: ")) tags.push(clean.slice(5));
29379
- else branchRefs.push(clean);
29399
+ const clean2 = d.replace(/^HEAD -> /, "");
29400
+ if (clean2 === "HEAD") continue;
29401
+ if (clean2.startsWith("tag: ")) tags.push(clean2.slice(5));
29402
+ else branchRefs.push(clean2);
29380
29403
  }
29381
29404
  return {
29382
29405
  sha,
@@ -29852,6 +29875,85 @@ async function deleteBranches(repoDir, names, opts) {
29852
29875
  return { base: report.base, results };
29853
29876
  }
29854
29877
 
29878
+ // src/services/repo-fs.ts
29879
+ import { lstat as lstat2, rename as rename2, rm as rm3 } from "node:fs/promises";
29880
+ import { createWriteStream } from "node:fs";
29881
+ import { join as join9 } from "node:path";
29882
+ import { randomUUID as randomUUID5 } from "node:crypto";
29883
+ import { pipeline } from "node:stream/promises";
29884
+ var EntryExistsError = class extends Error {
29885
+ code = "ENTRY_EXISTS";
29886
+ };
29887
+ var EntryMissingError = class extends Error {
29888
+ code = "ENTRY_MISSING";
29889
+ };
29890
+ var EntryTargetInsideError = class extends Error {
29891
+ code = "ENTRY_TARGET_INSIDE";
29892
+ };
29893
+ function assertNotGit(rel) {
29894
+ if (rel.split(/[\\/]/).includes(".git"))
29895
+ throw new PathContainmentError("repository path ditolak: tidak boleh menyentuh .git");
29896
+ }
29897
+ var clean = (s2) => s2.trim().replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
29898
+ function joinRel(dir2, name2) {
29899
+ const d = clean(dir2), n2 = clean(name2);
29900
+ if (!n2) throw new PathContainmentError("repository path ditolak: nama berkas kosong");
29901
+ return d ? `${d}/${n2}` : n2;
29902
+ }
29903
+ async function entryKind(repoDir, rel) {
29904
+ assertNotGit(rel);
29905
+ const { path: path2 } = await resolveRepoEntry(repoDir, rel, { allowMissingTail: true });
29906
+ const st = await lstat2(path2).catch(() => null);
29907
+ if (!st) return null;
29908
+ return st.isDirectory() ? "dir" : "file";
29909
+ }
29910
+ async function createEntry(repoDir, rel, kind) {
29911
+ if (await entryKind(repoDir, rel)) throw new EntryExistsError(`sudah ada: ${rel}`);
29912
+ await writeRepoFileAtomic(repoDir, kind === "dir" ? `${clean(rel)}/.gitkeep` : rel, "");
29913
+ return { path: rel };
29914
+ }
29915
+ async function renameEntry(repoDir, from, to) {
29916
+ const kind = await entryKind(repoDir, from);
29917
+ if (!kind) throw new EntryMissingError(`tidak ada: ${from}`);
29918
+ if (await entryKind(repoDir, to)) throw new EntryExistsError(`sudah ada: ${to}`);
29919
+ if (kind === "dir" && `${clean(to)}/`.startsWith(`${clean(from)}/`))
29920
+ throw new EntryTargetInsideError("tujuan di dalam sumber");
29921
+ await ensureRepoParents(repoDir, to);
29922
+ const src = await resolveRepoEntry(repoDir, from);
29923
+ const dst = await resolveRepoEntry(repoDir, to, { allowMissingFinal: true });
29924
+ await rename2(src.path, dst.path);
29925
+ return { from, to };
29926
+ }
29927
+ async function deleteEntry(repoDir, rel) {
29928
+ const kind = await entryKind(repoDir, rel);
29929
+ if (!kind) throw new EntryMissingError(`tidak ada: ${rel}`);
29930
+ const { path: path2 } = await resolveRepoEntry(repoDir, rel);
29931
+ await rm3(path2, { recursive: kind === "dir", force: false });
29932
+ return { path: rel, kind };
29933
+ }
29934
+ async function saveUpload(repoDir, rel, source, opts = {}) {
29935
+ assertNotGit(rel);
29936
+ await ensureRepoParents(repoDir, rel);
29937
+ const entry = await resolveRepoEntry(repoDir, rel, { allowMissingFinal: true });
29938
+ const current = await lstat2(entry.path).catch(() => null);
29939
+ if (current && !opts.overwrite) return { status: "exists" };
29940
+ if (current && !current.isFile())
29941
+ throw new PathContainmentError("repository path ditolak: target bukan file regular");
29942
+ const temp = join9(entry.parent, `.hanoman-${randomUUID5()}.tmp`);
29943
+ try {
29944
+ await pipeline(source, createWriteStream(temp, { flags: "wx", mode: 384 }));
29945
+ if (opts.isTruncated?.()) {
29946
+ await rm3(temp, { force: true });
29947
+ return { status: "too-large" };
29948
+ }
29949
+ await rename2(temp, entry.path);
29950
+ return { status: "written" };
29951
+ } catch (error) {
29952
+ await rm3(temp, { force: true });
29953
+ throw error;
29954
+ }
29955
+ }
29956
+
29855
29957
  // src/routes/ide.ts
29856
29958
  async function repoOf(id) {
29857
29959
  const p = await prisma.project.findUnique({ where: { id } });
@@ -29870,6 +29972,19 @@ async function lockInputs(id) {
29870
29972
  sessionBranches: new Set(sessions)
29871
29973
  };
29872
29974
  }
29975
+ function entryError(reply, e) {
29976
+ if (e instanceof EntryMissingError) return reply.code(404).send({ error: "not found" });
29977
+ if (e instanceof EntryExistsError) return reply.code(409).send({ error: "sudah ada" });
29978
+ if (e instanceof EntryTargetInsideError) return reply.code(400).send({ error: "tujuan di dalam sumber" });
29979
+ return reply.code(400).send({ error: e.message });
29980
+ }
29981
+ var UPLOAD_LIMITS = {
29982
+ fileSize: 100 * 1024 * 1024,
29983
+ files: 1e3,
29984
+ fields: 10,
29985
+ fieldSize: 1024 * 1024
29986
+ };
29987
+ var UPLOAD_TOTAL_MAX = 2 * 1024 * 1024 * 1024;
29873
29988
  async function ide_default(app2) {
29874
29989
  app2.get("/projects/:id/tree", async (req, reply) => {
29875
29990
  const repoDir = await repoOf(req.params.id);
@@ -29934,6 +30049,102 @@ async function ide_default(app2) {
29934
30049
  return reply.code(400).send({ error: e.message });
29935
30050
  }
29936
30051
  });
30052
+ app2.post("/projects/:id/entry", async (req, reply) => {
30053
+ const repoDir = await repoOf(req.params.id);
30054
+ if (repoDir === void 0) return reply.code(404).send({ error: "not found" });
30055
+ if (!repoDir) return reply.code(400).send({ error: "project tidak punya repoDir" });
30056
+ const b = req.body;
30057
+ if (!b?.path || typeof b.path !== "string") return reply.code(400).send({ error: "path wajib" });
30058
+ if (b.kind !== "file" && b.kind !== "dir") return reply.code(400).send({ error: "kind harus file atau dir" });
30059
+ try {
30060
+ return reply.code(201).send(await createEntry(repoDir, b.path, b.kind));
30061
+ } catch (e) {
30062
+ return entryError(reply, e);
30063
+ }
30064
+ });
30065
+ app2.patch("/projects/:id/entry", async (req, reply) => {
30066
+ const repoDir = await repoOf(req.params.id);
30067
+ if (repoDir === void 0) return reply.code(404).send({ error: "not found" });
30068
+ if (!repoDir) return reply.code(400).send({ error: "project tidak punya repoDir" });
30069
+ const b = req.body;
30070
+ if (!b?.from || !b?.to || typeof b.from !== "string" || typeof b.to !== "string")
30071
+ return reply.code(400).send({ error: "from & to wajib" });
30072
+ try {
30073
+ return await renameEntry(repoDir, b.from, b.to);
30074
+ } catch (e) {
30075
+ return entryError(reply, e);
30076
+ }
30077
+ });
30078
+ app2.delete("/projects/:id/entry", async (req, reply) => {
30079
+ const repoDir = await repoOf(req.params.id);
30080
+ if (repoDir === void 0) return reply.code(404).send({ error: "not found" });
30081
+ if (!repoDir) return reply.code(400).send({ error: "project tidak punya repoDir" });
30082
+ const { path: path2 } = req.query;
30083
+ if (!path2) return reply.code(400).send({ error: "path wajib" });
30084
+ try {
30085
+ return await deleteEntry(repoDir, path2);
30086
+ } catch (e) {
30087
+ return entryError(reply, e);
30088
+ }
30089
+ });
30090
+ app2.post("/projects/:id/upload", async (req, reply) => {
30091
+ const repoDir = await repoOf(req.params.id);
30092
+ if (repoDir === void 0) return reply.code(404).send({ error: "not found" });
30093
+ if (!repoDir) return reply.code(400).send({ error: "project tidak punya repoDir" });
30094
+ if (!req.isMultipart?.()) return reply.code(400).send({ error: "butuh multipart/form-data" });
30095
+ const totalMax = Number(process.env.HANOMAN_IDE_UPLOAD_MAX_BYTES) || UPLOAD_TOTAL_MAX;
30096
+ let dir2 = "", overwrite = false, manifest = null, seen = 0, total = 0;
30097
+ const written = [];
30098
+ const skipped2 = [];
30099
+ for await (const part of req.parts({ limits: UPLOAD_LIMITS })) {
30100
+ if (part.type === "field") {
30101
+ if (part.fieldname === "dir") dir2 = String(part.value ?? "");
30102
+ else if (part.fieldname === "overwrite") overwrite = part.value === "1" || part.value === "true";
30103
+ else if (part.fieldname === "manifest") {
30104
+ try {
30105
+ const parsed = JSON.parse(String(part.value));
30106
+ if (!Array.isArray(parsed) || parsed.some((p) => typeof p !== "string")) throw new Error("bentuk");
30107
+ manifest = parsed;
30108
+ } catch {
30109
+ return reply.code(400).send({ error: "manifest tak sah" });
30110
+ }
30111
+ }
30112
+ continue;
30113
+ }
30114
+ const name2 = manifest ? manifest[seen] : part.filename;
30115
+ seen++;
30116
+ if (name2 === void 0) {
30117
+ part.file.resume();
30118
+ return reply.code(400).send({ error: "manifest tak cocok dengan berkas" });
30119
+ }
30120
+ let rel = name2;
30121
+ try {
30122
+ rel = joinRel(dir2, name2);
30123
+ if (total >= totalMax) {
30124
+ part.file.resume();
30125
+ skipped2.push({ path: rel, reason: "budget" });
30126
+ continue;
30127
+ }
30128
+ const r = await saveUpload(repoDir, rel, part.file, {
30129
+ overwrite,
30130
+ isTruncated: () => part.file.truncated === true
30131
+ });
30132
+ if (r.status === "written") {
30133
+ written.push(rel);
30134
+ total += Number(part.file.bytesRead ?? 0);
30135
+ } else {
30136
+ part.file.resume();
30137
+ skipped2.push({ path: rel, reason: r.status });
30138
+ }
30139
+ } catch {
30140
+ part.file.resume();
30141
+ skipped2.push({ path: name2, reason: "denied" });
30142
+ }
30143
+ }
30144
+ if (manifest && seen !== manifest.length)
30145
+ return reply.code(400).send({ error: "manifest tak cocok dengan berkas" });
30146
+ return { written, skipped: skipped2 };
30147
+ });
29937
30148
  app2.get("/projects/:id/graph", async (req, reply) => {
29938
30149
  const repoDir = await repoOf(req.params.id);
29939
30150
  if (repoDir === void 0) return reply.code(404).send({ error: "not found" });
@@ -30172,14 +30383,14 @@ async function finishGraphOp(reply, id, repoDir, r, verb) {
30172
30383
  // src/routes/fs.ts
30173
30384
  import { readdir } from "node:fs/promises";
30174
30385
  import { homedir as homedir4 } from "node:os";
30175
- import { resolve as resolve13, dirname as dirname9, join as join9 } from "node:path";
30386
+ import { resolve as resolve13, dirname as dirname9, join as join10 } from "node:path";
30176
30387
  async function fs_default(app2) {
30177
30388
  app2.get("/fs/browse", async (req, reply) => {
30178
30389
  const q = req.query.path;
30179
30390
  const dir2 = q && q.trim() ? resolve13(q.trim()) : homedir4();
30180
30391
  try {
30181
30392
  const ents = await readdir(dir2, { withFileTypes: true });
30182
- const entries = ents.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => ({ name: e.name, path: join9(dir2, e.name) })).sort((a, b) => a.name.localeCompare(b.name));
30393
+ const entries = ents.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => ({ name: e.name, path: join10(dir2, e.name) })).sort((a, b) => a.name.localeCompare(b.name));
30183
30394
  const parent = dirname9(dir2);
30184
30395
  return { path: dir2, parent: parent === dir2 ? null : parent, entries };
30185
30396
  } catch {
@@ -30253,7 +30464,7 @@ async function startSpecSession(spec, opts) {
30253
30464
  const worktree = `${repoDir}/.worktrees/${id}`;
30254
30465
  const branchTo = `hanoman/${id}`;
30255
30466
  const resume = !isContinue && spec.baseSha ? resumeState(repoDir, worktree, branchTo, spec.headSha) : null;
30256
- const isGoalFlow = opts.flow === "goal";
30467
+ const isGoalFlow = isGoalShapedFlow(opts.flow);
30257
30468
  const goalArgs = {
30258
30469
  flow: opts.flow,
30259
30470
  specId: spec.id,
@@ -30295,7 +30506,7 @@ async function startSpecSession(spec, opts) {
30295
30506
  const resumeCtx = resume ? buildResumeCtx(repoDir, id, opts.flow, resume.worktreeKept) : void 0;
30296
30507
  let prompt;
30297
30508
  if (isGoalFlow) {
30298
- prompt = startGoalPrompt(brief, branchTo, {
30509
+ prompt = startGoalPrompt(opts.flow, brief, branchTo, {
30299
30510
  autonomy: opts.autonomy,
30300
30511
  verifyScope,
30301
30512
  resume: resumeCtx,
@@ -30625,15 +30836,15 @@ function ownsWorktree(repoDir, cwd) {
30625
30836
  init_src2();
30626
30837
  init_db();
30627
30838
  import { execFile as execFile8 } from "node:child_process";
30628
- import { readdir as readdir2, rm as rm3 } from "node:fs/promises";
30839
+ import { readdir as readdir2, rm as rm4 } from "node:fs/promises";
30629
30840
  import { promisify as promisify9 } from "node:util";
30630
- import { join as join10, resolve as resolve15 } from "node:path";
30841
+ import { join as join11, resolve as resolve15 } from "node:path";
30631
30842
  init_notifications();
30632
30843
  var TICK_MS = 6e4;
30633
30844
  var trashDirOf = (repoDir) => resolve15(repoDir, ".worktrees", ".trash");
30634
30845
  var exec8 = promisify9(execFile8);
30635
30846
  var prodReaperDeps = {
30636
- rm: (path2) => rm3(path2, { recursive: true, force: true }),
30847
+ rm: (path2) => rm4(path2, { recursive: true, force: true }),
30637
30848
  prune: async (repoDir) => {
30638
30849
  try {
30639
30850
  await exec8("git", ["worktree", "prune"], { cwd: repoDir, timeout: 3e4 });
@@ -30687,7 +30898,7 @@ async function sweepRepo(repoDir, projectId, deps = prodReaperDeps) {
30687
30898
  }
30688
30899
  let removed = 0;
30689
30900
  for (const entry of entries) {
30690
- const path2 = join10(dir2, entry);
30901
+ const path2 = join11(dir2, entry);
30691
30902
  const known = pending.get(path2);
30692
30903
  const row = known ?? { path: path2, repoDir, projectId, entry, sessionId: sessionIdOf(entry), since: Date.now() };
30693
30904
  pending.set(path2, row);
@@ -31306,7 +31517,7 @@ init_config2();
31306
31517
  import { spawn as spawn3 } from "node:child_process";
31307
31518
  import { mkdtempSync, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "node:fs";
31308
31519
  import { tmpdir as tmpdir3 } from "node:os";
31309
- import { dirname as dirname10, join as join11 } from "node:path";
31520
+ import { dirname as dirname10, join as join12 } from "node:path";
31310
31521
  var sshBin = () => effectiveStr("HANOMAN_SSH_BIN") ?? "ssh";
31311
31522
  function consoleArgv(t) {
31312
31523
  return [
@@ -31321,8 +31532,8 @@ function consoleArgv(t) {
31321
31532
  ];
31322
31533
  }
31323
31534
  function askpassScript() {
31324
- const dir2 = mkdtempSync(join11(tmpdir3(), "hanoman-askpass-"));
31325
- const path2 = join11(dir2, "askpass.sh");
31535
+ const dir2 = mkdtempSync(join12(tmpdir3(), "hanoman-askpass-"));
31536
+ const path2 = join12(dir2, "askpass.sh");
31326
31537
  writeFileSync4(path2, `#!/bin/sh
31327
31538
  printf '%s' "$HANOMAN_SSH_PASSWORD"
31328
31539
  `, { mode: 448 });
@@ -31383,15 +31594,15 @@ function sshExec(t, remoteCmd, opts = {}) {
31383
31594
  // src/services/vps-audit.ts
31384
31595
  init_db();
31385
31596
  import { readFileSync as readFileSync7 } from "node:fs";
31386
- import { join as join13 } from "node:path";
31597
+ import { join as join14 } from "node:path";
31387
31598
 
31388
31599
  // src/runner/deps.ts
31389
31600
  import { existsSync as existsSync11 } from "node:fs";
31390
- import { dirname as dirname11, join as join12 } from "node:path";
31601
+ import { dirname as dirname11, join as join13 } from "node:path";
31391
31602
  function repoRootFrom(startDir) {
31392
31603
  let dir2 = startDir;
31393
31604
  for (let i = 0; i < 8; i++) {
31394
- if (existsSync11(join12(dir2, "pnpm-workspace.yaml"))) return dir2;
31605
+ if (existsSync11(join13(dir2, "pnpm-workspace.yaml"))) return dir2;
31395
31606
  const parent = dirname11(dir2);
31396
31607
  if (parent === dir2) break;
31397
31608
  dir2 = parent;
@@ -31497,7 +31708,7 @@ function parseHealth(out3) {
31497
31708
  return { uptime: h.uptime ?? "", disk: h.disk ?? "", mem: h.mem ?? "", load: h.load ?? "" };
31498
31709
  }
31499
31710
  var target = (v) => ({ host: v.host, port: v.port, user: v.user, keyPath: v.keyPath });
31500
- var scriptPath = (f) => join13(repoRoot(), "server", "scripts", "vps", f);
31711
+ var scriptPath = (f) => join14(repoRoot(), "server", "scripts", "vps", f);
31501
31712
  async function itemStatesOf(vpsId) {
31502
31713
  const rows = await prisma.vpsItemState.findMany({ where: { vpsId } });
31503
31714
  return Object.fromEntries(rows.map((s2) => [s2.itemId, { na: s2.na, attested: s2.attested }]));
@@ -31646,11 +31857,11 @@ init_config2();
31646
31857
  import { execFileSync as execFileSync2 } from "node:child_process";
31647
31858
  import { existsSync as existsSync12, mkdirSync as mkdirSync8, readFileSync as readFileSync9 } from "node:fs";
31648
31859
  import { homedir as homedir5 } from "node:os";
31649
- import { join as join14 } from "node:path";
31650
- var keyDir = () => effectiveStr("HANOMAN_SSH_KEY_DIR") ?? join14(homedir5(), ".hanoman");
31860
+ import { join as join15 } from "node:path";
31861
+ var keyDir = () => effectiveStr("HANOMAN_SSH_KEY_DIR") ?? join15(homedir5(), ".hanoman");
31651
31862
  function ensureHanomanKey() {
31652
31863
  const dir2 = keyDir();
31653
- const privPath = join14(dir2, "id_ed25519");
31864
+ const privPath = join15(dir2, "id_ed25519");
31654
31865
  const pubPath = `${privPath}.pub`;
31655
31866
  if (!existsSync12(privPath)) {
31656
31867
  mkdirSync8(dir2, { recursive: true, mode: 448 });
@@ -31888,7 +32099,7 @@ init_config2();
31888
32099
  import { execFileSync as execFileSync3 } from "node:child_process";
31889
32100
  import { readFileSync as readFileSync11 } from "node:fs";
31890
32101
  import { homedir as homedir7 } from "node:os";
31891
- import { join as join15 } from "node:path";
32102
+ import { join as join16 } from "node:path";
31892
32103
  var USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
31893
32104
  var TTL_MS2 = 3e4;
31894
32105
  var lastOk = null;
@@ -31901,7 +32112,7 @@ var LABELS = {
31901
32112
  var humanize = (k) => k.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
31902
32113
  var nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
31903
32114
  function credsFile() {
31904
- return join15(effectiveStr("CLAUDE_CONFIG_DIR") ?? join15(homedir7(), ".claude"), ".credentials.json");
32115
+ return join16(effectiveStr("CLAUDE_CONFIG_DIR") ?? join16(homedir7(), ".claude"), ".credentials.json");
31905
32116
  }
31906
32117
  function readAccessToken() {
31907
32118
  if (process.platform === "darwin" && !effectiveStr("CLAUDE_CONFIG_DIR")) {
@@ -31975,14 +32186,14 @@ async function getLimits() {
31975
32186
  // src/services/codex-limits.ts
31976
32187
  import { open as open3, readdir as readdir3, stat } from "node:fs/promises";
31977
32188
  import { homedir as homedir8 } from "node:os";
31978
- import { join as join16 } from "node:path";
32189
+ import { join as join17 } from "node:path";
31979
32190
  var TTL_MS3 = 3e4;
31980
32191
  var STALE_AFTER_MS = 12 * 36e5;
31981
32192
  var TAIL_BYTES = 512 * 1024;
31982
32193
  var MAX_FILES = 8;
31983
32194
  var cache2 = null;
31984
32195
  var freshUntil2 = 0;
31985
- var codexSessionsDir = () => join16(process.env.CODEX_HOME ?? join16(homedir8(), ".codex"), "sessions");
32196
+ var codexSessionsDir = () => join17(process.env.CODEX_HOME ?? join17(homedir8(), ".codex"), "sessions");
31986
32197
  var UNAVAILABLE = { status: "unavailable", windows: [], fetchedAt: null, plan: null };
31987
32198
  function windowLabel(minutes) {
31988
32199
  if (minutes === 300) return "Sesi 5 jam";
@@ -32015,7 +32226,7 @@ async function recentRollouts(dir2) {
32015
32226
  return [];
32016
32227
  }
32017
32228
  const stamped = await Promise.all(entries.map(async (rel) => {
32018
- const full = join16(dir2, rel);
32229
+ const full = join17(dir2, rel);
32019
32230
  try {
32020
32231
  return { full, mtime: (await stat(full)).mtimeMs };
32021
32232
  } catch {
@@ -32671,17 +32882,17 @@ async function session_results_default(app2) {
32671
32882
  // src/services/session-history.ts
32672
32883
  init_db();
32673
32884
  init_pty();
32674
- import { randomUUID as randomUUID6 } from "node:crypto";
32885
+ import { randomUUID as randomUUID7 } from "node:crypto";
32675
32886
 
32676
32887
  // src/services/transcript-store.ts
32677
32888
  init_config2();
32678
32889
  init_src2();
32679
- import { randomUUID as randomUUID5 } from "node:crypto";
32890
+ import { randomUUID as randomUUID6 } from "node:crypto";
32680
32891
  import { mkdir as mkdir3, writeFile as writeFile2, readFile as readFile3, unlink as unlink3 } from "node:fs/promises";
32681
- import { join as join17, resolve as resolve17, basename as basename4 } from "node:path";
32892
+ import { join as join18, resolve as resolve17, basename as basename4 } from "node:path";
32682
32893
  var MAX_TRANSCRIPT_BYTES = 1024 * 1024;
32683
32894
  function transcriptDir() {
32684
- return resolve17(effectiveStr("HANOMAN_TRANSCRIPT_DIR") ?? join17(resolveHome(), "transcripts"));
32895
+ return resolve17(effectiveStr("HANOMAN_TRANSCRIPT_DIR") ?? join18(resolveHome(), "transcripts"));
32685
32896
  }
32686
32897
  function clamp(text) {
32687
32898
  const buf = Buffer.from(text, "utf8");
@@ -32698,21 +32909,21 @@ async function saveTranscript(text) {
32698
32909
  const { body, truncated } = clamp(text);
32699
32910
  const dir2 = transcriptDir();
32700
32911
  await mkdir3(dir2, { recursive: true, mode: 448 });
32701
- const key = `${randomUUID5()}.log`;
32702
- await writeFile2(join17(dir2, key), body, { encoding: "utf8", mode: 384 });
32912
+ const key = `${randomUUID6()}.log`;
32913
+ await writeFile2(join18(dir2, key), body, { encoding: "utf8", mode: 384 });
32703
32914
  return { key, bytes: Buffer.byteLength(body, "utf8"), truncated };
32704
32915
  }
32705
32916
  async function readTranscript(key) {
32706
32917
  if (!key) return null;
32707
32918
  try {
32708
- return await readFile3(join17(transcriptDir(), basename4(key)), "utf8");
32919
+ return await readFile3(join18(transcriptDir(), basename4(key)), "utf8");
32709
32920
  } catch {
32710
32921
  return null;
32711
32922
  }
32712
32923
  }
32713
32924
  async function deleteTranscript(key) {
32714
32925
  if (!key) return;
32715
- await unlink3(join17(transcriptDir(), basename4(key))).catch(() => {
32926
+ await unlink3(join18(transcriptDir(), basename4(key))).catch(() => {
32716
32927
  });
32717
32928
  }
32718
32929
 
@@ -32743,7 +32954,7 @@ async function titleFor(specId) {
32743
32954
  async function beginSession(b) {
32744
32955
  await prisma.sessionHistory.create({
32745
32956
  data: {
32746
- id: randomUUID6(),
32957
+ id: randomUUID7(),
32747
32958
  sessionId: b.sessionId,
32748
32959
  projectId: b.projectId,
32749
32960
  specId: b.specId ?? null,
@@ -32998,10 +33209,10 @@ init_notifications();
32998
33209
  init_uploads();
32999
33210
 
33000
33211
  // src/services/upload-pipeline.ts
33001
- import { randomUUID as randomUUID7 } from "node:crypto";
33212
+ import { randomUUID as randomUUID8 } from "node:crypto";
33002
33213
  import { spawn as spawn4 } from "node:child_process";
33003
- import { mkdir as mkdir4, rename as rename2, unlink as unlink4, writeFile as writeFile3 } from "node:fs/promises";
33004
- import { basename as basename5, isAbsolute as isAbsolute4, join as join18 } from "node:path";
33214
+ import { mkdir as mkdir4, rename as rename3, unlink as unlink4, writeFile as writeFile3 } from "node:fs/promises";
33215
+ import { basename as basename5, isAbsolute as isAbsolute4, join as join19 } from "node:path";
33005
33216
 
33006
33217
  // ../node_modules/.pnpm/strtok3@10.3.5/node_modules/strtok3/lib/stream/Errors.js
33007
33218
  var defaultMessages = "End-Of-Stream";
@@ -40365,7 +40576,7 @@ var dist_default = constructor_default;
40365
40576
  // src/services/upload-pipeline.ts
40366
40577
  init_db();
40367
40578
  init_uploads();
40368
- var UPLOAD_LIMITS = {
40579
+ var UPLOAD_LIMITS2 = {
40369
40580
  fileBytes: 5 * 1024 * 1024,
40370
40581
  ticketBytes: 10 * 1024 * 1024,
40371
40582
  projectBytes: 250 * 1024 * 1024,
@@ -40417,7 +40628,7 @@ function scannerFromEnv(path2) {
40417
40628
  const timer9 = setTimeout(() => {
40418
40629
  child.kill("SIGKILL");
40419
40630
  reject2(new UploadError("UPLOAD_SCAN", "scanner timeout"));
40420
- }, UPLOAD_LIMITS.scanMs);
40631
+ }, UPLOAD_LIMITS2.scanMs);
40421
40632
  child.once("error", (error) => {
40422
40633
  clearTimeout(timer9);
40423
40634
  reject2(error);
@@ -40434,9 +40645,9 @@ function safeFilename(input, extension2) {
40434
40645
  return `${stem || "upload"}${extension2}`;
40435
40646
  }
40436
40647
  async function processUpload(input, deps = {}) {
40437
- if (input.buffer.byteLength === 0 || input.buffer.byteLength > UPLOAD_LIMITS.fileBytes)
40648
+ if (input.buffer.byteLength === 0 || input.buffer.byteLength > UPLOAD_LIMITS2.fileBytes)
40438
40649
  throw new UploadError("UPLOAD_QUOTA", "file quota exceeded");
40439
- if (input.ticketBytes + input.buffer.byteLength > UPLOAD_LIMITS.ticketBytes)
40650
+ if (input.ticketBytes + input.buffer.byteLength > UPLOAD_LIMITS2.ticketBytes)
40440
40651
  throw new UploadError("UPLOAD_QUOTA", "ticket quota exceeded");
40441
40652
  const detected = await fileTypeFromBuffer(input.buffer);
40442
40653
  if (!detected || !(detected.mime in TYPES) || detected.mime !== input.clientMime)
@@ -40447,14 +40658,14 @@ async function processUpload(input, deps = {}) {
40447
40658
  let height;
40448
40659
  let normalized;
40449
40660
  try {
40450
- const source = dist_default(input.buffer, { failOn: "error", limitInputPixels: UPLOAD_LIMITS.pixels, sequentialRead: true });
40451
- const metadata2 = await timeout2(source.metadata(), UPLOAD_LIMITS.decodeMs, "UPLOAD_DECODE");
40661
+ const source = dist_default(input.buffer, { failOn: "error", limitInputPixels: UPLOAD_LIMITS2.pixels, sequentialRead: true });
40662
+ const metadata2 = await timeout2(source.metadata(), UPLOAD_LIMITS2.decodeMs, "UPLOAD_DECODE");
40452
40663
  width = metadata2.width ?? 0;
40453
40664
  height = metadata2.height ?? 0;
40454
- if (!width || !height || width > UPLOAD_LIMITS.dimension || height > UPLOAD_LIMITS.dimension || width * height > UPLOAD_LIMITS.pixels) throw new UploadError("UPLOAD_PIXELS", "image dimensions exceed policy");
40665
+ if (!width || !height || width > UPLOAD_LIMITS2.dimension || height > UPLOAD_LIMITS2.dimension || width * height > UPLOAD_LIMITS2.pixels) throw new UploadError("UPLOAD_PIXELS", "image dimensions exceed policy");
40455
40666
  normalized = await timeout2(
40456
- dist_default(input.buffer, { failOn: "error", limitInputPixels: UPLOAD_LIMITS.pixels, sequentialRead: true }).rotate().toFormat(type.format).toBuffer(),
40457
- UPLOAD_LIMITS.decodeMs,
40667
+ dist_default(input.buffer, { failOn: "error", limitInputPixels: UPLOAD_LIMITS2.pixels, sequentialRead: true }).rotate().toFormat(type.format).toBuffer(),
40668
+ UPLOAD_LIMITS2.decodeMs,
40458
40669
  "UPLOAD_DECODE"
40459
40670
  );
40460
40671
  } catch (error) {
@@ -40462,21 +40673,21 @@ async function processUpload(input, deps = {}) {
40462
40673
  throw new UploadError("UPLOAD_DECODE", "image decode failed");
40463
40674
  }
40464
40675
  const usage2 = await (deps.usage ?? defaultUsage)(input.projectId);
40465
- if (usage2.project + normalized.byteLength > UPLOAD_LIMITS.projectBytes || usage2.global + normalized.byteLength > UPLOAD_LIMITS.globalBytes)
40676
+ if (usage2.project + normalized.byteLength > UPLOAD_LIMITS2.projectBytes || usage2.global + normalized.byteLength > UPLOAD_LIMITS2.globalBytes)
40466
40677
  throw new UploadError("UPLOAD_QUOTA", "storage quota exceeded");
40467
40678
  const storageDir = deps.storageDir ?? uploadDir();
40468
- const quarantineDir = join18(storageDir, ".quarantine");
40679
+ const quarantineDir = join19(storageDir, ".quarantine");
40469
40680
  await mkdir4(quarantineDir, { recursive: true, mode: 448 });
40470
40681
  await mkdir4(storageDir, { recursive: true, mode: 448 });
40471
- const quarantine = join18(quarantineDir, `${randomUUID7()}.upload`);
40472
- const storageKey = `${randomUUID7()}${type.extension}`;
40682
+ const quarantine = join19(quarantineDir, `${randomUUID8()}.upload`);
40683
+ const storageKey = `${randomUUID8()}${type.extension}`;
40473
40684
  await writeFile3(quarantine, normalized, { mode: 384, flag: "wx" });
40474
40685
  try {
40475
- await timeout2((deps.scanner ?? scannerFromEnv)(quarantine), UPLOAD_LIMITS.scanMs, "UPLOAD_SCAN");
40686
+ await timeout2((deps.scanner ?? scannerFromEnv)(quarantine), UPLOAD_LIMITS2.scanMs, "UPLOAD_SCAN");
40476
40687
  const finalUsage = await (deps.usage ?? defaultUsage)(input.projectId);
40477
- if (finalUsage.project + normalized.byteLength > UPLOAD_LIMITS.projectBytes || finalUsage.global + normalized.byteLength > UPLOAD_LIMITS.globalBytes)
40688
+ if (finalUsage.project + normalized.byteLength > UPLOAD_LIMITS2.projectBytes || finalUsage.global + normalized.byteLength > UPLOAD_LIMITS2.globalBytes)
40478
40689
  throw new UploadError("UPLOAD_QUOTA", "storage quota exceeded");
40479
- await rename2(quarantine, join18(storageDir, storageKey));
40690
+ await rename3(quarantine, join19(storageDir, storageKey));
40480
40691
  } catch (error) {
40481
40692
  await unlink4(quarantine).catch(() => {
40482
40693
  });
@@ -40699,7 +40910,8 @@ async function acceptTicket(t, opts) {
40699
40910
  steps: "Reproduksi dari keluhan pelapor & lampiran.",
40700
40911
  expected: "Perilaku yang diharapkan pelapor.",
40701
40912
  actual: detail,
40702
- env: ""
40913
+ env: "",
40914
+ constraints: ""
40703
40915
  } : { context: detail, outcome: "", constraints: "" };
40704
40916
  const repoDir = await resolveRepoDir(t.projectId);
40705
40917
  let spec = null;
@@ -41594,10 +41806,10 @@ init_config2();
41594
41806
  init_pty();
41595
41807
  init_session_sandbox();
41596
41808
  import { execFile as execFile10 } from "node:child_process";
41597
- import { randomUUID as randomUUID8 } from "node:crypto";
41809
+ import { randomUUID as randomUUID9 } from "node:crypto";
41598
41810
  import { mkdirSync as mkdirSync9, rmSync as rmSync5, writeFileSync as writeFileSync5 } from "node:fs";
41599
41811
  import { tmpdir as tmpdir4 } from "node:os";
41600
- import { join as join19 } from "node:path";
41812
+ import { join as join20 } from "node:path";
41601
41813
  var binFor = (agent) => agent === "codex" ? effectiveStr("HANOMAN_CODEX_BIN") ?? "codex" : effectiveStr("HANOMAN_CLAUDE_BIN") ?? "claude";
41602
41814
  function leadArgv(o) {
41603
41815
  if (o.agent === "codex") {
@@ -41625,11 +41837,11 @@ function leadProcess(prompt, o, env = process.env) {
41625
41837
  const mode = env.HANOMAN_SESSION_SANDBOX ?? (env.NODE_ENV === "production" ? "required" : "off");
41626
41838
  if (mode === "off") return { file, args: directArgs, cwd: o.cwd, cleanup: () => {
41627
41839
  } };
41628
- const promptDir = join19(tmpdir4(), "hanoman-prompts");
41840
+ const promptDir = join20(tmpdir4(), "hanoman-prompts");
41629
41841
  mkdirSync9(promptDir, { recursive: true, mode: 448 });
41630
- const promptFile = join19(promptDir, `oneshot-${randomUUID8()}`);
41842
+ const promptFile = join20(promptDir, `oneshot-${randomUUID9()}`);
41631
41843
  writeFileSync5(promptFile, prompt, { flag: "wx", mode: 384 });
41632
- const workspace = o.cwd ?? join19(tmpdir4(), `hanoman-oneshot-${randomUUID8()}`);
41844
+ const workspace = o.cwd ?? join20(tmpdir4(), `hanoman-oneshot-${randomUUID9()}`);
41633
41845
  if (!o.cwd) mkdirSync9(workspace, { recursive: false, mode: 448 });
41634
41846
  try {
41635
41847
  const argsWithoutPrompt = directArgs.slice(0, -1);
@@ -43418,7 +43630,7 @@ init_db();
43418
43630
  init_src();
43419
43631
  import { readFileSync as readFileSync13 } from "node:fs";
43420
43632
  import { homedir as homedir9 } from "node:os";
43421
- import { join as join20 } from "node:path";
43633
+ import { join as join21 } from "node:path";
43422
43634
  var home = () => process.env.HOME || homedir9();
43423
43635
  var readJson2 = (path2) => {
43424
43636
  try {
@@ -43435,7 +43647,7 @@ var serversOf = (node) => {
43435
43647
  var codexServers = () => {
43436
43648
  let text;
43437
43649
  try {
43438
- text = readFileSync13(join20(home(), ".codex", "config.toml"), "utf8");
43650
+ text = readFileSync13(join21(home(), ".codex", "config.toml"), "utf8");
43439
43651
  } catch {
43440
43652
  return [];
43441
43653
  }
@@ -43448,12 +43660,12 @@ var codexServers = () => {
43448
43660
  };
43449
43661
  function mcpServerNames(repoDir) {
43450
43662
  const names = [];
43451
- const claudeJson = readJson2(join20(home(), ".claude.json"));
43663
+ const claudeJson = readJson2(join21(home(), ".claude.json"));
43452
43664
  names.push(...serversOf(claudeJson));
43453
43665
  if (repoDir) {
43454
43666
  const projects = claudeJson?.projects;
43455
43667
  if (projects && typeof projects === "object") names.push(...serversOf(projects[repoDir]));
43456
- names.push(...serversOf(readJson2(join20(repoDir, ".mcp.json"))));
43668
+ names.push(...serversOf(readJson2(join21(repoDir, ".mcp.json"))));
43457
43669
  }
43458
43670
  names.push(...codexServers());
43459
43671
  return [...new Set(names.filter(Boolean))].sort((a, b) => a.localeCompare(b));
@@ -43963,7 +44175,8 @@ ${backlink}`;
43963
44175
  steps: "Reproduksi dari deskripsi issue.",
43964
44176
  expected: "Perilaku yang diharapkan pelapor issue.",
43965
44177
  actual: detail,
43966
- env: ""
44178
+ env: "",
44179
+ constraints: ""
43967
44180
  } : { context: detail, outcome: "", constraints: "", priority };
43968
44181
  const repoDir = await resolveRepoDir(issue2.projectId).catch(() => null);
43969
44182
  let spec = null;
@@ -44906,17 +45119,17 @@ init_db();
44906
45119
 
44907
45120
  // src/services/bootstrap.ts
44908
45121
  import { createHash as createHash9, randomBytes as randomBytes9, timingSafeEqual as timingSafeEqual3 } from "node:crypto";
44909
- import { chmod, lstat as lstat2, mkdir as mkdir5, readFile as readFile4, unlink as unlink5, writeFile as writeFile4 } from "node:fs/promises";
44910
- import { join as join21 } from "node:path";
45122
+ import { chmod, lstat as lstat3, mkdir as mkdir5, readFile as readFile4, unlink as unlink5, writeFile as writeFile4 } from "node:fs/promises";
45123
+ import { join as join22 } from "node:path";
44911
45124
  var TTL_MS5 = 15 * 6e4;
44912
45125
  var SETUP_TOKEN_FILE = "setup.token";
44913
45126
  var BootstrapError = class extends Error {
44914
45127
  code = "BOOTSTRAP_PROOF";
44915
45128
  };
44916
- var tokenPath = (home3) => join21(home3, SETUP_TOKEN_FILE);
45129
+ var tokenPath = (home3) => join22(home3, SETUP_TOKEN_FILE);
44917
45130
  async function readStored(home3) {
44918
45131
  try {
44919
- const info = await lstat2(tokenPath(home3));
45132
+ const info = await lstat3(tokenPath(home3));
44920
45133
  if (info.isSymbolicLink() || !info.isFile()) throw new BootstrapError("invalid setup proof");
44921
45134
  const [token, expires] = (await readFile4(tokenPath(home3), "utf8")).trim().split("\n");
44922
45135
  const expiresAt = Date.parse(expires ?? "");
@@ -44929,7 +45142,7 @@ async function readStored(home3) {
44929
45142
  }
44930
45143
  async function ensureSetupToken(home3, now = Date.now()) {
44931
45144
  await mkdir5(home3, { recursive: true, mode: 448 });
44932
- const homeInfo = await lstat2(home3);
45145
+ const homeInfo = await lstat3(home3);
44933
45146
  if (homeInfo.isSymbolicLink() || !homeInfo.isDirectory()) throw new BootstrapError("invalid setup home");
44934
45147
  await chmod(home3, 448);
44935
45148
  try {
@@ -45123,7 +45336,12 @@ var IDE_SUBS = /* @__PURE__ */ new Set([
45123
45336
  "remotes",
45124
45337
  "compare",
45125
45338
  "archive",
45126
- "pr-url"
45339
+ "pr-url",
45340
+ // ADR-0121 · operasi berkas Explorer. `rw()` menurunkan read/write DARI METHOD, jadi
45341
+ // POST/PATCH/DELETE menuntut ide:write — capability yang sudah memberi hak menimpa isi
45342
+ // berkas apa pun lewat PUT /file (hindari kelas bug SPEC-405: prefix tanpa lihat method).
45343
+ "entry",
45344
+ "upload"
45127
45345
  ]);
45128
45346
  function capabilityForRoute(method, path2) {
45129
45347
  const read2 = method === "GET" || method === "HEAD";
@@ -45828,7 +46046,7 @@ init_tap();
45828
46046
  // src/services/webhooks/emit.ts
45829
46047
  init_src();
45830
46048
  init_db();
45831
- import { randomUUID as randomUUID9 } from "node:crypto";
46049
+ import { randomUUID as randomUUID10 } from "node:crypto";
45832
46050
  function skipped(def2, row) {
45833
46051
  if (!def2.skipWhen || !row) return false;
45834
46052
  return row[def2.skipWhen.field] === def2.skipWhen.equals;
@@ -45894,7 +46112,7 @@ async function emitWebhook(i) {
45894
46112
  const targets = matchingEndpoints(type, projectId);
45895
46113
  if (!targets.length) return;
45896
46114
  const name2 = projectId ? (await prisma.project.findUnique({ where: { id: projectId }, select: { name: true } }))?.name ?? null : null;
45897
- const env = buildEnvelope(i, name2, (/* @__PURE__ */ new Date()).toISOString(), `evt_${randomUUID9().replace(/-/g, "")}`);
46115
+ const env = buildEnvelope(i, name2, (/* @__PURE__ */ new Date()).toISOString(), `evt_${randomUUID10().replace(/-/g, "")}`);
45898
46116
  if (!env) return;
45899
46117
  await enqueueEnvelope(env, targets);
45900
46118
  } catch (e) {
@@ -46206,14 +46424,14 @@ init_session_sandbox();
46206
46424
  init_src2();
46207
46425
 
46208
46426
  // src/services/secure-home.ts
46209
- import { chmod as chmod2, lstat as lstat3, mkdir as mkdir6 } from "node:fs/promises";
46427
+ import { chmod as chmod2, lstat as lstat4, mkdir as mkdir6 } from "node:fs/promises";
46210
46428
  import { isAbsolute as isAbsolute6, resolve as resolve18 } from "node:path";
46211
46429
  var HomePermissionError = class extends Error {
46212
46430
  code = "HOME_SYMLINK";
46213
46431
  };
46214
46432
  async function assertNoSymlink(path2, allowMissing) {
46215
46433
  try {
46216
- const info = await lstat3(resolve18(path2));
46434
+ const info = await lstat4(resolve18(path2));
46217
46435
  if (info.isSymbolicLink()) throw new HomePermissionError(`symlink ditolak: ${path2}`);
46218
46436
  } catch (error) {
46219
46437
  if (error.code === "ENOENT" && allowMissing) return;