hanoman 0.7.1 → 0.9.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
  }
@@ -6787,7 +6792,7 @@ function subKey(topic, params) {
6787
6792
  for (const k of Object.keys(params).sort()) if (params[k] !== void 0) canon[k] = params[k];
6788
6793
  return `${topic}|${JSON.stringify(canon)}`;
6789
6794
  }
6790
- var zIssueDeviceToken, zSessionResult, zSessionHistory, zCreateProject, zProjectId, zRenameProject, zUpdateProject, zCreateSpec, zPatchSpec, zChangeSpecSource, zMarkSpecDone, zIntegrate, zSessionSummary, zProjectView, zSchedulerQueueItem, zSchedulerSourceView, zSchedulerSessionView, zSchedulerQueueCounts, zSchedulerState, zLeadDecisionView, zLeadFlowView, zLeadAnswer, zLeadProjectStatus, zLeadStatusView, zFlow, isGoalShapedFlow, zPrdBrief, zPrdDoc, zBreakdownItem, zBreakdownDoc, zBatchCreateSpec, zEscalationTarget, zEscalationPrefill, zAuditEscalation, zAuditEscalationView, zTerminalSession, zDocFileContent, zDocIndexCat, zDocIndex, HOST_RE, USER_RE, zCreateVps, zPatchVps, USER_ROLES, zUserRole, zLogin, zSignup, zSetup, zSetupApply, zChangePassword, zCreateClientAccount, zUpdateClientAccount, zVpsCheck, zMarkNa, zMarkNaBulk, zAttest, zRemediate, PROVISION_PROFILES, COMPONENT_IDS, zProvision, UPDATE_RESTART_EXIT, CONFIG_RESTART_EXIT, zUpdateApplyBody, MAX_SUBS, zSubPage, zSubLimit, zTopicParams, zEventsClientMsg, zTicketView, zTicketAttachmentView, zSpecAttachmentView, zTicketDetail, zTicketEditInput, zHelpInfo, zPublicTicketStatus, zCronExpr, zCreateCron, zPatchCron, zSchedulerCron, zSchedulerCronRun;
6795
+ var zIssueDeviceToken, zSessionResult, zSessionHistory, zCreateProject, zProjectId, zRenameProject, zUpdateProject, zCreateSpec, zPatchSpec, zChangeSpecSource, zMarkSpecDone, zIntegrate, zSessionSummary, zProjectView, zSchedulerQueueItem, zSchedulerSourceView, zSchedulerSessionView, zSchedulerQueueCounts, zSchedulerState, zLeadDecisionView, zLeadFlowView, zLeadAnswer, zLeadProjectStatus, zLeadStatusView, zFlow, isGoalShapedFlow, zPrdBrief, zPrdDoc, zBreakdownItem, zBreakdownDoc, zBatchCreateSpec, zEscalationTarget, zEscalationPrefill, zAuditEscalation, zAuditEscalationView, zTerminalSession, zDocFileContent, zDocIndexCat, zDocIndex, HOST_RE, USER_RE, zCreateVps, zPatchVps, USER_ROLES, zUserRole, zLogin, zSignup, zSetup, zSetupApply, zChangePassword, zCreateClientAccount, zUpdateClientAccount, zVpsCheck, zMarkNa, zMarkNaBulk, zAttest, zRemediate, PROVISION_PROFILES, COMPONENT_IDS, zProvision, UPDATE_RESTART_EXIT, CONFIG_RESTART_EXIT, MANUAL_RESTART_EXIT, zUpdateApplyBody, MAX_SUBS, zSubPage, zSubLimit, zTopicParams, zEventsClientMsg, zTicketView, zTicketAttachmentView, zSpecAttachmentView, zTicketDetail, zTicketEditInput, zHelpInfo, zPublicTicketStatus, zCronExpr, zCreateCron, zPatchCron, zSchedulerCron, zSchedulerCronRun;
6791
6796
  var init_dto = __esm({
6792
6797
  "../shared/src/dto.ts"() {
6793
6798
  "use strict";
@@ -7309,6 +7314,7 @@ var init_dto = __esm({
7309
7314
  });
7310
7315
  UPDATE_RESTART_EXIT = 75;
7311
7316
  CONFIG_RESTART_EXIT = 76;
7317
+ MANUAL_RESTART_EXIT = 77;
7312
7318
  zUpdateApplyBody = external_exports.object({ confirm: external_exports.boolean().optional() });
7313
7319
  MAX_SUBS = 16;
7314
7320
  zSubPage = external_exports.number().int().min(1).max(1e4);
@@ -7760,8 +7766,8 @@ function coverageOf(docs) {
7760
7766
  const linked = [...byCat.values()].filter(Boolean).length;
7761
7767
  return Math.round(linked / byCat.size * 100);
7762
7768
  }
7763
- function docStatusFor(pct2) {
7764
- return pct2 >= 90 ? "ok" : pct2 >= 60 ? "drift" : "broken";
7769
+ function docStatusFor(pct3) {
7770
+ return pct3 >= 90 ? "ok" : pct3 >= 60 ? "drift" : "broken";
7765
7771
  }
7766
7772
  function isExternalLink(target3) {
7767
7773
  return !target3 || /^(https?:|#|mailto:)/.test(target3);
@@ -9252,17 +9258,12 @@ var init_backlog = __esm({
9252
9258
  inputSchema: obj({ properties: { spec: str(ID_HINT) }, required: ["spec"] }),
9253
9259
  mode: "read",
9254
9260
  capability: "backlog:read",
9255
- samplePath: "/specs",
9261
+ samplePath: "/specs/SPEC-1",
9256
9262
  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
- }
9263
+ // SPEC-1267 · `GET /specs` tak lagi membawa `payload`/`sourceHistory`, jadi isi penuh HANYA ada di
9264
+ // `GET /specs/:id`. Pencocokan id persis kini urusan server (404 bila tak ada).
9265
+ build: (a) => ({ method: "GET", path: `/specs/${enc(String(a.spec).trim())}` }),
9266
+ shape: (raw) => shapeSpecDetail(raw)
9266
9267
  },
9267
9268
  {
9268
9269
  name: "hanoman_backlog_docs_list",
@@ -13042,8 +13043,8 @@ function guideLine(guide, phase) {
13042
13043
  }
13043
13044
  function phaseSkillsFor(flow2, phase, method) {
13044
13045
  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];
13046
+ const phases2 = PIPELINES[flow2];
13047
+ return writesCode(flow2) && phase === phases2[phases2.length - 1] ? [.../* @__PURE__ */ new Set([...own, ...method.exitSkills])] : [...own];
13047
13048
  }
13048
13049
  function orchestratorClause(plan, o = {}) {
13049
13050
  const codex2 = plan.runtime === "codex";
@@ -13320,15 +13321,15 @@ var init_prompt = __esm({
13320
13321
  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
13322
  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
13323
  autonomyClause = (mode) => mode === "full-control" ? AUTONOMY_CLAUSE_FULL : AUTONOMY_CLAUSE;
13323
- phaseInstruction = (phases, method) => {
13324
- const base2 = `Kerjakan fase berurutan: ${phases.join(" \u2192 ")}.
13324
+ phaseInstruction = (phases2, method) => {
13325
+ const base2 = `Kerjakan fase berurutan: ${phases2.join(" \u2192 ")}.
13325
13326
  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;
13327
+ if (!phases2.includes("Plan") || !phases2.includes("Execute")) return base2;
13327
13328
  return base2 + `
13328
13329
  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
13330
  };
13330
- skillInstruction = (flow2, phases, method) => {
13331
- const lines2 = phases.map((p3) => {
13331
+ skillInstruction = (flow2, phases2, method) => {
13332
+ const lines2 = phases2.map((p3) => {
13332
13333
  const skills = phaseSkillsFor(flow2, p3, method);
13333
13334
  return skills.length ? `- ${p3}: ${skills.join(", ")}` : "";
13334
13335
  }).filter(Boolean);
@@ -13645,6 +13646,20 @@ var init_settings2 = __esm({
13645
13646
  }];
13646
13647
  hooks2.SubagentStart = [{ hooks: [{ type: "command", command: EVENT_HOOK_COMMAND }] }];
13647
13648
  hooks2.SubagentStop = [{ hooks: [{ type: "command", command: EVENT_HOOK_COMMAND }] }];
13649
+ if (decisionFile) {
13650
+ const sub = `'${decisionFile.split("'").join("'\\''")}.sub'`;
13651
+ const f = `'${decisionFile.split("'").join("'\\''")}'`;
13652
+ hooks2.SubagentStart.push({ hooks: [{ type: "command", command: `echo 1 >> ${sub}; exit 0` }] });
13653
+ hooks2.SubagentStop.push({ hooks: [{
13654
+ type: "command",
13655
+ command: `[ -s ${sub} ] && { sed '$d' ${sub} > ${sub}.t; mv ${sub}.t ${sub}; }; exit 0`
13656
+ }] });
13657
+ hooks2.Notification = [{ hooks: [{
13658
+ type: "command",
13659
+ command: `m=$(cat); if [ -s ${sub} ]; then echo "$m" | grep -qiE 'permission|needs.?input'; else echo "$m" | grep -qiE 'idle|permission|waiting for|needs.?input'; fi && { [ -s ${f} ] || date +%s > ${f}; } || true`
13660
+ }] }];
13661
+ hooks2.UserPromptSubmit = [{ hooks: [{ type: "command", command: `: > ${f}; : > ${sub}` }] }];
13662
+ }
13648
13663
  }
13649
13664
  if (goal) hooks2.Stop = [{ hooks: [{ type: "prompt", prompt: goal }] }];
13650
13665
  return {
@@ -13671,10 +13686,10 @@ function goalFlowCondition(flow2, specId, branchTo, spec) {
13671
13686
  }
13672
13687
  function defaultGoalCondition({ flow: flow2, specId, branchTo, spec }) {
13673
13688
  if (isGoalShapedFlow(flow2)) return goalFlowCondition(flow2, specId, branchTo, spec);
13674
- const phases = PIPELINES[flow2];
13675
- const planGate = phases.includes("Plan") && phases.includes("Execute");
13689
+ const phases2 = PIPELINES[flow2];
13690
+ const planGate = phases2.includes("Plan") && phases2.includes("Execute");
13676
13691
  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\`;`
13692
+ `1. output \`cat "$HANOMAN_PHASE_FILE"\` yang memuat satu baris untuk SETIAP fase ${phases2.join(" \u2192 ")}, masing-masing berakhiran \`done\` atau \`skipped\`;`
13678
13693
  ];
13679
13694
  if (planGate) {
13680
13695
  const dirs = PLAN_DIRS.map((d) => `${d}/`).join(" ");
@@ -13734,8 +13749,8 @@ function codexHookArgs(o) {
13734
13749
  return args;
13735
13750
  }
13736
13751
  function codexGoalScript(o) {
13737
- const phases = PIPELINES[o.flow];
13738
- const planGate = phases.includes("Plan") && phases.includes("Execute");
13752
+ const phases2 = PIPELINES[o.flow];
13753
+ const planGate = phases2.includes("Plan") && phases2.includes("Execute");
13739
13754
  const max = o.maxBlocks ?? GOAL_MAX_BLOCKS;
13740
13755
  const lines2 = [
13741
13756
  "#!/bin/sh",
@@ -13745,7 +13760,7 @@ function codexGoalScript(o) {
13745
13760
  `ST=${shq(o.stateFile)}`,
13746
13761
  "missing=''"
13747
13762
  ];
13748
- for (const p3 of phases) {
13763
+ for (const p3 of phases2) {
13749
13764
  lines2.push(
13750
13765
  `grep -qE ${shq(`^${p3} (done|skipped)[[:space:]]*$`)} "$PF" 2>/dev/null || missing="$missing\\n- fase ${p3} belum tercatat di \\$HANOMAN_PHASE_FILE"`
13751
13766
  );
@@ -14494,8 +14509,8 @@ var init_custom_agent_eval_evidence = __esm({
14494
14509
  };
14495
14510
  textFile = (root, path) => {
14496
14511
  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");
14512
+ const stat7 = lstatSync(target3);
14513
+ if (!stat7.isFile() || stat7.size > 1024 * 1024) throw new Error("Evidence must be a bounded regular file");
14499
14514
  return readFileSync2(target3, "utf8");
14500
14515
  };
14501
14516
  snapshotEvalEvidence = (context) => {
@@ -14867,9 +14882,9 @@ var init_custom_agent_eval = __esm({
14867
14882
  hash4.update(`missing:${rel}\0`);
14868
14883
  continue;
14869
14884
  }
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));
14885
+ const stat7 = lstatSync2(path);
14886
+ if (stat7.isSymbolicLink()) hash4.update(`l:${rel}\0${readlinkSync(path)}\0`);
14887
+ else hash4.update(`f:${stat7.mode}:${rel}\0`).update(readFileSync3(path));
14873
14888
  }
14874
14889
  return hash4.digest("hex");
14875
14890
  };
@@ -15022,8 +15037,8 @@ ${result.error.message}` : result.stderr ?? ""
15022
15037
  for (const name2 of readdirSync2(eventDir).filter((entry) => entry.endsWith(".json")).sort()) {
15023
15038
  try {
15024
15039
  const path = evidencePath(eventDir, name2);
15025
- const stat5 = lstatSync2(path);
15026
- if (!stat5.isFile() || stat5.size > 1024 * 1024) continue;
15040
+ const stat7 = lstatSync2(path);
15041
+ if (!stat7.isFile() || stat7.size > 1024 * 1024) continue;
15027
15042
  const parsed = JSON.parse(readFileSync3(path, "utf8"));
15028
15043
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
15029
15044
  events.push(parsed);
@@ -15986,14 +16001,9 @@ var init_stage_machine = __esm({
15986
16001
 
15987
16002
  // src/services/session-phases.ts
15988
16003
  import { readFileSync as readFileSync6, readdirSync as readdirSync5 } from "node:fs";
15989
- function recorded(file) {
16004
+ import { readFile, readdir, stat } from "node:fs/promises";
16005
+ function parseRecorded(raw) {
15990
16006
  const out4 = /* @__PURE__ */ new Map();
15991
- let raw;
15992
- try {
15993
- raw = readFileSync6(file, "utf8");
15994
- } catch {
15995
- return out4;
15996
- }
15997
16007
  for (const line of raw.split("\n")) {
15998
16008
  const trimmed = line.trimEnd();
15999
16009
  const i = trimmed.lastIndexOf(" ");
@@ -16004,19 +16014,40 @@ function recorded(file) {
16004
16014
  }
16005
16015
  return out4;
16006
16016
  }
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
- });
16017
+ function recorded(file) {
16018
+ let raw;
16019
+ try {
16020
+ raw = readFileSync6(file, "utf8");
16021
+ } catch {
16022
+ return /* @__PURE__ */ new Map();
16023
+ }
16024
+ return parseRecorded(raw);
16017
16025
  }
16018
- function enrichPhases(phases, roster, invocations, doneSeenAt, now, bornAt2, doneAtBirth = /* @__PURE__ */ new Set()) {
16019
- return phases.map((p3) => {
16026
+ async function recordedAsync(file) {
16027
+ let st;
16028
+ try {
16029
+ st = await stat(file);
16030
+ } catch {
16031
+ phaseFileMemo.delete(file);
16032
+ return /* @__PURE__ */ new Map();
16033
+ }
16034
+ const hit = phaseFileMemo.get(file);
16035
+ if (hit && hit.mtimeMs === st.mtimeMs && hit.size === st.size) return hit.seen;
16036
+ let raw;
16037
+ try {
16038
+ raw = await readFile(file, "utf8");
16039
+ } catch {
16040
+ return /* @__PURE__ */ new Map();
16041
+ }
16042
+ const seen2 = parseRecorded(raw);
16043
+ phaseFileMemo.set(file, { mtimeMs: st.mtimeMs, size: st.size, seen: seen2 });
16044
+ return seen2;
16045
+ }
16046
+ async function readPhasesAsync(file, flow2) {
16047
+ return derivePhases(await recordedAsync(file), flow2);
16048
+ }
16049
+ function enrichPhases(phases2, roster, invocations, doneSeenAt, now, bornAt2, doneAtBirth = /* @__PURE__ */ new Set()) {
16050
+ return phases2.map((p3) => {
16020
16051
  const r = roster.find((entry) => entry.phase === p3.name);
16021
16052
  if (!r) return p3;
16022
16053
  const all = invocations.filter((i) => i.phase === p3.name);
@@ -16046,23 +16077,23 @@ function enrichPhases(phases, roster, invocations, doneSeenAt, now, bornAt2, don
16046
16077
  };
16047
16078
  });
16048
16079
  }
16049
- function trackDoneSeen(phases, doneSeenAt, now) {
16050
- const byName = new Map(phases.map((p3) => [p3.name, p3.state]));
16080
+ function trackDoneSeen(phases2, doneSeenAt, now) {
16081
+ const byName = new Map(phases2.map((p3) => [p3.name, p3.state]));
16051
16082
  for (const name2 of [...doneSeenAt.keys()])
16052
16083
  if (byName.get(name2) !== "done") doneSeenAt.delete(name2);
16053
- for (const p3 of phases)
16084
+ for (const p3 of phases2)
16054
16085
  if (p3.state === "done" && !doneSeenAt.has(p3.name)) doneSeenAt.set(p3.name, now);
16055
16086
  }
16056
- function stageFor(phases) {
16087
+ function stageFor(phases2) {
16057
16088
  let best = -1;
16058
- for (const p3 of phases) {
16089
+ for (const p3 of phases2) {
16059
16090
  if (WORK_PHASES.includes(p3.name) && p3.state === "active")
16060
16091
  best = Math.max(best, STAGES.indexOf("executing"));
16061
16092
  if (p3.state !== "done" && p3.state !== "skipped") continue;
16062
16093
  const s2 = REACHED[p3.name];
16063
16094
  if (s2) best = Math.max(best, STAGES.indexOf(s2));
16064
16095
  }
16065
- if (phases[0]?.state === "active") best = Math.max(best, STAGES.indexOf("brainstorming"));
16096
+ if (phases2[0]?.state === "active") best = Math.max(best, STAGES.indexOf("brainstorming"));
16066
16097
  return best < 0 ? null : STAGES[best];
16067
16098
  }
16068
16099
  function planComplete(worktree, specId) {
@@ -16085,16 +16116,45 @@ function planComplete(worktree, specId) {
16085
16116
  }
16086
16117
  return true;
16087
16118
  }
16088
- function sessionComplete(phases, worktree, specId) {
16089
- if (!phasesComplete(phases)) return false;
16119
+ async function planCompleteAsync(worktree, specId) {
16120
+ const re = new RegExp(`(^|[^a-z0-9])${specId.toLowerCase()}([^0-9]|$)`);
16121
+ for (const rel of PLAN_DIRS) {
16122
+ const dir2 = `${worktree}/${rel}`;
16123
+ let names;
16124
+ try {
16125
+ names = await readdir(dir2);
16126
+ } catch {
16127
+ continue;
16128
+ }
16129
+ for (const n2 of names) {
16130
+ if (!re.test(n2.toLowerCase())) continue;
16131
+ try {
16132
+ if (/^[ \t]*- \[ \]/m.test(await readFile(`${dir2}/${n2}`, "utf8"))) return false;
16133
+ } catch {
16134
+ }
16135
+ }
16136
+ }
16137
+ return true;
16138
+ }
16139
+ function sessionComplete(phases2, worktree, specId) {
16140
+ if (!phasesComplete(phases2)) return false;
16090
16141
  return specId ? planComplete(worktree, specId) : true;
16091
16142
  }
16092
- function stageForRun(phases, worktree, specId) {
16093
- const s2 = stageFor(phases);
16143
+ function stageForRun(phases2, worktree, specId) {
16144
+ const s2 = stageFor(phases2);
16094
16145
  if (s2 === "done" && !planComplete(worktree, specId)) return "executing";
16095
16146
  return s2;
16096
16147
  }
16097
- var phaseFilePath, decisionFilePath, REACHED, phasesComplete;
16148
+ async function stageForRunAsync(phases2, worktree, specId) {
16149
+ const s2 = stageFor(phases2);
16150
+ if (s2 === "done" && !await planCompleteAsync(worktree, specId)) return "executing";
16151
+ return s2;
16152
+ }
16153
+ async function sessionCompleteAsync(phases2, worktree, specId) {
16154
+ if (!phasesComplete(phases2)) return false;
16155
+ return specId ? planCompleteAsync(worktree, specId) : true;
16156
+ }
16157
+ var phaseFilePath, decisionFilePath, phaseFileMemo, derivePhases, readPhases, REACHED, phasesComplete;
16098
16158
  var init_session_phases = __esm({
16099
16159
  "src/services/session-phases.ts"() {
16100
16160
  "use strict";
@@ -16103,6 +16163,18 @@ var init_session_phases = __esm({
16103
16163
  init_stage_machine();
16104
16164
  phaseFilePath = (repoDir, sessionId2) => `${repoDir}/.worktrees/.phases/${sessionId2}`;
16105
16165
  decisionFilePath = (repoDir, sessionId2) => `${repoDir}/.worktrees/.decisions/${sessionId2}`;
16166
+ phaseFileMemo = /* @__PURE__ */ new Map();
16167
+ derivePhases = (seen2, flow2) => {
16168
+ let activeTaken = false;
16169
+ return PIPELINES[flow2].map((name2) => {
16170
+ const state = seen2.get(name2);
16171
+ if (state) return { name: name2, state };
16172
+ if (activeTaken) return { name: name2, state: "pending" };
16173
+ activeTaken = true;
16174
+ return { name: name2, state: "active" };
16175
+ });
16176
+ };
16177
+ readPhases = (file, flow2) => derivePhases(recorded(file), flow2);
16106
16178
  REACHED = {
16107
16179
  Objective: "objective",
16108
16180
  Audit: "objective",
@@ -16118,7 +16190,7 @@ var init_session_phases = __esm({
16118
16190
  // mencapai `done` — tak ada fase verifikasi untuk menutupnya.
16119
16191
  Kerjakan: "done"
16120
16192
  };
16121
- phasesComplete = (phases) => phases.length > 0 && phases.every((p3) => p3.state === "done" || p3.state === "skipped");
16193
+ phasesComplete = (phases2) => phases2.length > 0 && phases2.every((p3) => p3.state === "done" || p3.state === "skipped");
16122
16194
  }
16123
16195
  });
16124
16196
 
@@ -16467,7 +16539,7 @@ var init_safe_outbound_request = __esm({
16467
16539
 
16468
16540
  // src/services/uploads.ts
16469
16541
  import { randomUUID } from "node:crypto";
16470
- import { mkdir, writeFile, readFile, unlink, rm } from "node:fs/promises";
16542
+ import { mkdir, writeFile, readFile as readFile2, unlink, rm } from "node:fs/promises";
16471
16543
  import { join as join11, resolve as resolve8 } from "node:path";
16472
16544
  function extFor(mimeType) {
16473
16545
  return EXT[mimeType] ?? ".bin";
@@ -16498,13 +16570,13 @@ async function dropSessionUploads(sessionId2) {
16498
16570
  }
16499
16571
  async function readUpload(storageKey) {
16500
16572
  const safe = storageKey.replace(/[/\\]/g, "");
16501
- return readFile(join11(uploadDir(), safe));
16573
+ return readFile2(join11(uploadDir(), safe));
16502
16574
  }
16503
16575
  async function readUploadOrFetch(storageKey) {
16504
16576
  const safe = storageKey.replace(/[/\\]/g, "");
16505
16577
  const target3 = join11(uploadDir(), safe);
16506
16578
  try {
16507
- return await readFile(target3);
16579
+ return await readFile2(target3);
16508
16580
  } catch {
16509
16581
  const base2 = effectiveStr("SYNC_SERVER_URL");
16510
16582
  const token = effectiveStr("SYNC_DEVICE_TOKEN");
@@ -16976,22 +17048,22 @@ import { execFile, execFileSync as execFileSync2 } from "node:child_process";
16976
17048
  import { createRequire } from "node:module";
16977
17049
  import { randomUUID as randomUUID2 } from "node:crypto";
16978
17050
  import { chmodSync as chmodSync6, mkdirSync as mkdirSync7, readFileSync as readFileSync8, rmSync as rmSync4, statSync as statSync2, writeFileSync as writeFileSync8 } from "node:fs";
17051
+ import { stat as stat2 } from "node:fs/promises";
16979
17052
  import { dirname as dirname8 } from "node:path";
16980
17053
  import { tmpdir as tmpdir3 } from "node:os";
16981
- function phaseView(p3, a) {
16982
- const phases = readPhases(p3.phaseFile, p3.flow);
17054
+ function phaseView(p3, a, phases2) {
16983
17055
  const now = Date.now();
16984
- trackDoneSeen(phases, a.doneSeenAt, now);
17056
+ trackDoneSeen(phases2, a.doneSeenAt, now);
16985
17057
  const roster = (p3.agentRoster ?? []).flatMap((r) => r.phase ? [{ name: r.name, phase: r.phase, model: r.model, effort: r.effort }] : []);
16986
17058
  return roster.length ? enrichPhases(
16987
- phases,
17059
+ phases2,
16988
17060
  roster,
16989
17061
  phaseInvocations.get(p3.id) ?? [],
16990
17062
  a.doneSeenAt,
16991
17063
  now,
16992
17064
  p3.startedAt * 1e3,
16993
17065
  new Set(p3.doneAtBirth ?? [])
16994
- ) : phases;
17066
+ ) : phases2;
16995
17067
  }
16996
17068
  function noTtyPromptEnv() {
16997
17069
  const path = askpassDenyPath();
@@ -17164,10 +17236,10 @@ function sessionEventEnv(sessionId2, env = process.env) {
17164
17236
  ...host2 ? { HANOMAN_EVENT_HOST: host2 } : {}
17165
17237
  };
17166
17238
  }
17167
- function registerSessionHooks(h) {
17168
- hooks.add(h);
17239
+ function registerSessionHooks(h2) {
17240
+ hooks.add(h2);
17169
17241
  return () => {
17170
- hooks.delete(h);
17242
+ hooks.delete(h2);
17171
17243
  };
17172
17244
  }
17173
17245
  function registerCustomAgentSource(fn) {
@@ -17601,20 +17673,6 @@ function sessionPhases(id2) {
17601
17673
  if (!p3?.flow || !p3.phaseFile) return null;
17602
17674
  return readPhases(p3.phaseFile, p3.flow);
17603
17675
  }
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
17676
  function broadcast(a, f) {
17619
17677
  const msg = frame(f);
17620
17678
  for (const c of a.clients) c.send(msg);
@@ -17669,14 +17727,16 @@ function end(id2, code) {
17669
17727
  broadcast(a, { t: "exit", code });
17670
17728
  drop(id2);
17671
17729
  }
17672
- function pollPhases(p3, a) {
17730
+ async function pollPhases(p3, a) {
17673
17731
  if (!p3.flow || !p3.phaseFile) return;
17674
- const phases = phaseView(p3, a);
17675
- const complete = paneComplete(p3);
17676
- const json = phaseKey(phases, complete);
17732
+ const raw = await readPhasesAsync(p3.phaseFile, p3.flow);
17733
+ const complete = await sessionCompleteAsync(raw, p3.cwd, p3.specId);
17734
+ if (attached.get(p3.id) !== a) return;
17735
+ const phases2 = phaseView(p3, a, raw);
17736
+ const json = phaseKey(phases2, complete);
17677
17737
  if (json === a.lastPhases) return;
17678
17738
  a.lastPhases = json;
17679
- broadcast(a, { t: "phase", phases, complete });
17739
+ broadcast(a, { t: "phase", phases: phases2, complete });
17680
17740
  }
17681
17741
  function setPhaseInvocations(sessionId2, rows) {
17682
17742
  if (attached.has(sessionId2)) phaseInvocations.set(sessionId2, rows);
@@ -17692,7 +17752,7 @@ function startPoll() {
17692
17752
  if (polling) return;
17693
17753
  polling = true;
17694
17754
  const snapshot3 = [...attached.entries()];
17695
- listPanesAsync().then((panes) => {
17755
+ listPanesAsync().then(async (panes) => {
17696
17756
  const live = new Map(panes.map((p3) => [p3.id, p3]));
17697
17757
  for (const [id2, a] of snapshot3) {
17698
17758
  if (attached.get(id2) !== a) continue;
@@ -17700,7 +17760,8 @@ function startPoll() {
17700
17760
  if (!p3) end(id2, 0);
17701
17761
  else if (p3.exited) end(id2, p3.code);
17702
17762
  else {
17703
- pollPhases(p3, a);
17763
+ await pollPhases(p3, a).catch(() => {
17764
+ });
17704
17765
  pollAlt(p3, a);
17705
17766
  }
17706
17767
  }
@@ -17715,31 +17776,43 @@ function startPoll() {
17715
17776
  }, POLL_MS);
17716
17777
  poll.unref();
17717
17778
  }
17718
- function attach(id2, c) {
17719
- const p3 = getSession(id2);
17779
+ function replayExited(c, p3, screen) {
17780
+ if (screen.trim()) c.send(frame({ t: "data", d: stripTerminalQueries(screen.replace(/\n/g, "\r\n")) }));
17781
+ c.send(frame({ t: "exit", code: p3.code }));
17782
+ c.close();
17783
+ }
17784
+ async function screenResyncFrame(id2) {
17785
+ try {
17786
+ const screen = await tmuxAsync("capture-pane", "-p", "-e", "-J", "-t", name(id2));
17787
+ return frame({ t: "data", d: `\x1B[2J\x1B[H${stripTerminalQueries(screen.replace(/\n/g, "\r\n"))}` });
17788
+ } catch {
17789
+ return null;
17790
+ }
17791
+ }
17792
+ async function attachAsync(id2, c) {
17793
+ const p3 = await getSessionAsync(id2);
17720
17794
  if (!p3) {
17721
17795
  c.close();
17722
17796
  return;
17723
17797
  }
17724
17798
  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();
17799
+ replayExited(c, p3, await tmuxAsync(...CAPTURE_ARGS(id2)));
17731
17800
  return;
17732
17801
  }
17802
+ attachLive(id2, c, p3);
17803
+ }
17804
+ function attachLive(id2, c, p3) {
17733
17805
  const a = attached.get(id2) ?? open(id2);
17734
17806
  a.clients.add(c);
17735
17807
  if (a.scrollback) c.send(frame({ t: "data", d: stripTerminalQueries(a.scrollback) }));
17736
17808
  a.lastAlt = p3.altScreen;
17737
17809
  c.send(frame({ t: "alt", on: p3.altScreen }));
17738
17810
  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 }));
17811
+ const raw = readPhases(p3.phaseFile, p3.flow);
17812
+ const phases2 = phaseView(p3, a, raw);
17813
+ const complete = sessionComplete(raw, p3.cwd, p3.specId);
17814
+ a.lastPhases = phaseKey(phases2, complete);
17815
+ c.send(frame({ t: "phase", phases: phases2, complete }));
17743
17816
  }
17744
17817
  }
17745
17818
  function writeTo(id2, d, from) {
@@ -17791,7 +17864,7 @@ function spawnPty(...args) {
17791
17864
  );
17792
17865
  }
17793
17866
  }
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;
17867
+ 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
17868
  var init_pty = __esm({
17796
17869
  "src/services/pty.ts"() {
17797
17870
  "use strict";
@@ -17820,6 +17893,13 @@ var init_pty = __esm({
17820
17893
  return false;
17821
17894
  }
17822
17895
  };
17896
+ markerFilledAsync = async (f) => {
17897
+ try {
17898
+ return (await stat2(f)).size > 0;
17899
+ } catch {
17900
+ return false;
17901
+ }
17902
+ };
17823
17903
  clearMarker = (f) => {
17824
17904
  try {
17825
17905
  writeFileSync8(f, "");
@@ -17937,7 +18017,7 @@ exit 1
17937
18017
  });
17938
18018
  listSessions = () => listPanes().map(toSessionInfo);
17939
18019
  listSessionsAsync = async () => (await listPanesAsync()).map(toSessionInfo);
17940
- liveDecisions = () => listPanes().filter((p3) => !p3.exited && p3.decisionFile).map((p3) => ({
18020
+ decisionsOf = (panes) => panes.filter((p3) => !p3.exited && p3.decisionFile).map((p3) => ({
17941
18021
  id: p3.id,
17942
18022
  specId: p3.specId,
17943
18023
  projectId: p3.projectId,
@@ -17951,17 +18031,17 @@ exit 1
17951
18031
  getSessionAsync = async (id2) => (await listPanesAsync()).find((p3) => p3.id === id2);
17952
18032
  hooks = /* @__PURE__ */ new Set();
17953
18033
  emitBirth = (b) => {
17954
- for (const h of hooks) {
18034
+ for (const h2 of hooks) {
17955
18035
  try {
17956
- h.onBirth?.(b);
18036
+ h2.onBirth?.(b);
17957
18037
  } catch {
17958
18038
  }
17959
18039
  }
17960
18040
  };
17961
18041
  emitDeath = (d) => {
17962
- for (const h of hooks) {
18042
+ for (const h2 of hooks) {
17963
18043
  try {
17964
- h.onDeath?.(d);
18044
+ h2.onDeath?.(d);
17965
18045
  } catch {
17966
18046
  }
17967
18047
  }
@@ -18017,11 +18097,10 @@ exit 1
18017
18097
  COALESCE_MS = 16;
18018
18098
  COALESCE_MAX_BYTES = 64 * 1024;
18019
18099
  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);
18100
+ phaseKey = (phases2, complete) => JSON.stringify({ phases: phases2, complete });
18101
+ sessionFinishedAsync = async (id2) => {
18102
+ const p3 = await getSessionAsync(id2);
18103
+ return !!p3 && !!p3.flow && !!p3.phaseFile && sessionCompleteAsync(await readPhasesAsync(p3.phaseFile, p3.flow), p3.cwd, p3.specId);
18025
18104
  };
18026
18105
  polling = false;
18027
18106
  TERMINAL_QUERY = new RegExp([
@@ -18041,6 +18120,7 @@ exit 1
18041
18120
  // XTWINOPS laporan — berparameter TUNGGAL
18042
18121
  ].join("|"), "g");
18043
18122
  stripTerminalQueries = (s2) => s2.replace(TERMINAL_QUERY, "");
18123
+ CAPTURE_ARGS = (id2) => ["capture-pane", "-p", "-e", "-J", "-S", "-2000", "-t", name(id2)];
18044
18124
  detach = (id2, c) => {
18045
18125
  attached.get(id2)?.clients.delete(c);
18046
18126
  };
@@ -18066,7 +18146,7 @@ var init_redact_known = __esm({
18066
18146
 
18067
18147
  // src/services/transcript-store.ts
18068
18148
  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";
18149
+ import { mkdir as mkdir2, writeFile as writeFile2, readFile as readFile3, unlink as unlink2, readdir as readdir2, stat as stat3 } from "node:fs/promises";
18070
18150
  import { join as join13, resolve as resolve9, basename as basename4 } from "node:path";
18071
18151
  function transcriptDir() {
18072
18152
  return resolve9(effectiveStr("HANOMAN_TRANSCRIPT_DIR")?.trim() || resolveDataDirs().transcripts);
@@ -18093,7 +18173,7 @@ async function saveTranscript(text) {
18093
18173
  async function readTranscript(key) {
18094
18174
  if (!key) return null;
18095
18175
  try {
18096
- return await readFile2(join13(transcriptDir(), basename4(key)), "utf8");
18176
+ return await readFile3(join13(transcriptDir(), basename4(key)), "utf8");
18097
18177
  } catch {
18098
18178
  return null;
18099
18179
  }
@@ -18110,7 +18190,7 @@ async function listTranscripts() {
18110
18190
  const dir2 = transcriptDir();
18111
18191
  let names;
18112
18192
  try {
18113
- names = await readdir(dir2);
18193
+ names = await readdir2(dir2);
18114
18194
  } catch (e) {
18115
18195
  if (e.code === "ENOENT") return [];
18116
18196
  throw e;
@@ -18119,7 +18199,7 @@ async function listTranscripts() {
18119
18199
  for (const name2 of names) {
18120
18200
  if (!name2.endsWith(".log")) continue;
18121
18201
  try {
18122
- rows.push({ key: name2, mtimeMs: (await stat(join13(dir2, name2))).mtimeMs });
18202
+ rows.push({ key: name2, mtimeMs: (await stat3(join13(dir2, name2))).mtimeMs });
18123
18203
  } catch {
18124
18204
  }
18125
18205
  }
@@ -19181,6 +19261,118 @@ var init_codex_trust = __esm({
19181
19261
  }
19182
19262
  });
19183
19263
 
19264
+ // src/services/logs/phase-tap.ts
19265
+ function observePhases(rows) {
19266
+ const seen2 = /* @__PURE__ */ new Set();
19267
+ for (const r of rows) {
19268
+ seen2.add(r.sessionId);
19269
+ const prev = known.get(r.sessionId);
19270
+ if (!known.has(r.sessionId)) {
19271
+ known.set(r.sessionId, r.phase);
19272
+ continue;
19273
+ }
19274
+ if (prev !== r.phase) {
19275
+ void appendEvent({
19276
+ kind: "session.phase",
19277
+ msg: `sesi ${r.sessionId} fase ${prev ?? "?"} \u2192 ${r.phase ?? "?"}`,
19278
+ projectId: r.projectId,
19279
+ specId: r.specId ?? null,
19280
+ sessionId: r.sessionId,
19281
+ data: { from: prev, to: r.phase }
19282
+ });
19283
+ known.set(r.sessionId, r.phase);
19284
+ }
19285
+ }
19286
+ for (const id2 of [...known.keys()]) if (!seen2.has(id2)) known.delete(id2);
19287
+ }
19288
+ var known;
19289
+ var init_phase_tap = __esm({
19290
+ "src/services/logs/phase-tap.ts"() {
19291
+ "use strict";
19292
+ init_event_log();
19293
+ known = /* @__PURE__ */ new Map();
19294
+ }
19295
+ });
19296
+
19297
+ // src/services/presence/snapshot.ts
19298
+ function paneToPresence(p3, phase) {
19299
+ return {
19300
+ sessionId: p3.id,
19301
+ projectId: p3.projectId,
19302
+ ...p3.specId ? { specId: p3.specId } : {},
19303
+ ...p3.flow ? { flow: p3.flow } : {},
19304
+ ...phase ? { phase } : {},
19305
+ agent: p3.agent,
19306
+ // Presedensi: pane mati sudah berakhir apa pun isi markernya.
19307
+ status: p3.exited ? "exited" : p3.decision ? "waiting" : "working",
19308
+ startedAt: isoFromEpochSeconds(p3.startedAt)
19309
+ };
19310
+ }
19311
+ function activePhase(p3) {
19312
+ if (!p3.flow || !p3.phaseFile) return void 0;
19313
+ return readPhases(p3.phaseFile, p3.flow).find((f) => f.state === "active")?.name;
19314
+ }
19315
+ function listPanesShared(now = Date.now()) {
19316
+ if (panesMemo && now - panesMemo.at < PANES_MEMO_MS) return panesMemo.value;
19317
+ const value = listPanesAsync();
19318
+ panesMemo = { at: now, value };
19319
+ value.catch(() => {
19320
+ if (panesMemo?.value === value) panesMemo = null;
19321
+ });
19322
+ return value;
19323
+ }
19324
+ async function buildLocalPresence() {
19325
+ const panes = await listPanesShared();
19326
+ const rows = panes.slice(0, MAX_PRESENCE_SESSIONS).map((p3) => paneToPresence(p3, activePhase(p3)));
19327
+ observePhases(rows.map((r) => ({ sessionId: r.sessionId, projectId: r.projectId, specId: r.specId, phase: r.phase })));
19328
+ return rows;
19329
+ }
19330
+ async function buildLocalCapacity() {
19331
+ return currentLaunchStatus(await listPanesShared(), await getScheduler());
19332
+ }
19333
+ var isoFromEpochSeconds, PANES_MEMO_MS, panesMemo;
19334
+ var init_snapshot = __esm({
19335
+ "src/services/presence/snapshot.ts"() {
19336
+ "use strict";
19337
+ init_src();
19338
+ init_pty();
19339
+ init_session_phases();
19340
+ init_config3();
19341
+ init_session_launch_gate();
19342
+ init_phase_tap();
19343
+ isoFromEpochSeconds = (s2) => new Date((s2 || 0) * 1e3).toISOString();
19344
+ PANES_MEMO_MS = 1e3;
19345
+ panesMemo = null;
19346
+ }
19347
+ });
19348
+
19349
+ // src/services/live-phases.ts
19350
+ async function sessionPhasesBySpecAsync() {
19351
+ const out4 = /* @__PURE__ */ new Map();
19352
+ let panes;
19353
+ try {
19354
+ panes = await listPanesShared();
19355
+ } catch {
19356
+ return out4;
19357
+ }
19358
+ await Promise.all(panes.map(async (p3) => {
19359
+ if (!p3.specId || !p3.flow || !p3.phaseFile) return;
19360
+ out4.set(p3.specId, { phases: await readPhasesAsync(p3.phaseFile, p3.flow), cwd: p3.cwd });
19361
+ }));
19362
+ return out4;
19363
+ }
19364
+ async function liveDecisionsAsync() {
19365
+ return decisionsOf(await listPanesShared());
19366
+ }
19367
+ var init_live_phases = __esm({
19368
+ "src/services/live-phases.ts"() {
19369
+ "use strict";
19370
+ init_session_phases();
19371
+ init_snapshot();
19372
+ init_pty();
19373
+ }
19374
+ });
19375
+
19184
19376
  // src/services/notifications.ts
19185
19377
  async function recordDrift(vpsId, vpsName, drift, snapshotId) {
19186
19378
  if (drift.length === 0) return;
@@ -19282,11 +19474,11 @@ async function recordCronRun(cronId, cronName, projectId, dueAt, status2, note)
19282
19474
  }).catch(() => {
19283
19475
  });
19284
19476
  }
19285
- async function scanDecisions(read2 = liveDecisions) {
19477
+ async function scanDecisions(read2 = liveDecisionsAsync) {
19286
19478
  const next = /* @__PURE__ */ new Set();
19287
19479
  const fresh = [];
19288
- for (const s2 of read2()) {
19289
- if (!markerFilled(s2.decisionFile)) continue;
19480
+ for (const s2 of await read2()) {
19481
+ if (!await markerFilledAsync(s2.decisionFile)) continue;
19290
19482
  if (awaiting.has(s2.id)) {
19291
19483
  next.add(s2.id);
19292
19484
  continue;
@@ -19322,96 +19514,12 @@ var init_notifications2 = __esm({
19322
19514
  "use strict";
19323
19515
  init_db();
19324
19516
  init_pty();
19517
+ init_live_phases();
19325
19518
  awaiting = /* @__PURE__ */ new Set();
19326
19519
  DEFAULT_FEED_TAKE = 50;
19327
19520
  }
19328
19521
  });
19329
19522
 
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
19523
  // src/services/device-sockets.ts
19416
19524
  function registerDeviceSocket(deviceId, kind, socket2) {
19417
19525
  const entry = { kind, socket: socket2 };
@@ -19916,8 +20024,8 @@ var require_ms = __commonJS({
19916
20024
  "../node_modules/.pnpm/ms@2.1.3/node_modules/ms/index.js"(exports, module) {
19917
20025
  var s2 = 1e3;
19918
20026
  var m = s2 * 60;
19919
- var h = m * 60;
19920
- var d = h * 24;
20027
+ var h2 = m * 60;
20028
+ var d = h2 * 24;
19921
20029
  var w = d * 7;
19922
20030
  var y = d * 365.25;
19923
20031
  module.exports = function(val, options2) {
@@ -19965,7 +20073,7 @@ var require_ms = __commonJS({
19965
20073
  case "hrs":
19966
20074
  case "hr":
19967
20075
  case "h":
19968
- return n2 * h;
20076
+ return n2 * h2;
19969
20077
  case "minutes":
19970
20078
  case "minute":
19971
20079
  case "mins":
@@ -19993,8 +20101,8 @@ var require_ms = __commonJS({
19993
20101
  if (msAbs >= d) {
19994
20102
  return Math.round(ms / d) + "d";
19995
20103
  }
19996
- if (msAbs >= h) {
19997
- return Math.round(ms / h) + "h";
20104
+ if (msAbs >= h2) {
20105
+ return Math.round(ms / h2) + "h";
19998
20106
  }
19999
20107
  if (msAbs >= m) {
20000
20108
  return Math.round(ms / m) + "m";
@@ -20009,8 +20117,8 @@ var require_ms = __commonJS({
20009
20117
  if (msAbs >= d) {
20010
20118
  return plural(ms, msAbs, d, "day");
20011
20119
  }
20012
- if (msAbs >= h) {
20013
- return plural(ms, msAbs, h, "hour");
20120
+ if (msAbs >= h2) {
20121
+ return plural(ms, msAbs, h2, "hour");
20014
20122
  }
20015
20123
  if (msAbs >= m) {
20016
20124
  return plural(ms, msAbs, m, "minute");
@@ -20726,7 +20834,7 @@ var init_runtime_defaults2 = __esm({
20726
20834
  });
20727
20835
 
20728
20836
  // src/services/agent-token.ts
20729
- import { randomBytes as randomBytes2, createHash as createHash4, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
20837
+ import { randomBytes as randomBytes2, createHash as createHash5, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
20730
20838
  function toAgentTokenView(t) {
20731
20839
  return {
20732
20840
  id: t.id,
@@ -20792,7 +20900,7 @@ var init_agent_token = __esm({
20792
20900
  "src/services/agent-token.ts"() {
20793
20901
  "use strict";
20794
20902
  init_db();
20795
- hash2 = (token) => createHash4("sha256").update(token).digest("hex");
20903
+ hash2 = (token) => createHash5("sha256").update(token).digest("hex");
20796
20904
  }
20797
20905
  });
20798
20906
 
@@ -20932,7 +21040,7 @@ var init_config4 = __esm({
20932
21040
  });
20933
21041
 
20934
21042
  // src/services/telegram/protocol.ts
20935
- import { createHash as createHash5 } from "node:crypto";
21043
+ import { createHash as createHash6 } from "node:crypto";
20936
21044
  function normalizeTelegramCommand(text) {
20937
21045
  const clean2 = text.trim();
20938
21046
  const match = clean2.match(/^\/([a-z0-9_]+)(?:@[a-z0-9_]+)?(?:\s+([\s\S]*))?$/i);
@@ -20999,7 +21107,7 @@ function parseTelegramUpdate(update2, allowlist, maxText = 12e3) {
20999
21107
  };
21000
21108
  }
21001
21109
  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");
21110
+ return createHash6("sha256").update(`${input.updateId}\0${input.chatId}\0${input.userId}\0${input.kind}\0${input.text}`, "utf8").digest("hex");
21003
21111
  }
21004
21112
  function sanitizeTelegramOutput(text, exactSecrets = []) {
21005
21113
  let clean2 = text.replace(ANSI, "").replace(CONTROLS, "");
@@ -21166,7 +21274,7 @@ var init_typing = __esm({
21166
21274
  });
21167
21275
 
21168
21276
  // src/services/telegram/gateway.ts
21169
- import { createHash as createHash6 } from "node:crypto";
21277
+ import { createHash as createHash7 } from "node:crypto";
21170
21278
  var digestRaw, TelegramGateway;
21171
21279
  var init_gateway = __esm({
21172
21280
  "src/services/telegram/gateway.ts"() {
@@ -21175,7 +21283,7 @@ var init_gateway = __esm({
21175
21283
  init_protocol();
21176
21284
  init_runtime();
21177
21285
  init_typing();
21178
- digestRaw = (update2) => createHash6("sha256").update(JSON.stringify(update2)).digest("hex");
21286
+ digestRaw = (update2) => createHash7("sha256").update(JSON.stringify(update2)).digest("hex");
21179
21287
  TelegramGateway = class {
21180
21288
  constructor(deps) {
21181
21289
  this.deps = deps;
@@ -21482,14 +21590,14 @@ var init_engine_command = __esm({
21482
21590
  });
21483
21591
 
21484
21592
  // src/services/telegram/session.ts
21485
- import { createHash as createHash7 } from "node:crypto";
21593
+ import { createHash as createHash8 } from "node:crypto";
21486
21594
  var chatHash, telegramOperatorSessionId, formatTelegramTurn, TelegramSessionCoordinator;
21487
21595
  var init_session = __esm({
21488
21596
  "src/services/telegram/session.ts"() {
21489
21597
  "use strict";
21490
21598
  init_src2();
21491
21599
  init_engine_command();
21492
- chatHash = (chatId) => createHash7("sha256").update(chatId).digest("hex").slice(0, 16);
21600
+ chatHash = (chatId) => createHash8("sha256").update(chatId).digest("hex").slice(0, 16);
21493
21601
  telegramOperatorSessionId = (chatId) => `telegram-${chatHash(chatId)}`;
21494
21602
  formatTelegramTurn = (input) => `[Telegram update ${input.updateId} \xB7 chat ${input.chatId} \xB7 kind ${input.kind}]
21495
21603
  ${input.text}`;
@@ -24949,7 +25057,7 @@ var require_websocket = __commonJS({
24949
25057
  var http = __require("http");
24950
25058
  var net = __require("net");
24951
25059
  var tls = __require("tls");
24952
- var { randomBytes: randomBytes12, createHash: createHash15 } = __require("crypto");
25060
+ var { randomBytes: randomBytes12, createHash: createHash16 } = __require("crypto");
24953
25061
  var { Duplex, Readable } = __require("stream");
24954
25062
  var { URL: URL2 } = __require("url");
24955
25063
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -25617,7 +25725,7 @@ var require_websocket = __commonJS({
25617
25725
  abortHandshake(websocket2, socket2, "Invalid Upgrade header");
25618
25726
  return;
25619
25727
  }
25620
- const digest2 = createHash15("sha1").update(key + GUID).digest("base64");
25728
+ const digest2 = createHash16("sha1").update(key + GUID).digest("base64");
25621
25729
  if (res.headers["sec-websocket-accept"] !== digest2) {
25622
25730
  abortHandshake(websocket2, socket2, "Invalid Sec-WebSocket-Accept header");
25623
25731
  return;
@@ -25986,7 +26094,7 @@ var require_websocket_server = __commonJS({
25986
26094
  var EventEmitter = __require("events");
25987
26095
  var http = __require("http");
25988
26096
  var { Duplex } = __require("stream");
25989
- var { createHash: createHash15 } = __require("crypto");
26097
+ var { createHash: createHash16 } = __require("crypto");
25990
26098
  var extension2 = require_extension();
25991
26099
  var PerMessageDeflate2 = require_permessage_deflate();
25992
26100
  var subprotocol2 = require_subprotocol();
@@ -26293,7 +26401,7 @@ var require_websocket_server = __commonJS({
26293
26401
  );
26294
26402
  }
26295
26403
  if (this._state > RUNNING) return abortHandshake(socket2, 503);
26296
- const digest2 = createHash15("sha1").update(key + GUID).digest("base64");
26404
+ const digest2 = createHash16("sha1").update(key + GUID).digest("base64");
26297
26405
  const headers = [
26298
26406
  "HTTP/1.1 101 Switching Protocols",
26299
26407
  "Upgrade: websocket",
@@ -26365,7 +26473,7 @@ var require_websocket_server = __commonJS({
26365
26473
  socket2.once("finish", socket2.destroy);
26366
26474
  socket2.end(
26367
26475
  `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
26476
+ ` + Object.keys(headers).map((h2) => `${h2}: ${headers[h2]}`).join("\r\n") + "\r\n\r\n" + message
26369
26477
  );
26370
26478
  }
26371
26479
  function abortHandshakeOrEmitwsClientError(server, req, socket2, code, message, headers) {
@@ -26589,7 +26697,7 @@ var init_client2 = __esm({
26589
26697
  });
26590
26698
 
26591
26699
  // 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";
26700
+ import { appendFile, mkdir as mkdir8, readFile as readFile7, readdir as readdir9, rm as rm8, stat as stat6 } from "node:fs/promises";
26593
26701
  import { join as join35 } from "node:path";
26594
26702
  function spoolDir(lane) {
26595
26703
  return join35(resolveDataDirs().home, "log-spool", lane);
@@ -26613,7 +26721,7 @@ async function readSpoolSegments(lane) {
26613
26721
  const dir2 = spoolDir(lane);
26614
26722
  let names;
26615
26723
  try {
26616
- names = await readdir8(dir2);
26724
+ names = await readdir9(dir2);
26617
26725
  } catch {
26618
26726
  return [];
26619
26727
  }
@@ -26621,7 +26729,7 @@ async function readSpoolSegments(lane) {
26621
26729
  const out4 = [];
26622
26730
  for (const name2 of files) {
26623
26731
  const file = join35(dir2, name2);
26624
- const raw = await readFile6(file, "utf8").catch(() => "");
26732
+ const raw = await readFile7(file, "utf8").catch(() => "");
26625
26733
  const entries3 = raw.split("\n").filter(Boolean).map((l) => JSON.parse(l));
26626
26734
  out4.push({ file, entries: entries3 });
26627
26735
  }
@@ -27229,7 +27337,7 @@ var init_agent_tool_catalog = __esm({
27229
27337
  });
27230
27338
 
27231
27339
  // src/services/builtin-agents.ts
27232
- import { createHash as createHash12 } from "node:crypto";
27340
+ import { createHash as createHash13 } from "node:crypto";
27233
27341
  async function seedBuiltinAgents() {
27234
27342
  try {
27235
27343
  const setting = await getSetting();
@@ -27319,7 +27427,7 @@ var init_builtin_agents2 = __esm({
27319
27427
  init_settings3();
27320
27428
  init_tombstone();
27321
27429
  init_sync_notify();
27322
- digest = (parts) => createHash12("sha256").update(parts.join(" ")).digest("hex").slice(0, 16);
27430
+ digest = (parts) => createHash13("sha256").update(parts.join(" ")).digest("hex").slice(0, 16);
27323
27431
  legacyFingerprint = (name2, description, instructions, tools) => digest([name2, description, instructions, [...tools].join(",")]);
27324
27432
  fingerprint = (a) => digest([
27325
27433
  a.name,
@@ -27825,14 +27933,14 @@ var require_HeaderParser = __commonJS({
27825
27933
  }
27826
27934
  const lines2 = this.buffer.split(RE_CRLF);
27827
27935
  const len = lines2.length;
27828
- let m, h;
27936
+ let m, h2;
27829
27937
  for (var i = 0; i < len; ++i) {
27830
27938
  if (lines2[i].length === 0) {
27831
27939
  continue;
27832
27940
  }
27833
27941
  if (lines2[i][0] === " " || lines2[i][0] === " ") {
27834
- if (h) {
27835
- this.header[h][this.header[h].length - 1] += lines2[i];
27942
+ if (h2) {
27943
+ this.header[h2][this.header[h2].length - 1] += lines2[i];
27836
27944
  continue;
27837
27945
  }
27838
27946
  }
@@ -27841,9 +27949,9 @@ var require_HeaderParser = __commonJS({
27841
27949
  return;
27842
27950
  }
27843
27951
  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] || "");
27952
+ h2 = m[1].toLowerCase();
27953
+ this.header[h2] = this.header[h2] || [];
27954
+ this.header[h2].push(m[2] || "");
27847
27955
  if (++this.npairs === this.maxHeaderPairs) {
27848
27956
  break;
27849
27957
  }
@@ -30835,14 +30943,14 @@ async function health_default(app3) {
30835
30943
  }
30836
30944
 
30837
30945
  // src/routes/agent-doc.ts
30838
- import { readFile as readFile3 } from "node:fs/promises";
30946
+ import { readFile as readFile4 } from "node:fs/promises";
30839
30947
  async function agent_doc_default(app3, opts) {
30840
30948
  app3.get("/agent-integration.md", async (_req, reply) => {
30841
30949
  if (!opts.file)
30842
30950
  return reply.code(404).send({
30843
30951
  error: `dokumen panduan tak ada di instalasi ini (${AGENT_DOC_REL}) \u2014 pasang ulang paket hanoman atau set HANOMAN_AGENT_DOC`
30844
30952
  });
30845
- const text = await readFile3(opts.file, "utf8");
30953
+ const text = await readFile4(opts.file, "utf8");
30846
30954
  return reply.type("text/markdown; charset=utf-8").send(text);
30847
30955
  });
30848
30956
  }
@@ -30883,8 +30991,8 @@ function components(rel) {
30883
30991
  }
30884
30992
  async function canonicalRoot(root) {
30885
30993
  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");
30994
+ const stat7 = await lstat(out4);
30995
+ if (!stat7.isDirectory() || stat7.isSymbolicLink()) denied("root bukan direktori regular");
30888
30996
  return out4;
30889
30997
  }
30890
30998
  function beneath(root, candidate) {
@@ -30902,8 +31010,8 @@ async function ensureRepoParents(root, rel) {
30902
31010
  } catch (error) {
30903
31011
  if (error.code !== "EEXIST") throw error;
30904
31012
  }
30905
- const stat5 = await lstat(current);
30906
- if (stat5.isSymbolicLink() || !stat5.isDirectory()) denied("parent tidak aman");
31013
+ const stat7 = await lstat(current);
31014
+ if (stat7.isSymbolicLink() || !stat7.isDirectory()) denied("parent tidak aman");
30907
31015
  const canonical = await realpath(current);
30908
31016
  if (!beneath(base2, canonical) || canonical !== current) denied("parent keluar root");
30909
31017
  }
@@ -30919,8 +31027,8 @@ function ensureRepoParentsSync(root, rel) {
30919
31027
  } catch (error) {
30920
31028
  if (error.code !== "EEXIST") throw error;
30921
31029
  }
30922
- const stat5 = lstatSync3(current);
30923
- if (stat5.isSymbolicLink() || !stat5.isDirectory()) denied("parent tidak aman");
31030
+ const stat7 = lstatSync3(current);
31031
+ if (stat7.isSymbolicLink() || !stat7.isDirectory()) denied("parent tidak aman");
30924
31032
  if (realpathSync4(current) !== current || !beneath(base2, current)) denied("parent keluar root");
30925
31033
  }
30926
31034
  }
@@ -30930,14 +31038,14 @@ async function resolveRepoEntry(root, rel, opts = {}) {
30930
31038
  let current = base2;
30931
31039
  for (let i = 0; i < parts.length; i++) {
30932
31040
  current = join14(current, parts[i]);
30933
- const stat5 = await lstat(current).catch((error) => {
31041
+ const stat7 = await lstat(current).catch((error) => {
30934
31042
  if (error.code === "ENOENT" && opts.allowMissingTail) return null;
30935
31043
  if (error.code === "ENOENT" && opts.allowMissingFinal && i === parts.length - 1) return null;
30936
31044
  return denied("komponen tidak ada");
30937
31045
  });
30938
- if (!stat5) break;
30939
- if (stat5.isSymbolicLink()) denied("symlink");
30940
- if (i < parts.length - 1 && !stat5.isDirectory()) denied("parent bukan direktori");
31046
+ if (!stat7) break;
31047
+ if (stat7.isSymbolicLink()) denied("symlink");
31048
+ if (i < parts.length - 1 && !stat7.isDirectory()) denied("parent bukan direktori");
30941
31049
  }
30942
31050
  if (!beneath(base2, resolve12(current))) denied("keluar root");
30943
31051
  const parent = resolve12(current, "..");
@@ -30952,9 +31060,9 @@ function assertSafeRepoPathSync(root, rel, allowMissingFinal = false, allowMissi
30952
31060
  for (let i = 0; i < parts.length; i++) {
30953
31061
  current = join14(current, parts[i]);
30954
31062
  try {
30955
- const stat5 = lstatSync3(current);
30956
- if (stat5.isSymbolicLink()) denied("symlink");
30957
- if (i < parts.length - 1 && !stat5.isDirectory()) denied("parent bukan direktori");
31063
+ const stat7 = lstatSync3(current);
31064
+ if (stat7.isSymbolicLink()) denied("symlink");
31065
+ if (i < parts.length - 1 && !stat7.isDirectory()) denied("parent bukan direktori");
30958
31066
  } catch (error) {
30959
31067
  if (allowMissingTail && error.code === "ENOENT") break;
30960
31068
  if (allowMissingFinal && error.code === "ENOENT" && i === parts.length - 1) break;
@@ -31009,8 +31117,8 @@ async function readRepoFile(root, rel) {
31009
31117
  const entry = await resolveRepoEntry(root, rel);
31010
31118
  const handle = await open2(entry.path, constants2.O_RDONLY | (constants2.O_NOFOLLOW ?? 0)).catch(() => denied("final component tidak aman"));
31011
31119
  try {
31012
- const stat5 = await handle.stat();
31013
- if (!stat5.isFile()) denied("bukan file regular");
31120
+ const stat7 = await handle.stat();
31121
+ if (!stat7.isFile()) denied("bukan file regular");
31014
31122
  return await handle.readFile();
31015
31123
  } finally {
31016
31124
  await handle.close();
@@ -31338,7 +31446,7 @@ async function projects_default(app3) {
31338
31446
  const needle = (q ?? "").trim().toLowerCase();
31339
31447
  const filtered = needle ? views.filter((v) => `${v.name} ${v.desc} ${v.stack}`.toLowerCase().includes(needle)) : views;
31340
31448
  const device = (handledBy ?? "").trim();
31341
- const scoped = device ? filtered.filter((v) => v.handledBy.some((h) => h.deviceId === device)) : filtered;
31449
+ const scoped = device ? filtered.filter((v) => v.handledBy.some((h2) => h2.deviceId === device)) : filtered;
31342
31450
  return paginate(scoped, page, limit);
31343
31451
  });
31344
31452
  app3.get("/projects/:id", async (req, reply) => {
@@ -31916,7 +32024,7 @@ async function deleteDoc(projectId, path) {
31916
32024
  init_src2();
31917
32025
  init_db();
31918
32026
  import { execFile as execFile6 } from "node:child_process";
31919
- import { readdir as readdir2, rm as rm3 } from "node:fs/promises";
32027
+ import { readdir as readdir3, rm as rm3 } from "node:fs/promises";
31920
32028
  import { promisify as promisify5 } from "node:util";
31921
32029
  import { join as join17, resolve as resolve15 } from "node:path";
31922
32030
  init_notifications2();
@@ -31974,7 +32082,7 @@ async function sweepRepo(repoDir, projectId, deps = prodReaperDeps) {
31974
32082
  const dir2 = trashDirOf(repoDir);
31975
32083
  let entries3;
31976
32084
  try {
31977
- entries3 = await readdir2(dir2);
32085
+ entries3 = await readdir3(dir2);
31978
32086
  } catch {
31979
32087
  return 0;
31980
32088
  }
@@ -32033,7 +32141,7 @@ function ownsWorktree(repoDir, cwd) {
32033
32141
  import { execFile as execFile7 } from "node:child_process";
32034
32142
  import { promisify as promisify6 } from "node:util";
32035
32143
  import { existsSync as existsSync8 } from "node:fs";
32036
- import { readdir as readdir3 } from "node:fs/promises";
32144
+ import { readdir as readdir4 } from "node:fs/promises";
32037
32145
  import { resolve as resolve17 } from "node:path";
32038
32146
  var exec6 = promisify6(execFile7);
32039
32147
  var GIT4 = { maxBuffer: 1 << 24 };
@@ -32079,7 +32187,7 @@ async function listDirEntries(repoDir, rel) {
32079
32187
  const abs = repoAbsPath(repoDir, rel);
32080
32188
  let raw;
32081
32189
  try {
32082
- raw = await readdir3(abs, { withFileTypes: true });
32190
+ raw = await readdir4(abs, { withFileTypes: true });
32083
32191
  } catch {
32084
32192
  return empty;
32085
32193
  }
@@ -32245,12 +32353,12 @@ async function searchCommits(repoDir, q, by = "all") {
32245
32353
  };
32246
32354
  const byMessage = () => run4(["-i", "-F", `--grep=${q}`]);
32247
32355
  const byAuthor = () => run4(["-i", `--author=${q}`]);
32248
- const byHash = async () => (await run4([])).filter((h2) => h2.toLowerCase().startsWith(q.toLowerCase()));
32356
+ const byHash = async () => (await run4([])).filter((h3) => h3.toLowerCase().startsWith(q.toLowerCase()));
32249
32357
  if (by === "message") return byMessage();
32250
32358
  if (by === "author") return byAuthor();
32251
32359
  if (by === "hash") return byHash();
32252
- const [m, a, h] = await Promise.all([byMessage(), byAuthor(), byHash()]);
32253
- return [.../* @__PURE__ */ new Set([...m, ...a, ...h])];
32360
+ const [m, a, h2] = await Promise.all([byMessage(), byAuthor(), byHash()]);
32361
+ return [.../* @__PURE__ */ new Set([...m, ...a, ...h2])];
32254
32362
  }
32255
32363
  async function compareFile(repoDir, from, to, rel) {
32256
32364
  if (!repoDir) return null;
@@ -32271,9 +32379,9 @@ async function commitDetail(repoDir, sha) {
32271
32379
  try {
32272
32380
  const fmt = ["%H", "%P", "%an", "%aI", "%s", "%G?", "%cn", "%cI", "%ae", "%b"].join(US);
32273
32381
  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;
32382
+ const [h2, parents, author, at, subject, gsig, committer, committedAt, authorEmail] = parts;
32275
32383
  return {
32276
- sha: h,
32384
+ sha: h2,
32277
32385
  parents: parents ? parents.split(" ") : [],
32278
32386
  author: author ?? "",
32279
32387
  at: at ?? "",
@@ -33328,8 +33436,8 @@ var FileTokenizer = class _FileTokenizer extends AbstractTokenizer {
33328
33436
  */
33329
33437
  static async fromFile(sourceFilePath) {
33330
33438
  const fileHandle = await fsOpen(sourceFilePath, "r");
33331
- const stat5 = await fileHandle.stat();
33332
- return new _FileTokenizer(fileHandle, { fileInfo: { path: sourceFilePath, size: stat5.size } });
33439
+ const stat7 = await fileHandle.stat();
33440
+ return new _FileTokenizer(fileHandle, { fileInfo: { path: sourceFilePath, size: stat7.size } });
33333
33441
  }
33334
33442
  constructor(fileHandle, options2) {
33335
33443
  super(options2);
@@ -33979,7 +34087,7 @@ function indexOf(buffer, portion) {
33979
34087
  return -1;
33980
34088
  }
33981
34089
  function mergeArrays(chunks) {
33982
- const totalLength = chunks.reduce((acc, curr) => acc + curr.length, 0);
34090
+ const totalLength = chunks.reduce((acc2, curr) => acc2 + curr.length, 0);
33983
34091
  const mergedArray = new Uint8Array(totalLength);
33984
34092
  let offset = 0;
33985
34093
  for (const chunk of chunks) {
@@ -36939,7 +37047,7 @@ async function dropSpecAttachments(specId) {
36939
37047
 
36940
37048
  // src/services/spec-attachment-dir.ts
36941
37049
  init_db();
36942
- import { mkdir as mkdir5, readdir as readdir4, readFile as readFile4, rm as rm4, writeFile as writeFile4 } from "node:fs/promises";
37050
+ import { mkdir as mkdir5, readdir as readdir5, readFile as readFile5, rm as rm4, writeFile as writeFile4 } from "node:fs/promises";
36943
37051
  import { join as join20 } from "node:path";
36944
37052
  init_session_id();
36945
37053
  init_uploads();
@@ -36993,7 +37101,7 @@ async function syncSpecAttachmentsDir(specId, projectId) {
36993
37101
  for (const a of rows) {
36994
37102
  let bytes;
36995
37103
  try {
36996
- bytes = await readFile4(join20(uploadDir(), a.storageKey));
37104
+ bytes = await readFile5(join20(uploadDir(), a.storageKey));
36997
37105
  } catch {
36998
37106
  continue;
36999
37107
  }
@@ -37004,7 +37112,7 @@ async function syncSpecAttachmentsDir(specId, projectId) {
37004
37112
  }
37005
37113
  await writeFile4(join20(dir2, INDEX), renderIndex(specId, items), { mode: 384 });
37006
37114
  const keep = /* @__PURE__ */ new Set([INDEX, ...items.map((a) => a.filename)]);
37007
- for (const name2 of await readdir4(dir2)) {
37115
+ for (const name2 of await readdir5(dir2)) {
37008
37116
  if (!keep.has(name2)) await rm4(join20(dir2, name2), { recursive: true, force: true }).catch(() => {
37009
37117
  });
37010
37118
  }
@@ -39439,12 +39547,12 @@ function drawCode(doc, text) {
39439
39547
  used += hl;
39440
39548
  }
39441
39549
  const body = seg.join("\n");
39442
- const h = doc.heightOfString(body, opts) + 12;
39550
+ const h2 = doc.heightOfString(body, opts) + 12;
39443
39551
  const top = doc.y;
39444
- doc.save().rect(left, top, right - left, h).fill(PANEL).restore();
39552
+ doc.save().rect(left, top, right - left, h2).fill(PANEL).restore();
39445
39553
  doc.fillColor(INK).text(body, left + 8, top + 6, opts);
39446
39554
  doc.x = left;
39447
- doc.y = top + h;
39555
+ doc.y = top + h2;
39448
39556
  if (i < lines2.length) doc.addPage();
39449
39557
  }
39450
39558
  doc.moveDown(0.5);
@@ -39478,12 +39586,12 @@ function drawTable(doc, t) {
39478
39586
  const row = (cells, bold) => {
39479
39587
  const texts = cells.map((c) => toWinAnsi(flatten(c.tokens).map((r) => r.text).join("") || c.text || ""));
39480
39588
  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();
39589
+ const h2 = Math.max(...texts.map((s2) => doc.heightOfString(s2, { width: w - 10 })), 0) + 7;
39590
+ if (doc.y + h2 > doc.page.height - doc.page.margins.bottom) doc.addPage();
39483
39591
  const y0 = doc.y;
39484
39592
  texts.forEach((s2, i) => doc.text(s2, left + i * w + 5, y0 + 3, { width: w - 10 }));
39485
39593
  doc.x = left;
39486
- doc.y = y0 + h;
39594
+ doc.y = y0 + h2;
39487
39595
  rule(doc, HAIR, 2);
39488
39596
  };
39489
39597
  rule(doc, HAIR, 2);
@@ -39632,12 +39740,14 @@ function inDayRange(at, from, to) {
39632
39740
  }
39633
39741
 
39634
39742
  // src/services/live-specs.ts
39743
+ init_src();
39635
39744
  init_db();
39636
- init_pty();
39745
+ init_live_phases();
39637
39746
  init_session_phases();
39638
39747
  init_stage_machine();
39639
39748
  init_notifications2();
39640
39749
  init_sync_notify();
39750
+ import { createHash as createHash4 } from "node:crypto";
39641
39751
 
39642
39752
  // src/services/spec-head.ts
39643
39753
  init_db();
@@ -39659,34 +39769,102 @@ async function recordHeadSha(specId, worktree, read2 = readHead) {
39659
39769
 
39660
39770
  // src/services/live-specs.ts
39661
39771
  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);
39772
+ var selectOf = (shape) => Object.fromEntries(
39773
+ Object.keys(shape).filter((k) => k !== "blockedBy").map((k) => [k, true])
39774
+ );
39775
+ var SLIM_SELECT = selectOf(zSpecSlim.shape);
39776
+ var LIST_SELECT = selectOf(zSpecListItem.shape);
39777
+ var phases = () => sessionPhasesBySpecAsync().catch(() => /* @__PURE__ */ new Map());
39778
+ var sortByNumber = (rows) => {
39779
+ const n2 = new Map(rows.map((r) => [r.id, specNum(r.id)]));
39780
+ return rows.sort((a, b) => n2.get(b.id) - n2.get(a.id));
39781
+ };
39782
+ async function applyOverlay(specs, live) {
39783
+ if (live.size === 0) return { out: specs, advanced: [], doneNow: [] };
39669
39784
  const advanced = [];
39670
39785
  const doneNow = [];
39671
- const out4 = specs.map((s2) => {
39786
+ const out4 = await Promise.all(specs.map(async (s2) => {
39672
39787
  const entry = live.get(s2.id);
39673
39788
  if (!entry) return s2;
39674
- const next = stageForRun(entry.phases, entry.cwd, s2.id);
39789
+ const next = await stageForRunAsync(entry.phases, entry.cwd, s2.id);
39675
39790
  if (!next || STAGES.indexOf(next) <= STAGES.indexOf(s2.stage)) return s2;
39676
39791
  advanced.push({ id: s2.id, from: s2.stage, stage: next, cwd: entry.cwd });
39677
39792
  if (next === "done") doneNow.push({ specId: s2.id, title: s2.title, projectId: s2.projectId });
39678
39793
  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
- }));
39794
+ }));
39795
+ return { out: out4, advanced, doneNow };
39796
+ }
39797
+ async function liveOverlayTick() {
39798
+ const live = await phases();
39799
+ if (live.size === 0) return;
39800
+ const specs = await prisma.spec.findMany({
39801
+ where: { id: { in: [...live.keys()] } },
39802
+ select: { id: true, stage: true, title: true, projectId: true }
39803
+ });
39804
+ const { advanced, doneNow } = await applyOverlay(specs, live);
39805
+ await Promise.all(advanced.map(async (a) => {
39806
+ const res = await prisma.spec.updateMany({ where: { id: a.id, stage: a.from }, data: { stage: a.stage } }).catch(() => ({ count: 0 }));
39807
+ if (res.count === 0) return;
39808
+ if (a.stage === "done") await recordHeadSha(a.id, a.cwd);
39809
+ await notifySynced("spec", a.id);
39810
+ }));
39687
39811
  await Promise.all(doneNow.map((d) => recordCompletion(d.specId, d.title, d.projectId)));
39812
+ }
39813
+ async function listSpecsLive(filter = {}) {
39814
+ const specs = sortByNumber(await prisma.spec.findMany({
39815
+ where: { projectId: filter.project, source: filter.source },
39816
+ orderBy: { id: "desc" }
39817
+ }));
39818
+ return decorateBlocked((await applyOverlay(specs, await phases())).out);
39819
+ }
39820
+ async function listSelected(select, where = {}) {
39821
+ const rows = sortByNumber(await prisma.spec.findMany({ where, select, orderBy: { id: "desc" } }));
39822
+ const { out: out4 } = await applyOverlay(rows, await phases());
39688
39823
  return decorateBlocked(out4);
39689
39824
  }
39825
+ var slimCache = null;
39826
+ async function slimRows() {
39827
+ const count = await prisma.spec.count();
39828
+ if (slimCache) {
39829
+ const changed = await prisma.spec.findMany({
39830
+ where: { updatedAt: { gte: slimCache.max } },
39831
+ select: SLIM_SELECT
39832
+ });
39833
+ for (const r of changed) {
39834
+ slimCache.rows.set(r.id, r);
39835
+ if (r.updatedAt > slimCache.max) slimCache.max = r.updatedAt;
39836
+ }
39837
+ if (slimCache.rows.size === count) return [...slimCache.rows.values()];
39838
+ }
39839
+ const rows = await prisma.spec.findMany({ select: SLIM_SELECT });
39840
+ 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)) };
39841
+ return rows;
39842
+ }
39843
+ async function listSpecsSlim() {
39844
+ const { out: out4 } = await applyOverlay(sortByNumber(await slimRows()), await phases());
39845
+ return await decorateBlocked(out4);
39846
+ }
39847
+ var listSpecsItems = (filter = {}) => listSelected(LIST_SELECT, { projectId: filter.project, source: filter.source });
39848
+ async function overlayOne(spec) {
39849
+ const live = await phases();
39850
+ return (await applyOverlay([spec], live)).out[0];
39851
+ }
39852
+ async function liveSignature() {
39853
+ const live = await phases();
39854
+ const parts = await Promise.all([...live.entries()].sort(([a], [b]) => a.localeCompare(b)).map(
39855
+ async ([id2, e]) => `${id2}:${e.phases.map((p3) => p3.state[0]).join("")}${phasesComplete(e.phases) ? `:${await planCompleteAsync(e.cwd, id2)}` : ""}`
39856
+ ));
39857
+ return parts.join("|");
39858
+ }
39859
+ async function specsDigest() {
39860
+ const agg = await prisma.spec.aggregate({ _count: { _all: true }, _max: { updatedAt: true }, _sum: { version: true } });
39861
+ const db = `${agg._count._all}:${agg._max.updatedAt?.getTime() ?? 0}:${agg._sum.version ?? 0}`;
39862
+ return createHash4("sha1").update(`${db}|${await liveSignature()}`).digest("hex");
39863
+ }
39864
+ async function liveSpecs(filter = {}) {
39865
+ await liveOverlayTick();
39866
+ return listSpecsLive(filter);
39867
+ }
39690
39868
 
39691
39869
  // src/services/launch-authority.ts
39692
39870
  init_src();
@@ -39721,8 +39899,15 @@ function filterSpecs(specs, f) {
39721
39899
  async function specs_default(app3) {
39722
39900
  app3.get("/specs", async (req) => {
39723
39901
  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);
39902
+ await liveOverlayTick();
39903
+ const items = await listSpecsItems({ project, source });
39904
+ return paginate(filterSpecs(items, { q, stage, priority, startable, dateField, from, to }), page, limit);
39905
+ });
39906
+ app3.get("/specs/:id", async (req, reply) => {
39907
+ const { id: id2 } = req.params;
39908
+ const row = await prisma.spec.findUnique({ where: { id: id2 } });
39909
+ if (!row) return reply.code(404).send({ error: "spec tak ditemukan" });
39910
+ return (await decorateBlocked([await overlayOne(row)]))[0];
39726
39911
  });
39727
39912
  app3.post("/specs", async (req, reply) => {
39728
39913
  const parsed = zCreateSpec.safeParse(req.body);
@@ -40583,7 +40768,7 @@ async function deleteBranches(repoDir, names, opts) {
40583
40768
  // src/services/worktree-list.ts
40584
40769
  import { execFile as execFile11 } from "node:child_process";
40585
40770
  import { realpathSync as realpathSync6 } from "node:fs";
40586
- import { stat as stat2 } from "node:fs/promises";
40771
+ import { stat as stat4 } from "node:fs/promises";
40587
40772
  import { basename as basename6, dirname as dirname11, join as join21, resolve as resolve19, sep as sep6 } from "node:path";
40588
40773
  import { promisify as promisify10 } from "node:util";
40589
40774
  var exec10 = promisify10(execFile11);
@@ -40638,7 +40823,7 @@ function hasWorktreeSession(path, sessionId2, sessions) {
40638
40823
  }
40639
40824
  async function bornAt(path) {
40640
40825
  try {
40641
- const st = await stat2(join21(path, ".git"));
40826
+ const st = await stat4(join21(path, ".git"));
40642
40827
  const ms = st.birthtimeMs > 0 ? st.birthtimeMs : st.mtimeMs;
40643
40828
  return new Date(ms).toISOString();
40644
40829
  } catch {
@@ -40653,10 +40838,10 @@ async function listWorktrees(repoDir, inputs) {
40653
40838
  const trash = resolve19(baseReal, ".worktrees", ".trash");
40654
40839
  const sessions = new Map(inputs.sessions.map((s2) => [real(s2.cwd), { id: s2.id, specId: s2.specId }]));
40655
40840
  const history = /* @__PURE__ */ new Map();
40656
- for (const h of inputs.history ?? []) {
40657
- const path = real(h.cwd);
40841
+ for (const h2 of inputs.history ?? []) {
40842
+ const path = real(h2.cwd);
40658
40843
  const previous = history.get(path);
40659
- if (!previous || h.startedAt > previous.startedAt) history.set(path, h);
40844
+ if (!previous || h2.startedAt > previous.startedAt) history.set(path, h2);
40660
40845
  }
40661
40846
  const raw = parseWorktreePorcelain(text ?? "");
40662
40847
  const mainPath = raw[0] ? resolve19(raw[0].path) : null;
@@ -41054,10 +41239,14 @@ function installSessionHistory() {
41054
41239
 
41055
41240
  // src/services/worktree-project.ts
41056
41241
  init_session_id();
41242
+ var toWorktreeSession = (s2) => ({ cwd: s2.cwd, id: s2.id, specId: s2.specId ?? null });
41057
41243
  function worktreeSessions() {
41058
- return listSessions().map((s2) => ({ cwd: s2.cwd, id: s2.id, specId: s2.specId ?? null }));
41244
+ return listSessions().map(toWorktreeSession);
41245
+ }
41246
+ async function worktreeSessionsAsync() {
41247
+ return (await listSessionsAsync()).map(toWorktreeSession);
41059
41248
  }
41060
- async function projectWorktreeInputs(projectId, sessions = worktreeSessions) {
41249
+ async function projectWorktreeInputs(projectId, sessions = worktreeSessionsAsync) {
41061
41250
  const [specs, history] = await Promise.all([
41062
41251
  prisma.spec.findMany({ where: { projectId }, select: { id: true, stage: true } }),
41063
41252
  worktreeHistory(projectId)
@@ -41065,7 +41254,7 @@ async function projectWorktreeInputs(projectId, sessions = worktreeSessions) {
41065
41254
  return {
41066
41255
  specs: new Map(specs.map((s2) => [sessionIdForSpec(s2.id), s2])),
41067
41256
  history,
41068
- sessions: sessions()
41257
+ sessions: await sessions()
41069
41258
  };
41070
41259
  }
41071
41260
  async function detectOrphanWorktrees(deps = { repos: prodReaperDeps.repos, inputs: projectWorktreeInputs }) {
@@ -41665,7 +41854,7 @@ async function finishGraphOp(reply, id2, repoDir, r, verb) {
41665
41854
  }
41666
41855
 
41667
41856
  // src/routes/fs.ts
41668
- import { readdir as readdir5 } from "node:fs/promises";
41857
+ import { readdir as readdir6 } from "node:fs/promises";
41669
41858
  import { homedir as homedir4 } from "node:os";
41670
41859
  import { resolve as resolve21, dirname as dirname12, join as join23 } from "node:path";
41671
41860
  async function fs_default(app3) {
@@ -41673,7 +41862,7 @@ async function fs_default(app3) {
41673
41862
  const q = req.query.path;
41674
41863
  const dir2 = q && q.trim() ? resolve21(q.trim()) : homedir4();
41675
41864
  try {
41676
- const ents = await readdir5(dir2, { withFileTypes: true });
41865
+ const ents = await readdir6(dir2, { withFileTypes: true });
41677
41866
  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
41867
  const parent = dirname12(dir2);
41679
41868
  return { path: dir2, parent: parent === dir2 ? null : parent, entries: entries3 };
@@ -41735,10 +41924,10 @@ function resumeState(repoDir, worktree, branchTo, headSha) {
41735
41924
  return base2 ? { worktreeKept: false, base: base2 } : null;
41736
41925
  }
41737
41926
  function buildResumeCtx(repoDir, id2, flow2, worktreeKept) {
41738
- const phases = readPhases(phaseFilePath(repoDir, id2), flow2);
41927
+ const phases2 = readPhases(phaseFilePath(repoDir, id2), flow2);
41739
41928
  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,
41929
+ recorded: phases2.filter((p3) => p3.state === "done" || p3.state === "skipped").map((p3) => `${p3.name} ${p3.state}`),
41930
+ next: phases2.find((p3) => p3.state === "active")?.name,
41742
41931
  worktreeKept
41743
41932
  };
41744
41933
  }
@@ -41834,8 +42023,8 @@ async function startSpecSession(spec, opts) {
41834
42023
  const fullPlan = sessionPhasePlan(setting, opts.flow, agent, { model, effort: effort2 }, opts.phaseOverrides);
41835
42024
  let plan = fullPlan;
41836
42025
  if (fullPlan && isContinue && !isGoalFlow) {
41837
- const phases = fullPlan.phases.filter((p3) => p3.phase === "Execute");
41838
- plan = phases.length ? { ...fullPlan, phases } : null;
42026
+ const phases2 = fullPlan.phases.filter((p3) => p3.phase === "Execute");
42027
+ plan = phases2.length ? { ...fullPlan, phases: phases2 } : null;
41839
42028
  }
41840
42029
  const buildPrompt = (p3) => {
41841
42030
  if (isGoalFlow) {
@@ -41936,7 +42125,7 @@ function setBounded(map, key, value, maxKeys) {
41936
42125
  // src/services/auth.ts
41937
42126
  init_src2();
41938
42127
  init_db();
41939
- import { randomBytes as randomBytes3, scrypt as scryptCb, timingSafeEqual as timingSafeEqual3, createHash as createHash8 } from "node:crypto";
42128
+ import { randomBytes as randomBytes3, scrypt as scryptCb, timingSafeEqual as timingSafeEqual3, createHash as createHash9 } from "node:crypto";
41940
42129
  import { promisify as promisify12 } from "node:util";
41941
42130
  var scrypt = promisify12(scryptCb);
41942
42131
  var COOKIE_NAME = "hn_session";
@@ -41954,7 +42143,7 @@ async function verifyPassword(pw, stored) {
41954
42143
  return key.length === want.length && timingSafeEqual3(key, want);
41955
42144
  }
41956
42145
  var newSessionToken = () => randomBytes3(32).toString("base64url");
41957
- var sessionId = (token) => createHash8("sha256").update(token).digest("hex");
42146
+ var sessionId = (token) => createHash9("sha256").update(token).digest("hex");
41958
42147
  async function createSession2(userId) {
41959
42148
  const token = newSessionToken();
41960
42149
  await prisma.session.create({
@@ -42015,9 +42204,9 @@ function cookieOpts(req) {
42015
42204
 
42016
42205
  // src/services/device-token.ts
42017
42206
  init_db();
42018
- import { randomBytes as randomBytes4, createHash as createHash9 } from "node:crypto";
42207
+ import { randomBytes as randomBytes4, createHash as createHash10 } from "node:crypto";
42019
42208
  var newDeviceToken = () => randomBytes4(32).toString("base64url");
42020
- var tokenHash = (token) => createHash9("sha256").update(token).digest("hex");
42209
+ var tokenHash = (token) => createHash10("sha256").update(token).digest("hex");
42021
42210
  async function issueDeviceToken(userId, name2) {
42022
42211
  const token = newDeviceToken();
42023
42212
  const row = await prisma.deviceToken.create({ data: { userId, name: name2, tokenHash: tokenHash(token) } });
@@ -42253,12 +42442,77 @@ function appendDiag(home3, sessionId2, events) {
42253
42442
  init_notifications2();
42254
42443
  init_stage_machine();
42255
42444
  init_pty();
42445
+
42446
+ // src/services/bounded-sender.ts
42447
+ var HIGH = 1024 * 1024;
42448
+ var CAP = 256 * 1024;
42449
+ var DRAIN_MS = 20;
42450
+ var isData = (frame2) => frame2.startsWith('{"t":"data"');
42451
+ function createBoundedSender(ws2, o = {}) {
42452
+ const high = o.high ?? HIGH;
42453
+ const cap = o.cap ?? CAP;
42454
+ const drainMs = o.drainMs ?? DRAIN_MS;
42455
+ let pending2 = [];
42456
+ let pendingBytes = 0;
42457
+ let timer10;
42458
+ let resyncNeeded = false;
42459
+ const trim = () => {
42460
+ for (let i = 0; i < pending2.length && pendingBytes > cap; ) {
42461
+ if (!isData(pending2[i])) {
42462
+ i++;
42463
+ continue;
42464
+ }
42465
+ pendingBytes -= pending2[i].length;
42466
+ pending2.splice(i, 1);
42467
+ resyncNeeded = true;
42468
+ }
42469
+ };
42470
+ const drain2 = () => {
42471
+ timer10 = void 0;
42472
+ if (ws2.bufferedAmount > high) {
42473
+ timer10 = setTimeout(drain2, drainMs);
42474
+ timer10.unref?.();
42475
+ return;
42476
+ }
42477
+ const out4 = pending2;
42478
+ pending2 = [];
42479
+ pendingBytes = 0;
42480
+ for (const f of out4) ws2.send(f);
42481
+ if (resyncNeeded) {
42482
+ resyncNeeded = false;
42483
+ o.onResync?.();
42484
+ }
42485
+ };
42486
+ return {
42487
+ send(frame2) {
42488
+ if (pending2.length === 0 && ws2.bufferedAmount <= high) {
42489
+ ws2.send(frame2);
42490
+ return;
42491
+ }
42492
+ pending2.push(frame2);
42493
+ pendingBytes += frame2.length;
42494
+ trim();
42495
+ if (!timer10) {
42496
+ timer10 = setTimeout(drain2, drainMs);
42497
+ timer10.unref?.();
42498
+ }
42499
+ },
42500
+ dispose() {
42501
+ if (timer10) clearTimeout(timer10);
42502
+ timer10 = void 0;
42503
+ pending2 = [];
42504
+ pendingBytes = 0;
42505
+ }
42506
+ };
42507
+ }
42508
+
42509
+ // src/routes/terminal.ts
42256
42510
  init_uploads();
42257
42511
 
42258
42512
  // src/services/agent-invocations.ts
42259
42513
  init_src();
42260
42514
  init_db();
42261
- import { createHash as createHash10, randomUUID as randomUUID9 } from "node:crypto";
42515
+ import { createHash as createHash11, randomUUID as randomUUID9 } from "node:crypto";
42262
42516
  import { execFileSync as execFileSync3 } from "node:child_process";
42263
42517
  import { homedir as homedir5 } from "node:os";
42264
42518
  import { readFileSync as readFileSync12, realpathSync as realpathSync7, statSync as statSync4 } from "node:fs";
@@ -42268,7 +42522,7 @@ import { isAbsolute as isAbsolute8, relative as relative4, resolve as resolve22
42268
42522
  init_session_event_token();
42269
42523
  init_session_event_spool();
42270
42524
  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";
42525
+ import { mkdir as mkdir6, open as open3, readdir as readdir7, rm as rm6 } from "node:fs/promises";
42272
42526
  import { join as join25 } from "node:path";
42273
42527
  var MAX_EVENT_BYTES = 1e6;
42274
42528
  var MAX_FILES_PER_DRAIN = 1e3;
@@ -42314,12 +42568,12 @@ async function drainSpool(app3, root, observation) {
42314
42568
  let delivered = 0;
42315
42569
  let examined = 0;
42316
42570
  let readBuffer;
42317
- for (const session of await readdir6(root, { withFileTypes: true })) {
42571
+ for (const session of await readdir7(root, { withFileTypes: true })) {
42318
42572
  if (!session.isDirectory() || !SESSION_ID_RE.test(session.name)) continue;
42319
42573
  const dir2 = join25(root, session.name);
42320
42574
  let entries3;
42321
42575
  try {
42322
- entries3 = await readdir6(dir2, { withFileTypes: true });
42576
+ entries3 = await readdir7(dir2, { withFileTypes: true });
42323
42577
  } catch {
42324
42578
  continue;
42325
42579
  }
@@ -42331,8 +42585,8 @@ async function drainSpool(app3, root, observation) {
42331
42585
  let handle;
42332
42586
  try {
42333
42587
  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");
42588
+ const stat7 = await handle.stat();
42589
+ if (!stat7.isFile() || stat7.size > MAX_EVENT_BYTES) throw new Error("payload terlalu besar");
42336
42590
  readBuffer ??= Buffer.allocUnsafe(MAX_EVENT_BYTES + 1);
42337
42591
  const { bytesRead } = await handle.read(readBuffer, 0, readBuffer.length, 0);
42338
42592
  if (bytesRead > MAX_EVENT_BYTES) throw new Error("payload terlalu besar");
@@ -42405,7 +42659,7 @@ var MAX_TRANSCRIPT_BYTES2 = 10 * 1024 * 1024;
42405
42659
  var ANSI2 = /[\u001b\u009b](?:\][^\u0007]*(?:\u0007|\u001b\\)|\[[0-?]*[ -/]*[@-~]|[@-_])/g;
42406
42660
  var snapshotHashes = /* @__PURE__ */ new Map();
42407
42661
  var keyOf = (x) => `${x.sessionId}\0${x.runtimeInvocationId}`;
42408
- var hash3 = (value) => createHash10("sha256").update(value).digest("hex");
42662
+ var hash3 = (value) => createHash11("sha256").update(value).digest("hex");
42409
42663
  var defaultGitStatus = (cwd) => {
42410
42664
  try {
42411
42665
  return execFileSync3("git", ["-C", cwd, "status", "--porcelain=v1", "-z"], {
@@ -42751,9 +43005,9 @@ async function refreshPhaseInvocations(sessionId2) {
42751
43005
  // src/services/session-dialog.ts
42752
43006
  init_tui_dialog();
42753
43007
  init_pty();
42754
- import { createHash as createHash11 } from "node:crypto";
43008
+ import { createHash as createHash12 } from "node:crypto";
42755
43009
  var DIALOG_CHUNK_MS = 50;
42756
- var screenHashOf = (paneText2) => createHash11("sha256").update(dialogKey(paneText2)).digest("hex").slice(0, 16);
43010
+ var screenHashOf = (paneText2) => createHash12("sha256").update(dialogKey(paneText2)).digest("hex").slice(0, 16);
42757
43011
  var answerable = (paneText2) => {
42758
43012
  const s2 = readDialogScreen(paneText2);
42759
43013
  if (s2?.kind !== "question") return null;
@@ -44363,9 +44617,9 @@ async function terminal_default(app3, opts) {
44363
44617
  app3.get("/terminal/sessions/:id/phases", async (req, reply) => {
44364
44618
  const { id: id2 } = req.params;
44365
44619
  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 };
44620
+ const phases2 = sessionPhases(id2);
44621
+ if (!s2?.flow || !phases2) return reply.code(404).send({ error: "not found" });
44622
+ return { flow: s2.flow, phases: phases2 };
44369
44623
  });
44370
44624
  app3.post("/terminal/sessions/:id/steer", async (req, reply) => {
44371
44625
  const parsed = zTerminalSteerInput.safeParse(req.body);
@@ -44503,7 +44757,6 @@ async function terminal_default(app3, opts) {
44503
44757
  }
44504
44758
  }, (socket2, req) => {
44505
44759
  const { id: id2 } = req.params;
44506
- if (!getSession(id2)) return socket2.close(4004, "not found");
44507
44760
  const isClientRemote = req.remote !== void 0;
44508
44761
  const principal = isClientRemote ? { kind: "remote", id: `client:${req.remote.actor.hubOrigin}:${req.remote.actor.userId}` } : req.wsPrincipal;
44509
44762
  let release;
@@ -44514,14 +44767,38 @@ async function terminal_default(app3, opts) {
44514
44767
  return;
44515
44768
  }
44516
44769
  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);
44770
+ const bounded = createBoundedSender(socket2, {
44771
+ onResync: () => {
44772
+ void screenResyncFrame(id2).then((f) => {
44773
+ if (f) bounded.send(f);
44774
+ });
44775
+ }
44776
+ });
44777
+ const client = { send: (m) => bounded.send(m), close: () => socket2.close() };
44778
+ let ready = false;
44779
+ let closed = false;
44780
+ const early = [];
44781
+ const attachReady = (async () => {
44782
+ const found = await getSessionAsync(id2);
44783
+ if (!found) {
44784
+ socket2.close(4004, "not found");
44785
+ return;
44786
+ }
44787
+ if (closed) return;
44788
+ await attachAsync(id2, client);
44789
+ if (closed) {
44790
+ detach(id2, client);
44791
+ return;
44792
+ }
44793
+ void refreshPhaseInvocations(id2);
44794
+ })().catch(() => {
44795
+ socket2.close(1011, "attach failed");
44796
+ });
44520
44797
  const watch = isClientRemote ? null : createPrincipalWatch({
44521
44798
  check: () => revalidateWsPrincipal(req, principal),
44522
44799
  onRevoked: () => socket2.close(1008, "session revoked")
44523
44800
  });
44524
- socket2.on("message", (raw) => {
44801
+ const onMessage = (raw) => {
44525
44802
  const verdict = guard.accept(raw);
44526
44803
  if (!verdict.ok) {
44527
44804
  socket2.close(verdict.code, verdict.reason);
@@ -44544,10 +44821,20 @@ async function terminal_default(app3, opts) {
44544
44821
  } catch {
44545
44822
  }
44546
44823
  }
44824
+ };
44825
+ socket2.on("message", (raw) => {
44826
+ if (ready) onMessage(raw);
44827
+ else early.push(raw);
44828
+ });
44829
+ void attachReady.then(() => {
44830
+ ready = true;
44831
+ for (const raw of early.splice(0)) onMessage(raw);
44547
44832
  });
44548
44833
  const revalidate = watch ? setInterval(() => watch.refresh(), 6e4) : void 0;
44549
44834
  revalidate?.unref?.();
44550
44835
  socket2.on("close", () => {
44836
+ closed = true;
44837
+ bounded.dispose();
44551
44838
  if (revalidate) clearInterval(revalidate);
44552
44839
  watch?.dispose();
44553
44840
  release();
@@ -45037,13 +45324,13 @@ function mapToCatalog(checks) {
45037
45324
  var isHardened = (checks) => CRITICAL.every((c) => checks.find((x) => x.check === c)?.status === "pass");
45038
45325
  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
45326
  function parseHealth(out4) {
45040
- const h = {};
45327
+ const h2 = {};
45041
45328
  for (const line of out4.split("\n")) {
45042
45329
  const m = line.match(/^HEALTH (\S+) ?(.*)$/);
45043
- if (m) h[m[1]] = m[2].trim();
45330
+ if (m) h2[m[1]] = m[2].trim();
45044
45331
  }
45045
- if (!h.uptime && !h.disk) return null;
45046
- return { uptime: h.uptime ?? "", disk: h.disk ?? "", mem: h.mem ?? "", load: h.load ?? "" };
45332
+ if (!h2.uptime && !h2.disk) return null;
45333
+ return { uptime: h2.uptime ?? "", disk: h2.disk ?? "", mem: h2.mem ?? "", load: h2.load ?? "" };
45047
45334
  }
45048
45335
  var target = (v) => ({ host: v.host, port: v.port, user: v.user, keyPath: v.keyPath });
45049
45336
  var packagedScript = (f) => fileURLToPath4(new URL(`../scripts/vps/${f}`, import.meta.url));
@@ -45625,9 +45912,9 @@ function readAccessToken() {
45625
45912
  return null;
45626
45913
  }
45627
45914
  }
45628
- function normalizeSeverity(s2, pct2) {
45915
+ function normalizeSeverity(s2, pct3) {
45629
45916
  if (s2 === "normal" || s2 === "warning" || s2 === "critical") return s2;
45630
- return pct2 >= 90 ? "critical" : pct2 >= 70 ? "warning" : "normal";
45917
+ return pct3 >= 90 ? "critical" : pct3 >= 70 ? "warning" : "normal";
45631
45918
  }
45632
45919
  function mapWindows(json) {
45633
45920
  const arr = json?.limits;
@@ -45676,7 +45963,7 @@ async function getLimits() {
45676
45963
  }
45677
45964
 
45678
45965
  // src/services/codex-limits.ts
45679
- import { open as open4, readdir as readdir7, stat as stat3 } from "node:fs/promises";
45966
+ import { open as open4, readdir as readdir8, stat as stat5 } from "node:fs/promises";
45680
45967
  import { homedir as homedir9 } from "node:os";
45681
45968
  import { join as join32 } from "node:path";
45682
45969
  var TTL_MS3 = 3e4;
@@ -45694,7 +45981,7 @@ function windowLabel(minutes) {
45694
45981
  if (minutes % 60 === 0) return `${minutes / 60} jam`;
45695
45982
  return `${minutes} menit`;
45696
45983
  }
45697
- var severityOf = (pct2) => pct2 >= 90 ? "critical" : pct2 >= 70 ? "warning" : "normal";
45984
+ var severityOf = (pct3) => pct3 >= 90 ? "critical" : pct3 >= 70 ? "warning" : "normal";
45698
45985
  function toWindow(slot, raw, reached) {
45699
45986
  const minutes = raw.window_minutes ?? 0;
45700
45987
  const usedPct = Math.round(raw.used_percent ?? 0);
@@ -45713,14 +46000,14 @@ function toWindow(slot, raw, reached) {
45713
46000
  async function recentRollouts(dir2) {
45714
46001
  let entries3;
45715
46002
  try {
45716
- entries3 = (await readdir7(dir2, { recursive: true })).filter((f) => f.endsWith(".jsonl"));
46003
+ entries3 = (await readdir8(dir2, { recursive: true })).filter((f) => f.endsWith(".jsonl"));
45717
46004
  } catch {
45718
46005
  return [];
45719
46006
  }
45720
46007
  const stamped = await Promise.all(entries3.map(async (rel) => {
45721
46008
  const full = join32(dir2, rel);
45722
46009
  try {
45723
- return { full, mtime: (await stat3(full)).mtimeMs };
46010
+ return { full, mtime: (await stat5(full)).mtimeMs };
45724
46011
  } catch {
45725
46012
  return null;
45726
46013
  }
@@ -45804,7 +46091,7 @@ async function codex(app3) {
45804
46091
  // src/services/model-catalog.ts
45805
46092
  init_src2();
45806
46093
  init_src();
45807
- import { readFile as readFile5, writeFile as writeFile5, rename as rename4, mkdir as mkdir7 } from "node:fs/promises";
46094
+ import { readFile as readFile6, writeFile as writeFile5, rename as rename4, mkdir as mkdir7 } from "node:fs/promises";
45808
46095
  import { dirname as dirname15, join as join34 } from "node:path";
45809
46096
 
45810
46097
  // src/services/model-catalog-parser.ts
@@ -46059,7 +46346,7 @@ var cacheFile = () => join34(resolveHome(), "model-catalog.json");
46059
46346
  var modelCatalogService = createModelCatalogService({
46060
46347
  probe: probeModelCatalog,
46061
46348
  read: async () => {
46062
- const content = await readFile5(cacheFile(), "utf8");
46349
+ const content = await readFile6(cacheFile(), "utf8");
46063
46350
  if (Buffer.byteLength(content) > 4 * 1024 * 1024) throw new Error("oversized cache");
46064
46351
  return JSON.parse(content);
46065
46352
  },
@@ -46096,6 +46383,17 @@ async function methods(app3) {
46096
46383
  init_src();
46097
46384
  init_update();
46098
46385
  init_pty();
46386
+
46387
+ // src/services/restart.ts
46388
+ init_src();
46389
+ function requestConfigRestart(delayMs = 50) {
46390
+ setTimeout(() => process.exit(CONFIG_RESTART_EXIT), delayMs);
46391
+ }
46392
+ function requestManualRestart(delayMs = 250) {
46393
+ setTimeout(() => process.exit(MANUAL_RESTART_EXIT), delayMs);
46394
+ }
46395
+
46396
+ // src/routes/update.ts
46099
46397
  async function update(app3) {
46100
46398
  app3.get("/update", async () => getUpdateStatus());
46101
46399
  app3.post("/update/apply", async (req, reply) => {
@@ -46111,6 +46409,15 @@ async function update(app3) {
46111
46409
  requestRestartForUpdate();
46112
46410
  return reply.code(202).send({ accepted: true, from, to, liveSessions });
46113
46411
  });
46412
+ app3.post("/restart", async (req, reply) => {
46413
+ const parsed = zUpdateApplyBody.safeParse(req.body ?? {});
46414
+ if (!parsed.success) return reply.code(400).send({ error: "bad-body" });
46415
+ if (!supervised()) return reply.code(409).send({ error: "unsupervised" });
46416
+ const liveSessions = listSessions().filter((s2) => !s2.exited).length;
46417
+ if (!parsed.data.confirm) return reply.code(409).send({ error: "confirm-required", liveSessions });
46418
+ requestManualRestart();
46419
+ return reply.code(202).send({ accepted: true, liveSessions });
46420
+ });
46114
46421
  }
46115
46422
 
46116
46423
  // src/services/events.ts
@@ -46396,6 +46703,7 @@ async function buildTasksPage(f) {
46396
46703
  // src/services/lead/views.ts
46397
46704
  init_db();
46398
46705
  init_pty();
46706
+ init_live_phases();
46399
46707
 
46400
46708
  // src/services/lead/pulse.ts
46401
46709
  init_db();
@@ -46576,13 +46884,7 @@ function findCollisions(areas) {
46576
46884
  return out4;
46577
46885
  }
46578
46886
  var prodPulseDeps = {
46579
- sessions: () => {
46580
- try {
46581
- return listSessions();
46582
- } catch {
46583
- return [];
46584
- }
46585
- },
46887
+ sessions: () => listSessionsAsync().catch(() => []),
46586
46888
  areas: async (s2) => {
46587
46889
  const repoDir = await resolveRepoDir(s2.projectId);
46588
46890
  if (!repoDir) return [];
@@ -46595,14 +46897,8 @@ var prodPulseDeps = {
46595
46897
  return [];
46596
46898
  }
46597
46899
  },
46598
- planDone: planComplete,
46599
- finished: (id2) => {
46600
- try {
46601
- return sessionFinished(id2);
46602
- } catch {
46603
- return false;
46604
- }
46605
- },
46900
+ planDone: planCompleteAsync,
46901
+ finished: (id2) => sessionFinishedAsync(id2).catch(() => false),
46606
46902
  decide,
46607
46903
  decideDeps: prodDecideDeps,
46608
46904
  apply: applyAction,
@@ -46648,10 +46944,10 @@ async function pulse(deps = prodPulseDeps) {
46648
46944
  async function followUpFinished(cfg, optIn, deps) {
46649
46945
  let n2 = 0;
46650
46946
  const opt = new Set(optIn);
46651
- for (const s2 of deps.sessions()) {
46947
+ for (const s2 of await deps.sessions()) {
46652
46948
  if (!s2.exited || !s2.specId || !opt.has(s2.projectId)) continue;
46653
46949
  const bad = (s2.exitCode ?? 0) !== 0;
46654
- const unfinished = !deps.planDone(s2.cwd, s2.specId);
46950
+ const unfinished = !await deps.planDone(s2.cwd, s2.specId);
46655
46951
  if (!bad && !unfinished) continue;
46656
46952
  const mark = `Sesi ${s2.id} untuk backlog ${s2.specId}`;
46657
46953
  const seen2 = await prisma.leadDecision.findFirst({
@@ -46690,10 +46986,10 @@ async function followUpFinished(cfg, optIn, deps) {
46690
46986
  async function followUpComplete(optIn, deps) {
46691
46987
  let n2 = 0;
46692
46988
  const opt = new Set(optIn);
46693
- for (const s2 of deps.sessions()) {
46989
+ for (const s2 of await deps.sessions()) {
46694
46990
  if (!s2.specId || !opt.has(s2.projectId)) continue;
46695
46991
  if ((s2.exitCode ?? 0) !== 0) continue;
46696
- if (!deps.finished(s2.id)) continue;
46992
+ if (!await deps.finished(s2.id)) continue;
46697
46993
  const mark = `Backlog ${s2.specId} sudah selesai di sesi ${s2.id}`;
46698
46994
  const seen2 = await prisma.leadDecision.findFirst({
46699
46995
  where: { sessionId: s2.id, gate: "pulse", question: { startsWith: mark } }
@@ -46732,7 +47028,7 @@ async function followUpComplete(optIn, deps) {
46732
47028
  }
46733
47029
  async function detectCollisions(optIn, deps) {
46734
47030
  const opt = new Set(optIn);
46735
- const live = deps.sessions().filter((s2) => !s2.exited && s2.specId && opt.has(s2.projectId));
47031
+ const live = (await deps.sessions()).filter((s2) => !s2.exited && s2.specId && opt.has(s2.projectId));
46736
47032
  if (live.length < 2) return 0;
46737
47033
  const areas = [];
46738
47034
  for (const s2 of live) {
@@ -46827,7 +47123,7 @@ async function orderProject(projectId, deps) {
46827
47123
 
46828
47124
  // src/services/lead/engine.ts
46829
47125
  init_notifications2();
46830
- init_pty();
47126
+ init_live_phases();
46831
47127
  var HOUSEKEEPING_MS = 6e4;
46832
47128
  var busyPulse = false;
46833
47129
  var lastPulseAt = 0;
@@ -46862,9 +47158,9 @@ async function tick(now, deps = {}) {
46862
47158
  console.error("lead expire:", e);
46863
47159
  }));
46864
47160
  jobs.push((async () => {
46865
- const live = deps.live ?? liveDecisions;
47161
+ const live = deps.live ?? liveDecisionsAsync;
46866
47162
  const legacy = deps.legacy ?? recordLegacySession;
46867
- for (const s2 of live()) {
47163
+ for (const s2 of await live()) {
46868
47164
  if (s2.eventHook || !s2.waiting) continue;
46869
47165
  await legacy(s2.id, s2.projectId || null, s2.specId ?? null);
46870
47166
  }
@@ -46909,7 +47205,7 @@ async function buildLeadStatus() {
46909
47205
  }
46910
47206
  let waiting2 = [];
46911
47207
  try {
46912
- waiting2 = liveDecisions().filter((d) => d.waiting).map((d) => d.id);
47208
+ waiting2 = (await liveDecisionsAsync()).filter((d) => d.waiting).map((d) => d.id);
46913
47209
  } catch {
46914
47210
  }
46915
47211
  const rows = await Promise.all(projects.map(async (p3) => ({
@@ -46982,11 +47278,48 @@ function parseParams(topic, params) {
46982
47278
  return r.success ? r.data : void 0;
46983
47279
  }
46984
47280
 
47281
+ // src/services/events-profile.ts
47282
+ import { monitorEventLoopDelay } from "node:perf_hooks";
47283
+ var PROFILE = process.env.HANOMAN_EVENTS_PROFILE === "1";
47284
+ var acc = {};
47285
+ var h;
47286
+ var nowUs = () => Number(process.hrtime.bigint() / 1000n);
47287
+ var profStart = () => PROFILE ? nowUs() : 0;
47288
+ function profEnd(group2, t0, bytes, emitted) {
47289
+ if (!PROFILE) return;
47290
+ const a = acc[group2] ??= { builds: [], frames: 0, bytes: 0, ticks: 0 };
47291
+ a.builds.push((nowUs() - t0) / 1e3);
47292
+ a.ticks++;
47293
+ if (emitted) {
47294
+ a.frames++;
47295
+ a.bytes += bytes;
47296
+ }
47297
+ }
47298
+ var pct2 = (xs, p3) => {
47299
+ const s2 = [...xs].sort((a, b) => a - b);
47300
+ return s2[Math.min(s2.length - 1, Math.floor(s2.length * p3))] ?? 0;
47301
+ };
47302
+ function startLagMonitor() {
47303
+ if (!PROFILE || h) return;
47304
+ const mon = h = monitorEventLoopDelay({ resolution: 10 });
47305
+ mon.enable();
47306
+ setInterval(() => {
47307
+ const ms = (n2) => +(n2 / 1e6).toFixed(1);
47308
+ 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}`);
47309
+ console.log(`[events-profile] loop p50=${ms(mon.percentile(50))} p99=${ms(mon.percentile(99))} max=${ms(mon.max)}ms | ${rows.join(" | ")}`);
47310
+ mon.reset();
47311
+ for (const k of Object.keys(acc)) delete acc[k];
47312
+ }, 1e4).unref();
47313
+ }
47314
+
46985
47315
  // src/services/events.ts
46986
47316
  var clients = /* @__PURE__ */ new Set();
46987
47317
  var cookieClients = /* @__PURE__ */ new WeakSet();
46988
47318
  var clientGroups = /* @__PURE__ */ new WeakMap();
46989
47319
  var tickMs = () => effectiveInt("HANOMAN_EVENTS_TICK_MS") ?? 1e3;
47320
+ var presenceKey = (m) => JSON.stringify({ ...m, devices: m.devices.map(
47321
+ (d) => d.local ? { ...d, lastSeenAt: null } : d
47322
+ ) });
46990
47323
  var GROUPS = [
46991
47324
  { everyTicks: 3, cookieOnly: true, last: "", build: async () => ({
46992
47325
  t: "models",
@@ -47002,7 +47335,16 @@ var GROUPS = [
47002
47335
  // loop — termasuk frame ketikan terminal — selama spawn (terukur sampai 916 ms saat mesin sibuk).
47003
47336
  sessions: (await listSessionsAsync()).map((s2) => isDeciding(s2.id) ? { ...s2, deciding: true } : s2)
47004
47337
  }) },
47005
- { everyTicks: 1, last: "", build: async () => ({ t: "specs", specs: await liveSpecs() }) },
47338
+ // SPEC-1267 · frame ringkas (`SpecSlim`: tanpa payload/objective/sourceHistory) yang hanya dibangun
47339
+ // saat `specsDigest` berubah. Kemajuan stage dari fase sesi (`liveOverlayTick`) tetap dijalankan
47340
+ // tiap tick: ia hidup di berkas/tmux, bukan di DB, jadi digest DB tak melihatnya sebelum ditulis.
47341
+ {
47342
+ everyTicks: 1,
47343
+ last: "",
47344
+ pre: liveOverlayTick,
47345
+ gate: specsDigest,
47346
+ build: async () => ({ t: "specs", specs: await listSpecsSlim() })
47347
+ },
47006
47348
  { everyTicks: 3, last: "", build: async () => ({ t: "notifications", ...await notificationsFeed() }) },
47007
47349
  // SPEC-742 · ADR-0116 · pembersihan worktree yang masih jalan. `listCleanups()` membaca peta di
47008
47350
  // memori, bukan disk — nol I/O per tick, dan dedup signature membuat frame lahir hanya saat
@@ -47028,7 +47370,15 @@ var GROUPS = [
47028
47370
  // dihitung). 3 dtk: presence berdenyut 30 dtk, jadi kadens lebih rapat hanya menambah build tanpa
47029
47371
  // menambah informasi. `presenceView` menyegarkan sesi mesin ini sendiri di dalamnya — satu
47030
47372
  // `tmux list-panes` asinkron, tak menahan event loop.
47031
- { everyTicks: 3, cookieOnly: true, last: "", build: async () => ({ t: "presence", ...await presenceView() }) },
47373
+ // SPEC-1267 · `lastSeenAt` device lokal = jam build ini, jadi berubah tiap build; kunci dedup
47374
+ // mengosongkannya supaya frame hanya lahir saat isi presence benar-benar berubah.
47375
+ {
47376
+ everyTicks: 3,
47377
+ cookieOnly: true,
47378
+ last: "",
47379
+ build: async () => ({ t: "presence", ...await presenceView() }),
47380
+ sig: presenceKey
47381
+ },
47032
47382
  // SPEC-961 · grup GLOBAL ke-11 · angka "butuh pengajuan" untuk badge sidebar. 5 dtk: badge bukan
47033
47383
  // board — yang dijanjikannya adalah "ada yang menunggu", bukan detik keberapa ia muncul — dan
47034
47384
  // dedup signature membuat frame lahir hanya saat salah satu angka berubah (jarang). Empat angka,
@@ -47037,8 +47387,7 @@ var GROUPS = [
47037
47387
  // id/judul, dan agent token sudah boleh membaca daftar sumbernya lewat capability masing-masing.
47038
47388
  { everyTicks: 5, last: "", build: async () => ({ t: "pending", counts: await pendingCounts() }) }
47039
47389
  ];
47040
- function broadcast2(msg, cookieOnly = false) {
47041
- const s2 = JSON.stringify(msg);
47390
+ function broadcast2(msg, s2, cookieOnly = false) {
47042
47391
  for (const c of clients) {
47043
47392
  if (cookieOnly && !cookieClients.has(c)) continue;
47044
47393
  const groups = clientGroups.get(c);
@@ -47126,31 +47475,54 @@ function subscribeClient(c, subs) {
47126
47475
  var tick2 = 0;
47127
47476
  var busy = false;
47128
47477
  var timer3;
47478
+ async function runGroup(g) {
47479
+ const t0 = profStart();
47480
+ let msg;
47481
+ try {
47482
+ await g.pre?.();
47483
+ let gate2;
47484
+ if (g.gate) {
47485
+ try {
47486
+ gate2 = await g.gate();
47487
+ } catch {
47488
+ gate2 = void 0;
47489
+ }
47490
+ if (gate2 !== void 0 && gate2 === g.lastGate) {
47491
+ profEnd("gate", t0, 0, false);
47492
+ return null;
47493
+ }
47494
+ }
47495
+ msg = await g.build();
47496
+ if (g.failing) {
47497
+ g.failing = false;
47498
+ console.log(`siar dashboard pulih: ${msg.t}`);
47499
+ }
47500
+ const json = JSON.stringify(msg);
47501
+ const key = g.sig ? g.sig(msg) : json;
47502
+ const emit = key !== g.lastSig;
47503
+ profEnd(msg.t, t0, json.length, emit);
47504
+ if (g.gate) g.lastGate = gate2;
47505
+ return emit ? { g, msg, json, key, gate: gate2 } : null;
47506
+ } catch (e) {
47507
+ if (!g.failing) {
47508
+ g.failing = true;
47509
+ console.error("siar dashboard gagal membangun frame:", e);
47510
+ }
47511
+ return null;
47512
+ }
47513
+ }
47129
47514
  async function __tick() {
47130
47515
  if (busy) return;
47131
47516
  busy = true;
47132
47517
  tick2++;
47133
47518
  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);
47519
+ const results = await Promise.all(GROUPS.filter((g) => tick2 % g.everyTicks === 0).map(runGroup));
47520
+ for (const r of results) {
47521
+ if (!r) continue;
47522
+ r.g.last = r.json;
47523
+ r.g.lastSig = r.key;
47524
+ r.g.lastT = r.msg.t;
47525
+ broadcast2(r.msg, r.json, r.g.cookieOnly);
47154
47526
  }
47155
47527
  for (const e of entries2.values()) {
47156
47528
  e.tick++;
@@ -47163,6 +47535,7 @@ async function __tick() {
47163
47535
  }
47164
47536
  function startLoop() {
47165
47537
  if (timer3) return;
47538
+ startLagMonitor();
47166
47539
  timer3 = setInterval(() => {
47167
47540
  void __tick();
47168
47541
  }, tickMs());
@@ -47174,9 +47547,13 @@ function stopLoop() {
47174
47547
  timer3 = void 0;
47175
47548
  }
47176
47549
  tick2 = 0;
47177
- for (const g of GROUPS) g.last = "";
47550
+ for (const g of GROUPS) {
47551
+ g.last = "";
47552
+ g.lastSig = void 0;
47553
+ g.lastGate = void 0;
47554
+ }
47178
47555
  }
47179
- async function attach2(c, o = {}) {
47556
+ async function attach(c, o = {}) {
47180
47557
  clients.add(c);
47181
47558
  if (o.maySubscribe !== false) cookieClients.add(c);
47182
47559
  if (o.groups) clientGroups.set(c, o.groups);
@@ -47187,17 +47564,20 @@ async function attach2(c, o = {}) {
47187
47564
  } catch {
47188
47565
  return;
47189
47566
  }
47190
- for (const g of GROUPS) {
47191
- if (g.cookieOnly && !cookieClients.has(c)) continue;
47192
- let msg;
47567
+ const wanted = GROUPS.filter((g) => !(g.cookieOnly && !cookieClients.has(c)));
47568
+ const frames = await Promise.all(wanted.map(async (g) => {
47569
+ if (g.last && g.lastT) return { t: g.lastT, json: g.last };
47193
47570
  try {
47194
- msg = await g.build();
47571
+ const msg = await g.build();
47572
+ return { t: msg.t, json: JSON.stringify(msg) };
47195
47573
  } catch {
47196
- continue;
47574
+ return null;
47197
47575
  }
47198
- if (o.groups && !o.groups.has(msg.t)) continue;
47576
+ }));
47577
+ for (const f of frames) {
47578
+ if (!f || o.groups && !o.groups.has(f.t)) continue;
47199
47579
  try {
47200
- c.send(JSON.stringify(msg));
47580
+ c.send(f.json);
47201
47581
  } catch {
47202
47582
  return;
47203
47583
  }
@@ -47231,7 +47611,7 @@ async function events_default(app3, opts) {
47231
47611
  const client = { send: (m) => socket2.send(m), close: () => socket2.close() };
47232
47612
  if (req.remote) {
47233
47613
  const ideRead = req.remote.capabilities.includes("ide:read");
47234
- void attach2(client, { maySubscribe: false, groups: remoteEventGroups(ideRead) });
47614
+ void attach(client, { maySubscribe: false, groups: remoteEventGroups(ideRead) });
47235
47615
  socket2.on("close", () => detach2(client));
47236
47616
  return;
47237
47617
  }
@@ -47244,7 +47624,7 @@ async function events_default(app3, opts) {
47244
47624
  return;
47245
47625
  }
47246
47626
  const maySubscribe = canSubscribeTopics(principal);
47247
- void attach2(client, { maySubscribe });
47627
+ void attach(client, { maySubscribe });
47248
47628
  const guard = new WsMessageGuard({ perWindow: SUB_FRAMES_PER_MINUTE });
47249
47629
  socket2.on("message", (raw) => {
47250
47630
  const verdict = guard.accept(raw);
@@ -47354,8 +47734,8 @@ import { join as join36 } from "node:path";
47354
47734
 
47355
47735
  // src/services/device-auth.ts
47356
47736
  function bearerToken2(req) {
47357
- const h = req.headers["authorization"];
47358
- return typeof h === "string" && h.startsWith("Bearer ") ? h.slice(7) : void 0;
47737
+ const h2 = req.headers["authorization"];
47738
+ return typeof h2 === "string" && h2.startsWith("Bearer ") ? h2.slice(7) : void 0;
47359
47739
  }
47360
47740
  async function requireDeviceToken(req, reply) {
47361
47741
  const token = bearerToken2(req);
@@ -47790,7 +48170,7 @@ async function session_history_default(app3) {
47790
48170
  init_src();
47791
48171
  init_session_event_token();
47792
48172
  init_pty();
47793
- var bearer = (h) => /^Bearer (.+)$/.exec(h ?? "")?.[1] ?? "";
48173
+ var bearer = (h2) => /^Bearer (.+)$/.exec(h2 ?? "")?.[1] ?? "";
47794
48174
  var recordOf = (value) => value && typeof value === "object" ? value : null;
47795
48175
  var boundedString = (value, max) => typeof value === "string" && value.length > 0 && value.length <= max ? value : void 0;
47796
48176
  async function session_events_default(app3) {
@@ -47938,9 +48318,9 @@ init_db();
47938
48318
  init_db();
47939
48319
  init_uploads();
47940
48320
  init_src();
47941
- import { createHash as createHash13, randomBytes as randomBytes7 } from "node:crypto";
48321
+ import { createHash as createHash14, randomBytes as randomBytes7 } from "node:crypto";
47942
48322
  function hashAccessKey(key) {
47943
- return createHash13("sha256").update(key).digest("hex");
48323
+ return createHash14("sha256").update(key).digest("hex");
47944
48324
  }
47945
48325
  function generateAccessKey() {
47946
48326
  const key = "hnm_tkt_" + randomBytes7(24).toString("hex");
@@ -50842,8 +51222,8 @@ function systemPromptFor(type, nonce) {
50842
51222
  ${type === "brainstorm" ? BRAINSTORM : TANYA}`;
50843
51223
  }
50844
51224
  function renderTurnPrompt(o) {
50845
- const riwayat = o.history.map((h) => h.role === "klien" ? wrapClientMessage(h.text, o.nonce) : `<jawaban-hanoman>
50846
- ${h.text}
51225
+ const riwayat = o.history.map((h2) => h2.role === "klien" ? wrapClientMessage(h2.text, o.nonce) : `<jawaban-hanoman>
51226
+ ${h2.text}
50847
51227
  </jawaban-hanoman>`).join("\n\n");
50848
51228
  const baru = wrapClientMessage(o.message, o.nonce);
50849
51229
  return riwayat ? `Percakapan sejauh ini:
@@ -51463,8 +51843,8 @@ init_src();
51463
51843
  init_db();
51464
51844
 
51465
51845
  // 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";
51846
+ import { createHash as createHash15, randomBytes as randomBytes11, timingSafeEqual as timingSafeEqual5 } from "node:crypto";
51847
+ import { chmod, lstat as lstat3, mkdir as mkdir10, readFile as readFile8, unlink as unlink5, writeFile as writeFile7 } from "node:fs/promises";
51468
51848
  import { join as join39 } from "node:path";
51469
51849
  var TTL_MS5 = 15 * 6e4;
51470
51850
  var SETUP_TOKEN_FILE = "setup.token";
@@ -51476,7 +51856,7 @@ async function readStored(home3) {
51476
51856
  try {
51477
51857
  const info = await lstat3(tokenPath(home3));
51478
51858
  if (info.isSymbolicLink() || !info.isFile()) throw new BootstrapError("invalid setup proof");
51479
- const [token, expires] = (await readFile7(tokenPath(home3), "utf8")).trim().split("\n");
51859
+ const [token, expires] = (await readFile8(tokenPath(home3), "utf8")).trim().split("\n");
51480
51860
  const expiresAt = Date.parse(expires ?? "");
51481
51861
  if (!token || !Number.isFinite(expiresAt)) throw new BootstrapError("invalid setup proof");
51482
51862
  return { token, expiresAt };
@@ -51516,8 +51896,8 @@ ${new Date(expiresAt).toISOString()}
51516
51896
  }
51517
51897
  async function verifySetupToken(candidate, home3, now = Date.now()) {
51518
51898
  const stored = await readStored(home3);
51519
- const got = createHash14("sha256").update(candidate).digest();
51520
- const want = createHash14("sha256").update(stored.token).digest();
51899
+ const got = createHash15("sha256").update(candidate).digest();
51900
+ const want = createHash15("sha256").update(stored.token).digest();
51521
51901
  if (stored.expiresAt <= now || !timingSafeEqual5(got, want)) throw new BootstrapError("invalid setup proof");
51522
51902
  }
51523
51903
  async function consumeSetupToken(home3) {
@@ -51666,12 +52046,6 @@ function applySetup(home3, input) {
51666
52046
  return next;
51667
52047
  }
51668
52048
 
51669
- // src/services/restart.ts
51670
- init_src();
51671
- function requestConfigRestart(delayMs = 50) {
51672
- setTimeout(() => process.exit(CONFIG_RESTART_EXIT), delayMs);
51673
- }
51674
-
51675
52049
  // src/routes/setup.ts
51676
52050
  async function setup_default(app3, opts) {
51677
52051
  const attempts = new BoundedRateLimiter({ windowMs: 6e4, limit: 10, maxKeys: 4096 });
@@ -52012,7 +52386,7 @@ init_db();
52012
52386
  init_event_log();
52013
52387
  init_transcript_store();
52014
52388
  init_src2();
52015
- import { readFile as readFile8 } from "node:fs/promises";
52389
+ import { readFile as readFile9 } from "node:fs/promises";
52016
52390
  import { join as join40 } from "node:path";
52017
52391
  function decodeCursor(c) {
52018
52392
  if (!c) return null;
@@ -52053,7 +52427,7 @@ async function readRemoteTranscript(id2) {
52053
52427
  if (row.deviceId === "local") return readTranscript(row.transcriptKey);
52054
52428
  const path = join40(resolveDataDirs().home, "remote-transcripts", row.transcriptKey);
52055
52429
  try {
52056
- return await readFile8(path, "utf8");
52430
+ return await readFile9(path, "utf8");
52057
52431
  } catch {
52058
52432
  return null;
52059
52433
  }
@@ -52093,8 +52467,8 @@ async function logs_default(app3) {
52093
52467
  init_agent_token();
52094
52468
  init_settings3();
52095
52469
  function agentTokenFromReq(req) {
52096
- const h = req.headers["authorization"];
52097
- if (typeof h === "string" && h.startsWith("Bearer ")) return h.slice(7);
52470
+ const h2 = req.headers["authorization"];
52471
+ if (typeof h2 === "string" && h2.startsWith("Bearer ")) return h2.slice(7);
52098
52472
  return void 0;
52099
52473
  }
52100
52474
  async function authenticateAgent(token) {
@@ -52415,11 +52789,11 @@ async function auditSweep(now = Date.now) {
52415
52789
  var timers = [];
52416
52790
  function startVpsMonitor() {
52417
52791
  if (timers.length) return;
52418
- const h = setInterval(() => void healthSweep(), HEALTH_MS);
52792
+ const h2 = setInterval(() => void healthSweep(), HEALTH_MS);
52419
52793
  const a = setInterval(() => void auditSweep(), AUDIT_MS);
52420
- h.unref();
52794
+ h2.unref();
52421
52795
  a.unref();
52422
- timers = [h, a];
52796
+ timers = [h2, a];
52423
52797
  void healthSweep();
52424
52798
  void auditSweep();
52425
52799
  }
@@ -52589,10 +52963,10 @@ async function reconcile(deps) {
52589
52963
  await markFailed(item.id, "spec hilang");
52590
52964
  continue;
52591
52965
  }
52592
- const p3 = deps.pane(item.sessionId ?? "");
52966
+ const p3 = await deps.pane(item.sessionId ?? "");
52593
52967
  let stage = spec.stage;
52594
52968
  if (p3?.flow && p3.phaseFile) {
52595
- const d = deps.deriveStage(p3.phaseFile, p3.flow, p3.cwd, item.specId);
52969
+ const d = await deps.deriveStage(p3.phaseFile, p3.flow, p3.cwd, item.specId);
52596
52970
  if (d && STAGES.indexOf(d) > STAGES.indexOf(stage)) {
52597
52971
  const { count } = await prisma.spec.updateMany({ where: { id: item.specId, stage }, data: { stage: d } });
52598
52972
  if (count > 0) await notifySynced("spec", item.specId).catch(() => {
@@ -52625,11 +52999,11 @@ async function reconcile(deps) {
52625
52999
  }
52626
53000
  }
52627
53001
  var reconcileProdDeps = {
52628
- pane: (sessionId2) => {
52629
- const s2 = getSession(sessionId2);
53002
+ pane: async (sessionId2) => {
53003
+ const s2 = await getSessionAsync(sessionId2);
52630
53004
  return s2 ? { exited: s2.exited, flow: s2.flow, phaseFile: s2.phaseFile, cwd: s2.cwd } : void 0;
52631
53005
  },
52632
- deriveStage: (phaseFile, flow2, cwd, specId) => stageForRun(readPhases(phaseFile, flow2), cwd, specId),
53006
+ deriveStage: async (phaseFile, flow2, cwd, specId) => stageForRunAsync(await readPhasesAsync(phaseFile, flow2), cwd, specId),
52633
53007
  headSha: (wt) => {
52634
53008
  try {
52635
53009
  return realGit.headSha(wt);
@@ -53234,13 +53608,13 @@ async function pruneLogs(now, retention, opts = {}) {
53234
53608
  return { logsPruned, logBytesFreed };
53235
53609
  }
53236
53610
  async function reconcileRemoteTranscripts() {
53237
- const { readdir: readdir9, unlink: unlink6, stat: stat5 } = await import("node:fs/promises");
53611
+ const { readdir: readdir10, unlink: unlink6, stat: stat7 } = await import("node:fs/promises");
53238
53612
  const { join: join41 } = await import("node:path");
53239
53613
  const { resolveDataDirs: resolveDataDirs2 } = await Promise.resolve().then(() => (init_src2(), src_exports));
53240
53614
  const root = join41(resolveDataDirs2().home, "remote-transcripts");
53241
53615
  let deviceDirs;
53242
53616
  try {
53243
- deviceDirs = await readdir9(root);
53617
+ deviceDirs = await readdir10(root);
53244
53618
  } catch {
53245
53619
  return { orphans: 0 };
53246
53620
  }
@@ -53251,14 +53625,14 @@ async function reconcileRemoteTranscripts() {
53251
53625
  for (const dev of deviceDirs) {
53252
53626
  let files;
53253
53627
  try {
53254
- files = await readdir9(join41(root, dev));
53628
+ files = await readdir10(join41(root, dev));
53255
53629
  } catch {
53256
53630
  continue;
53257
53631
  }
53258
53632
  for (const f of files) {
53259
53633
  const key = `${dev}/${f}`;
53260
53634
  if (referenced.has(key)) continue;
53261
- const st = await stat5(join41(root, dev, f)).catch(() => null);
53635
+ const st = await stat7(join41(root, dev, f)).catch(() => null);
53262
53636
  if (!st || Date.now() - st.mtimeMs < gracePeriodMs) continue;
53263
53637
  await unlink6(join41(root, dev, f)).catch(() => {
53264
53638
  });