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/build-info.json +3 -3
- package/dist/cli.js +16 -6
- package/dist/server.js +346 -128
- package/docs/agent-integration.md +5 -4
- package/package.json +1 -1
- package/web/assets/{index-e01zHm_c.js → index-Bwx_Vz8-.js} +131 -130
- package/web/assets/{index-TRbs6vrh.css → index-ChXVk9Y2.css} +1 -1
- package/web/index.html +2 -2
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
|
|
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
|
-
},
|
|
4208
|
+
}, []);
|
|
4208
4209
|
return done({
|
|
4209
4210
|
severity: severityFromPriority(prio()),
|
|
4210
4211
|
steps: "",
|
|
4211
4212
|
expected: str3("goal"),
|
|
4212
4213
|
actual: "",
|
|
4213
|
-
env: ""
|
|
4214
|
-
|
|
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
|
|
6270
|
-
if (!
|
|
6271
|
-
if (
|
|
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(
|
|
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}.
|
|
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: {
|
|
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
|
|
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(
|
|
8131
|
-
codeStyleClause(
|
|
8143
|
+
scopeClause(flow, opts.verifyScope),
|
|
8144
|
+
codeStyleClause(flow),
|
|
8132
8145
|
methodClause(m),
|
|
8133
|
-
skillInstruction(PIPELINES
|
|
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].
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
11630
|
-
const match =
|
|
11631
|
-
if (!match) return
|
|
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
|
|
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
|
-
|
|
11721
|
+
clean2 = clean2.split(secret).join("[REDACTED]");
|
|
11700
11722
|
}
|
|
11701
|
-
for (const pattern of CREDENTIAL_PATTERNS)
|
|
11702
|
-
return
|
|
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
|
|
11707
|
-
if (!
|
|
11728
|
+
const clean2 = sanitizeTelegramOutput(text, exactSecrets);
|
|
11729
|
+
if (!clean2) return [];
|
|
11708
11730
|
const chunks = [];
|
|
11709
|
-
let rest =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
29377
|
-
if (
|
|
29378
|
-
if (
|
|
29379
|
-
else branchRefs.push(
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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) =>
|
|
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 =
|
|
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
|
|
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(
|
|
31325
|
-
const path2 =
|
|
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
|
|
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
|
|
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(
|
|
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) =>
|
|
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
|
|
31650
|
-
var keyDir = () => effectiveStr("HANOMAN_SSH_KEY_DIR") ??
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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 = () =>
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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") ??
|
|
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 = `${
|
|
32702
|
-
await writeFile2(
|
|
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(
|
|
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(
|
|
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:
|
|
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
|
|
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
|
|
33004
|
-
import { basename as basename5, isAbsolute as isAbsolute4, join as
|
|
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
|
|
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
|
-
},
|
|
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 >
|
|
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 >
|
|
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:
|
|
40451
|
-
const metadata2 = await timeout2(source.metadata(),
|
|
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 >
|
|
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:
|
|
40457
|
-
|
|
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 >
|
|
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 =
|
|
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 =
|
|
40472
|
-
const storageKey = `${
|
|
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),
|
|
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 >
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
41840
|
+
const promptDir = join20(tmpdir4(), "hanoman-prompts");
|
|
41629
41841
|
mkdirSync9(promptDir, { recursive: true, mode: 448 });
|
|
41630
|
-
const promptFile =
|
|
41842
|
+
const promptFile = join20(promptDir, `oneshot-${randomUUID9()}`);
|
|
41631
41843
|
writeFileSync5(promptFile, prompt, { flag: "wx", mode: 384 });
|
|
41632
|
-
const workspace = o.cwd ??
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
44910
|
-
import { join as
|
|
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) =>
|
|
45129
|
+
var tokenPath = (home3) => join22(home3, SETUP_TOKEN_FILE);
|
|
44917
45130
|
async function readStored(home3) {
|
|
44918
45131
|
try {
|
|
44919
|
-
const info = await
|
|
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
|
|
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
|
|
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_${
|
|
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
|
|
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
|
|
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;
|