hanoman 0.7.1 → 0.8.0

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
@@ -3612,15 +3612,15 @@ var init_types = __esm({
3612
3612
  }
3613
3613
  }
3614
3614
  if (effect.type === "refinement") {
3615
- const executeRefinement = (acc) => {
3616
- const result = effect.refinement(acc, checkCtx);
3615
+ const executeRefinement = (acc2) => {
3616
+ const result = effect.refinement(acc2, checkCtx);
3617
3617
  if (ctx.common.async) {
3618
3618
  return Promise.resolve(result);
3619
3619
  }
3620
3620
  if (result instanceof Promise) {
3621
3621
  throw new Error("Async refinement encountered during synchronous parse operation. Use .parseAsync instead.");
3622
3622
  }
3623
- return acc;
3623
+ return acc2;
3624
3624
  };
3625
3625
  if (ctx.common.async === false) {
3626
3626
  const inner = this._def.schema._parseSync({
@@ -5023,7 +5023,7 @@ function cmpVersion(a, b) {
5023
5023
  }
5024
5024
  return 0;
5025
5025
  }
5026
- var zProject, zBriefPayload, zQaPayload, zGoalPayload, zSpecBlocker, zSourceChange, zManualDone, zSpec, NOTIFY_SOUNDS, MODELS, EFFORTS, E_5_6, E_LUNA, E_BASE, CODEX_MODELS, RETIRED_CODEX_MODELS, zCodex, CODEX_DEFAULTS, zSourceCommon, zScheduler, SCHEDULER_DEFAULTS, zGoal, GOAL_DEFAULTS, zConflict, CONFLICT_DEFAULTS, CHANGELOG_ENGINE_DEFAULTS, zLeadEngine, zLead, LEAD_DEFAULTS, zPortalChat, PORTAL_CHAT_DEFAULTS, zPhaseCell, zPhaseOverride, zPhaseOverrides, zFlowOrchestration, zOrchestration, ORCHESTRATION_DEFAULTS, zSetting, zNotification, zDocFile, zDeviceTokenView;
5026
+ var zProject, zBriefPayload, zQaPayload, zGoalPayload, zSpecBlocker, zSourceChange, zManualDone, zSpec, zSpecListItem, zSpecSlim, NOTIFY_SOUNDS, MODELS, EFFORTS, E_5_6, E_LUNA, E_BASE, CODEX_MODELS, RETIRED_CODEX_MODELS, zCodex, CODEX_DEFAULTS, zSourceCommon, zScheduler, SCHEDULER_DEFAULTS, zGoal, GOAL_DEFAULTS, zConflict, CONFLICT_DEFAULTS, CHANGELOG_ENGINE_DEFAULTS, zLeadEngine, zLead, LEAD_DEFAULTS, zPortalChat, PORTAL_CHAT_DEFAULTS, zPhaseCell, zPhaseOverride, zPhaseOverrides, zFlowOrchestration, zOrchestration, ORCHESTRATION_DEFAULTS, zSetting, zNotification, zDocFile, zDeviceTokenView;
5027
5027
  var init_entities = __esm({
5028
5028
  "../shared/src/entities.ts"() {
5029
5029
  "use strict";
@@ -5129,6 +5129,11 @@ var init_entities = __esm({
5129
5129
  // sesi atau belum selesai. `.nullable().default(null)` menjaga respons/klien versi lama parse.
5130
5130
  manualDone: zManualDone.nullable().default(null)
5131
5131
  });
5132
+ zSpecListItem = zSpec.omit({ payload: true, sourceHistory: true }).extend({
5133
+ version: external_exports.number().optional(),
5134
+ updatedAt: external_exports.string().optional()
5135
+ });
5136
+ zSpecSlim = zSpecListItem.omit({ objective: true });
5132
5137
  NOTIFY_SOUNDS = [
5133
5138
  "off",
5134
5139
  "short",
@@ -6738,9 +6743,9 @@ function nextRun(spec, after, limitDays = 400) {
6738
6743
  for (let d = 0; d <= limitDays; d++) {
6739
6744
  const day2 = new Date(after.getFullYear(), after.getMonth(), after.getDate() + d);
6740
6745
  if (!dayMatches(spec, day2)) continue;
6741
- for (const h of hours) {
6746
+ for (const h2 of hours) {
6742
6747
  for (const mi of minutes) {
6743
- const cand = new Date(day2.getFullYear(), day2.getMonth(), day2.getDate(), h, mi, 0, 0);
6748
+ const cand = new Date(day2.getFullYear(), day2.getMonth(), day2.getDate(), h2, mi, 0, 0);
6744
6749
  if (cand.getTime() > t) return cand;
6745
6750
  }
6746
6751
  }
@@ -7760,8 +7765,8 @@ function coverageOf(docs) {
7760
7765
  const linked = [...byCat.values()].filter(Boolean).length;
7761
7766
  return Math.round(linked / byCat.size * 100);
7762
7767
  }
7763
- function docStatusFor(pct2) {
7764
- return pct2 >= 90 ? "ok" : pct2 >= 60 ? "drift" : "broken";
7768
+ function docStatusFor(pct3) {
7769
+ return pct3 >= 90 ? "ok" : pct3 >= 60 ? "drift" : "broken";
7765
7770
  }
7766
7771
  function isExternalLink(target3) {
7767
7772
  return !target3 || /^(https?:|#|mailto:)/.test(target3);
@@ -9252,17 +9257,12 @@ var init_backlog = __esm({
9252
9257
  inputSchema: obj({ properties: { spec: str(ID_HINT) }, required: ["spec"] }),
9253
9258
  mode: "read",
9254
9259
  capability: "backlog:read",
9255
- samplePath: "/specs",
9260
+ samplePath: "/specs/SPEC-1",
9256
9261
  sampleMethod: "GET",
9257
- build: (a) => ({ method: "GET", path: "/specs", query: { q: String(a.spec), limit: "100" } }),
9258
- // REST tak punya `GET /specs/:id`; `q` adalah SUBSTRING, jadi `SPEC-48` mengembalikan
9259
- // SPEC-480…489. Pencocokan persis dilakukan di sini, bukan dipercayakan ke server.
9260
- shape: (raw, a) => {
9261
- const want = String(a.spec).trim().toLowerCase();
9262
- const items = raw?.items ?? [];
9263
- const hit = items.find((i) => String(i.id).toLowerCase() === want);
9264
- return hit ? shapeSpecDetail(hit) : { error: `backlog "${String(a.spec)}" tidak ada. Cek ejaannya (bentuknya SPEC-nnn) atau cari dengan hanoman_backlog_search.` };
9265
- }
9262
+ // SPEC-1267 · `GET /specs` tak lagi membawa `payload`/`sourceHistory`, jadi isi penuh HANYA ada di
9263
+ // `GET /specs/:id`. Pencocokan id persis kini urusan server (404 bila tak ada).
9264
+ build: (a) => ({ method: "GET", path: `/specs/${enc(String(a.spec).trim())}` }),
9265
+ shape: (raw) => shapeSpecDetail(raw)
9266
9266
  },
9267
9267
  {
9268
9268
  name: "hanoman_backlog_docs_list",
@@ -13042,8 +13042,8 @@ function guideLine(guide, phase) {
13042
13042
  }
13043
13043
  function phaseSkillsFor(flow2, phase, method) {
13044
13044
  const own = method.phaseSkills[phase] ?? [];
13045
- const phases = PIPELINES[flow2];
13046
- return writesCode(flow2) && phase === phases[phases.length - 1] ? [.../* @__PURE__ */ new Set([...own, ...method.exitSkills])] : [...own];
13045
+ const phases2 = PIPELINES[flow2];
13046
+ return writesCode(flow2) && phase === phases2[phases2.length - 1] ? [.../* @__PURE__ */ new Set([...own, ...method.exitSkills])] : [...own];
13047
13047
  }
13048
13048
  function orchestratorClause(plan, o = {}) {
13049
13049
  const codex2 = plan.runtime === "codex";
@@ -13320,15 +13320,15 @@ var init_prompt = __esm({
13320
13320
  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 untuk ${AMBIGUOUS}: JANGAN memutuskannya sendiri, ${ASK_ROUTE} Selain itu, terus lanjut.`;
13321
13321
  AUTONOMY_CLAUSE_FULL = `Kamu diluncurkan scheduler \u2014 mungkin tak ada manusia yang sedang menonton terminal ini. Tembus seluruh pipeline sampai stage \`done\`, lalu commit & push: checkpoint "review"/"approval" milik skill BUKAN titik berhenti dan jangan menunggu persetujuan siapa pun. Tetapi JANGAN memutuskan sendiri ${AMBIGUOUS}: ${ASK_ROUTE} Catat keputusan penting di pesan commit. Merge ke branch utama tetap dilakukan manusia, bukan kamu.`;
13322
13322
  autonomyClause = (mode) => mode === "full-control" ? AUTONOMY_CLAUSE_FULL : AUTONOMY_CLAUSE;
13323
- phaseInstruction = (phases, method) => {
13324
- const base2 = `Kerjakan fase berurutan: ${phases.join(" \u2192 ")}.
13323
+ phaseInstruction = (phases2, method) => {
13324
+ const base2 = `Kerjakan fase berurutan: ${phases2.join(" \u2192 ")}.
13325
13325
  Setiap kali sebuah fase selesai (atau kamu putuskan dilewati), append satu baris ke berkas di $HANOMAN_PHASE_FILE \u2014 persis: \`echo "<Nama Fase> done" >> "$HANOMAN_PHASE_FILE"\`, atau \`skipped\` sebagai ganti \`done\`. Nama fase ditulis apa adanya seperti di atas.`;
13326
- if (!phases.includes("Plan") || !phases.includes("Execute")) return base2;
13326
+ if (!phases2.includes("Plan") || !phases2.includes("Execute")) return base2;
13327
13327
  return base2 + `
13328
13328
  Execute BELUM selesai selama plan (\`${method.planDir}/**\`) masih punya task \`- [ ]\`: kerjakan SEMUA PR/task sampai tiap kotak jadi \`- [x]\` sebelum menulis \`Execute done\`. hanoman menahan backlog di \`executing\`, bukan \`done\`, selama masih ada \`- [ ]\`.`;
13329
13329
  };
13330
- skillInstruction = (flow2, phases, method) => {
13331
- const lines2 = phases.map((p3) => {
13330
+ skillInstruction = (flow2, phases2, method) => {
13331
+ const lines2 = phases2.map((p3) => {
13332
13332
  const skills = phaseSkillsFor(flow2, p3, method);
13333
13333
  return skills.length ? `- ${p3}: ${skills.join(", ")}` : "";
13334
13334
  }).filter(Boolean);
@@ -13671,10 +13671,10 @@ function goalFlowCondition(flow2, specId, branchTo, spec) {
13671
13671
  }
13672
13672
  function defaultGoalCondition({ flow: flow2, specId, branchTo, spec }) {
13673
13673
  if (isGoalShapedFlow(flow2)) return goalFlowCondition(flow2, specId, branchTo, spec);
13674
- const phases = PIPELINES[flow2];
13675
- const planGate = phases.includes("Plan") && phases.includes("Execute");
13674
+ const phases2 = PIPELINES[flow2];
13675
+ const planGate = phases2.includes("Plan") && phases2.includes("Execute");
13676
13676
  const clauses = [
13677
- `1. output \`cat "$HANOMAN_PHASE_FILE"\` yang memuat satu baris untuk SETIAP fase ${phases.join(" \u2192 ")}, masing-masing berakhiran \`done\` atau \`skipped\`;`
13677
+ `1. output \`cat "$HANOMAN_PHASE_FILE"\` yang memuat satu baris untuk SETIAP fase ${phases2.join(" \u2192 ")}, masing-masing berakhiran \`done\` atau \`skipped\`;`
13678
13678
  ];
13679
13679
  if (planGate) {
13680
13680
  const dirs = PLAN_DIRS.map((d) => `${d}/`).join(" ");
@@ -13734,8 +13734,8 @@ function codexHookArgs(o) {
13734
13734
  return args;
13735
13735
  }
13736
13736
  function codexGoalScript(o) {
13737
- const phases = PIPELINES[o.flow];
13738
- const planGate = phases.includes("Plan") && phases.includes("Execute");
13737
+ const phases2 = PIPELINES[o.flow];
13738
+ const planGate = phases2.includes("Plan") && phases2.includes("Execute");
13739
13739
  const max = o.maxBlocks ?? GOAL_MAX_BLOCKS;
13740
13740
  const lines2 = [
13741
13741
  "#!/bin/sh",
@@ -13745,7 +13745,7 @@ function codexGoalScript(o) {
13745
13745
  `ST=${shq(o.stateFile)}`,
13746
13746
  "missing=''"
13747
13747
  ];
13748
- for (const p3 of phases) {
13748
+ for (const p3 of phases2) {
13749
13749
  lines2.push(
13750
13750
  `grep -qE ${shq(`^${p3} (done|skipped)[[:space:]]*$`)} "$PF" 2>/dev/null || missing="$missing\\n- fase ${p3} belum tercatat di \\$HANOMAN_PHASE_FILE"`
13751
13751
  );
@@ -14494,8 +14494,8 @@ var init_custom_agent_eval_evidence = __esm({
14494
14494
  };
14495
14495
  textFile = (root, path) => {
14496
14496
  const target3 = evidencePath(root, path);
14497
- const stat5 = lstatSync(target3);
14498
- if (!stat5.isFile() || stat5.size > 1024 * 1024) throw new Error("Evidence must be a bounded regular file");
14497
+ const stat7 = lstatSync(target3);
14498
+ if (!stat7.isFile() || stat7.size > 1024 * 1024) throw new Error("Evidence must be a bounded regular file");
14499
14499
  return readFileSync2(target3, "utf8");
14500
14500
  };
14501
14501
  snapshotEvalEvidence = (context) => {
@@ -14867,9 +14867,9 @@ var init_custom_agent_eval = __esm({
14867
14867
  hash4.update(`missing:${rel}\0`);
14868
14868
  continue;
14869
14869
  }
14870
- const stat5 = lstatSync2(path);
14871
- if (stat5.isSymbolicLink()) hash4.update(`l:${rel}\0${readlinkSync(path)}\0`);
14872
- else hash4.update(`f:${stat5.mode}:${rel}\0`).update(readFileSync3(path));
14870
+ const stat7 = lstatSync2(path);
14871
+ if (stat7.isSymbolicLink()) hash4.update(`l:${rel}\0${readlinkSync(path)}\0`);
14872
+ else hash4.update(`f:${stat7.mode}:${rel}\0`).update(readFileSync3(path));
14873
14873
  }
14874
14874
  return hash4.digest("hex");
14875
14875
  };
@@ -15022,8 +15022,8 @@ ${result.error.message}` : result.stderr ?? ""
15022
15022
  for (const name2 of readdirSync2(eventDir).filter((entry) => entry.endsWith(".json")).sort()) {
15023
15023
  try {
15024
15024
  const path = evidencePath(eventDir, name2);
15025
- const stat5 = lstatSync2(path);
15026
- if (!stat5.isFile() || stat5.size > 1024 * 1024) continue;
15025
+ const stat7 = lstatSync2(path);
15026
+ if (!stat7.isFile() || stat7.size > 1024 * 1024) continue;
15027
15027
  const parsed = JSON.parse(readFileSync3(path, "utf8"));
15028
15028
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
15029
15029
  events.push(parsed);
@@ -15986,14 +15986,9 @@ var init_stage_machine = __esm({
15986
15986
 
15987
15987
  // src/services/session-phases.ts
15988
15988
  import { readFileSync as readFileSync6, readdirSync as readdirSync5 } from "node:fs";
15989
- function recorded(file) {
15989
+ import { readFile, readdir, stat } from "node:fs/promises";
15990
+ function parseRecorded(raw) {
15990
15991
  const out4 = /* @__PURE__ */ new Map();
15991
- let raw;
15992
- try {
15993
- raw = readFileSync6(file, "utf8");
15994
- } catch {
15995
- return out4;
15996
- }
15997
15992
  for (const line of raw.split("\n")) {
15998
15993
  const trimmed = line.trimEnd();
15999
15994
  const i = trimmed.lastIndexOf(" ");
@@ -16004,19 +15999,40 @@ function recorded(file) {
16004
15999
  }
16005
16000
  return out4;
16006
16001
  }
16007
- function readPhases(file, flow2) {
16008
- const seen2 = recorded(file);
16009
- let activeTaken = false;
16010
- return PIPELINES[flow2].map((name2) => {
16011
- const state = seen2.get(name2);
16012
- if (state) return { name: name2, state };
16013
- if (activeTaken) return { name: name2, state: "pending" };
16014
- activeTaken = true;
16015
- return { name: name2, state: "active" };
16016
- });
16002
+ function recorded(file) {
16003
+ let raw;
16004
+ try {
16005
+ raw = readFileSync6(file, "utf8");
16006
+ } catch {
16007
+ return /* @__PURE__ */ new Map();
16008
+ }
16009
+ return parseRecorded(raw);
16010
+ }
16011
+ async function recordedAsync(file) {
16012
+ let st;
16013
+ try {
16014
+ st = await stat(file);
16015
+ } catch {
16016
+ phaseFileMemo.delete(file);
16017
+ return /* @__PURE__ */ new Map();
16018
+ }
16019
+ const hit = phaseFileMemo.get(file);
16020
+ if (hit && hit.mtimeMs === st.mtimeMs && hit.size === st.size) return hit.seen;
16021
+ let raw;
16022
+ try {
16023
+ raw = await readFile(file, "utf8");
16024
+ } catch {
16025
+ return /* @__PURE__ */ new Map();
16026
+ }
16027
+ const seen2 = parseRecorded(raw);
16028
+ phaseFileMemo.set(file, { mtimeMs: st.mtimeMs, size: st.size, seen: seen2 });
16029
+ return seen2;
16030
+ }
16031
+ async function readPhasesAsync(file, flow2) {
16032
+ return derivePhases(await recordedAsync(file), flow2);
16017
16033
  }
16018
- function enrichPhases(phases, roster, invocations, doneSeenAt, now, bornAt2, doneAtBirth = /* @__PURE__ */ new Set()) {
16019
- return phases.map((p3) => {
16034
+ function enrichPhases(phases2, roster, invocations, doneSeenAt, now, bornAt2, doneAtBirth = /* @__PURE__ */ new Set()) {
16035
+ return phases2.map((p3) => {
16020
16036
  const r = roster.find((entry) => entry.phase === p3.name);
16021
16037
  if (!r) return p3;
16022
16038
  const all = invocations.filter((i) => i.phase === p3.name);
@@ -16046,23 +16062,23 @@ function enrichPhases(phases, roster, invocations, doneSeenAt, now, bornAt2, don
16046
16062
  };
16047
16063
  });
16048
16064
  }
16049
- function trackDoneSeen(phases, doneSeenAt, now) {
16050
- const byName = new Map(phases.map((p3) => [p3.name, p3.state]));
16065
+ function trackDoneSeen(phases2, doneSeenAt, now) {
16066
+ const byName = new Map(phases2.map((p3) => [p3.name, p3.state]));
16051
16067
  for (const name2 of [...doneSeenAt.keys()])
16052
16068
  if (byName.get(name2) !== "done") doneSeenAt.delete(name2);
16053
- for (const p3 of phases)
16069
+ for (const p3 of phases2)
16054
16070
  if (p3.state === "done" && !doneSeenAt.has(p3.name)) doneSeenAt.set(p3.name, now);
16055
16071
  }
16056
- function stageFor(phases) {
16072
+ function stageFor(phases2) {
16057
16073
  let best = -1;
16058
- for (const p3 of phases) {
16074
+ for (const p3 of phases2) {
16059
16075
  if (WORK_PHASES.includes(p3.name) && p3.state === "active")
16060
16076
  best = Math.max(best, STAGES.indexOf("executing"));
16061
16077
  if (p3.state !== "done" && p3.state !== "skipped") continue;
16062
16078
  const s2 = REACHED[p3.name];
16063
16079
  if (s2) best = Math.max(best, STAGES.indexOf(s2));
16064
16080
  }
16065
- if (phases[0]?.state === "active") best = Math.max(best, STAGES.indexOf("brainstorming"));
16081
+ if (phases2[0]?.state === "active") best = Math.max(best, STAGES.indexOf("brainstorming"));
16066
16082
  return best < 0 ? null : STAGES[best];
16067
16083
  }
16068
16084
  function planComplete(worktree, specId) {
@@ -16085,16 +16101,45 @@ function planComplete(worktree, specId) {
16085
16101
  }
16086
16102
  return true;
16087
16103
  }
16088
- function sessionComplete(phases, worktree, specId) {
16089
- if (!phasesComplete(phases)) return false;
16104
+ async function planCompleteAsync(worktree, specId) {
16105
+ const re = new RegExp(`(^|[^a-z0-9])${specId.toLowerCase()}([^0-9]|$)`);
16106
+ for (const rel of PLAN_DIRS) {
16107
+ const dir2 = `${worktree}/${rel}`;
16108
+ let names;
16109
+ try {
16110
+ names = await readdir(dir2);
16111
+ } catch {
16112
+ continue;
16113
+ }
16114
+ for (const n2 of names) {
16115
+ if (!re.test(n2.toLowerCase())) continue;
16116
+ try {
16117
+ if (/^[ \t]*- \[ \]/m.test(await readFile(`${dir2}/${n2}`, "utf8"))) return false;
16118
+ } catch {
16119
+ }
16120
+ }
16121
+ }
16122
+ return true;
16123
+ }
16124
+ function sessionComplete(phases2, worktree, specId) {
16125
+ if (!phasesComplete(phases2)) return false;
16090
16126
  return specId ? planComplete(worktree, specId) : true;
16091
16127
  }
16092
- function stageForRun(phases, worktree, specId) {
16093
- const s2 = stageFor(phases);
16128
+ function stageForRun(phases2, worktree, specId) {
16129
+ const s2 = stageFor(phases2);
16094
16130
  if (s2 === "done" && !planComplete(worktree, specId)) return "executing";
16095
16131
  return s2;
16096
16132
  }
16097
- var phaseFilePath, decisionFilePath, REACHED, phasesComplete;
16133
+ async function stageForRunAsync(phases2, worktree, specId) {
16134
+ const s2 = stageFor(phases2);
16135
+ if (s2 === "done" && !await planCompleteAsync(worktree, specId)) return "executing";
16136
+ return s2;
16137
+ }
16138
+ async function sessionCompleteAsync(phases2, worktree, specId) {
16139
+ if (!phasesComplete(phases2)) return false;
16140
+ return specId ? planCompleteAsync(worktree, specId) : true;
16141
+ }
16142
+ var phaseFilePath, decisionFilePath, phaseFileMemo, derivePhases, readPhases, REACHED, phasesComplete;
16098
16143
  var init_session_phases = __esm({
16099
16144
  "src/services/session-phases.ts"() {
16100
16145
  "use strict";
@@ -16103,6 +16148,18 @@ var init_session_phases = __esm({
16103
16148
  init_stage_machine();
16104
16149
  phaseFilePath = (repoDir, sessionId2) => `${repoDir}/.worktrees/.phases/${sessionId2}`;
16105
16150
  decisionFilePath = (repoDir, sessionId2) => `${repoDir}/.worktrees/.decisions/${sessionId2}`;
16151
+ phaseFileMemo = /* @__PURE__ */ new Map();
16152
+ derivePhases = (seen2, flow2) => {
16153
+ let activeTaken = false;
16154
+ return PIPELINES[flow2].map((name2) => {
16155
+ const state = seen2.get(name2);
16156
+ if (state) return { name: name2, state };
16157
+ if (activeTaken) return { name: name2, state: "pending" };
16158
+ activeTaken = true;
16159
+ return { name: name2, state: "active" };
16160
+ });
16161
+ };
16162
+ readPhases = (file, flow2) => derivePhases(recorded(file), flow2);
16106
16163
  REACHED = {
16107
16164
  Objective: "objective",
16108
16165
  Audit: "objective",
@@ -16118,7 +16175,7 @@ var init_session_phases = __esm({
16118
16175
  // mencapai `done` — tak ada fase verifikasi untuk menutupnya.
16119
16176
  Kerjakan: "done"
16120
16177
  };
16121
- phasesComplete = (phases) => phases.length > 0 && phases.every((p3) => p3.state === "done" || p3.state === "skipped");
16178
+ phasesComplete = (phases2) => phases2.length > 0 && phases2.every((p3) => p3.state === "done" || p3.state === "skipped");
16122
16179
  }
16123
16180
  });
16124
16181
 
@@ -16467,7 +16524,7 @@ var init_safe_outbound_request = __esm({
16467
16524
 
16468
16525
  // src/services/uploads.ts
16469
16526
  import { randomUUID } from "node:crypto";
16470
- import { mkdir, writeFile, readFile, unlink, rm } from "node:fs/promises";
16527
+ import { mkdir, writeFile, readFile as readFile2, unlink, rm } from "node:fs/promises";
16471
16528
  import { join as join11, resolve as resolve8 } from "node:path";
16472
16529
  function extFor(mimeType) {
16473
16530
  return EXT[mimeType] ?? ".bin";
@@ -16498,13 +16555,13 @@ async function dropSessionUploads(sessionId2) {
16498
16555
  }
16499
16556
  async function readUpload(storageKey) {
16500
16557
  const safe = storageKey.replace(/[/\\]/g, "");
16501
- return readFile(join11(uploadDir(), safe));
16558
+ return readFile2(join11(uploadDir(), safe));
16502
16559
  }
16503
16560
  async function readUploadOrFetch(storageKey) {
16504
16561
  const safe = storageKey.replace(/[/\\]/g, "");
16505
16562
  const target3 = join11(uploadDir(), safe);
16506
16563
  try {
16507
- return await readFile(target3);
16564
+ return await readFile2(target3);
16508
16565
  } catch {
16509
16566
  const base2 = effectiveStr("SYNC_SERVER_URL");
16510
16567
  const token = effectiveStr("SYNC_DEVICE_TOKEN");
@@ -16976,22 +17033,22 @@ import { execFile, execFileSync as execFileSync2 } from "node:child_process";
16976
17033
  import { createRequire } from "node:module";
16977
17034
  import { randomUUID as randomUUID2 } from "node:crypto";
16978
17035
  import { chmodSync as chmodSync6, mkdirSync as mkdirSync7, readFileSync as readFileSync8, rmSync as rmSync4, statSync as statSync2, writeFileSync as writeFileSync8 } from "node:fs";
17036
+ import { stat as stat2 } from "node:fs/promises";
16979
17037
  import { dirname as dirname8 } from "node:path";
16980
17038
  import { tmpdir as tmpdir3 } from "node:os";
16981
- function phaseView(p3, a) {
16982
- const phases = readPhases(p3.phaseFile, p3.flow);
17039
+ function phaseView(p3, a, phases2) {
16983
17040
  const now = Date.now();
16984
- trackDoneSeen(phases, a.doneSeenAt, now);
17041
+ trackDoneSeen(phases2, a.doneSeenAt, now);
16985
17042
  const roster = (p3.agentRoster ?? []).flatMap((r) => r.phase ? [{ name: r.name, phase: r.phase, model: r.model, effort: r.effort }] : []);
16986
17043
  return roster.length ? enrichPhases(
16987
- phases,
17044
+ phases2,
16988
17045
  roster,
16989
17046
  phaseInvocations.get(p3.id) ?? [],
16990
17047
  a.doneSeenAt,
16991
17048
  now,
16992
17049
  p3.startedAt * 1e3,
16993
17050
  new Set(p3.doneAtBirth ?? [])
16994
- ) : phases;
17051
+ ) : phases2;
16995
17052
  }
16996
17053
  function noTtyPromptEnv() {
16997
17054
  const path = askpassDenyPath();
@@ -17164,10 +17221,10 @@ function sessionEventEnv(sessionId2, env = process.env) {
17164
17221
  ...host2 ? { HANOMAN_EVENT_HOST: host2 } : {}
17165
17222
  };
17166
17223
  }
17167
- function registerSessionHooks(h) {
17168
- hooks.add(h);
17224
+ function registerSessionHooks(h2) {
17225
+ hooks.add(h2);
17169
17226
  return () => {
17170
- hooks.delete(h);
17227
+ hooks.delete(h2);
17171
17228
  };
17172
17229
  }
17173
17230
  function registerCustomAgentSource(fn) {
@@ -17601,20 +17658,6 @@ function sessionPhases(id2) {
17601
17658
  if (!p3?.flow || !p3.phaseFile) return null;
17602
17659
  return readPhases(p3.phaseFile, p3.flow);
17603
17660
  }
17604
- function sessionPhasesBySpec() {
17605
- const out4 = /* @__PURE__ */ new Map();
17606
- let panes;
17607
- try {
17608
- panes = listPanes();
17609
- } catch {
17610
- return out4;
17611
- }
17612
- for (const p3 of panes) {
17613
- if (!p3.specId || !p3.flow || !p3.phaseFile) continue;
17614
- out4.set(p3.specId, { phases: readPhases(p3.phaseFile, p3.flow), cwd: p3.cwd });
17615
- }
17616
- return out4;
17617
- }
17618
17661
  function broadcast(a, f) {
17619
17662
  const msg = frame(f);
17620
17663
  for (const c of a.clients) c.send(msg);
@@ -17669,14 +17712,16 @@ function end(id2, code) {
17669
17712
  broadcast(a, { t: "exit", code });
17670
17713
  drop(id2);
17671
17714
  }
17672
- function pollPhases(p3, a) {
17715
+ async function pollPhases(p3, a) {
17673
17716
  if (!p3.flow || !p3.phaseFile) return;
17674
- const phases = phaseView(p3, a);
17675
- const complete = paneComplete(p3);
17676
- const json = phaseKey(phases, complete);
17717
+ const raw = await readPhasesAsync(p3.phaseFile, p3.flow);
17718
+ const complete = await sessionCompleteAsync(raw, p3.cwd, p3.specId);
17719
+ if (attached.get(p3.id) !== a) return;
17720
+ const phases2 = phaseView(p3, a, raw);
17721
+ const json = phaseKey(phases2, complete);
17677
17722
  if (json === a.lastPhases) return;
17678
17723
  a.lastPhases = json;
17679
- broadcast(a, { t: "phase", phases, complete });
17724
+ broadcast(a, { t: "phase", phases: phases2, complete });
17680
17725
  }
17681
17726
  function setPhaseInvocations(sessionId2, rows) {
17682
17727
  if (attached.has(sessionId2)) phaseInvocations.set(sessionId2, rows);
@@ -17692,7 +17737,7 @@ function startPoll() {
17692
17737
  if (polling) return;
17693
17738
  polling = true;
17694
17739
  const snapshot3 = [...attached.entries()];
17695
- listPanesAsync().then((panes) => {
17740
+ listPanesAsync().then(async (panes) => {
17696
17741
  const live = new Map(panes.map((p3) => [p3.id, p3]));
17697
17742
  for (const [id2, a] of snapshot3) {
17698
17743
  if (attached.get(id2) !== a) continue;
@@ -17700,7 +17745,8 @@ function startPoll() {
17700
17745
  if (!p3) end(id2, 0);
17701
17746
  else if (p3.exited) end(id2, p3.code);
17702
17747
  else {
17703
- pollPhases(p3, a);
17748
+ await pollPhases(p3, a).catch(() => {
17749
+ });
17704
17750
  pollAlt(p3, a);
17705
17751
  }
17706
17752
  }
@@ -17715,31 +17761,43 @@ function startPoll() {
17715
17761
  }, POLL_MS);
17716
17762
  poll.unref();
17717
17763
  }
17718
- function attach(id2, c) {
17719
- const p3 = getSession(id2);
17764
+ function replayExited(c, p3, screen) {
17765
+ if (screen.trim()) c.send(frame({ t: "data", d: stripTerminalQueries(screen.replace(/\n/g, "\r\n")) }));
17766
+ c.send(frame({ t: "exit", code: p3.code }));
17767
+ c.close();
17768
+ }
17769
+ async function screenResyncFrame(id2) {
17770
+ try {
17771
+ const screen = await tmuxAsync("capture-pane", "-p", "-e", "-J", "-t", name(id2));
17772
+ return frame({ t: "data", d: `\x1B[2J\x1B[H${stripTerminalQueries(screen.replace(/\n/g, "\r\n"))}` });
17773
+ } catch {
17774
+ return null;
17775
+ }
17776
+ }
17777
+ async function attachAsync(id2, c) {
17778
+ const p3 = await getSessionAsync(id2);
17720
17779
  if (!p3) {
17721
17780
  c.close();
17722
17781
  return;
17723
17782
  }
17724
17783
  if (p3.exited) {
17725
- const screen = tmux("capture-pane", "-p", "-e", "-J", "-S", "-2000", "-t", name(id2));
17726
- if (screen.trim()) {
17727
- c.send(frame({ t: "data", d: stripTerminalQueries(screen.replace(/\n/g, "\r\n")) }));
17728
- }
17729
- c.send(frame({ t: "exit", code: p3.code }));
17730
- c.close();
17784
+ replayExited(c, p3, await tmuxAsync(...CAPTURE_ARGS(id2)));
17731
17785
  return;
17732
17786
  }
17787
+ attachLive(id2, c, p3);
17788
+ }
17789
+ function attachLive(id2, c, p3) {
17733
17790
  const a = attached.get(id2) ?? open(id2);
17734
17791
  a.clients.add(c);
17735
17792
  if (a.scrollback) c.send(frame({ t: "data", d: stripTerminalQueries(a.scrollback) }));
17736
17793
  a.lastAlt = p3.altScreen;
17737
17794
  c.send(frame({ t: "alt", on: p3.altScreen }));
17738
17795
  if (p3.flow && p3.phaseFile) {
17739
- const phases = phaseView(p3, a);
17740
- const complete = paneComplete(p3);
17741
- a.lastPhases = phaseKey(phases, complete);
17742
- c.send(frame({ t: "phase", phases, complete }));
17796
+ const raw = readPhases(p3.phaseFile, p3.flow);
17797
+ const phases2 = phaseView(p3, a, raw);
17798
+ const complete = sessionComplete(raw, p3.cwd, p3.specId);
17799
+ a.lastPhases = phaseKey(phases2, complete);
17800
+ c.send(frame({ t: "phase", phases: phases2, complete }));
17743
17801
  }
17744
17802
  }
17745
17803
  function writeTo(id2, d, from) {
@@ -17791,7 +17849,7 @@ function spawnPty(...args) {
17791
17849
  );
17792
17850
  }
17793
17851
  }
17794
- var socket, PREFIX, MAX_SCROLLBACK, SCROLLBACK_SLACK, POLL_MS, markerFilled, clearMarker, PANE_QUIET_MS, paneQuiet, markerOnset, decisionOnset, attached, phaseInvocations, claudeBin, shellBin, codexBin, agentBin, rootBypassEnv, frame, name, promptFilePath, goalGatePath, goalStatePath, agentTempDir, agentsFilePath, askpassDenyPath, ASKPASS_DENY, NO_SERVER, TmuxError, sq, idFor, FMT, toSessionInfo, listSessions, listSessionsAsync, liveDecisions, getSession, getSessionAsync, hooks, emitBirth, emitDeath, customAgentSource, codexNativeAgentSupport, nativeAgentsAvailable, customAgentsFor, runGitDiff, sleep, paneText, paneIO, DIALOG_CAPTURE_LINES, GOAL_ARMED_MARKERS, goalArmed, COALESCE_MS, COALESCE_MAX_BYTES, trimScrollback, phaseKey, paneComplete, sessionFinished, poll, polling, TERMINAL_QUERY, stripTerminalQueries, detach;
17852
+ var socket, PREFIX, MAX_SCROLLBACK, SCROLLBACK_SLACK, POLL_MS, markerFilled, markerFilledAsync, clearMarker, PANE_QUIET_MS, paneQuiet, markerOnset, decisionOnset, attached, phaseInvocations, claudeBin, shellBin, codexBin, agentBin, rootBypassEnv, frame, name, promptFilePath, goalGatePath, goalStatePath, agentTempDir, agentsFilePath, askpassDenyPath, ASKPASS_DENY, NO_SERVER, TmuxError, sq, idFor, FMT, toSessionInfo, listSessions, listSessionsAsync, decisionsOf, getSession, getSessionAsync, hooks, emitBirth, emitDeath, customAgentSource, codexNativeAgentSupport, nativeAgentsAvailable, customAgentsFor, runGitDiff, sleep, paneText, paneIO, DIALOG_CAPTURE_LINES, GOAL_ARMED_MARKERS, goalArmed, COALESCE_MS, COALESCE_MAX_BYTES, trimScrollback, phaseKey, sessionFinishedAsync, poll, polling, TERMINAL_QUERY, stripTerminalQueries, CAPTURE_ARGS, detach;
17795
17853
  var init_pty = __esm({
17796
17854
  "src/services/pty.ts"() {
17797
17855
  "use strict";
@@ -17820,6 +17878,13 @@ var init_pty = __esm({
17820
17878
  return false;
17821
17879
  }
17822
17880
  };
17881
+ markerFilledAsync = async (f) => {
17882
+ try {
17883
+ return (await stat2(f)).size > 0;
17884
+ } catch {
17885
+ return false;
17886
+ }
17887
+ };
17823
17888
  clearMarker = (f) => {
17824
17889
  try {
17825
17890
  writeFileSync8(f, "");
@@ -17937,7 +18002,7 @@ exit 1
17937
18002
  });
17938
18003
  listSessions = () => listPanes().map(toSessionInfo);
17939
18004
  listSessionsAsync = async () => (await listPanesAsync()).map(toSessionInfo);
17940
- liveDecisions = () => listPanes().filter((p3) => !p3.exited && p3.decisionFile).map((p3) => ({
18005
+ decisionsOf = (panes) => panes.filter((p3) => !p3.exited && p3.decisionFile).map((p3) => ({
17941
18006
  id: p3.id,
17942
18007
  specId: p3.specId,
17943
18008
  projectId: p3.projectId,
@@ -17951,17 +18016,17 @@ exit 1
17951
18016
  getSessionAsync = async (id2) => (await listPanesAsync()).find((p3) => p3.id === id2);
17952
18017
  hooks = /* @__PURE__ */ new Set();
17953
18018
  emitBirth = (b) => {
17954
- for (const h of hooks) {
18019
+ for (const h2 of hooks) {
17955
18020
  try {
17956
- h.onBirth?.(b);
18021
+ h2.onBirth?.(b);
17957
18022
  } catch {
17958
18023
  }
17959
18024
  }
17960
18025
  };
17961
18026
  emitDeath = (d) => {
17962
- for (const h of hooks) {
18027
+ for (const h2 of hooks) {
17963
18028
  try {
17964
- h.onDeath?.(d);
18029
+ h2.onDeath?.(d);
17965
18030
  } catch {
17966
18031
  }
17967
18032
  }
@@ -18017,11 +18082,10 @@ exit 1
18017
18082
  COALESCE_MS = 16;
18018
18083
  COALESCE_MAX_BYTES = 64 * 1024;
18019
18084
  trimScrollback = (s2) => s2.length > MAX_SCROLLBACK + SCROLLBACK_SLACK ? s2.slice(-MAX_SCROLLBACK) : s2;
18020
- phaseKey = (phases, complete) => JSON.stringify({ phases, complete });
18021
- paneComplete = (p3) => !!p3.flow && !!p3.phaseFile && sessionComplete(readPhases(p3.phaseFile, p3.flow), p3.cwd, p3.specId);
18022
- sessionFinished = (id2) => {
18023
- const p3 = getSession(id2);
18024
- return !!p3 && paneComplete(p3);
18085
+ phaseKey = (phases2, complete) => JSON.stringify({ phases: phases2, complete });
18086
+ sessionFinishedAsync = async (id2) => {
18087
+ const p3 = await getSessionAsync(id2);
18088
+ return !!p3 && !!p3.flow && !!p3.phaseFile && sessionCompleteAsync(await readPhasesAsync(p3.phaseFile, p3.flow), p3.cwd, p3.specId);
18025
18089
  };
18026
18090
  polling = false;
18027
18091
  TERMINAL_QUERY = new RegExp([
@@ -18041,6 +18105,7 @@ exit 1
18041
18105
  // XTWINOPS laporan — berparameter TUNGGAL
18042
18106
  ].join("|"), "g");
18043
18107
  stripTerminalQueries = (s2) => s2.replace(TERMINAL_QUERY, "");
18108
+ CAPTURE_ARGS = (id2) => ["capture-pane", "-p", "-e", "-J", "-S", "-2000", "-t", name(id2)];
18044
18109
  detach = (id2, c) => {
18045
18110
  attached.get(id2)?.clients.delete(c);
18046
18111
  };
@@ -18066,7 +18131,7 @@ var init_redact_known = __esm({
18066
18131
 
18067
18132
  // src/services/transcript-store.ts
18068
18133
  import { randomUUID as randomUUID3 } from "node:crypto";
18069
- import { mkdir as mkdir2, writeFile as writeFile2, readFile as readFile2, unlink as unlink2, readdir, stat } from "node:fs/promises";
18134
+ import { mkdir as mkdir2, writeFile as writeFile2, readFile as readFile3, unlink as unlink2, readdir as readdir2, stat as stat3 } from "node:fs/promises";
18070
18135
  import { join as join13, resolve as resolve9, basename as basename4 } from "node:path";
18071
18136
  function transcriptDir() {
18072
18137
  return resolve9(effectiveStr("HANOMAN_TRANSCRIPT_DIR")?.trim() || resolveDataDirs().transcripts);
@@ -18093,7 +18158,7 @@ async function saveTranscript(text) {
18093
18158
  async function readTranscript(key) {
18094
18159
  if (!key) return null;
18095
18160
  try {
18096
- return await readFile2(join13(transcriptDir(), basename4(key)), "utf8");
18161
+ return await readFile3(join13(transcriptDir(), basename4(key)), "utf8");
18097
18162
  } catch {
18098
18163
  return null;
18099
18164
  }
@@ -18110,7 +18175,7 @@ async function listTranscripts() {
18110
18175
  const dir2 = transcriptDir();
18111
18176
  let names;
18112
18177
  try {
18113
- names = await readdir(dir2);
18178
+ names = await readdir2(dir2);
18114
18179
  } catch (e) {
18115
18180
  if (e.code === "ENOENT") return [];
18116
18181
  throw e;
@@ -18119,7 +18184,7 @@ async function listTranscripts() {
18119
18184
  for (const name2 of names) {
18120
18185
  if (!name2.endsWith(".log")) continue;
18121
18186
  try {
18122
- rows.push({ key: name2, mtimeMs: (await stat(join13(dir2, name2))).mtimeMs });
18187
+ rows.push({ key: name2, mtimeMs: (await stat3(join13(dir2, name2))).mtimeMs });
18123
18188
  } catch {
18124
18189
  }
18125
18190
  }
@@ -19181,6 +19246,118 @@ var init_codex_trust = __esm({
19181
19246
  }
19182
19247
  });
19183
19248
 
19249
+ // src/services/logs/phase-tap.ts
19250
+ function observePhases(rows) {
19251
+ const seen2 = /* @__PURE__ */ new Set();
19252
+ for (const r of rows) {
19253
+ seen2.add(r.sessionId);
19254
+ const prev = known.get(r.sessionId);
19255
+ if (!known.has(r.sessionId)) {
19256
+ known.set(r.sessionId, r.phase);
19257
+ continue;
19258
+ }
19259
+ if (prev !== r.phase) {
19260
+ void appendEvent({
19261
+ kind: "session.phase",
19262
+ msg: `sesi ${r.sessionId} fase ${prev ?? "?"} \u2192 ${r.phase ?? "?"}`,
19263
+ projectId: r.projectId,
19264
+ specId: r.specId ?? null,
19265
+ sessionId: r.sessionId,
19266
+ data: { from: prev, to: r.phase }
19267
+ });
19268
+ known.set(r.sessionId, r.phase);
19269
+ }
19270
+ }
19271
+ for (const id2 of [...known.keys()]) if (!seen2.has(id2)) known.delete(id2);
19272
+ }
19273
+ var known;
19274
+ var init_phase_tap = __esm({
19275
+ "src/services/logs/phase-tap.ts"() {
19276
+ "use strict";
19277
+ init_event_log();
19278
+ known = /* @__PURE__ */ new Map();
19279
+ }
19280
+ });
19281
+
19282
+ // src/services/presence/snapshot.ts
19283
+ function paneToPresence(p3, phase) {
19284
+ return {
19285
+ sessionId: p3.id,
19286
+ projectId: p3.projectId,
19287
+ ...p3.specId ? { specId: p3.specId } : {},
19288
+ ...p3.flow ? { flow: p3.flow } : {},
19289
+ ...phase ? { phase } : {},
19290
+ agent: p3.agent,
19291
+ // Presedensi: pane mati sudah berakhir apa pun isi markernya.
19292
+ status: p3.exited ? "exited" : p3.decision ? "waiting" : "working",
19293
+ startedAt: isoFromEpochSeconds(p3.startedAt)
19294
+ };
19295
+ }
19296
+ function activePhase(p3) {
19297
+ if (!p3.flow || !p3.phaseFile) return void 0;
19298
+ return readPhases(p3.phaseFile, p3.flow).find((f) => f.state === "active")?.name;
19299
+ }
19300
+ function listPanesShared(now = Date.now()) {
19301
+ if (panesMemo && now - panesMemo.at < PANES_MEMO_MS) return panesMemo.value;
19302
+ const value = listPanesAsync();
19303
+ panesMemo = { at: now, value };
19304
+ value.catch(() => {
19305
+ if (panesMemo?.value === value) panesMemo = null;
19306
+ });
19307
+ return value;
19308
+ }
19309
+ async function buildLocalPresence() {
19310
+ const panes = await listPanesShared();
19311
+ const rows = panes.slice(0, MAX_PRESENCE_SESSIONS).map((p3) => paneToPresence(p3, activePhase(p3)));
19312
+ observePhases(rows.map((r) => ({ sessionId: r.sessionId, projectId: r.projectId, specId: r.specId, phase: r.phase })));
19313
+ return rows;
19314
+ }
19315
+ async function buildLocalCapacity() {
19316
+ return currentLaunchStatus(await listPanesShared(), await getScheduler());
19317
+ }
19318
+ var isoFromEpochSeconds, PANES_MEMO_MS, panesMemo;
19319
+ var init_snapshot = __esm({
19320
+ "src/services/presence/snapshot.ts"() {
19321
+ "use strict";
19322
+ init_src();
19323
+ init_pty();
19324
+ init_session_phases();
19325
+ init_config3();
19326
+ init_session_launch_gate();
19327
+ init_phase_tap();
19328
+ isoFromEpochSeconds = (s2) => new Date((s2 || 0) * 1e3).toISOString();
19329
+ PANES_MEMO_MS = 1e3;
19330
+ panesMemo = null;
19331
+ }
19332
+ });
19333
+
19334
+ // src/services/live-phases.ts
19335
+ async function sessionPhasesBySpecAsync() {
19336
+ const out4 = /* @__PURE__ */ new Map();
19337
+ let panes;
19338
+ try {
19339
+ panes = await listPanesShared();
19340
+ } catch {
19341
+ return out4;
19342
+ }
19343
+ await Promise.all(panes.map(async (p3) => {
19344
+ if (!p3.specId || !p3.flow || !p3.phaseFile) return;
19345
+ out4.set(p3.specId, { phases: await readPhasesAsync(p3.phaseFile, p3.flow), cwd: p3.cwd });
19346
+ }));
19347
+ return out4;
19348
+ }
19349
+ async function liveDecisionsAsync() {
19350
+ return decisionsOf(await listPanesShared());
19351
+ }
19352
+ var init_live_phases = __esm({
19353
+ "src/services/live-phases.ts"() {
19354
+ "use strict";
19355
+ init_session_phases();
19356
+ init_snapshot();
19357
+ init_pty();
19358
+ }
19359
+ });
19360
+
19184
19361
  // src/services/notifications.ts
19185
19362
  async function recordDrift(vpsId, vpsName, drift, snapshotId) {
19186
19363
  if (drift.length === 0) return;
@@ -19282,11 +19459,11 @@ async function recordCronRun(cronId, cronName, projectId, dueAt, status2, note)
19282
19459
  }).catch(() => {
19283
19460
  });
19284
19461
  }
19285
- async function scanDecisions(read2 = liveDecisions) {
19462
+ async function scanDecisions(read2 = liveDecisionsAsync) {
19286
19463
  const next = /* @__PURE__ */ new Set();
19287
19464
  const fresh = [];
19288
- for (const s2 of read2()) {
19289
- if (!markerFilled(s2.decisionFile)) continue;
19465
+ for (const s2 of await read2()) {
19466
+ if (!await markerFilledAsync(s2.decisionFile)) continue;
19290
19467
  if (awaiting.has(s2.id)) {
19291
19468
  next.add(s2.id);
19292
19469
  continue;
@@ -19322,96 +19499,12 @@ var init_notifications2 = __esm({
19322
19499
  "use strict";
19323
19500
  init_db();
19324
19501
  init_pty();
19502
+ init_live_phases();
19325
19503
  awaiting = /* @__PURE__ */ new Set();
19326
19504
  DEFAULT_FEED_TAKE = 50;
19327
19505
  }
19328
19506
  });
19329
19507
 
19330
- // src/services/logs/phase-tap.ts
19331
- function observePhases(rows) {
19332
- const seen2 = /* @__PURE__ */ new Set();
19333
- for (const r of rows) {
19334
- seen2.add(r.sessionId);
19335
- const prev = known.get(r.sessionId);
19336
- if (!known.has(r.sessionId)) {
19337
- known.set(r.sessionId, r.phase);
19338
- continue;
19339
- }
19340
- if (prev !== r.phase) {
19341
- void appendEvent({
19342
- kind: "session.phase",
19343
- msg: `sesi ${r.sessionId} fase ${prev ?? "?"} \u2192 ${r.phase ?? "?"}`,
19344
- projectId: r.projectId,
19345
- specId: r.specId ?? null,
19346
- sessionId: r.sessionId,
19347
- data: { from: prev, to: r.phase }
19348
- });
19349
- known.set(r.sessionId, r.phase);
19350
- }
19351
- }
19352
- for (const id2 of [...known.keys()]) if (!seen2.has(id2)) known.delete(id2);
19353
- }
19354
- var known;
19355
- var init_phase_tap = __esm({
19356
- "src/services/logs/phase-tap.ts"() {
19357
- "use strict";
19358
- init_event_log();
19359
- known = /* @__PURE__ */ new Map();
19360
- }
19361
- });
19362
-
19363
- // src/services/presence/snapshot.ts
19364
- function paneToPresence(p3, phase) {
19365
- return {
19366
- sessionId: p3.id,
19367
- projectId: p3.projectId,
19368
- ...p3.specId ? { specId: p3.specId } : {},
19369
- ...p3.flow ? { flow: p3.flow } : {},
19370
- ...phase ? { phase } : {},
19371
- agent: p3.agent,
19372
- // Presedensi: pane mati sudah berakhir apa pun isi markernya.
19373
- status: p3.exited ? "exited" : p3.decision ? "waiting" : "working",
19374
- startedAt: isoFromEpochSeconds(p3.startedAt)
19375
- };
19376
- }
19377
- function activePhase(p3) {
19378
- if (!p3.flow || !p3.phaseFile) return void 0;
19379
- return readPhases(p3.phaseFile, p3.flow).find((f) => f.state === "active")?.name;
19380
- }
19381
- function listPanesShared(now = Date.now()) {
19382
- if (panesMemo && now - panesMemo.at < PANES_MEMO_MS) return panesMemo.value;
19383
- const value = listPanesAsync();
19384
- panesMemo = { at: now, value };
19385
- value.catch(() => {
19386
- if (panesMemo?.value === value) panesMemo = null;
19387
- });
19388
- return value;
19389
- }
19390
- async function buildLocalPresence() {
19391
- const panes = await listPanesShared();
19392
- const rows = panes.slice(0, MAX_PRESENCE_SESSIONS).map((p3) => paneToPresence(p3, activePhase(p3)));
19393
- observePhases(rows.map((r) => ({ sessionId: r.sessionId, projectId: r.projectId, specId: r.specId, phase: r.phase })));
19394
- return rows;
19395
- }
19396
- async function buildLocalCapacity() {
19397
- return currentLaunchStatus(await listPanesShared(), await getScheduler());
19398
- }
19399
- var isoFromEpochSeconds, PANES_MEMO_MS, panesMemo;
19400
- var init_snapshot = __esm({
19401
- "src/services/presence/snapshot.ts"() {
19402
- "use strict";
19403
- init_src();
19404
- init_pty();
19405
- init_session_phases();
19406
- init_config3();
19407
- init_session_launch_gate();
19408
- init_phase_tap();
19409
- isoFromEpochSeconds = (s2) => new Date((s2 || 0) * 1e3).toISOString();
19410
- PANES_MEMO_MS = 1e3;
19411
- panesMemo = null;
19412
- }
19413
- });
19414
-
19415
19508
  // src/services/device-sockets.ts
19416
19509
  function registerDeviceSocket(deviceId, kind, socket2) {
19417
19510
  const entry = { kind, socket: socket2 };
@@ -19916,8 +20009,8 @@ var require_ms = __commonJS({
19916
20009
  "../node_modules/.pnpm/ms@2.1.3/node_modules/ms/index.js"(exports, module) {
19917
20010
  var s2 = 1e3;
19918
20011
  var m = s2 * 60;
19919
- var h = m * 60;
19920
- var d = h * 24;
20012
+ var h2 = m * 60;
20013
+ var d = h2 * 24;
19921
20014
  var w = d * 7;
19922
20015
  var y = d * 365.25;
19923
20016
  module.exports = function(val, options2) {
@@ -19965,7 +20058,7 @@ var require_ms = __commonJS({
19965
20058
  case "hrs":
19966
20059
  case "hr":
19967
20060
  case "h":
19968
- return n2 * h;
20061
+ return n2 * h2;
19969
20062
  case "minutes":
19970
20063
  case "minute":
19971
20064
  case "mins":
@@ -19993,8 +20086,8 @@ var require_ms = __commonJS({
19993
20086
  if (msAbs >= d) {
19994
20087
  return Math.round(ms / d) + "d";
19995
20088
  }
19996
- if (msAbs >= h) {
19997
- return Math.round(ms / h) + "h";
20089
+ if (msAbs >= h2) {
20090
+ return Math.round(ms / h2) + "h";
19998
20091
  }
19999
20092
  if (msAbs >= m) {
20000
20093
  return Math.round(ms / m) + "m";
@@ -20009,8 +20102,8 @@ var require_ms = __commonJS({
20009
20102
  if (msAbs >= d) {
20010
20103
  return plural(ms, msAbs, d, "day");
20011
20104
  }
20012
- if (msAbs >= h) {
20013
- return plural(ms, msAbs, h, "hour");
20105
+ if (msAbs >= h2) {
20106
+ return plural(ms, msAbs, h2, "hour");
20014
20107
  }
20015
20108
  if (msAbs >= m) {
20016
20109
  return plural(ms, msAbs, m, "minute");
@@ -20726,7 +20819,7 @@ var init_runtime_defaults2 = __esm({
20726
20819
  });
20727
20820
 
20728
20821
  // src/services/agent-token.ts
20729
- import { randomBytes as randomBytes2, createHash as createHash4, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
20822
+ import { randomBytes as randomBytes2, createHash as createHash5, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
20730
20823
  function toAgentTokenView(t) {
20731
20824
  return {
20732
20825
  id: t.id,
@@ -20792,7 +20885,7 @@ var init_agent_token = __esm({
20792
20885
  "src/services/agent-token.ts"() {
20793
20886
  "use strict";
20794
20887
  init_db();
20795
- hash2 = (token) => createHash4("sha256").update(token).digest("hex");
20888
+ hash2 = (token) => createHash5("sha256").update(token).digest("hex");
20796
20889
  }
20797
20890
  });
20798
20891
 
@@ -20932,7 +21025,7 @@ var init_config4 = __esm({
20932
21025
  });
20933
21026
 
20934
21027
  // src/services/telegram/protocol.ts
20935
- import { createHash as createHash5 } from "node:crypto";
21028
+ import { createHash as createHash6 } from "node:crypto";
20936
21029
  function normalizeTelegramCommand(text) {
20937
21030
  const clean2 = text.trim();
20938
21031
  const match = clean2.match(/^\/([a-z0-9_]+)(?:@[a-z0-9_]+)?(?:\s+([\s\S]*))?$/i);
@@ -20999,7 +21092,7 @@ function parseTelegramUpdate(update2, allowlist, maxText = 12e3) {
20999
21092
  };
21000
21093
  }
21001
21094
  function inboundDigest(input) {
21002
- return createHash5("sha256").update(`${input.updateId}\0${input.chatId}\0${input.userId}\0${input.kind}\0${input.text}`, "utf8").digest("hex");
21095
+ return createHash6("sha256").update(`${input.updateId}\0${input.chatId}\0${input.userId}\0${input.kind}\0${input.text}`, "utf8").digest("hex");
21003
21096
  }
21004
21097
  function sanitizeTelegramOutput(text, exactSecrets = []) {
21005
21098
  let clean2 = text.replace(ANSI, "").replace(CONTROLS, "");
@@ -21166,7 +21259,7 @@ var init_typing = __esm({
21166
21259
  });
21167
21260
 
21168
21261
  // src/services/telegram/gateway.ts
21169
- import { createHash as createHash6 } from "node:crypto";
21262
+ import { createHash as createHash7 } from "node:crypto";
21170
21263
  var digestRaw, TelegramGateway;
21171
21264
  var init_gateway = __esm({
21172
21265
  "src/services/telegram/gateway.ts"() {
@@ -21175,7 +21268,7 @@ var init_gateway = __esm({
21175
21268
  init_protocol();
21176
21269
  init_runtime();
21177
21270
  init_typing();
21178
- digestRaw = (update2) => createHash6("sha256").update(JSON.stringify(update2)).digest("hex");
21271
+ digestRaw = (update2) => createHash7("sha256").update(JSON.stringify(update2)).digest("hex");
21179
21272
  TelegramGateway = class {
21180
21273
  constructor(deps) {
21181
21274
  this.deps = deps;
@@ -21482,14 +21575,14 @@ var init_engine_command = __esm({
21482
21575
  });
21483
21576
 
21484
21577
  // src/services/telegram/session.ts
21485
- import { createHash as createHash7 } from "node:crypto";
21578
+ import { createHash as createHash8 } from "node:crypto";
21486
21579
  var chatHash, telegramOperatorSessionId, formatTelegramTurn, TelegramSessionCoordinator;
21487
21580
  var init_session = __esm({
21488
21581
  "src/services/telegram/session.ts"() {
21489
21582
  "use strict";
21490
21583
  init_src2();
21491
21584
  init_engine_command();
21492
- chatHash = (chatId) => createHash7("sha256").update(chatId).digest("hex").slice(0, 16);
21585
+ chatHash = (chatId) => createHash8("sha256").update(chatId).digest("hex").slice(0, 16);
21493
21586
  telegramOperatorSessionId = (chatId) => `telegram-${chatHash(chatId)}`;
21494
21587
  formatTelegramTurn = (input) => `[Telegram update ${input.updateId} \xB7 chat ${input.chatId} \xB7 kind ${input.kind}]
21495
21588
  ${input.text}`;
@@ -24949,7 +25042,7 @@ var require_websocket = __commonJS({
24949
25042
  var http = __require("http");
24950
25043
  var net = __require("net");
24951
25044
  var tls = __require("tls");
24952
- var { randomBytes: randomBytes12, createHash: createHash15 } = __require("crypto");
25045
+ var { randomBytes: randomBytes12, createHash: createHash16 } = __require("crypto");
24953
25046
  var { Duplex, Readable } = __require("stream");
24954
25047
  var { URL: URL2 } = __require("url");
24955
25048
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -25617,7 +25710,7 @@ var require_websocket = __commonJS({
25617
25710
  abortHandshake(websocket2, socket2, "Invalid Upgrade header");
25618
25711
  return;
25619
25712
  }
25620
- const digest2 = createHash15("sha1").update(key + GUID).digest("base64");
25713
+ const digest2 = createHash16("sha1").update(key + GUID).digest("base64");
25621
25714
  if (res.headers["sec-websocket-accept"] !== digest2) {
25622
25715
  abortHandshake(websocket2, socket2, "Invalid Sec-WebSocket-Accept header");
25623
25716
  return;
@@ -25986,7 +26079,7 @@ var require_websocket_server = __commonJS({
25986
26079
  var EventEmitter = __require("events");
25987
26080
  var http = __require("http");
25988
26081
  var { Duplex } = __require("stream");
25989
- var { createHash: createHash15 } = __require("crypto");
26082
+ var { createHash: createHash16 } = __require("crypto");
25990
26083
  var extension2 = require_extension();
25991
26084
  var PerMessageDeflate2 = require_permessage_deflate();
25992
26085
  var subprotocol2 = require_subprotocol();
@@ -26293,7 +26386,7 @@ var require_websocket_server = __commonJS({
26293
26386
  );
26294
26387
  }
26295
26388
  if (this._state > RUNNING) return abortHandshake(socket2, 503);
26296
- const digest2 = createHash15("sha1").update(key + GUID).digest("base64");
26389
+ const digest2 = createHash16("sha1").update(key + GUID).digest("base64");
26297
26390
  const headers = [
26298
26391
  "HTTP/1.1 101 Switching Protocols",
26299
26392
  "Upgrade: websocket",
@@ -26365,7 +26458,7 @@ var require_websocket_server = __commonJS({
26365
26458
  socket2.once("finish", socket2.destroy);
26366
26459
  socket2.end(
26367
26460
  `HTTP/1.1 ${code} ${http.STATUS_CODES[code]}\r
26368
- ` + Object.keys(headers).map((h) => `${h}: ${headers[h]}`).join("\r\n") + "\r\n\r\n" + message
26461
+ ` + Object.keys(headers).map((h2) => `${h2}: ${headers[h2]}`).join("\r\n") + "\r\n\r\n" + message
26369
26462
  );
26370
26463
  }
26371
26464
  function abortHandshakeOrEmitwsClientError(server, req, socket2, code, message, headers) {
@@ -26589,7 +26682,7 @@ var init_client2 = __esm({
26589
26682
  });
26590
26683
 
26591
26684
  // src/services/logs/spool.ts
26592
- import { appendFile, mkdir as mkdir8, readFile as readFile6, readdir as readdir8, rm as rm8, stat as stat4 } from "node:fs/promises";
26685
+ import { appendFile, mkdir as mkdir8, readFile as readFile7, readdir as readdir9, rm as rm8, stat as stat6 } from "node:fs/promises";
26593
26686
  import { join as join35 } from "node:path";
26594
26687
  function spoolDir(lane) {
26595
26688
  return join35(resolveDataDirs().home, "log-spool", lane);
@@ -26613,7 +26706,7 @@ async function readSpoolSegments(lane) {
26613
26706
  const dir2 = spoolDir(lane);
26614
26707
  let names;
26615
26708
  try {
26616
- names = await readdir8(dir2);
26709
+ names = await readdir9(dir2);
26617
26710
  } catch {
26618
26711
  return [];
26619
26712
  }
@@ -26621,7 +26714,7 @@ async function readSpoolSegments(lane) {
26621
26714
  const out4 = [];
26622
26715
  for (const name2 of files) {
26623
26716
  const file = join35(dir2, name2);
26624
- const raw = await readFile6(file, "utf8").catch(() => "");
26717
+ const raw = await readFile7(file, "utf8").catch(() => "");
26625
26718
  const entries3 = raw.split("\n").filter(Boolean).map((l) => JSON.parse(l));
26626
26719
  out4.push({ file, entries: entries3 });
26627
26720
  }
@@ -27229,7 +27322,7 @@ var init_agent_tool_catalog = __esm({
27229
27322
  });
27230
27323
 
27231
27324
  // src/services/builtin-agents.ts
27232
- import { createHash as createHash12 } from "node:crypto";
27325
+ import { createHash as createHash13 } from "node:crypto";
27233
27326
  async function seedBuiltinAgents() {
27234
27327
  try {
27235
27328
  const setting = await getSetting();
@@ -27319,7 +27412,7 @@ var init_builtin_agents2 = __esm({
27319
27412
  init_settings3();
27320
27413
  init_tombstone();
27321
27414
  init_sync_notify();
27322
- digest = (parts) => createHash12("sha256").update(parts.join(" ")).digest("hex").slice(0, 16);
27415
+ digest = (parts) => createHash13("sha256").update(parts.join(" ")).digest("hex").slice(0, 16);
27323
27416
  legacyFingerprint = (name2, description, instructions, tools) => digest([name2, description, instructions, [...tools].join(",")]);
27324
27417
  fingerprint = (a) => digest([
27325
27418
  a.name,
@@ -27825,14 +27918,14 @@ var require_HeaderParser = __commonJS({
27825
27918
  }
27826
27919
  const lines2 = this.buffer.split(RE_CRLF);
27827
27920
  const len = lines2.length;
27828
- let m, h;
27921
+ let m, h2;
27829
27922
  for (var i = 0; i < len; ++i) {
27830
27923
  if (lines2[i].length === 0) {
27831
27924
  continue;
27832
27925
  }
27833
27926
  if (lines2[i][0] === " " || lines2[i][0] === " ") {
27834
- if (h) {
27835
- this.header[h][this.header[h].length - 1] += lines2[i];
27927
+ if (h2) {
27928
+ this.header[h2][this.header[h2].length - 1] += lines2[i];
27836
27929
  continue;
27837
27930
  }
27838
27931
  }
@@ -27841,9 +27934,9 @@ var require_HeaderParser = __commonJS({
27841
27934
  return;
27842
27935
  }
27843
27936
  m = RE_HDR.exec(lines2[i]);
27844
- h = m[1].toLowerCase();
27845
- this.header[h] = this.header[h] || [];
27846
- this.header[h].push(m[2] || "");
27937
+ h2 = m[1].toLowerCase();
27938
+ this.header[h2] = this.header[h2] || [];
27939
+ this.header[h2].push(m[2] || "");
27847
27940
  if (++this.npairs === this.maxHeaderPairs) {
27848
27941
  break;
27849
27942
  }
@@ -30835,14 +30928,14 @@ async function health_default(app3) {
30835
30928
  }
30836
30929
 
30837
30930
  // src/routes/agent-doc.ts
30838
- import { readFile as readFile3 } from "node:fs/promises";
30931
+ import { readFile as readFile4 } from "node:fs/promises";
30839
30932
  async function agent_doc_default(app3, opts) {
30840
30933
  app3.get("/agent-integration.md", async (_req, reply) => {
30841
30934
  if (!opts.file)
30842
30935
  return reply.code(404).send({
30843
30936
  error: `dokumen panduan tak ada di instalasi ini (${AGENT_DOC_REL}) \u2014 pasang ulang paket hanoman atau set HANOMAN_AGENT_DOC`
30844
30937
  });
30845
- const text = await readFile3(opts.file, "utf8");
30938
+ const text = await readFile4(opts.file, "utf8");
30846
30939
  return reply.type("text/markdown; charset=utf-8").send(text);
30847
30940
  });
30848
30941
  }
@@ -30883,8 +30976,8 @@ function components(rel) {
30883
30976
  }
30884
30977
  async function canonicalRoot(root) {
30885
30978
  const out4 = await realpath(root).catch(() => denied("root tidak ada"));
30886
- const stat5 = await lstat(out4);
30887
- if (!stat5.isDirectory() || stat5.isSymbolicLink()) denied("root bukan direktori regular");
30979
+ const stat7 = await lstat(out4);
30980
+ if (!stat7.isDirectory() || stat7.isSymbolicLink()) denied("root bukan direktori regular");
30888
30981
  return out4;
30889
30982
  }
30890
30983
  function beneath(root, candidate) {
@@ -30902,8 +30995,8 @@ async function ensureRepoParents(root, rel) {
30902
30995
  } catch (error) {
30903
30996
  if (error.code !== "EEXIST") throw error;
30904
30997
  }
30905
- const stat5 = await lstat(current);
30906
- if (stat5.isSymbolicLink() || !stat5.isDirectory()) denied("parent tidak aman");
30998
+ const stat7 = await lstat(current);
30999
+ if (stat7.isSymbolicLink() || !stat7.isDirectory()) denied("parent tidak aman");
30907
31000
  const canonical = await realpath(current);
30908
31001
  if (!beneath(base2, canonical) || canonical !== current) denied("parent keluar root");
30909
31002
  }
@@ -30919,8 +31012,8 @@ function ensureRepoParentsSync(root, rel) {
30919
31012
  } catch (error) {
30920
31013
  if (error.code !== "EEXIST") throw error;
30921
31014
  }
30922
- const stat5 = lstatSync3(current);
30923
- if (stat5.isSymbolicLink() || !stat5.isDirectory()) denied("parent tidak aman");
31015
+ const stat7 = lstatSync3(current);
31016
+ if (stat7.isSymbolicLink() || !stat7.isDirectory()) denied("parent tidak aman");
30924
31017
  if (realpathSync4(current) !== current || !beneath(base2, current)) denied("parent keluar root");
30925
31018
  }
30926
31019
  }
@@ -30930,14 +31023,14 @@ async function resolveRepoEntry(root, rel, opts = {}) {
30930
31023
  let current = base2;
30931
31024
  for (let i = 0; i < parts.length; i++) {
30932
31025
  current = join14(current, parts[i]);
30933
- const stat5 = await lstat(current).catch((error) => {
31026
+ const stat7 = await lstat(current).catch((error) => {
30934
31027
  if (error.code === "ENOENT" && opts.allowMissingTail) return null;
30935
31028
  if (error.code === "ENOENT" && opts.allowMissingFinal && i === parts.length - 1) return null;
30936
31029
  return denied("komponen tidak ada");
30937
31030
  });
30938
- if (!stat5) break;
30939
- if (stat5.isSymbolicLink()) denied("symlink");
30940
- if (i < parts.length - 1 && !stat5.isDirectory()) denied("parent bukan direktori");
31031
+ if (!stat7) break;
31032
+ if (stat7.isSymbolicLink()) denied("symlink");
31033
+ if (i < parts.length - 1 && !stat7.isDirectory()) denied("parent bukan direktori");
30941
31034
  }
30942
31035
  if (!beneath(base2, resolve12(current))) denied("keluar root");
30943
31036
  const parent = resolve12(current, "..");
@@ -30952,9 +31045,9 @@ function assertSafeRepoPathSync(root, rel, allowMissingFinal = false, allowMissi
30952
31045
  for (let i = 0; i < parts.length; i++) {
30953
31046
  current = join14(current, parts[i]);
30954
31047
  try {
30955
- const stat5 = lstatSync3(current);
30956
- if (stat5.isSymbolicLink()) denied("symlink");
30957
- if (i < parts.length - 1 && !stat5.isDirectory()) denied("parent bukan direktori");
31048
+ const stat7 = lstatSync3(current);
31049
+ if (stat7.isSymbolicLink()) denied("symlink");
31050
+ if (i < parts.length - 1 && !stat7.isDirectory()) denied("parent bukan direktori");
30958
31051
  } catch (error) {
30959
31052
  if (allowMissingTail && error.code === "ENOENT") break;
30960
31053
  if (allowMissingFinal && error.code === "ENOENT" && i === parts.length - 1) break;
@@ -31009,8 +31102,8 @@ async function readRepoFile(root, rel) {
31009
31102
  const entry = await resolveRepoEntry(root, rel);
31010
31103
  const handle = await open2(entry.path, constants2.O_RDONLY | (constants2.O_NOFOLLOW ?? 0)).catch(() => denied("final component tidak aman"));
31011
31104
  try {
31012
- const stat5 = await handle.stat();
31013
- if (!stat5.isFile()) denied("bukan file regular");
31105
+ const stat7 = await handle.stat();
31106
+ if (!stat7.isFile()) denied("bukan file regular");
31014
31107
  return await handle.readFile();
31015
31108
  } finally {
31016
31109
  await handle.close();
@@ -31338,7 +31431,7 @@ async function projects_default(app3) {
31338
31431
  const needle = (q ?? "").trim().toLowerCase();
31339
31432
  const filtered = needle ? views.filter((v) => `${v.name} ${v.desc} ${v.stack}`.toLowerCase().includes(needle)) : views;
31340
31433
  const device = (handledBy ?? "").trim();
31341
- const scoped = device ? filtered.filter((v) => v.handledBy.some((h) => h.deviceId === device)) : filtered;
31434
+ const scoped = device ? filtered.filter((v) => v.handledBy.some((h2) => h2.deviceId === device)) : filtered;
31342
31435
  return paginate(scoped, page, limit);
31343
31436
  });
31344
31437
  app3.get("/projects/:id", async (req, reply) => {
@@ -31916,7 +32009,7 @@ async function deleteDoc(projectId, path) {
31916
32009
  init_src2();
31917
32010
  init_db();
31918
32011
  import { execFile as execFile6 } from "node:child_process";
31919
- import { readdir as readdir2, rm as rm3 } from "node:fs/promises";
32012
+ import { readdir as readdir3, rm as rm3 } from "node:fs/promises";
31920
32013
  import { promisify as promisify5 } from "node:util";
31921
32014
  import { join as join17, resolve as resolve15 } from "node:path";
31922
32015
  init_notifications2();
@@ -31974,7 +32067,7 @@ async function sweepRepo(repoDir, projectId, deps = prodReaperDeps) {
31974
32067
  const dir2 = trashDirOf(repoDir);
31975
32068
  let entries3;
31976
32069
  try {
31977
- entries3 = await readdir2(dir2);
32070
+ entries3 = await readdir3(dir2);
31978
32071
  } catch {
31979
32072
  return 0;
31980
32073
  }
@@ -32033,7 +32126,7 @@ function ownsWorktree(repoDir, cwd) {
32033
32126
  import { execFile as execFile7 } from "node:child_process";
32034
32127
  import { promisify as promisify6 } from "node:util";
32035
32128
  import { existsSync as existsSync8 } from "node:fs";
32036
- import { readdir as readdir3 } from "node:fs/promises";
32129
+ import { readdir as readdir4 } from "node:fs/promises";
32037
32130
  import { resolve as resolve17 } from "node:path";
32038
32131
  var exec6 = promisify6(execFile7);
32039
32132
  var GIT4 = { maxBuffer: 1 << 24 };
@@ -32079,7 +32172,7 @@ async function listDirEntries(repoDir, rel) {
32079
32172
  const abs = repoAbsPath(repoDir, rel);
32080
32173
  let raw;
32081
32174
  try {
32082
- raw = await readdir3(abs, { withFileTypes: true });
32175
+ raw = await readdir4(abs, { withFileTypes: true });
32083
32176
  } catch {
32084
32177
  return empty;
32085
32178
  }
@@ -32245,12 +32338,12 @@ async function searchCommits(repoDir, q, by = "all") {
32245
32338
  };
32246
32339
  const byMessage = () => run4(["-i", "-F", `--grep=${q}`]);
32247
32340
  const byAuthor = () => run4(["-i", `--author=${q}`]);
32248
- const byHash = async () => (await run4([])).filter((h2) => h2.toLowerCase().startsWith(q.toLowerCase()));
32341
+ const byHash = async () => (await run4([])).filter((h3) => h3.toLowerCase().startsWith(q.toLowerCase()));
32249
32342
  if (by === "message") return byMessage();
32250
32343
  if (by === "author") return byAuthor();
32251
32344
  if (by === "hash") return byHash();
32252
- const [m, a, h] = await Promise.all([byMessage(), byAuthor(), byHash()]);
32253
- return [.../* @__PURE__ */ new Set([...m, ...a, ...h])];
32345
+ const [m, a, h2] = await Promise.all([byMessage(), byAuthor(), byHash()]);
32346
+ return [.../* @__PURE__ */ new Set([...m, ...a, ...h2])];
32254
32347
  }
32255
32348
  async function compareFile(repoDir, from, to, rel) {
32256
32349
  if (!repoDir) return null;
@@ -32271,9 +32364,9 @@ async function commitDetail(repoDir, sha) {
32271
32364
  try {
32272
32365
  const fmt = ["%H", "%P", "%an", "%aI", "%s", "%G?", "%cn", "%cI", "%ae", "%b"].join(US);
32273
32366
  const parts = (await exec6("git", ["show", "-s", `--pretty=format:${fmt}`, sha], { cwd: repoDir, ...GIT4 })).stdout.split(US);
32274
- const [h, parents, author, at, subject, gsig, committer, committedAt, authorEmail] = parts;
32367
+ const [h2, parents, author, at, subject, gsig, committer, committedAt, authorEmail] = parts;
32275
32368
  return {
32276
- sha: h,
32369
+ sha: h2,
32277
32370
  parents: parents ? parents.split(" ") : [],
32278
32371
  author: author ?? "",
32279
32372
  at: at ?? "",
@@ -33328,8 +33421,8 @@ var FileTokenizer = class _FileTokenizer extends AbstractTokenizer {
33328
33421
  */
33329
33422
  static async fromFile(sourceFilePath) {
33330
33423
  const fileHandle = await fsOpen(sourceFilePath, "r");
33331
- const stat5 = await fileHandle.stat();
33332
- return new _FileTokenizer(fileHandle, { fileInfo: { path: sourceFilePath, size: stat5.size } });
33424
+ const stat7 = await fileHandle.stat();
33425
+ return new _FileTokenizer(fileHandle, { fileInfo: { path: sourceFilePath, size: stat7.size } });
33333
33426
  }
33334
33427
  constructor(fileHandle, options2) {
33335
33428
  super(options2);
@@ -33979,7 +34072,7 @@ function indexOf(buffer, portion) {
33979
34072
  return -1;
33980
34073
  }
33981
34074
  function mergeArrays(chunks) {
33982
- const totalLength = chunks.reduce((acc, curr) => acc + curr.length, 0);
34075
+ const totalLength = chunks.reduce((acc2, curr) => acc2 + curr.length, 0);
33983
34076
  const mergedArray = new Uint8Array(totalLength);
33984
34077
  let offset = 0;
33985
34078
  for (const chunk of chunks) {
@@ -36939,7 +37032,7 @@ async function dropSpecAttachments(specId) {
36939
37032
 
36940
37033
  // src/services/spec-attachment-dir.ts
36941
37034
  init_db();
36942
- import { mkdir as mkdir5, readdir as readdir4, readFile as readFile4, rm as rm4, writeFile as writeFile4 } from "node:fs/promises";
37035
+ import { mkdir as mkdir5, readdir as readdir5, readFile as readFile5, rm as rm4, writeFile as writeFile4 } from "node:fs/promises";
36943
37036
  import { join as join20 } from "node:path";
36944
37037
  init_session_id();
36945
37038
  init_uploads();
@@ -36993,7 +37086,7 @@ async function syncSpecAttachmentsDir(specId, projectId) {
36993
37086
  for (const a of rows) {
36994
37087
  let bytes;
36995
37088
  try {
36996
- bytes = await readFile4(join20(uploadDir(), a.storageKey));
37089
+ bytes = await readFile5(join20(uploadDir(), a.storageKey));
36997
37090
  } catch {
36998
37091
  continue;
36999
37092
  }
@@ -37004,7 +37097,7 @@ async function syncSpecAttachmentsDir(specId, projectId) {
37004
37097
  }
37005
37098
  await writeFile4(join20(dir2, INDEX), renderIndex(specId, items), { mode: 384 });
37006
37099
  const keep = /* @__PURE__ */ new Set([INDEX, ...items.map((a) => a.filename)]);
37007
- for (const name2 of await readdir4(dir2)) {
37100
+ for (const name2 of await readdir5(dir2)) {
37008
37101
  if (!keep.has(name2)) await rm4(join20(dir2, name2), { recursive: true, force: true }).catch(() => {
37009
37102
  });
37010
37103
  }
@@ -39439,12 +39532,12 @@ function drawCode(doc, text) {
39439
39532
  used += hl;
39440
39533
  }
39441
39534
  const body = seg.join("\n");
39442
- const h = doc.heightOfString(body, opts) + 12;
39535
+ const h2 = doc.heightOfString(body, opts) + 12;
39443
39536
  const top = doc.y;
39444
- doc.save().rect(left, top, right - left, h).fill(PANEL).restore();
39537
+ doc.save().rect(left, top, right - left, h2).fill(PANEL).restore();
39445
39538
  doc.fillColor(INK).text(body, left + 8, top + 6, opts);
39446
39539
  doc.x = left;
39447
- doc.y = top + h;
39540
+ doc.y = top + h2;
39448
39541
  if (i < lines2.length) doc.addPage();
39449
39542
  }
39450
39543
  doc.moveDown(0.5);
@@ -39478,12 +39571,12 @@ function drawTable(doc, t) {
39478
39571
  const row = (cells, bold) => {
39479
39572
  const texts = cells.map((c) => toWinAnsi(flatten(c.tokens).map((r) => r.text).join("") || c.text || ""));
39480
39573
  doc.font(bold ? "Helvetica-Bold" : "Helvetica").fontSize(9.2).fillColor(bold ? STRONG : INK);
39481
- const h = Math.max(...texts.map((s2) => doc.heightOfString(s2, { width: w - 10 })), 0) + 7;
39482
- if (doc.y + h > doc.page.height - doc.page.margins.bottom) doc.addPage();
39574
+ const h2 = Math.max(...texts.map((s2) => doc.heightOfString(s2, { width: w - 10 })), 0) + 7;
39575
+ if (doc.y + h2 > doc.page.height - doc.page.margins.bottom) doc.addPage();
39483
39576
  const y0 = doc.y;
39484
39577
  texts.forEach((s2, i) => doc.text(s2, left + i * w + 5, y0 + 3, { width: w - 10 }));
39485
39578
  doc.x = left;
39486
- doc.y = y0 + h;
39579
+ doc.y = y0 + h2;
39487
39580
  rule(doc, HAIR, 2);
39488
39581
  };
39489
39582
  rule(doc, HAIR, 2);
@@ -39632,12 +39725,14 @@ function inDayRange(at, from, to) {
39632
39725
  }
39633
39726
 
39634
39727
  // src/services/live-specs.ts
39728
+ init_src();
39635
39729
  init_db();
39636
- init_pty();
39730
+ init_live_phases();
39637
39731
  init_session_phases();
39638
39732
  init_stage_machine();
39639
39733
  init_notifications2();
39640
39734
  init_sync_notify();
39735
+ import { createHash as createHash4 } from "node:crypto";
39641
39736
 
39642
39737
  // src/services/spec-head.ts
39643
39738
  init_db();
@@ -39659,34 +39754,102 @@ async function recordHeadSha(specId, worktree, read2 = readHead) {
39659
39754
 
39660
39755
  // src/services/live-specs.ts
39661
39756
  var specNum = (id2) => Number.parseInt(id2.match(/\d+/)?.[0] ?? "0", 10);
39662
- async function liveSpecs(filter = {}) {
39663
- const specs = (await prisma.spec.findMany({
39664
- where: { projectId: filter.project, source: filter.source },
39665
- orderBy: { id: "desc" }
39666
- })).sort((a, b) => specNum(b.id) - specNum(a.id));
39667
- const live = sessionPhasesBySpec();
39668
- if (live.size === 0) return decorateBlocked(specs);
39757
+ var selectOf = (shape) => Object.fromEntries(
39758
+ Object.keys(shape).filter((k) => k !== "blockedBy").map((k) => [k, true])
39759
+ );
39760
+ var SLIM_SELECT = selectOf(zSpecSlim.shape);
39761
+ var LIST_SELECT = selectOf(zSpecListItem.shape);
39762
+ var phases = () => sessionPhasesBySpecAsync().catch(() => /* @__PURE__ */ new Map());
39763
+ var sortByNumber = (rows) => {
39764
+ const n2 = new Map(rows.map((r) => [r.id, specNum(r.id)]));
39765
+ return rows.sort((a, b) => n2.get(b.id) - n2.get(a.id));
39766
+ };
39767
+ async function applyOverlay(specs, live) {
39768
+ if (live.size === 0) return { out: specs, advanced: [], doneNow: [] };
39669
39769
  const advanced = [];
39670
39770
  const doneNow = [];
39671
- const out4 = specs.map((s2) => {
39771
+ const out4 = await Promise.all(specs.map(async (s2) => {
39672
39772
  const entry = live.get(s2.id);
39673
39773
  if (!entry) return s2;
39674
- const next = stageForRun(entry.phases, entry.cwd, s2.id);
39774
+ const next = await stageForRunAsync(entry.phases, entry.cwd, s2.id);
39675
39775
  if (!next || STAGES.indexOf(next) <= STAGES.indexOf(s2.stage)) return s2;
39676
39776
  advanced.push({ id: s2.id, from: s2.stage, stage: next, cwd: entry.cwd });
39677
39777
  if (next === "done") doneNow.push({ specId: s2.id, title: s2.title, projectId: s2.projectId });
39678
39778
  return { ...s2, stage: next };
39679
- });
39680
- if (advanced.length)
39681
- await Promise.all(advanced.map(async (a) => {
39682
- const res = await prisma.spec.updateMany({ where: { id: a.id, stage: a.from }, data: { stage: a.stage } }).catch(() => ({ count: 0 }));
39683
- if (res.count === 0) return;
39684
- if (a.stage === "done") await recordHeadSha(a.id, a.cwd);
39685
- await notifySynced("spec", a.id);
39686
- }));
39779
+ }));
39780
+ return { out: out4, advanced, doneNow };
39781
+ }
39782
+ async function liveOverlayTick() {
39783
+ const live = await phases();
39784
+ if (live.size === 0) return;
39785
+ const specs = await prisma.spec.findMany({
39786
+ where: { id: { in: [...live.keys()] } },
39787
+ select: { id: true, stage: true, title: true, projectId: true }
39788
+ });
39789
+ const { advanced, doneNow } = await applyOverlay(specs, live);
39790
+ await Promise.all(advanced.map(async (a) => {
39791
+ const res = await prisma.spec.updateMany({ where: { id: a.id, stage: a.from }, data: { stage: a.stage } }).catch(() => ({ count: 0 }));
39792
+ if (res.count === 0) return;
39793
+ if (a.stage === "done") await recordHeadSha(a.id, a.cwd);
39794
+ await notifySynced("spec", a.id);
39795
+ }));
39687
39796
  await Promise.all(doneNow.map((d) => recordCompletion(d.specId, d.title, d.projectId)));
39797
+ }
39798
+ async function listSpecsLive(filter = {}) {
39799
+ const specs = sortByNumber(await prisma.spec.findMany({
39800
+ where: { projectId: filter.project, source: filter.source },
39801
+ orderBy: { id: "desc" }
39802
+ }));
39803
+ return decorateBlocked((await applyOverlay(specs, await phases())).out);
39804
+ }
39805
+ async function listSelected(select, where = {}) {
39806
+ const rows = sortByNumber(await prisma.spec.findMany({ where, select, orderBy: { id: "desc" } }));
39807
+ const { out: out4 } = await applyOverlay(rows, await phases());
39688
39808
  return decorateBlocked(out4);
39689
39809
  }
39810
+ var slimCache = null;
39811
+ async function slimRows() {
39812
+ const count = await prisma.spec.count();
39813
+ if (slimCache) {
39814
+ const changed = await prisma.spec.findMany({
39815
+ where: { updatedAt: { gte: slimCache.max } },
39816
+ select: SLIM_SELECT
39817
+ });
39818
+ for (const r of changed) {
39819
+ slimCache.rows.set(r.id, r);
39820
+ if (r.updatedAt > slimCache.max) slimCache.max = r.updatedAt;
39821
+ }
39822
+ if (slimCache.rows.size === count) return [...slimCache.rows.values()];
39823
+ }
39824
+ const rows = await prisma.spec.findMany({ select: SLIM_SELECT });
39825
+ slimCache = { rows: new Map(rows.map((r) => [r.id, r])), max: rows.reduce((m, r) => r.updatedAt > m ? r.updatedAt : m, /* @__PURE__ */ new Date(0)) };
39826
+ return rows;
39827
+ }
39828
+ async function listSpecsSlim() {
39829
+ const { out: out4 } = await applyOverlay(sortByNumber(await slimRows()), await phases());
39830
+ return await decorateBlocked(out4);
39831
+ }
39832
+ var listSpecsItems = (filter = {}) => listSelected(LIST_SELECT, { projectId: filter.project, source: filter.source });
39833
+ async function overlayOne(spec) {
39834
+ const live = await phases();
39835
+ return (await applyOverlay([spec], live)).out[0];
39836
+ }
39837
+ async function liveSignature() {
39838
+ const live = await phases();
39839
+ const parts = await Promise.all([...live.entries()].sort(([a], [b]) => a.localeCompare(b)).map(
39840
+ async ([id2, e]) => `${id2}:${e.phases.map((p3) => p3.state[0]).join("")}${phasesComplete(e.phases) ? `:${await planCompleteAsync(e.cwd, id2)}` : ""}`
39841
+ ));
39842
+ return parts.join("|");
39843
+ }
39844
+ async function specsDigest() {
39845
+ const agg = await prisma.spec.aggregate({ _count: { _all: true }, _max: { updatedAt: true }, _sum: { version: true } });
39846
+ const db = `${agg._count._all}:${agg._max.updatedAt?.getTime() ?? 0}:${agg._sum.version ?? 0}`;
39847
+ return createHash4("sha1").update(`${db}|${await liveSignature()}`).digest("hex");
39848
+ }
39849
+ async function liveSpecs(filter = {}) {
39850
+ await liveOverlayTick();
39851
+ return listSpecsLive(filter);
39852
+ }
39690
39853
 
39691
39854
  // src/services/launch-authority.ts
39692
39855
  init_src();
@@ -39721,8 +39884,15 @@ function filterSpecs(specs, f) {
39721
39884
  async function specs_default(app3) {
39722
39885
  app3.get("/specs", async (req) => {
39723
39886
  const { project, source, q, stage, priority, startable, dateField, from, to, page, limit } = req.query;
39724
- const overlaid = await liveSpecs({ project, source });
39725
- return paginate(filterSpecs(overlaid, { q, stage, priority, startable, dateField, from, to }), page, limit);
39887
+ await liveOverlayTick();
39888
+ const items = await listSpecsItems({ project, source });
39889
+ return paginate(filterSpecs(items, { q, stage, priority, startable, dateField, from, to }), page, limit);
39890
+ });
39891
+ app3.get("/specs/:id", async (req, reply) => {
39892
+ const { id: id2 } = req.params;
39893
+ const row = await prisma.spec.findUnique({ where: { id: id2 } });
39894
+ if (!row) return reply.code(404).send({ error: "spec tak ditemukan" });
39895
+ return (await decorateBlocked([await overlayOne(row)]))[0];
39726
39896
  });
39727
39897
  app3.post("/specs", async (req, reply) => {
39728
39898
  const parsed = zCreateSpec.safeParse(req.body);
@@ -40583,7 +40753,7 @@ async function deleteBranches(repoDir, names, opts) {
40583
40753
  // src/services/worktree-list.ts
40584
40754
  import { execFile as execFile11 } from "node:child_process";
40585
40755
  import { realpathSync as realpathSync6 } from "node:fs";
40586
- import { stat as stat2 } from "node:fs/promises";
40756
+ import { stat as stat4 } from "node:fs/promises";
40587
40757
  import { basename as basename6, dirname as dirname11, join as join21, resolve as resolve19, sep as sep6 } from "node:path";
40588
40758
  import { promisify as promisify10 } from "node:util";
40589
40759
  var exec10 = promisify10(execFile11);
@@ -40638,7 +40808,7 @@ function hasWorktreeSession(path, sessionId2, sessions) {
40638
40808
  }
40639
40809
  async function bornAt(path) {
40640
40810
  try {
40641
- const st = await stat2(join21(path, ".git"));
40811
+ const st = await stat4(join21(path, ".git"));
40642
40812
  const ms = st.birthtimeMs > 0 ? st.birthtimeMs : st.mtimeMs;
40643
40813
  return new Date(ms).toISOString();
40644
40814
  } catch {
@@ -40653,10 +40823,10 @@ async function listWorktrees(repoDir, inputs) {
40653
40823
  const trash = resolve19(baseReal, ".worktrees", ".trash");
40654
40824
  const sessions = new Map(inputs.sessions.map((s2) => [real(s2.cwd), { id: s2.id, specId: s2.specId }]));
40655
40825
  const history = /* @__PURE__ */ new Map();
40656
- for (const h of inputs.history ?? []) {
40657
- const path = real(h.cwd);
40826
+ for (const h2 of inputs.history ?? []) {
40827
+ const path = real(h2.cwd);
40658
40828
  const previous = history.get(path);
40659
- if (!previous || h.startedAt > previous.startedAt) history.set(path, h);
40829
+ if (!previous || h2.startedAt > previous.startedAt) history.set(path, h2);
40660
40830
  }
40661
40831
  const raw = parseWorktreePorcelain(text ?? "");
40662
40832
  const mainPath = raw[0] ? resolve19(raw[0].path) : null;
@@ -41054,10 +41224,14 @@ function installSessionHistory() {
41054
41224
 
41055
41225
  // src/services/worktree-project.ts
41056
41226
  init_session_id();
41227
+ var toWorktreeSession = (s2) => ({ cwd: s2.cwd, id: s2.id, specId: s2.specId ?? null });
41057
41228
  function worktreeSessions() {
41058
- return listSessions().map((s2) => ({ cwd: s2.cwd, id: s2.id, specId: s2.specId ?? null }));
41229
+ return listSessions().map(toWorktreeSession);
41230
+ }
41231
+ async function worktreeSessionsAsync() {
41232
+ return (await listSessionsAsync()).map(toWorktreeSession);
41059
41233
  }
41060
- async function projectWorktreeInputs(projectId, sessions = worktreeSessions) {
41234
+ async function projectWorktreeInputs(projectId, sessions = worktreeSessionsAsync) {
41061
41235
  const [specs, history] = await Promise.all([
41062
41236
  prisma.spec.findMany({ where: { projectId }, select: { id: true, stage: true } }),
41063
41237
  worktreeHistory(projectId)
@@ -41065,7 +41239,7 @@ async function projectWorktreeInputs(projectId, sessions = worktreeSessions) {
41065
41239
  return {
41066
41240
  specs: new Map(specs.map((s2) => [sessionIdForSpec(s2.id), s2])),
41067
41241
  history,
41068
- sessions: sessions()
41242
+ sessions: await sessions()
41069
41243
  };
41070
41244
  }
41071
41245
  async function detectOrphanWorktrees(deps = { repos: prodReaperDeps.repos, inputs: projectWorktreeInputs }) {
@@ -41665,7 +41839,7 @@ async function finishGraphOp(reply, id2, repoDir, r, verb) {
41665
41839
  }
41666
41840
 
41667
41841
  // src/routes/fs.ts
41668
- import { readdir as readdir5 } from "node:fs/promises";
41842
+ import { readdir as readdir6 } from "node:fs/promises";
41669
41843
  import { homedir as homedir4 } from "node:os";
41670
41844
  import { resolve as resolve21, dirname as dirname12, join as join23 } from "node:path";
41671
41845
  async function fs_default(app3) {
@@ -41673,7 +41847,7 @@ async function fs_default(app3) {
41673
41847
  const q = req.query.path;
41674
41848
  const dir2 = q && q.trim() ? resolve21(q.trim()) : homedir4();
41675
41849
  try {
41676
- const ents = await readdir5(dir2, { withFileTypes: true });
41850
+ const ents = await readdir6(dir2, { withFileTypes: true });
41677
41851
  const entries3 = ents.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => ({ name: e.name, path: join23(dir2, e.name) })).sort((a, b) => a.name.localeCompare(b.name));
41678
41852
  const parent = dirname12(dir2);
41679
41853
  return { path: dir2, parent: parent === dir2 ? null : parent, entries: entries3 };
@@ -41735,10 +41909,10 @@ function resumeState(repoDir, worktree, branchTo, headSha) {
41735
41909
  return base2 ? { worktreeKept: false, base: base2 } : null;
41736
41910
  }
41737
41911
  function buildResumeCtx(repoDir, id2, flow2, worktreeKept) {
41738
- const phases = readPhases(phaseFilePath(repoDir, id2), flow2);
41912
+ const phases2 = readPhases(phaseFilePath(repoDir, id2), flow2);
41739
41913
  return {
41740
- recorded: phases.filter((p3) => p3.state === "done" || p3.state === "skipped").map((p3) => `${p3.name} ${p3.state}`),
41741
- next: phases.find((p3) => p3.state === "active")?.name,
41914
+ recorded: phases2.filter((p3) => p3.state === "done" || p3.state === "skipped").map((p3) => `${p3.name} ${p3.state}`),
41915
+ next: phases2.find((p3) => p3.state === "active")?.name,
41742
41916
  worktreeKept
41743
41917
  };
41744
41918
  }
@@ -41834,8 +42008,8 @@ async function startSpecSession(spec, opts) {
41834
42008
  const fullPlan = sessionPhasePlan(setting, opts.flow, agent, { model, effort: effort2 }, opts.phaseOverrides);
41835
42009
  let plan = fullPlan;
41836
42010
  if (fullPlan && isContinue && !isGoalFlow) {
41837
- const phases = fullPlan.phases.filter((p3) => p3.phase === "Execute");
41838
- plan = phases.length ? { ...fullPlan, phases } : null;
42011
+ const phases2 = fullPlan.phases.filter((p3) => p3.phase === "Execute");
42012
+ plan = phases2.length ? { ...fullPlan, phases: phases2 } : null;
41839
42013
  }
41840
42014
  const buildPrompt = (p3) => {
41841
42015
  if (isGoalFlow) {
@@ -41936,7 +42110,7 @@ function setBounded(map, key, value, maxKeys) {
41936
42110
  // src/services/auth.ts
41937
42111
  init_src2();
41938
42112
  init_db();
41939
- import { randomBytes as randomBytes3, scrypt as scryptCb, timingSafeEqual as timingSafeEqual3, createHash as createHash8 } from "node:crypto";
42113
+ import { randomBytes as randomBytes3, scrypt as scryptCb, timingSafeEqual as timingSafeEqual3, createHash as createHash9 } from "node:crypto";
41940
42114
  import { promisify as promisify12 } from "node:util";
41941
42115
  var scrypt = promisify12(scryptCb);
41942
42116
  var COOKIE_NAME = "hn_session";
@@ -41954,7 +42128,7 @@ async function verifyPassword(pw, stored) {
41954
42128
  return key.length === want.length && timingSafeEqual3(key, want);
41955
42129
  }
41956
42130
  var newSessionToken = () => randomBytes3(32).toString("base64url");
41957
- var sessionId = (token) => createHash8("sha256").update(token).digest("hex");
42131
+ var sessionId = (token) => createHash9("sha256").update(token).digest("hex");
41958
42132
  async function createSession2(userId) {
41959
42133
  const token = newSessionToken();
41960
42134
  await prisma.session.create({
@@ -42015,9 +42189,9 @@ function cookieOpts(req) {
42015
42189
 
42016
42190
  // src/services/device-token.ts
42017
42191
  init_db();
42018
- import { randomBytes as randomBytes4, createHash as createHash9 } from "node:crypto";
42192
+ import { randomBytes as randomBytes4, createHash as createHash10 } from "node:crypto";
42019
42193
  var newDeviceToken = () => randomBytes4(32).toString("base64url");
42020
- var tokenHash = (token) => createHash9("sha256").update(token).digest("hex");
42194
+ var tokenHash = (token) => createHash10("sha256").update(token).digest("hex");
42021
42195
  async function issueDeviceToken(userId, name2) {
42022
42196
  const token = newDeviceToken();
42023
42197
  const row = await prisma.deviceToken.create({ data: { userId, name: name2, tokenHash: tokenHash(token) } });
@@ -42253,12 +42427,77 @@ function appendDiag(home3, sessionId2, events) {
42253
42427
  init_notifications2();
42254
42428
  init_stage_machine();
42255
42429
  init_pty();
42430
+
42431
+ // src/services/bounded-sender.ts
42432
+ var HIGH = 1024 * 1024;
42433
+ var CAP = 256 * 1024;
42434
+ var DRAIN_MS = 20;
42435
+ var isData = (frame2) => frame2.startsWith('{"t":"data"');
42436
+ function createBoundedSender(ws2, o = {}) {
42437
+ const high = o.high ?? HIGH;
42438
+ const cap = o.cap ?? CAP;
42439
+ const drainMs = o.drainMs ?? DRAIN_MS;
42440
+ let pending2 = [];
42441
+ let pendingBytes = 0;
42442
+ let timer10;
42443
+ let resyncNeeded = false;
42444
+ const trim = () => {
42445
+ for (let i = 0; i < pending2.length && pendingBytes > cap; ) {
42446
+ if (!isData(pending2[i])) {
42447
+ i++;
42448
+ continue;
42449
+ }
42450
+ pendingBytes -= pending2[i].length;
42451
+ pending2.splice(i, 1);
42452
+ resyncNeeded = true;
42453
+ }
42454
+ };
42455
+ const drain2 = () => {
42456
+ timer10 = void 0;
42457
+ if (ws2.bufferedAmount > high) {
42458
+ timer10 = setTimeout(drain2, drainMs);
42459
+ timer10.unref?.();
42460
+ return;
42461
+ }
42462
+ const out4 = pending2;
42463
+ pending2 = [];
42464
+ pendingBytes = 0;
42465
+ for (const f of out4) ws2.send(f);
42466
+ if (resyncNeeded) {
42467
+ resyncNeeded = false;
42468
+ o.onResync?.();
42469
+ }
42470
+ };
42471
+ return {
42472
+ send(frame2) {
42473
+ if (pending2.length === 0 && ws2.bufferedAmount <= high) {
42474
+ ws2.send(frame2);
42475
+ return;
42476
+ }
42477
+ pending2.push(frame2);
42478
+ pendingBytes += frame2.length;
42479
+ trim();
42480
+ if (!timer10) {
42481
+ timer10 = setTimeout(drain2, drainMs);
42482
+ timer10.unref?.();
42483
+ }
42484
+ },
42485
+ dispose() {
42486
+ if (timer10) clearTimeout(timer10);
42487
+ timer10 = void 0;
42488
+ pending2 = [];
42489
+ pendingBytes = 0;
42490
+ }
42491
+ };
42492
+ }
42493
+
42494
+ // src/routes/terminal.ts
42256
42495
  init_uploads();
42257
42496
 
42258
42497
  // src/services/agent-invocations.ts
42259
42498
  init_src();
42260
42499
  init_db();
42261
- import { createHash as createHash10, randomUUID as randomUUID9 } from "node:crypto";
42500
+ import { createHash as createHash11, randomUUID as randomUUID9 } from "node:crypto";
42262
42501
  import { execFileSync as execFileSync3 } from "node:child_process";
42263
42502
  import { homedir as homedir5 } from "node:os";
42264
42503
  import { readFileSync as readFileSync12, realpathSync as realpathSync7, statSync as statSync4 } from "node:fs";
@@ -42268,7 +42507,7 @@ import { isAbsolute as isAbsolute8, relative as relative4, resolve as resolve22
42268
42507
  init_session_event_token();
42269
42508
  init_session_event_spool();
42270
42509
  import { constants as constants3 } from "node:fs";
42271
- import { mkdir as mkdir6, open as open3, readdir as readdir6, rm as rm6 } from "node:fs/promises";
42510
+ import { mkdir as mkdir6, open as open3, readdir as readdir7, rm as rm6 } from "node:fs/promises";
42272
42511
  import { join as join25 } from "node:path";
42273
42512
  var MAX_EVENT_BYTES = 1e6;
42274
42513
  var MAX_FILES_PER_DRAIN = 1e3;
@@ -42314,12 +42553,12 @@ async function drainSpool(app3, root, observation) {
42314
42553
  let delivered = 0;
42315
42554
  let examined = 0;
42316
42555
  let readBuffer;
42317
- for (const session of await readdir6(root, { withFileTypes: true })) {
42556
+ for (const session of await readdir7(root, { withFileTypes: true })) {
42318
42557
  if (!session.isDirectory() || !SESSION_ID_RE.test(session.name)) continue;
42319
42558
  const dir2 = join25(root, session.name);
42320
42559
  let entries3;
42321
42560
  try {
42322
- entries3 = await readdir6(dir2, { withFileTypes: true });
42561
+ entries3 = await readdir7(dir2, { withFileTypes: true });
42323
42562
  } catch {
42324
42563
  continue;
42325
42564
  }
@@ -42331,8 +42570,8 @@ async function drainSpool(app3, root, observation) {
42331
42570
  let handle;
42332
42571
  try {
42333
42572
  handle = await open3(path, constants3.O_RDONLY | constants3.O_NOFOLLOW);
42334
- const stat5 = await handle.stat();
42335
- if (!stat5.isFile() || stat5.size > MAX_EVENT_BYTES) throw new Error("payload terlalu besar");
42573
+ const stat7 = await handle.stat();
42574
+ if (!stat7.isFile() || stat7.size > MAX_EVENT_BYTES) throw new Error("payload terlalu besar");
42336
42575
  readBuffer ??= Buffer.allocUnsafe(MAX_EVENT_BYTES + 1);
42337
42576
  const { bytesRead } = await handle.read(readBuffer, 0, readBuffer.length, 0);
42338
42577
  if (bytesRead > MAX_EVENT_BYTES) throw new Error("payload terlalu besar");
@@ -42405,7 +42644,7 @@ var MAX_TRANSCRIPT_BYTES2 = 10 * 1024 * 1024;
42405
42644
  var ANSI2 = /[\u001b\u009b](?:\][^\u0007]*(?:\u0007|\u001b\\)|\[[0-?]*[ -/]*[@-~]|[@-_])/g;
42406
42645
  var snapshotHashes = /* @__PURE__ */ new Map();
42407
42646
  var keyOf = (x) => `${x.sessionId}\0${x.runtimeInvocationId}`;
42408
- var hash3 = (value) => createHash10("sha256").update(value).digest("hex");
42647
+ var hash3 = (value) => createHash11("sha256").update(value).digest("hex");
42409
42648
  var defaultGitStatus = (cwd) => {
42410
42649
  try {
42411
42650
  return execFileSync3("git", ["-C", cwd, "status", "--porcelain=v1", "-z"], {
@@ -42751,9 +42990,9 @@ async function refreshPhaseInvocations(sessionId2) {
42751
42990
  // src/services/session-dialog.ts
42752
42991
  init_tui_dialog();
42753
42992
  init_pty();
42754
- import { createHash as createHash11 } from "node:crypto";
42993
+ import { createHash as createHash12 } from "node:crypto";
42755
42994
  var DIALOG_CHUNK_MS = 50;
42756
- var screenHashOf = (paneText2) => createHash11("sha256").update(dialogKey(paneText2)).digest("hex").slice(0, 16);
42995
+ var screenHashOf = (paneText2) => createHash12("sha256").update(dialogKey(paneText2)).digest("hex").slice(0, 16);
42757
42996
  var answerable = (paneText2) => {
42758
42997
  const s2 = readDialogScreen(paneText2);
42759
42998
  if (s2?.kind !== "question") return null;
@@ -44363,9 +44602,9 @@ async function terminal_default(app3, opts) {
44363
44602
  app3.get("/terminal/sessions/:id/phases", async (req, reply) => {
44364
44603
  const { id: id2 } = req.params;
44365
44604
  const s2 = getSession(id2);
44366
- const phases = sessionPhases(id2);
44367
- if (!s2?.flow || !phases) return reply.code(404).send({ error: "not found" });
44368
- return { flow: s2.flow, phases };
44605
+ const phases2 = sessionPhases(id2);
44606
+ if (!s2?.flow || !phases2) return reply.code(404).send({ error: "not found" });
44607
+ return { flow: s2.flow, phases: phases2 };
44369
44608
  });
44370
44609
  app3.post("/terminal/sessions/:id/steer", async (req, reply) => {
44371
44610
  const parsed = zTerminalSteerInput.safeParse(req.body);
@@ -44503,7 +44742,6 @@ async function terminal_default(app3, opts) {
44503
44742
  }
44504
44743
  }, (socket2, req) => {
44505
44744
  const { id: id2 } = req.params;
44506
- if (!getSession(id2)) return socket2.close(4004, "not found");
44507
44745
  const isClientRemote = req.remote !== void 0;
44508
44746
  const principal = isClientRemote ? { kind: "remote", id: `client:${req.remote.actor.hubOrigin}:${req.remote.actor.userId}` } : req.wsPrincipal;
44509
44747
  let release;
@@ -44514,14 +44752,38 @@ async function terminal_default(app3, opts) {
44514
44752
  return;
44515
44753
  }
44516
44754
  const guard = new WsMessageGuard({ perWindow: TERMINAL_WS_MESSAGES_PER_MINUTE });
44517
- const client = { send: (m) => socket2.send(m), close: () => socket2.close() };
44518
- attach(id2, client);
44519
- void refreshPhaseInvocations(id2);
44755
+ const bounded = createBoundedSender(socket2, {
44756
+ onResync: () => {
44757
+ void screenResyncFrame(id2).then((f) => {
44758
+ if (f) bounded.send(f);
44759
+ });
44760
+ }
44761
+ });
44762
+ const client = { send: (m) => bounded.send(m), close: () => socket2.close() };
44763
+ let ready = false;
44764
+ let closed = false;
44765
+ const early = [];
44766
+ const attachReady = (async () => {
44767
+ const found = await getSessionAsync(id2);
44768
+ if (!found) {
44769
+ socket2.close(4004, "not found");
44770
+ return;
44771
+ }
44772
+ if (closed) return;
44773
+ await attachAsync(id2, client);
44774
+ if (closed) {
44775
+ detach(id2, client);
44776
+ return;
44777
+ }
44778
+ void refreshPhaseInvocations(id2);
44779
+ })().catch(() => {
44780
+ socket2.close(1011, "attach failed");
44781
+ });
44520
44782
  const watch = isClientRemote ? null : createPrincipalWatch({
44521
44783
  check: () => revalidateWsPrincipal(req, principal),
44522
44784
  onRevoked: () => socket2.close(1008, "session revoked")
44523
44785
  });
44524
- socket2.on("message", (raw) => {
44786
+ const onMessage = (raw) => {
44525
44787
  const verdict = guard.accept(raw);
44526
44788
  if (!verdict.ok) {
44527
44789
  socket2.close(verdict.code, verdict.reason);
@@ -44544,10 +44806,20 @@ async function terminal_default(app3, opts) {
44544
44806
  } catch {
44545
44807
  }
44546
44808
  }
44809
+ };
44810
+ socket2.on("message", (raw) => {
44811
+ if (ready) onMessage(raw);
44812
+ else early.push(raw);
44813
+ });
44814
+ void attachReady.then(() => {
44815
+ ready = true;
44816
+ for (const raw of early.splice(0)) onMessage(raw);
44547
44817
  });
44548
44818
  const revalidate = watch ? setInterval(() => watch.refresh(), 6e4) : void 0;
44549
44819
  revalidate?.unref?.();
44550
44820
  socket2.on("close", () => {
44821
+ closed = true;
44822
+ bounded.dispose();
44551
44823
  if (revalidate) clearInterval(revalidate);
44552
44824
  watch?.dispose();
44553
44825
  release();
@@ -45037,13 +45309,13 @@ function mapToCatalog(checks) {
45037
45309
  var isHardened = (checks) => CRITICAL.every((c) => checks.find((x) => x.check === c)?.status === "pass");
45038
45310
  var HEALTH_CMD = `echo HEALTH uptime $(uptime -p 2>/dev/null || uptime); echo HEALTH disk $(df -P / | awk 'NR==2{print $5}'); echo HEALTH mem $(free -m 2>/dev/null | awk 'NR==2{printf "%d/%dMB", $3, $2}'); echo HEALTH load $(cut -d" " -f1-3 /proc/loadavg)`;
45039
45311
  function parseHealth(out4) {
45040
- const h = {};
45312
+ const h2 = {};
45041
45313
  for (const line of out4.split("\n")) {
45042
45314
  const m = line.match(/^HEALTH (\S+) ?(.*)$/);
45043
- if (m) h[m[1]] = m[2].trim();
45315
+ if (m) h2[m[1]] = m[2].trim();
45044
45316
  }
45045
- if (!h.uptime && !h.disk) return null;
45046
- return { uptime: h.uptime ?? "", disk: h.disk ?? "", mem: h.mem ?? "", load: h.load ?? "" };
45317
+ if (!h2.uptime && !h2.disk) return null;
45318
+ return { uptime: h2.uptime ?? "", disk: h2.disk ?? "", mem: h2.mem ?? "", load: h2.load ?? "" };
45047
45319
  }
45048
45320
  var target = (v) => ({ host: v.host, port: v.port, user: v.user, keyPath: v.keyPath });
45049
45321
  var packagedScript = (f) => fileURLToPath4(new URL(`../scripts/vps/${f}`, import.meta.url));
@@ -45625,9 +45897,9 @@ function readAccessToken() {
45625
45897
  return null;
45626
45898
  }
45627
45899
  }
45628
- function normalizeSeverity(s2, pct2) {
45900
+ function normalizeSeverity(s2, pct3) {
45629
45901
  if (s2 === "normal" || s2 === "warning" || s2 === "critical") return s2;
45630
- return pct2 >= 90 ? "critical" : pct2 >= 70 ? "warning" : "normal";
45902
+ return pct3 >= 90 ? "critical" : pct3 >= 70 ? "warning" : "normal";
45631
45903
  }
45632
45904
  function mapWindows(json) {
45633
45905
  const arr = json?.limits;
@@ -45676,7 +45948,7 @@ async function getLimits() {
45676
45948
  }
45677
45949
 
45678
45950
  // src/services/codex-limits.ts
45679
- import { open as open4, readdir as readdir7, stat as stat3 } from "node:fs/promises";
45951
+ import { open as open4, readdir as readdir8, stat as stat5 } from "node:fs/promises";
45680
45952
  import { homedir as homedir9 } from "node:os";
45681
45953
  import { join as join32 } from "node:path";
45682
45954
  var TTL_MS3 = 3e4;
@@ -45694,7 +45966,7 @@ function windowLabel(minutes) {
45694
45966
  if (minutes % 60 === 0) return `${minutes / 60} jam`;
45695
45967
  return `${minutes} menit`;
45696
45968
  }
45697
- var severityOf = (pct2) => pct2 >= 90 ? "critical" : pct2 >= 70 ? "warning" : "normal";
45969
+ var severityOf = (pct3) => pct3 >= 90 ? "critical" : pct3 >= 70 ? "warning" : "normal";
45698
45970
  function toWindow(slot, raw, reached) {
45699
45971
  const minutes = raw.window_minutes ?? 0;
45700
45972
  const usedPct = Math.round(raw.used_percent ?? 0);
@@ -45713,14 +45985,14 @@ function toWindow(slot, raw, reached) {
45713
45985
  async function recentRollouts(dir2) {
45714
45986
  let entries3;
45715
45987
  try {
45716
- entries3 = (await readdir7(dir2, { recursive: true })).filter((f) => f.endsWith(".jsonl"));
45988
+ entries3 = (await readdir8(dir2, { recursive: true })).filter((f) => f.endsWith(".jsonl"));
45717
45989
  } catch {
45718
45990
  return [];
45719
45991
  }
45720
45992
  const stamped = await Promise.all(entries3.map(async (rel) => {
45721
45993
  const full = join32(dir2, rel);
45722
45994
  try {
45723
- return { full, mtime: (await stat3(full)).mtimeMs };
45995
+ return { full, mtime: (await stat5(full)).mtimeMs };
45724
45996
  } catch {
45725
45997
  return null;
45726
45998
  }
@@ -45804,7 +46076,7 @@ async function codex(app3) {
45804
46076
  // src/services/model-catalog.ts
45805
46077
  init_src2();
45806
46078
  init_src();
45807
- import { readFile as readFile5, writeFile as writeFile5, rename as rename4, mkdir as mkdir7 } from "node:fs/promises";
46079
+ import { readFile as readFile6, writeFile as writeFile5, rename as rename4, mkdir as mkdir7 } from "node:fs/promises";
45808
46080
  import { dirname as dirname15, join as join34 } from "node:path";
45809
46081
 
45810
46082
  // src/services/model-catalog-parser.ts
@@ -46059,7 +46331,7 @@ var cacheFile = () => join34(resolveHome(), "model-catalog.json");
46059
46331
  var modelCatalogService = createModelCatalogService({
46060
46332
  probe: probeModelCatalog,
46061
46333
  read: async () => {
46062
- const content = await readFile5(cacheFile(), "utf8");
46334
+ const content = await readFile6(cacheFile(), "utf8");
46063
46335
  if (Buffer.byteLength(content) > 4 * 1024 * 1024) throw new Error("oversized cache");
46064
46336
  return JSON.parse(content);
46065
46337
  },
@@ -46396,6 +46668,7 @@ async function buildTasksPage(f) {
46396
46668
  // src/services/lead/views.ts
46397
46669
  init_db();
46398
46670
  init_pty();
46671
+ init_live_phases();
46399
46672
 
46400
46673
  // src/services/lead/pulse.ts
46401
46674
  init_db();
@@ -46576,13 +46849,7 @@ function findCollisions(areas) {
46576
46849
  return out4;
46577
46850
  }
46578
46851
  var prodPulseDeps = {
46579
- sessions: () => {
46580
- try {
46581
- return listSessions();
46582
- } catch {
46583
- return [];
46584
- }
46585
- },
46852
+ sessions: () => listSessionsAsync().catch(() => []),
46586
46853
  areas: async (s2) => {
46587
46854
  const repoDir = await resolveRepoDir(s2.projectId);
46588
46855
  if (!repoDir) return [];
@@ -46595,14 +46862,8 @@ var prodPulseDeps = {
46595
46862
  return [];
46596
46863
  }
46597
46864
  },
46598
- planDone: planComplete,
46599
- finished: (id2) => {
46600
- try {
46601
- return sessionFinished(id2);
46602
- } catch {
46603
- return false;
46604
- }
46605
- },
46865
+ planDone: planCompleteAsync,
46866
+ finished: (id2) => sessionFinishedAsync(id2).catch(() => false),
46606
46867
  decide,
46607
46868
  decideDeps: prodDecideDeps,
46608
46869
  apply: applyAction,
@@ -46648,10 +46909,10 @@ async function pulse(deps = prodPulseDeps) {
46648
46909
  async function followUpFinished(cfg, optIn, deps) {
46649
46910
  let n2 = 0;
46650
46911
  const opt = new Set(optIn);
46651
- for (const s2 of deps.sessions()) {
46912
+ for (const s2 of await deps.sessions()) {
46652
46913
  if (!s2.exited || !s2.specId || !opt.has(s2.projectId)) continue;
46653
46914
  const bad = (s2.exitCode ?? 0) !== 0;
46654
- const unfinished = !deps.planDone(s2.cwd, s2.specId);
46915
+ const unfinished = !await deps.planDone(s2.cwd, s2.specId);
46655
46916
  if (!bad && !unfinished) continue;
46656
46917
  const mark = `Sesi ${s2.id} untuk backlog ${s2.specId}`;
46657
46918
  const seen2 = await prisma.leadDecision.findFirst({
@@ -46690,10 +46951,10 @@ async function followUpFinished(cfg, optIn, deps) {
46690
46951
  async function followUpComplete(optIn, deps) {
46691
46952
  let n2 = 0;
46692
46953
  const opt = new Set(optIn);
46693
- for (const s2 of deps.sessions()) {
46954
+ for (const s2 of await deps.sessions()) {
46694
46955
  if (!s2.specId || !opt.has(s2.projectId)) continue;
46695
46956
  if ((s2.exitCode ?? 0) !== 0) continue;
46696
- if (!deps.finished(s2.id)) continue;
46957
+ if (!await deps.finished(s2.id)) continue;
46697
46958
  const mark = `Backlog ${s2.specId} sudah selesai di sesi ${s2.id}`;
46698
46959
  const seen2 = await prisma.leadDecision.findFirst({
46699
46960
  where: { sessionId: s2.id, gate: "pulse", question: { startsWith: mark } }
@@ -46732,7 +46993,7 @@ async function followUpComplete(optIn, deps) {
46732
46993
  }
46733
46994
  async function detectCollisions(optIn, deps) {
46734
46995
  const opt = new Set(optIn);
46735
- const live = deps.sessions().filter((s2) => !s2.exited && s2.specId && opt.has(s2.projectId));
46996
+ const live = (await deps.sessions()).filter((s2) => !s2.exited && s2.specId && opt.has(s2.projectId));
46736
46997
  if (live.length < 2) return 0;
46737
46998
  const areas = [];
46738
46999
  for (const s2 of live) {
@@ -46827,7 +47088,7 @@ async function orderProject(projectId, deps) {
46827
47088
 
46828
47089
  // src/services/lead/engine.ts
46829
47090
  init_notifications2();
46830
- init_pty();
47091
+ init_live_phases();
46831
47092
  var HOUSEKEEPING_MS = 6e4;
46832
47093
  var busyPulse = false;
46833
47094
  var lastPulseAt = 0;
@@ -46862,9 +47123,9 @@ async function tick(now, deps = {}) {
46862
47123
  console.error("lead expire:", e);
46863
47124
  }));
46864
47125
  jobs.push((async () => {
46865
- const live = deps.live ?? liveDecisions;
47126
+ const live = deps.live ?? liveDecisionsAsync;
46866
47127
  const legacy = deps.legacy ?? recordLegacySession;
46867
- for (const s2 of live()) {
47128
+ for (const s2 of await live()) {
46868
47129
  if (s2.eventHook || !s2.waiting) continue;
46869
47130
  await legacy(s2.id, s2.projectId || null, s2.specId ?? null);
46870
47131
  }
@@ -46909,7 +47170,7 @@ async function buildLeadStatus() {
46909
47170
  }
46910
47171
  let waiting2 = [];
46911
47172
  try {
46912
- waiting2 = liveDecisions().filter((d) => d.waiting).map((d) => d.id);
47173
+ waiting2 = (await liveDecisionsAsync()).filter((d) => d.waiting).map((d) => d.id);
46913
47174
  } catch {
46914
47175
  }
46915
47176
  const rows = await Promise.all(projects.map(async (p3) => ({
@@ -46982,11 +47243,48 @@ function parseParams(topic, params) {
46982
47243
  return r.success ? r.data : void 0;
46983
47244
  }
46984
47245
 
47246
+ // src/services/events-profile.ts
47247
+ import { monitorEventLoopDelay } from "node:perf_hooks";
47248
+ var PROFILE = process.env.HANOMAN_EVENTS_PROFILE === "1";
47249
+ var acc = {};
47250
+ var h;
47251
+ var nowUs = () => Number(process.hrtime.bigint() / 1000n);
47252
+ var profStart = () => PROFILE ? nowUs() : 0;
47253
+ function profEnd(group2, t0, bytes, emitted) {
47254
+ if (!PROFILE) return;
47255
+ const a = acc[group2] ??= { builds: [], frames: 0, bytes: 0, ticks: 0 };
47256
+ a.builds.push((nowUs() - t0) / 1e3);
47257
+ a.ticks++;
47258
+ if (emitted) {
47259
+ a.frames++;
47260
+ a.bytes += bytes;
47261
+ }
47262
+ }
47263
+ var pct2 = (xs, p3) => {
47264
+ const s2 = [...xs].sort((a, b) => a - b);
47265
+ return s2[Math.min(s2.length - 1, Math.floor(s2.length * p3))] ?? 0;
47266
+ };
47267
+ function startLagMonitor() {
47268
+ if (!PROFILE || h) return;
47269
+ const mon = h = monitorEventLoopDelay({ resolution: 10 });
47270
+ mon.enable();
47271
+ setInterval(() => {
47272
+ const ms = (n2) => +(n2 / 1e6).toFixed(1);
47273
+ const rows = Object.entries(acc).map(([g, a]) => `${g}: build p95=${pct2(a.builds, 0.95).toFixed(1)}ms frames=${a.frames}/${a.ticks} bytes=${a.bytes}`);
47274
+ console.log(`[events-profile] loop p50=${ms(mon.percentile(50))} p99=${ms(mon.percentile(99))} max=${ms(mon.max)}ms | ${rows.join(" | ")}`);
47275
+ mon.reset();
47276
+ for (const k of Object.keys(acc)) delete acc[k];
47277
+ }, 1e4).unref();
47278
+ }
47279
+
46985
47280
  // src/services/events.ts
46986
47281
  var clients = /* @__PURE__ */ new Set();
46987
47282
  var cookieClients = /* @__PURE__ */ new WeakSet();
46988
47283
  var clientGroups = /* @__PURE__ */ new WeakMap();
46989
47284
  var tickMs = () => effectiveInt("HANOMAN_EVENTS_TICK_MS") ?? 1e3;
47285
+ var presenceKey = (m) => JSON.stringify({ ...m, devices: m.devices.map(
47286
+ (d) => d.local ? { ...d, lastSeenAt: null } : d
47287
+ ) });
46990
47288
  var GROUPS = [
46991
47289
  { everyTicks: 3, cookieOnly: true, last: "", build: async () => ({
46992
47290
  t: "models",
@@ -47002,7 +47300,16 @@ var GROUPS = [
47002
47300
  // loop — termasuk frame ketikan terminal — selama spawn (terukur sampai 916 ms saat mesin sibuk).
47003
47301
  sessions: (await listSessionsAsync()).map((s2) => isDeciding(s2.id) ? { ...s2, deciding: true } : s2)
47004
47302
  }) },
47005
- { everyTicks: 1, last: "", build: async () => ({ t: "specs", specs: await liveSpecs() }) },
47303
+ // SPEC-1267 · frame ringkas (`SpecSlim`: tanpa payload/objective/sourceHistory) yang hanya dibangun
47304
+ // saat `specsDigest` berubah. Kemajuan stage dari fase sesi (`liveOverlayTick`) tetap dijalankan
47305
+ // tiap tick: ia hidup di berkas/tmux, bukan di DB, jadi digest DB tak melihatnya sebelum ditulis.
47306
+ {
47307
+ everyTicks: 1,
47308
+ last: "",
47309
+ pre: liveOverlayTick,
47310
+ gate: specsDigest,
47311
+ build: async () => ({ t: "specs", specs: await listSpecsSlim() })
47312
+ },
47006
47313
  { everyTicks: 3, last: "", build: async () => ({ t: "notifications", ...await notificationsFeed() }) },
47007
47314
  // SPEC-742 · ADR-0116 · pembersihan worktree yang masih jalan. `listCleanups()` membaca peta di
47008
47315
  // memori, bukan disk — nol I/O per tick, dan dedup signature membuat frame lahir hanya saat
@@ -47028,7 +47335,15 @@ var GROUPS = [
47028
47335
  // dihitung). 3 dtk: presence berdenyut 30 dtk, jadi kadens lebih rapat hanya menambah build tanpa
47029
47336
  // menambah informasi. `presenceView` menyegarkan sesi mesin ini sendiri di dalamnya — satu
47030
47337
  // `tmux list-panes` asinkron, tak menahan event loop.
47031
- { everyTicks: 3, cookieOnly: true, last: "", build: async () => ({ t: "presence", ...await presenceView() }) },
47338
+ // SPEC-1267 · `lastSeenAt` device lokal = jam build ini, jadi berubah tiap build; kunci dedup
47339
+ // mengosongkannya supaya frame hanya lahir saat isi presence benar-benar berubah.
47340
+ {
47341
+ everyTicks: 3,
47342
+ cookieOnly: true,
47343
+ last: "",
47344
+ build: async () => ({ t: "presence", ...await presenceView() }),
47345
+ sig: presenceKey
47346
+ },
47032
47347
  // SPEC-961 · grup GLOBAL ke-11 · angka "butuh pengajuan" untuk badge sidebar. 5 dtk: badge bukan
47033
47348
  // board — yang dijanjikannya adalah "ada yang menunggu", bukan detik keberapa ia muncul — dan
47034
47349
  // dedup signature membuat frame lahir hanya saat salah satu angka berubah (jarang). Empat angka,
@@ -47037,8 +47352,7 @@ var GROUPS = [
47037
47352
  // id/judul, dan agent token sudah boleh membaca daftar sumbernya lewat capability masing-masing.
47038
47353
  { everyTicks: 5, last: "", build: async () => ({ t: "pending", counts: await pendingCounts() }) }
47039
47354
  ];
47040
- function broadcast2(msg, cookieOnly = false) {
47041
- const s2 = JSON.stringify(msg);
47355
+ function broadcast2(msg, s2, cookieOnly = false) {
47042
47356
  for (const c of clients) {
47043
47357
  if (cookieOnly && !cookieClients.has(c)) continue;
47044
47358
  const groups = clientGroups.get(c);
@@ -47126,31 +47440,54 @@ function subscribeClient(c, subs) {
47126
47440
  var tick2 = 0;
47127
47441
  var busy = false;
47128
47442
  var timer3;
47443
+ async function runGroup(g) {
47444
+ const t0 = profStart();
47445
+ let msg;
47446
+ try {
47447
+ await g.pre?.();
47448
+ let gate2;
47449
+ if (g.gate) {
47450
+ try {
47451
+ gate2 = await g.gate();
47452
+ } catch {
47453
+ gate2 = void 0;
47454
+ }
47455
+ if (gate2 !== void 0 && gate2 === g.lastGate) {
47456
+ profEnd("gate", t0, 0, false);
47457
+ return null;
47458
+ }
47459
+ }
47460
+ msg = await g.build();
47461
+ if (g.failing) {
47462
+ g.failing = false;
47463
+ console.log(`siar dashboard pulih: ${msg.t}`);
47464
+ }
47465
+ const json = JSON.stringify(msg);
47466
+ const key = g.sig ? g.sig(msg) : json;
47467
+ const emit = key !== g.lastSig;
47468
+ profEnd(msg.t, t0, json.length, emit);
47469
+ if (g.gate) g.lastGate = gate2;
47470
+ return emit ? { g, msg, json, key, gate: gate2 } : null;
47471
+ } catch (e) {
47472
+ if (!g.failing) {
47473
+ g.failing = true;
47474
+ console.error("siar dashboard gagal membangun frame:", e);
47475
+ }
47476
+ return null;
47477
+ }
47478
+ }
47129
47479
  async function __tick() {
47130
47480
  if (busy) return;
47131
47481
  busy = true;
47132
47482
  tick2++;
47133
47483
  try {
47134
- for (const g of GROUPS) {
47135
- if (tick2 % g.everyTicks !== 0) continue;
47136
- let msg;
47137
- try {
47138
- msg = await g.build();
47139
- } catch (e) {
47140
- if (!g.failing) {
47141
- g.failing = true;
47142
- console.error("siar dashboard gagal membangun frame:", e);
47143
- }
47144
- continue;
47145
- }
47146
- if (g.failing) {
47147
- g.failing = false;
47148
- console.log(`siar dashboard pulih: ${msg.t}`);
47149
- }
47150
- const sig = JSON.stringify(msg);
47151
- if (sig === g.last) continue;
47152
- g.last = sig;
47153
- broadcast2(msg, g.cookieOnly);
47484
+ const results = await Promise.all(GROUPS.filter((g) => tick2 % g.everyTicks === 0).map(runGroup));
47485
+ for (const r of results) {
47486
+ if (!r) continue;
47487
+ r.g.last = r.json;
47488
+ r.g.lastSig = r.key;
47489
+ r.g.lastT = r.msg.t;
47490
+ broadcast2(r.msg, r.json, r.g.cookieOnly);
47154
47491
  }
47155
47492
  for (const e of entries2.values()) {
47156
47493
  e.tick++;
@@ -47163,6 +47500,7 @@ async function __tick() {
47163
47500
  }
47164
47501
  function startLoop() {
47165
47502
  if (timer3) return;
47503
+ startLagMonitor();
47166
47504
  timer3 = setInterval(() => {
47167
47505
  void __tick();
47168
47506
  }, tickMs());
@@ -47174,9 +47512,13 @@ function stopLoop() {
47174
47512
  timer3 = void 0;
47175
47513
  }
47176
47514
  tick2 = 0;
47177
- for (const g of GROUPS) g.last = "";
47515
+ for (const g of GROUPS) {
47516
+ g.last = "";
47517
+ g.lastSig = void 0;
47518
+ g.lastGate = void 0;
47519
+ }
47178
47520
  }
47179
- async function attach2(c, o = {}) {
47521
+ async function attach(c, o = {}) {
47180
47522
  clients.add(c);
47181
47523
  if (o.maySubscribe !== false) cookieClients.add(c);
47182
47524
  if (o.groups) clientGroups.set(c, o.groups);
@@ -47187,17 +47529,20 @@ async function attach2(c, o = {}) {
47187
47529
  } catch {
47188
47530
  return;
47189
47531
  }
47190
- for (const g of GROUPS) {
47191
- if (g.cookieOnly && !cookieClients.has(c)) continue;
47192
- let msg;
47532
+ const wanted = GROUPS.filter((g) => !(g.cookieOnly && !cookieClients.has(c)));
47533
+ const frames = await Promise.all(wanted.map(async (g) => {
47534
+ if (g.last && g.lastT) return { t: g.lastT, json: g.last };
47193
47535
  try {
47194
- msg = await g.build();
47536
+ const msg = await g.build();
47537
+ return { t: msg.t, json: JSON.stringify(msg) };
47195
47538
  } catch {
47196
- continue;
47539
+ return null;
47197
47540
  }
47198
- if (o.groups && !o.groups.has(msg.t)) continue;
47541
+ }));
47542
+ for (const f of frames) {
47543
+ if (!f || o.groups && !o.groups.has(f.t)) continue;
47199
47544
  try {
47200
- c.send(JSON.stringify(msg));
47545
+ c.send(f.json);
47201
47546
  } catch {
47202
47547
  return;
47203
47548
  }
@@ -47231,7 +47576,7 @@ async function events_default(app3, opts) {
47231
47576
  const client = { send: (m) => socket2.send(m), close: () => socket2.close() };
47232
47577
  if (req.remote) {
47233
47578
  const ideRead = req.remote.capabilities.includes("ide:read");
47234
- void attach2(client, { maySubscribe: false, groups: remoteEventGroups(ideRead) });
47579
+ void attach(client, { maySubscribe: false, groups: remoteEventGroups(ideRead) });
47235
47580
  socket2.on("close", () => detach2(client));
47236
47581
  return;
47237
47582
  }
@@ -47244,7 +47589,7 @@ async function events_default(app3, opts) {
47244
47589
  return;
47245
47590
  }
47246
47591
  const maySubscribe = canSubscribeTopics(principal);
47247
- void attach2(client, { maySubscribe });
47592
+ void attach(client, { maySubscribe });
47248
47593
  const guard = new WsMessageGuard({ perWindow: SUB_FRAMES_PER_MINUTE });
47249
47594
  socket2.on("message", (raw) => {
47250
47595
  const verdict = guard.accept(raw);
@@ -47354,8 +47699,8 @@ import { join as join36 } from "node:path";
47354
47699
 
47355
47700
  // src/services/device-auth.ts
47356
47701
  function bearerToken2(req) {
47357
- const h = req.headers["authorization"];
47358
- return typeof h === "string" && h.startsWith("Bearer ") ? h.slice(7) : void 0;
47702
+ const h2 = req.headers["authorization"];
47703
+ return typeof h2 === "string" && h2.startsWith("Bearer ") ? h2.slice(7) : void 0;
47359
47704
  }
47360
47705
  async function requireDeviceToken(req, reply) {
47361
47706
  const token = bearerToken2(req);
@@ -47790,7 +48135,7 @@ async function session_history_default(app3) {
47790
48135
  init_src();
47791
48136
  init_session_event_token();
47792
48137
  init_pty();
47793
- var bearer = (h) => /^Bearer (.+)$/.exec(h ?? "")?.[1] ?? "";
48138
+ var bearer = (h2) => /^Bearer (.+)$/.exec(h2 ?? "")?.[1] ?? "";
47794
48139
  var recordOf = (value) => value && typeof value === "object" ? value : null;
47795
48140
  var boundedString = (value, max) => typeof value === "string" && value.length > 0 && value.length <= max ? value : void 0;
47796
48141
  async function session_events_default(app3) {
@@ -47938,9 +48283,9 @@ init_db();
47938
48283
  init_db();
47939
48284
  init_uploads();
47940
48285
  init_src();
47941
- import { createHash as createHash13, randomBytes as randomBytes7 } from "node:crypto";
48286
+ import { createHash as createHash14, randomBytes as randomBytes7 } from "node:crypto";
47942
48287
  function hashAccessKey(key) {
47943
- return createHash13("sha256").update(key).digest("hex");
48288
+ return createHash14("sha256").update(key).digest("hex");
47944
48289
  }
47945
48290
  function generateAccessKey() {
47946
48291
  const key = "hnm_tkt_" + randomBytes7(24).toString("hex");
@@ -50842,8 +51187,8 @@ function systemPromptFor(type, nonce) {
50842
51187
  ${type === "brainstorm" ? BRAINSTORM : TANYA}`;
50843
51188
  }
50844
51189
  function renderTurnPrompt(o) {
50845
- const riwayat = o.history.map((h) => h.role === "klien" ? wrapClientMessage(h.text, o.nonce) : `<jawaban-hanoman>
50846
- ${h.text}
51190
+ const riwayat = o.history.map((h2) => h2.role === "klien" ? wrapClientMessage(h2.text, o.nonce) : `<jawaban-hanoman>
51191
+ ${h2.text}
50847
51192
  </jawaban-hanoman>`).join("\n\n");
50848
51193
  const baru = wrapClientMessage(o.message, o.nonce);
50849
51194
  return riwayat ? `Percakapan sejauh ini:
@@ -51463,8 +51808,8 @@ init_src();
51463
51808
  init_db();
51464
51809
 
51465
51810
  // src/services/bootstrap.ts
51466
- import { createHash as createHash14, randomBytes as randomBytes11, timingSafeEqual as timingSafeEqual5 } from "node:crypto";
51467
- import { chmod, lstat as lstat3, mkdir as mkdir10, readFile as readFile7, unlink as unlink5, writeFile as writeFile7 } from "node:fs/promises";
51811
+ import { createHash as createHash15, randomBytes as randomBytes11, timingSafeEqual as timingSafeEqual5 } from "node:crypto";
51812
+ import { chmod, lstat as lstat3, mkdir as mkdir10, readFile as readFile8, unlink as unlink5, writeFile as writeFile7 } from "node:fs/promises";
51468
51813
  import { join as join39 } from "node:path";
51469
51814
  var TTL_MS5 = 15 * 6e4;
51470
51815
  var SETUP_TOKEN_FILE = "setup.token";
@@ -51476,7 +51821,7 @@ async function readStored(home3) {
51476
51821
  try {
51477
51822
  const info = await lstat3(tokenPath(home3));
51478
51823
  if (info.isSymbolicLink() || !info.isFile()) throw new BootstrapError("invalid setup proof");
51479
- const [token, expires] = (await readFile7(tokenPath(home3), "utf8")).trim().split("\n");
51824
+ const [token, expires] = (await readFile8(tokenPath(home3), "utf8")).trim().split("\n");
51480
51825
  const expiresAt = Date.parse(expires ?? "");
51481
51826
  if (!token || !Number.isFinite(expiresAt)) throw new BootstrapError("invalid setup proof");
51482
51827
  return { token, expiresAt };
@@ -51516,8 +51861,8 @@ ${new Date(expiresAt).toISOString()}
51516
51861
  }
51517
51862
  async function verifySetupToken(candidate, home3, now = Date.now()) {
51518
51863
  const stored = await readStored(home3);
51519
- const got = createHash14("sha256").update(candidate).digest();
51520
- const want = createHash14("sha256").update(stored.token).digest();
51864
+ const got = createHash15("sha256").update(candidate).digest();
51865
+ const want = createHash15("sha256").update(stored.token).digest();
51521
51866
  if (stored.expiresAt <= now || !timingSafeEqual5(got, want)) throw new BootstrapError("invalid setup proof");
51522
51867
  }
51523
51868
  async function consumeSetupToken(home3) {
@@ -52012,7 +52357,7 @@ init_db();
52012
52357
  init_event_log();
52013
52358
  init_transcript_store();
52014
52359
  init_src2();
52015
- import { readFile as readFile8 } from "node:fs/promises";
52360
+ import { readFile as readFile9 } from "node:fs/promises";
52016
52361
  import { join as join40 } from "node:path";
52017
52362
  function decodeCursor(c) {
52018
52363
  if (!c) return null;
@@ -52053,7 +52398,7 @@ async function readRemoteTranscript(id2) {
52053
52398
  if (row.deviceId === "local") return readTranscript(row.transcriptKey);
52054
52399
  const path = join40(resolveDataDirs().home, "remote-transcripts", row.transcriptKey);
52055
52400
  try {
52056
- return await readFile8(path, "utf8");
52401
+ return await readFile9(path, "utf8");
52057
52402
  } catch {
52058
52403
  return null;
52059
52404
  }
@@ -52093,8 +52438,8 @@ async function logs_default(app3) {
52093
52438
  init_agent_token();
52094
52439
  init_settings3();
52095
52440
  function agentTokenFromReq(req) {
52096
- const h = req.headers["authorization"];
52097
- if (typeof h === "string" && h.startsWith("Bearer ")) return h.slice(7);
52441
+ const h2 = req.headers["authorization"];
52442
+ if (typeof h2 === "string" && h2.startsWith("Bearer ")) return h2.slice(7);
52098
52443
  return void 0;
52099
52444
  }
52100
52445
  async function authenticateAgent(token) {
@@ -52415,11 +52760,11 @@ async function auditSweep(now = Date.now) {
52415
52760
  var timers = [];
52416
52761
  function startVpsMonitor() {
52417
52762
  if (timers.length) return;
52418
- const h = setInterval(() => void healthSweep(), HEALTH_MS);
52763
+ const h2 = setInterval(() => void healthSweep(), HEALTH_MS);
52419
52764
  const a = setInterval(() => void auditSweep(), AUDIT_MS);
52420
- h.unref();
52765
+ h2.unref();
52421
52766
  a.unref();
52422
- timers = [h, a];
52767
+ timers = [h2, a];
52423
52768
  void healthSweep();
52424
52769
  void auditSweep();
52425
52770
  }
@@ -52589,10 +52934,10 @@ async function reconcile(deps) {
52589
52934
  await markFailed(item.id, "spec hilang");
52590
52935
  continue;
52591
52936
  }
52592
- const p3 = deps.pane(item.sessionId ?? "");
52937
+ const p3 = await deps.pane(item.sessionId ?? "");
52593
52938
  let stage = spec.stage;
52594
52939
  if (p3?.flow && p3.phaseFile) {
52595
- const d = deps.deriveStage(p3.phaseFile, p3.flow, p3.cwd, item.specId);
52940
+ const d = await deps.deriveStage(p3.phaseFile, p3.flow, p3.cwd, item.specId);
52596
52941
  if (d && STAGES.indexOf(d) > STAGES.indexOf(stage)) {
52597
52942
  const { count } = await prisma.spec.updateMany({ where: { id: item.specId, stage }, data: { stage: d } });
52598
52943
  if (count > 0) await notifySynced("spec", item.specId).catch(() => {
@@ -52625,11 +52970,11 @@ async function reconcile(deps) {
52625
52970
  }
52626
52971
  }
52627
52972
  var reconcileProdDeps = {
52628
- pane: (sessionId2) => {
52629
- const s2 = getSession(sessionId2);
52973
+ pane: async (sessionId2) => {
52974
+ const s2 = await getSessionAsync(sessionId2);
52630
52975
  return s2 ? { exited: s2.exited, flow: s2.flow, phaseFile: s2.phaseFile, cwd: s2.cwd } : void 0;
52631
52976
  },
52632
- deriveStage: (phaseFile, flow2, cwd, specId) => stageForRun(readPhases(phaseFile, flow2), cwd, specId),
52977
+ deriveStage: async (phaseFile, flow2, cwd, specId) => stageForRunAsync(await readPhasesAsync(phaseFile, flow2), cwd, specId),
52633
52978
  headSha: (wt) => {
52634
52979
  try {
52635
52980
  return realGit.headSha(wt);
@@ -53234,13 +53579,13 @@ async function pruneLogs(now, retention, opts = {}) {
53234
53579
  return { logsPruned, logBytesFreed };
53235
53580
  }
53236
53581
  async function reconcileRemoteTranscripts() {
53237
- const { readdir: readdir9, unlink: unlink6, stat: stat5 } = await import("node:fs/promises");
53582
+ const { readdir: readdir10, unlink: unlink6, stat: stat7 } = await import("node:fs/promises");
53238
53583
  const { join: join41 } = await import("node:path");
53239
53584
  const { resolveDataDirs: resolveDataDirs2 } = await Promise.resolve().then(() => (init_src2(), src_exports));
53240
53585
  const root = join41(resolveDataDirs2().home, "remote-transcripts");
53241
53586
  let deviceDirs;
53242
53587
  try {
53243
- deviceDirs = await readdir9(root);
53588
+ deviceDirs = await readdir10(root);
53244
53589
  } catch {
53245
53590
  return { orphans: 0 };
53246
53591
  }
@@ -53251,14 +53596,14 @@ async function reconcileRemoteTranscripts() {
53251
53596
  for (const dev of deviceDirs) {
53252
53597
  let files;
53253
53598
  try {
53254
- files = await readdir9(join41(root, dev));
53599
+ files = await readdir10(join41(root, dev));
53255
53600
  } catch {
53256
53601
  continue;
53257
53602
  }
53258
53603
  for (const f of files) {
53259
53604
  const key = `${dev}/${f}`;
53260
53605
  if (referenced.has(key)) continue;
53261
- const st = await stat5(join41(root, dev, f)).catch(() => null);
53606
+ const st = await stat7(join41(root, dev, f)).catch(() => null);
53262
53607
  if (!st || Date.now() - st.mtimeMs < gracePeriodMs) continue;
53263
53608
  await unlink6(join41(root, dev, f)).catch(() => {
53264
53609
  });