@dadado/agent-kit-cli 4.8.2 → 4.8.3

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.
@@ -5582,7 +5582,7 @@ function renderFlightLogWarningCard(warning, idx) {
5582
5582
  /**
5583
5583
  * Quiet-state open-triage row (untriaged external review). Prompt chrome;
5584
5584
  * per-row Copy triage + path only. No Review all / Resolve all.
5585
- * Cap mirrors FLIGHT_LOG_QUIET_OPEN_TRIAGES_CAP in semantic-model.mjs.
5585
+ * Kind class from flightLogKindClassName('prompt') (SoT map; not a literal).
5586
5586
  * @param {{ id?: string, label?: string, sourcePath?: string, action?: { target?: string, label?: string, subject?: string } }} item
5587
5587
  * @param {number} idx
5588
5588
  */
@@ -5603,8 +5603,9 @@ function renderFlightLogQuietOpenTriageCard(item, idx) {
5603
5603
  const copyTriageHandler = copyForPasteHandler(triageCmd, 'triage command', 'chatInput');
5604
5604
  const copyPathHandler = copyRepoPathHandler(sourcePath);
5605
5605
  const aria = `Review: ${label}`;
5606
+ const kindClass = flightLogKindClassName('prompt');
5606
5607
  return `
5607
- <div class="flight-log-card flight-log-card-current flight-log-kind-advice" role="listitem" tabindex="0" data-focus-key="flight-log-open-triage-${idx}" data-flight-log-kind="prompt" aria-label="${escapeAttr(aria)}">
5608
+ <div class="flight-log-card flight-log-card-current ${kindClass}" role="listitem" tabindex="0" data-focus-key="flight-log-open-triage-${idx}" data-flight-log-kind="prompt" aria-label="${escapeAttr(aria)}">
5608
5609
  <span class="flight-log-card-label">Review</span>
5609
5610
  <div class="flight-log-card-text">${escapeHtml(label)}</div>
5610
5611
  <div class="flight-log-card-meta">${escapeHtml(sourcePath)}</div>
@@ -5616,6 +5617,43 @@ function renderFlightLogQuietOpenTriageCard(item, idx) {
5616
5617
  `;
5617
5618
  }
5618
5619
 
5620
+ /**
5621
+ * Resolve Live Gaps text for Flight Log render + fingerprint (same fallback chain).
5622
+ * Prefer fl.current; with Plan:none, fall back to now.gaps then system.handoff.gaps.
5623
+ */
5624
+ function resolveFlightLogCurrent(d) {
5625
+ const fl = d?.missionControl?.flightLog ?? d?.flightLog ?? null;
5626
+ if (typeof fl?.current === 'string' && fl.current.trim()) return fl.current.trim();
5627
+ const nowGaps = d?.missionControl?.now?.gaps ?? d?.now?.gaps;
5628
+ if (typeof nowGaps === 'string' && nowGaps.trim()) return nowGaps.trim();
5629
+ const handoffGaps = d?.system?.handoff?.gaps;
5630
+ if (typeof handoffGaps === 'string' && handoffGaps.trim()) return handoffGaps.trim();
5631
+ return null;
5632
+ }
5633
+
5634
+ function flightLogHasPastEntries(fl) {
5635
+ if (!fl || !Array.isArray(fl.past)) return false;
5636
+ return fl.past.some((e) => {
5637
+ if (typeof e === 'string') return Boolean(e.trim());
5638
+ return Boolean(e && typeof e.text === 'string' && e.text.trim());
5639
+ });
5640
+ }
5641
+
5642
+ /** Match renderFlightLogWarningCard: text-less warnings do not count. */
5643
+ function flightLogHasWarningEntries(fl) {
5644
+ if (!fl || !Array.isArray(fl.warnings)) return false;
5645
+ return fl.warnings.some((w) => {
5646
+ if (typeof w === 'string') return Boolean(w.trim());
5647
+ return Boolean(w && typeof w.text === 'string' && w.text.trim());
5648
+ });
5649
+ }
5650
+
5651
+ /** Shared quiet gate for renderAttentionPanel + flightLogFingerprint. */
5652
+ function isFlightLogQuiet(d) {
5653
+ const fl = d?.missionControl?.flightLog ?? d?.flightLog ?? null;
5654
+ return !resolveFlightLogCurrent(d) && !flightLogHasPastEntries(fl) && !flightLogHasWarningEntries(fl);
5655
+ }
5656
+
5619
5657
  /**
5620
5658
  * Flight Log panel: HANDOFF Gaps log (live large + earlier smaller) + Warnings lane.
5621
5659
  * When Gaps + Warnings are empty, may show bounded quiet open-triage rows.
@@ -5626,22 +5664,22 @@ function renderAttentionPanel(d, attentionChanged) {
5626
5664
  const sourcePath =
5627
5665
  (typeof fl?.sourcePath === 'string' && fl.sourcePath.trim()) ||
5628
5666
  '.cursor/HANDOFF.md';
5629
- const current =
5630
- typeof fl?.current === 'string' && fl.current.trim()
5631
- ? fl.current.trim()
5632
- : typeof d.missionControl?.now?.gaps === 'string' && d.missionControl.now.gaps.trim()
5633
- ? d.missionControl.now.gaps.trim()
5634
- : typeof d.system?.handoff?.gaps === 'string' && d.system.handoff.gaps.trim()
5635
- ? d.system.handoff.gaps.trim()
5636
- : null;
5667
+ const current = resolveFlightLogCurrent(d);
5637
5668
  const past = Array.isArray(fl?.past) ? fl.past : [];
5638
5669
  const warnings = Array.isArray(fl?.warnings) ? fl.warnings : [];
5670
+ const quietCap =
5671
+ typeof d.missionControl?.flightLogQuietOpenTriagesCap === 'number' &&
5672
+ d.missionControl.flightLogQuietOpenTriagesCap > 0
5673
+ ? Math.floor(d.missionControl.flightLogQuietOpenTriagesCap)
5674
+ : 0;
5639
5675
  const quietOpenTriages = Array.isArray(fl?.quietOpenTriages)
5640
5676
  ? fl.quietOpenTriages
5641
5677
  : Array.isArray(d.missionControl?.attention)
5642
- ? d.missionControl.attention.filter(
5643
- (i) => i && i.kind === 'report' && typeof i.sourcePath === 'string' && i.sourcePath.trim(),
5644
- ).slice(0, 5)
5678
+ ? d.missionControl.attention
5679
+ .filter(
5680
+ (i) => i && i.kind === 'report' && typeof i.sourcePath === 'string' && i.sourcePath.trim(),
5681
+ )
5682
+ .slice(0, quietCap)
5645
5683
  : [];
5646
5684
  const pastCards = past
5647
5685
  .filter((e) => e && typeof e.text === 'string' && e.text.trim())
@@ -5675,6 +5713,7 @@ function renderAttentionPanel(d, attentionChanged) {
5675
5713
  const hasCurrent = Boolean(current);
5676
5714
  const hasPast = pastCards.length > 0;
5677
5715
  const hasWarnings = warningCards.length > 0;
5716
+ // Keep local flags aligned with isFlightLogQuiet(d) for the quiet branch.
5678
5717
  if (!hasCurrent && !hasPast && !hasWarnings) {
5679
5718
  if (hasOpenTriages) {
5680
5719
  body = `<div class="flight-log-stack" role="list" aria-label="Reviews awaiting triage">${openTriageCards}</div>`;
@@ -5780,18 +5819,48 @@ function previousStageElapsedTitle(now) {
5780
5819
  return `Previous step elapsed ${formatElapsedPlain(row.elapsedMs)}`;
5781
5820
  }
5782
5821
 
5783
- function flightLogFingerprint(fl) {
5822
+ /**
5823
+ * Fingerprint Flight Log for SSE re-render. Accepts full dashboard `d` or a
5824
+ * slim prev snapshot `{ flightLog, now, system }` so quiet-gate matches render.
5825
+ */
5826
+ function flightLogFingerprint(d) {
5827
+ if (!d || typeof d !== 'object') return '';
5828
+ const fl = d.missionControl?.flightLog ?? d.flightLog ?? null;
5784
5829
  if (!fl || typeof fl !== 'object') return '';
5785
- const current = typeof fl.current === 'string' ? fl.current : '';
5830
+ const current = resolveFlightLogCurrent(d) || '';
5786
5831
  const past = Array.isArray(fl.past)
5787
- ? fl.past.map((e) => (e && typeof e.text === 'string' ? e.text : '')).join('\n')
5832
+ ? fl.past
5833
+ .map((e) =>
5834
+ typeof e === 'string' ? e : e && typeof e.text === 'string' ? e.text : '',
5835
+ )
5836
+ .join('\n')
5788
5837
  : '';
5789
5838
  const warnings = Array.isArray(fl.warnings)
5790
- ? fl.warnings.map((w) => (w && (w.id || w.text) ? String(w.id || w.text) : '')).join('\n')
5791
- : '';
5792
- const openTriages = Array.isArray(fl.quietOpenTriages)
5793
- ? fl.quietOpenTriages.map((i) => (i && (i.id || i.sourcePath) ? String(i.id || i.sourcePath) : '')).join('\n')
5839
+ ? fl.warnings
5840
+ .map((w) =>
5841
+ typeof w === 'string'
5842
+ ? w
5843
+ : w && (w.id || w.text)
5844
+ ? String(w.id || w.text)
5845
+ : '',
5846
+ )
5847
+ .join('\n')
5794
5848
  : '';
5849
+ // Quiet-only: include open-triage ids when Gaps + Warnings are empty so a new
5850
+ // monitor does not flash the non-quiet Flight Log card with no visible change.
5851
+ // Must use the same gate as renderAttentionPanel (Plan:none Gaps fallback).
5852
+ const openTriages =
5853
+ isFlightLogQuiet(d) && Array.isArray(fl.quietOpenTriages)
5854
+ ? fl.quietOpenTriages
5855
+ .map((i) =>
5856
+ typeof i === 'string'
5857
+ ? i
5858
+ : i && (i.id || i.sourcePath)
5859
+ ? String(i.id || i.sourcePath)
5860
+ : '',
5861
+ )
5862
+ .join('\n')
5863
+ : '';
5795
5864
  return `${current}\0${past}\0${warnings}\0${openTriages}`;
5796
5865
  }
5797
5866
 
@@ -6085,8 +6154,12 @@ function renderUnsafe() {
6085
6154
  const nowChanged = prevData && nowFingerprint(d.missionControl?.now) !== nowFingerprint(prevData.now);
6086
6155
  const attentionChanged =
6087
6156
  prevData &&
6088
- flightLogFingerprint(d.missionControl?.flightLog) !==
6089
- flightLogFingerprint(prevData.flightLog);
6157
+ flightLogFingerprint(d) !==
6158
+ flightLogFingerprint({
6159
+ flightLog: prevData.flightLog,
6160
+ now: prevData.now,
6161
+ system: prevData.system,
6162
+ });
6090
6163
  prevData = snapshotPrevData(d);
6091
6164
 
6092
6165
  dataLoading = false;
@@ -29,6 +29,22 @@ export const DEFAULT_PORT_BASE = 3333;
29
29
  */
30
30
  export const DEFAULT_PORT_RANGE = 256;
31
31
 
32
+ /**
33
+ * Escape a path/string for embedding in a Perl double-quoted literal.
34
+ * Scoped npm package paths contain `@` (e.g. `node_modules/@dadado/...`);
35
+ * unescaped `@name` is array interpolation in Perl and strips the scope segment,
36
+ * so macOS detach-start via `perl -e` cannot exec `dashboard/serve.mjs`.
37
+ * @param {string} value
38
+ * @returns {string}
39
+ */
40
+ export function escapePerlDoubleQuoted(value) {
41
+ return String(value)
42
+ .replace(/\\/g, "\\\\")
43
+ .replace(/"/g, '\\"')
44
+ .replace(/\$/g, "\\$")
45
+ .replace(/@/g, "\\@");
46
+ }
47
+
32
48
  /**
33
49
  * Resolve the repository root Mission Control should snapshot.
34
50
  * @param {NodeJS.ProcessEnv | Record<string, string | undefined>} [env]
@@ -141,7 +157,9 @@ export function sameRepoRoot(a, b) {
141
157
  export function resolveMissionControlPort({ repoRoot, envPort, probe, opts = {} }) {
142
158
  const root = resolve(String(repoRoot || "").trim() || ".");
143
159
  const raw =
144
- envPort != null && String(envPort).trim() !== "" ? Number.parseInt(String(envPort), 10) : NaN;
160
+ envPort != null && String(envPort).trim() !== ""
161
+ ? Number.parseInt(String(envPort), 10)
162
+ : Number.NaN;
145
163
 
146
164
  if (Number.isFinite(raw) && raw > 0) {
147
165
  const info = probe(raw);
@@ -2,6 +2,10 @@
2
2
  // Pure Mission Control view-model helpers (testable; no fs/git I/O).
3
3
 
4
4
  import { truncateStr } from "./guards.mjs";
5
+ import { TRIAGE_HEADING_RE, hasTriageHeading } from "./triage-heading.mjs";
6
+
7
+ /** Durable triage heading SoT (shared with CLI `monitors --untriaged`). */
8
+ export { TRIAGE_HEADING_RE, hasTriageHeading };
5
9
 
6
10
  export const MAX_ACTIVITY = 28;
7
11
  export const MAX_ATTENTION = 15;
@@ -62,15 +66,6 @@ export const MAX_CHECKLIST_NOTES = 15;
62
66
  * External review reports are `.cursor/memory/plan-monitor-<slug>.md`. */
63
67
  export const EXTERNAL_REPORT_FILE_RE = /^plan-monitor-(.+)\.md$/;
64
68
 
65
- /**
66
- * A heading the triage step leaves behind in the report itself. Confirmed
67
- * against the local reports: `## Triage note - residual (A) verified` and
68
- * `## Follow-up plan - hitl_ask_questions_residuals_2026_07_20.plan.md`.
69
- * `/plan-review-triage` must write one of these for every outcome, including
70
- * Ack and stop, so Field Report can clear the untriaged row.
71
- */
72
- export const TRIAGE_HEADING_RE = /^#{2,6}\s+.*\b(triage|follow-?up plan|residuals plan)\b/im;
73
-
74
69
  /**
75
70
  * Local Field Report dismissals store (IDs only). Valid attention ids that
76
71
  * `/field-report-resolve` may append: External reviews, agent prompts, and
@@ -576,21 +571,23 @@ export function buildFlightLogWarnings(handoff, opts = {}) {
576
571
 
577
572
  /**
578
573
  * Bounded untriaged external-review rows for Flight Log quiet state.
579
- * Filters attention to `kind === "report"` only (no cadence, prompts, readiness,
580
- * or bulk FR CTAs). Used when Gaps + Warnings are empty; callers must not mix
581
- * these rows into a non-quiet Gaps/Warnings stack.
582
- * @param {object[]|null|undefined} attention - buildAttentionItems output
574
+ * Filters to `kind === "report"` with a non-empty `sourcePath` (no cadence,
575
+ * prompts, readiness, or bulk FR CTAs). Prefer calling with
576
+ * `buildExternalReportItems(...)` output so the quiet lane is not starved by
577
+ * the shared `buildAttentionItems` cap. Callers must not mix these rows into a
578
+ * non-quiet Gaps/Warnings stack.
579
+ * @param {object[]|null|undefined} reportOrAttentionItems
583
580
  * @param {{ limit?: number }} [opts]
584
581
  * @returns {object[]}
585
582
  */
586
- export function listFlightLogQuietOpenTriages(attention, opts = {}) {
583
+ export function listFlightLogQuietOpenTriages(reportOrAttentionItems, opts = {}) {
587
584
  const limit =
588
585
  typeof opts.limit === "number" && opts.limit > 0
589
586
  ? Math.floor(opts.limit)
590
587
  : FLIGHT_LOG_QUIET_OPEN_TRIAGES_CAP;
591
- if (!Array.isArray(attention) || attention.length === 0) return [];
588
+ if (!Array.isArray(reportOrAttentionItems) || reportOrAttentionItems.length === 0) return [];
592
589
  const out = [];
593
- for (const item of attention) {
590
+ for (const item of reportOrAttentionItems) {
594
591
  if (!item || item.kind !== "report") continue;
595
592
  if (typeof item.sourcePath !== "string" || !item.sourcePath.trim()) continue;
596
593
  out.push(item);
@@ -1369,26 +1366,33 @@ export function normalizeHandoffGaps(raw) {
1369
1366
 
1370
1367
  /**
1371
1368
  * Classify a Flight Log Gaps/Warning body for palette chrome.
1369
+ * Runs heuristics on whitespace-collapsed text **before** display truncation so
1370
+ * long Gaps whose only warning/prompt/advice keyword sits past MAX_SEMANTIC_LABEL
1371
+ * still match the inline dashboard.html classifier (which does not truncate).
1372
1372
  * @param {string | null | undefined} text
1373
1373
  * @param {{ lane?: 'gaps' | 'warning' }} [opts]
1374
1374
  * @returns {FlightLogMessageKind}
1375
1375
  */
1376
1376
  export function classifyFlightLogMessageKind(text, opts = {}) {
1377
1377
  if (opts.lane === "warning") return "warning";
1378
- const normalized = typeof text === "string" ? normalizeHandoffGaps(text) : null;
1379
- if (normalized == null) return "ok";
1378
+ if (!text || typeof text !== "string") return "ok";
1379
+ const collapsed = text.replace(/\s+/g, " ").trim();
1380
+ if (!collapsed) return "ok";
1381
+ if (/^(none|n\/a)$/i.test(collapsed)) return "ok";
1382
+ if (/^(none|n\/a)\s*[.:,;\/(\-–—…]/i.test(collapsed)) return "ok";
1383
+ if (/^([-–—.…]|empty|no gaps?|cleared|all clear|ok)$/i.test(collapsed)) return "ok";
1380
1384
  if (
1381
- /\bAPI\s*\/\s*usage\s+limit\b|\bAPI\s+usage\s+limit\b|\bSTOPPED:\s*API\b/i.test(normalized) ||
1382
- /\b(hard.?stop|quota\s+pause)\b/i.test(normalized)
1385
+ /\bAPI\s*\/\s*usage\s+limit\b|\bAPI\s+usage\s+limit\b|\bSTOPPED:\s*API\b/i.test(collapsed) ||
1386
+ /\b(hard.?stop|quota\s+pause)\b/i.test(collapsed)
1383
1387
  ) {
1384
1388
  return "warning";
1385
1389
  }
1386
1390
  if (
1387
- /\b(confirm|ask questions|hitl|\bpaste\b|choose\b|approve\b|operator yes)\b/i.test(normalized)
1391
+ /\b(confirm|ask questions|hitl|\bpaste\b|choose\b|approve\b|operator yes)\b/i.test(collapsed)
1388
1392
  ) {
1389
1393
  return "prompt";
1390
1394
  }
1391
- if (/\b(tip:|advice:|consider\b|recommends?\b|recommended\b|prefer\b)/i.test(normalized)) {
1395
+ if (/\b(tip:|advice:|consider\b|recommends?\b|recommended\b|prefer\b)/i.test(collapsed)) {
1392
1396
  return "advice";
1393
1397
  }
1394
1398
  return "residual";
@@ -3544,6 +3548,15 @@ export function buildMissionControlView({
3544
3548
  // Field Report attention inbox left the Flight Log card; builders stay
3545
3549
  // exported for /field-report-resolve + cadence scripts (ADR keep).
3546
3550
  // Quiet Gaps+Warnings: bounded report rows may surface on Flight Log.
3551
+ // Build quiet lane from external reports directly (not capped attention) so
3552
+ // prompt/readiness pressure cannot starve Reviews awaiting triage to All clear.
3553
+ const dismissedForQuiet = new Set(
3554
+ (dismissedIds || []).filter((id) => typeof id === "string" && id.length > 0),
3555
+ );
3556
+ const quietReportItems = buildExternalReportItems(externalReports, plans, {
3557
+ handoff,
3558
+ archivedPlanFiles,
3559
+ }).filter((item) => item && !dismissedForQuiet.has(item.id));
3547
3560
  const attention = buildAttentionItems({
3548
3561
  plans,
3549
3562
  handoff,
@@ -3556,7 +3569,7 @@ export function buildMissionControlView({
3556
3569
  cadenceLedger,
3557
3570
  cadenceConfig,
3558
3571
  });
3559
- flightLog.quietOpenTriages = listFlightLogQuietOpenTriages(attention);
3572
+ flightLog.quietOpenTriages = listFlightLogQuietOpenTriages(quietReportItems);
3560
3573
  // Deprecated: attention owns Field Report rows. Kept empty so older panel
3561
3574
  // code that still reads the field does not double-render.
3562
3575
  const checklistNotes = [];
@@ -3571,6 +3584,8 @@ export function buildMissionControlView({
3571
3584
  plans: classifiedPlans,
3572
3585
  // Crew Monitor hero display cap (SoT for dashboard.html; no HTML literal).
3573
3586
  monitorFeedCap: MONITOR_FEED_CAP,
3587
+ // Quiet open-triage fallback cap for dashboard.html attention mirror.
3588
+ flightLogQuietOpenTriagesCap: FLIGHT_LOG_QUIET_OPEN_TRIAGES_CAP,
3574
3589
  // /run-plan-all queue slice (null outside queue mode). Copy-only data:
3575
3590
  // display order and roles; the panel never writes the queue back.
3576
3591
  runQueue: buildRunQueueView(handoff),
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Durable triage headings written by `/plan-review-triage` (L0).
3
+ *
4
+ * Shared SoT for Mission Control (`isReportTriaged`) and
5
+ * `agent-kit monitors --untriaged`. Match ONLY these heading titles, not tick
6
+ * headings that merely name a `triage-*` to-do id (hyphens are word boundaries,
7
+ * so `\btriage\b` falsely matched those).
8
+ *
9
+ * Allowed forms (case-insensitive; optional suffix after the title):
10
+ * ## Triage note
11
+ * ## Follow-up plan (also "Followup plan")
12
+ * ## Residuals plan
13
+ */
14
+ export const TRIAGE_HEADING_RE =
15
+ /^#{2,6}\s+(?:Triage note|Follow-?up plan|Residuals plan)\b/im;
16
+
17
+ /** True when markdown carries a durable triage heading. */
18
+ export function hasTriageHeading(text) {
19
+ return TRIAGE_HEADING_RE.test(String(text ?? ""));
20
+ }
@@ -7,13 +7,14 @@
7
7
  * prints LAN URL(s) with token. Does not weaken loopback `/dashboard`.
8
8
  */
9
9
 
10
- import { spawn, execFileSync, execSync } from "node:child_process";
10
+ import { execFileSync, execSync, spawn } from "node:child_process";
11
11
  import { existsSync, openSync } from "node:fs";
12
+ import { platform } from "node:os";
12
13
  import { dirname, join } from "node:path";
13
14
  import { fileURLToPath } from "node:url";
14
- import { platform } from "node:os";
15
15
  import {
16
16
  BROADCAST_TOKEN_ENV,
17
+ escapePerlDoubleQuoted,
17
18
  generateBroadcastToken,
18
19
  isLoopbackBindHost,
19
20
  isValidBroadcastToken,
@@ -26,7 +27,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
26
27
  const ROOT = join(__dirname, "..");
27
28
  const SERVE = join(__dirname, "serve.mjs");
28
29
  const LOG = process.env.MISSION_CONTROL_LOG || "/tmp/mission-control-broadcast.log";
29
- const PORT = parseInt(process.env.PORT || "3333", 10);
30
+ const PORT = Number.parseInt(process.env.PORT || "3333", 10);
30
31
  const READY_TIMEOUT_MS = 20_000;
31
32
  const READY_POLL_MS = 250;
32
33
 
@@ -57,11 +58,10 @@ function urlsForProbe(token) {
57
58
 
58
59
  function probeHttp(url) {
59
60
  try {
60
- const code = execFileSync(
61
- "curl",
62
- ["-sf", "-o", "/dev/null", "-w", "%{http_code}", url],
63
- { encoding: "utf8", timeout: 3000 },
64
- ).trim();
61
+ const code = execFileSync("curl", ["-sf", "-o", "/dev/null", "-w", "%{http_code}", url], {
62
+ encoding: "utf8",
63
+ timeout: 3000,
64
+ }).trim();
65
65
  return code === "200";
66
66
  } catch {
67
67
  return false;
@@ -70,11 +70,10 @@ function probeHttp(url) {
70
70
 
71
71
  function listeningPids() {
72
72
  try {
73
- const out = execFileSync(
74
- "lsof",
75
- ["-nP", `-iTCP:${PORT}`, "-sTCP:LISTEN", "-t"],
76
- { encoding: "utf8", timeout: 3000 },
77
- ).trim();
73
+ const out = execFileSync("lsof", ["-nP", `-iTCP:${PORT}`, "-sTCP:LISTEN", "-t"], {
74
+ encoding: "utf8",
75
+ timeout: 3000,
76
+ }).trim();
78
77
  return out ? out.split(/\n+/).filter(Boolean) : [];
79
78
  } catch {
80
79
  return [];
@@ -107,12 +106,13 @@ function detachStart(env) {
107
106
  return;
108
107
  }
109
108
 
110
- const rootEsc = ROOT.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
111
- const serveEsc = SERVE.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
112
- const logEsc = LOG.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
113
- const hostEsc = String(env.HOST).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
114
- const tokenEsc = String(env[BROADCAST_TOKEN_ENV]).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
115
- const portEsc = String(PORT);
109
+ // Escape @/$ so scoped package paths (node_modules/@scope/...) survive Perl qq.
110
+ const rootEsc = escapePerlDoubleQuoted(ROOT);
111
+ const serveEsc = escapePerlDoubleQuoted(SERVE);
112
+ const logEsc = escapePerlDoubleQuoted(LOG);
113
+ const hostEsc = escapePerlDoubleQuoted(String(env.HOST));
114
+ const tokenEsc = escapePerlDoubleQuoted(String(env[BROADCAST_TOKEN_ENV]));
115
+ const portEsc = escapePerlDoubleQuoted(String(PORT));
116
116
  const perl = [
117
117
  "use POSIX qw(setsid);",
118
118
  "exit if fork;",
@@ -169,7 +169,9 @@ function openBrowser(url) {
169
169
  async function main() {
170
170
  const { env, host, token } = resolveBroadcastEnv();
171
171
  if (isLoopbackBindHost(host)) {
172
- console.error("Broadcast refused: bind host resolved to loopback. Set HOST to a non-loopback address.");
172
+ console.error(
173
+ "Broadcast refused: bind host resolved to loopback. Set HOST to a non-loopback address.",
174
+ );
173
175
  process.exit(1);
174
176
  }
175
177
 
@@ -193,9 +195,7 @@ async function main() {
193
195
  detachStart(env);
194
196
  const ready = await waitReady(urls);
195
197
  if (!ready) {
196
- console.error(
197
- `Mission Control broadcast did not answer within ${READY_TIMEOUT_MS}ms.`,
198
- );
198
+ console.error(`Mission Control broadcast did not answer within ${READY_TIMEOUT_MS}ms.`);
199
199
  console.error(`Check the log: ${LOG}`);
200
200
  process.exit(1);
201
201
  }
@@ -15,17 +15,18 @@
15
15
  * Foreground serve for debugging remains: `npm run start:dashboard`.
16
16
  */
17
17
 
18
- import { spawn, execFileSync, execSync } from "node:child_process";
18
+ import { execFileSync, execSync, spawn } from "node:child_process";
19
19
  import { existsSync, openSync } from "node:fs";
20
+ import { platform } from "node:os";
20
21
  import { dirname, join, resolve } from "node:path";
21
22
  import { fileURLToPath } from "node:url";
22
- import { platform } from "node:os";
23
23
  import {
24
24
  REPO_ROOT_ENV,
25
- resolveSnapshotRepoRoot,
25
+ escapePerlDoubleQuoted,
26
+ repoRootLogId,
26
27
  resolveMissionControlPort,
28
+ resolveSnapshotRepoRoot,
27
29
  sameRepoRoot,
28
- repoRootLogId,
29
30
  } from "./lib/guards.mjs";
30
31
 
31
32
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -50,18 +51,15 @@ function setPort(port) {
50
51
  PORT = port;
51
52
  URL = `http://${DISPLAY_HOST}:${PORT}/`;
52
53
  DATA_URL = `http://${DISPLAY_HOST}:${PORT}/dashboard-data.json`;
53
- LOG =
54
- process.env.MISSION_CONTROL_LOG ||
55
- `/tmp/mission-control-${repoRootLogId(ROOT)}.log`;
54
+ LOG = process.env.MISSION_CONTROL_LOG || `/tmp/mission-control-${repoRootLogId(ROOT)}.log`;
56
55
  }
57
56
 
58
57
  function probeHttp(url = URL) {
59
58
  try {
60
- const code = execFileSync(
61
- "curl",
62
- ["-sf", "-o", "/dev/null", "-w", "%{http_code}", url],
63
- { encoding: "utf8", timeout: 3000 },
64
- ).trim();
59
+ const code = execFileSync("curl", ["-sf", "-o", "/dev/null", "-w", "%{http_code}", url], {
60
+ encoding: "utf8",
61
+ timeout: 3000,
62
+ }).trim();
65
63
  return code === "200";
66
64
  } catch {
67
65
  return false;
@@ -70,11 +68,10 @@ function probeHttp(url = URL) {
70
68
 
71
69
  function listeningPids(port = PORT) {
72
70
  try {
73
- const out = execFileSync(
74
- "lsof",
75
- ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"],
76
- { encoding: "utf8", timeout: 3000 },
77
- ).trim();
71
+ const out = execFileSync("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], {
72
+ encoding: "utf8",
73
+ timeout: 3000,
74
+ }).trim();
78
75
  return out ? out.split(/\n+/).filter(Boolean) : [];
79
76
  } catch {
80
77
  return [];
@@ -153,11 +150,12 @@ function detachStart() {
153
150
  }
154
151
 
155
152
  // macOS and other hosts without setsid: Perl double-fork + setsid().
156
- const rootEsc = KIT_ROOT.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
157
- const serveEsc = SERVE.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
158
- const logEsc = LOG.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
159
- const portEsc = String(PORT);
160
- const snapEsc = ROOT.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
153
+ // Escape @/$ so scoped package paths (node_modules/@scope/...) survive Perl qq.
154
+ const rootEsc = escapePerlDoubleQuoted(KIT_ROOT);
155
+ const serveEsc = escapePerlDoubleQuoted(SERVE);
156
+ const logEsc = escapePerlDoubleQuoted(LOG);
157
+ const portEsc = escapePerlDoubleQuoted(String(PORT));
158
+ const snapEsc = escapePerlDoubleQuoted(ROOT);
161
159
  const perl = [
162
160
  "use POSIX qw(setsid);",
163
161
  "exit if fork;",
@@ -258,9 +256,7 @@ async function ensureServer() {
258
256
  detachStart();
259
257
  const ready = await waitReady();
260
258
  if (!ready) {
261
- console.error(
262
- `Mission Control did not answer ${URL} within ${READY_TIMEOUT_MS}ms.`,
263
- );
259
+ console.error(`Mission Control did not answer ${URL} within ${READY_TIMEOUT_MS}ms.`);
264
260
  console.error(`Check the log: ${LOG}`);
265
261
  process.exit(1);
266
262
  }
@@ -280,9 +276,7 @@ async function main() {
280
276
  "Opened in the default browser. In Cursor, Simple Browser or /dashboard also works.",
281
277
  );
282
278
  } else {
283
- console.log(
284
- "Open that URL in a browser (Cursor: Simple Browser, or run /dashboard in chat).",
285
- );
279
+ console.log("Open that URL in a browser (Cursor: Simple Browser, or run /dashboard in chat).");
286
280
  }
287
281
  }
288
282
 
package/dist/index.js CHANGED
@@ -1767,8 +1767,11 @@ import path22 from "path";
1767
1767
  import { defineCommand as defineCommand6 } from "citty";
1768
1768
 
1769
1769
  // src/invariants/hooks-health.ts
1770
- import { access as access4, readFile as readFile6 } from "fs/promises";
1770
+ import { execFile as execFile2 } from "child_process";
1771
+ import { constants as constants2, access as access4, readFile as readFile6, stat } from "fs/promises";
1771
1772
  import path12 from "path";
1773
+ import { promisify as promisify2 } from "util";
1774
+ var execFileAsync2 = promisify2(execFile2);
1772
1775
  var EXPECTED_EVENTS = [
1773
1776
  "sessionStart",
1774
1777
  "preCompact",
@@ -1784,6 +1787,37 @@ async function exists(p) {
1784
1787
  return false;
1785
1788
  }
1786
1789
  }
1790
+ async function isExecutable(p) {
1791
+ try {
1792
+ await access4(p, constants2.X_OK);
1793
+ return true;
1794
+ } catch {
1795
+ return false;
1796
+ }
1797
+ }
1798
+ async function resolveAgentKitCli(rootDir) {
1799
+ const root = path12.resolve(rootDir);
1800
+ const candidates = [
1801
+ path12.join(root, "node_modules", ".bin", "agent-kit"),
1802
+ path12.join(root, "packages", "cli", "dist", "index.js")
1803
+ ];
1804
+ for (const c of candidates) {
1805
+ if (await exists(c)) return c;
1806
+ }
1807
+ try {
1808
+ const { stdout } = await execFileAsync2("which", ["agent-kit"], { encoding: "utf8" });
1809
+ const hit = stdout.trim().split("\n")[0]?.trim();
1810
+ if (hit) return hit;
1811
+ } catch {
1812
+ }
1813
+ return null;
1814
+ }
1815
+ function commandLooksLikeAdapter(command) {
1816
+ const trimmed = command.trim();
1817
+ if (!trimmed) return null;
1818
+ const m = trimmed.match(/(\.cursor\/hooks\/agent\/[A-Za-z0-9._-]+\.sh)\b/);
1819
+ return m?.[1] ?? null;
1820
+ }
1787
1821
  async function assessHooksHealth(rootDir) {
1788
1822
  const root = path12.resolve(rootDir);
1789
1823
  const hooksJsonPath = ".cursor/hooks.json";
@@ -1812,6 +1846,7 @@ async function assessHooksHealth(rootDir) {
1812
1846
  };
1813
1847
  }
1814
1848
  const hooks = parsed.hooks ?? {};
1849
+ const adapterRels = /* @__PURE__ */ new Set();
1815
1850
  for (const event of EXPECTED_EVENTS) {
1816
1851
  const list = hooks[event];
1817
1852
  if (Array.isArray(list) && list.length > 0) {
@@ -1822,6 +1857,8 @@ async function assessHooksHealth(rootDir) {
1822
1857
  if (command.endsWith(".py") || command.includes("python")) {
1823
1858
  reasons.push(`${event} still points at a Python script (${command})`);
1824
1859
  }
1860
+ const rel = commandLooksLikeAdapter(command);
1861
+ if (rel) adapterRels.add(rel);
1825
1862
  }
1826
1863
  } else {
1827
1864
  reasons.push(`missing hook event: ${event}`);
@@ -1830,6 +1867,34 @@ async function assessHooksHealth(rootDir) {
1830
1867
  const resolveLib = path12.join(root, ".cursor", "hooks", "agent", "resolve-agent-kit.sh");
1831
1868
  if (!await exists(resolveLib)) {
1832
1869
  reasons.push("missing `.cursor/hooks/agent/resolve-agent-kit.sh` (thin adapter resolver)");
1870
+ } else if (!await isExecutable(resolveLib)) {
1871
+ reasons.push("`.cursor/hooks/agent/resolve-agent-kit.sh` is not executable (chmod +x)");
1872
+ }
1873
+ for (const rel of adapterRels) {
1874
+ const abs = path12.join(root, rel);
1875
+ if (!await exists(abs)) {
1876
+ reasons.push(`missing adapter script: \`${rel}\``);
1877
+ continue;
1878
+ }
1879
+ try {
1880
+ const st = await stat(abs);
1881
+ if (!st.isFile()) {
1882
+ reasons.push(`adapter path is not a file: \`${rel}\``);
1883
+ continue;
1884
+ }
1885
+ } catch {
1886
+ reasons.push(`unreadable adapter script: \`${rel}\``);
1887
+ continue;
1888
+ }
1889
+ if (!await isExecutable(abs)) {
1890
+ reasons.push(`adapter not executable: \`${rel}\` (chmod +x)`);
1891
+ }
1892
+ }
1893
+ const cli = await resolveAgentKitCli(root);
1894
+ if (!cli) {
1895
+ reasons.push(
1896
+ "agent-kit CLI not resolvable (PATH, node_modules/.bin/agent-kit, or packages/cli/dist)"
1897
+ );
1833
1898
  }
1834
1899
  if (Array.isArray(hooks.stop) && hooks.stop.length > 0) {
1835
1900
  reasons.push("`stop` hook is registered (forbidden; remove it)");
@@ -2240,10 +2305,10 @@ async function detectSafety(rootDir, trackedFiles) {
2240
2305
  import path19 from "path";
2241
2306
 
2242
2307
  // src/scanner/detect-git.ts
2243
- import { execFile as execFile2 } from "child_process";
2308
+ import { execFile as execFile3 } from "child_process";
2244
2309
  import path14 from "path";
2245
- import { promisify as promisify2 } from "util";
2246
- var exec = promisify2(execFile2);
2310
+ import { promisify as promisify3 } from "util";
2311
+ var exec = promisify3(execFile3);
2247
2312
  function remoteHostname(remoteUrl) {
2248
2313
  const scpMatch = remoteUrl.match(/^[^@]+@([^:]+):/);
2249
2314
  if (scpMatch?.[1]) return scpMatch[1].toLowerCase();
@@ -3034,6 +3099,8 @@ var doctorCommand = defineCommand6({
3034
3099
  });
3035
3100
 
3036
3101
  // src/commands/guard.ts
3102
+ import { execFile as execFile4 } from "child_process";
3103
+ import { promisify as promisify4 } from "util";
3037
3104
  import { defineCommand as defineCommand7 } from "citty";
3038
3105
 
3039
3106
  // src/hooks/read-stdin-json.ts
@@ -3075,10 +3142,21 @@ var SECRET_PATTERNS2 = [
3075
3142
  re: /\bsk-[A-Za-z0-9]{20,}\b/
3076
3143
  }
3077
3144
  ];
3145
+ function maskSecretExcerpt(raw) {
3146
+ return raw.replace(/\b(ghp_|sk-|AKIA)([A-Za-z0-9_]{4,})/g, (_m, p1, p2) => {
3147
+ return `${p1}${"*".repeat(Math.min(8, p2.length))}`;
3148
+ }).replace(
3149
+ /(=\s*['"]?)([^\s'"]{4,})/g,
3150
+ (_m, p1, p2) => `${p1}${"*".repeat(Math.min(8, p2.length))}`
3151
+ ).replace(
3152
+ /("(?:password|apiKey|api_key|secret|token|auth)"\s*:\s*")([^"]{4,})(")/gi,
3153
+ (_m, p1, p2, p3) => `${p1}${"*".repeat(Math.min(8, p2.length))}${p3}`
3154
+ );
3155
+ }
3078
3156
  function excerptAround(text, index, len) {
3079
3157
  const start = Math.max(0, index - 8);
3080
3158
  const end = Math.min(text.length, index + len + 8);
3081
- return text.slice(start, end).replace(/\s+/g, " ");
3159
+ return maskSecretExcerpt(text.slice(start, end).replace(/\s+/g, " "));
3082
3160
  }
3083
3161
  function scanTextForSecrets(text) {
3084
3162
  if (!text) return [];
@@ -3105,6 +3183,7 @@ function secretsAdviseMessage(hits) {
3105
3183
 
3106
3184
  // src/invariants/shell-guard.ts
3107
3185
  var CITE2 = "agent-kit guard shell (ADR 2026-07-29_cli-invariants-thin-hook-adapters)";
3186
+ var PROTECTED_BRANCH_RE = /^(?:main|master|prod)$/;
3108
3187
  function normalizeShellCommand(command) {
3109
3188
  return command.replace(/\s+/g, " ").trim();
3110
3189
  }
@@ -3116,11 +3195,61 @@ function shellInvocationHeads(command) {
3116
3195
  function anyHeadMatches(command, re) {
3117
3196
  return shellInvocationHeads(command).some((head) => re.test(head));
3118
3197
  }
3198
+ function isProtectedBranch(name) {
3199
+ return typeof name === "string" && PROTECTED_BRANCH_RE.test(name.trim());
3200
+ }
3201
+ function normalizePushRefspecToken(token) {
3202
+ let t = token.trim();
3203
+ if (t.startsWith("'") && t.endsWith("'") && t.length >= 2 || t.startsWith('"') && t.endsWith('"') && t.length >= 2) {
3204
+ t = t.slice(1, -1).trim();
3205
+ }
3206
+ if (t.startsWith("+")) t = t.slice(1);
3207
+ if (t.startsWith("refs/heads/")) t = t.slice("refs/heads/".length);
3208
+ if (t.startsWith("origin/")) t = t.slice("origin/".length);
3209
+ return t;
3210
+ }
3211
+ function pushHeadHasProtectedDest(head) {
3212
+ if (/HEAD:(?:refs\/heads\/)?(?:main|master|prod)\b/.test(head)) return true;
3213
+ if (/(?:^|\s)-(?:u|--set-upstream)\s+\S+\s+(?:main|master|prod)(?:\s|$)/.test(head)) {
3214
+ return true;
3215
+ }
3216
+ const after = head.replace(/^(?:[\w./-]+\/)?git\s+push\b/, "");
3217
+ for (const raw of after.split(/\s+/).filter(Boolean)) {
3218
+ if (raw.startsWith("-")) continue;
3219
+ const dest = raw.includes(":") ? raw.slice(raw.lastIndexOf(":") + 1) : raw;
3220
+ if (PROTECTED_BRANCH_RE.test(normalizePushRefspecToken(dest))) return true;
3221
+ }
3222
+ return false;
3223
+ }
3224
+ function isBareOrHeadPushToCurrent(head) {
3225
+ if (!/^(?:[\w./-]+\/)?git\s+push\b/.test(head)) return false;
3226
+ if (pushHeadHasProtectedDest(head)) {
3227
+ return false;
3228
+ }
3229
+ if (/(?:^|\s)\+?(?:refs\/heads\/)?(?:origin\/)?(?:staging|develop|homologacao)(?:\s|$|:)/.test(
3230
+ head
3231
+ ) || /HEAD:(?:refs\/heads\/)?(?!main|master|prod)[A-Za-z0-9._/-]+/.test(head)) {
3232
+ return false;
3233
+ }
3234
+ const after = head.replace(/^(?:[\w./-]+\/)?git\s+push\b/, "").trim();
3235
+ const withoutFlags = after.replace(/(?:^|\s)(?:--force|-f|-u|--set-upstream|--tags|--all|--prune)(?=\s|$)/g, " ").replace(/(?:^|\s)--\w[\w-]*(?:=\S+)?/g, " ").replace(/\s+/g, " ").trim();
3236
+ if (!withoutFlags) return true;
3237
+ const tokens = withoutFlags.split(/\s+/);
3238
+ if (tokens.length === 1) return true;
3239
+ if (tokens.length >= 2 && tokens[1] === "HEAD") return true;
3240
+ if (/\bHEAD\b/.test(withoutFlags) && !/HEAD:/.test(withoutFlags)) return true;
3241
+ return false;
3242
+ }
3119
3243
  var SHELL_DENY_RULES = [
3120
3244
  {
3121
3245
  id: "git-checkout-path",
3122
- description: "git checkout -- <paths> discards working-tree edits",
3123
- test: (cmd) => anyHeadMatches(cmd, /^(?:[\w./-]+\/)?git\s+checkout\s+--(?:\s|$)/)
3246
+ description: "git checkout -- / HEAD -- / . discards working-tree edits",
3247
+ test: (cmd) => shellInvocationHeads(cmd).some((head) => {
3248
+ if (!/^(?:[\w./-]+\/)?git\s+checkout\b/.test(head)) return false;
3249
+ if (/\s--(?:\s|$)/.test(head)) return true;
3250
+ if (/\scheckout\s+\.(?:\s|$)/.test(head)) return true;
3251
+ return false;
3252
+ })
3124
3253
  },
3125
3254
  {
3126
3255
  id: "git-restore",
@@ -3142,19 +3271,25 @@ var SHELL_DENY_RULES = [
3142
3271
  {
3143
3272
  id: "git-push-main",
3144
3273
  description: "direct push to main/master/prod bypasses staging",
3145
- test: (cmd) => shellInvocationHeads(cmd).some((head) => {
3274
+ test: (cmd, opts) => shellInvocationHeads(cmd).some((head) => {
3146
3275
  if (!/^(?:[\w./-]+\/)?git\s+push\b/.test(head)) return false;
3147
- return /(?:^|\s)(?:origin\/)?(?:main|master|prod)(?:\s|$|:)/.test(head) || /HEAD:(?:refs\/heads\/)?(?:main|master|prod)\b/.test(head) || /(?:^|\s)-(?:u|--set-upstream)\s+\S+\s+(?:main|master|prod)(?:\s|$)/.test(head);
3276
+ if (pushHeadHasProtectedDest(head)) {
3277
+ return true;
3278
+ }
3279
+ if (isProtectedBranch(opts?.currentBranch) && isBareOrHeadPushToCurrent(head)) {
3280
+ return true;
3281
+ }
3282
+ return false;
3148
3283
  })
3149
3284
  }
3150
3285
  ];
3151
- function evaluateShellCommand(command) {
3286
+ function evaluateShellCommand(command, opts = {}) {
3152
3287
  const normalized = normalizeShellCommand(command);
3153
3288
  if (!normalized) {
3154
3289
  return { permission: "allow" };
3155
3290
  }
3156
3291
  for (const rule of SHELL_DENY_RULES) {
3157
- if (rule.test(normalized)) {
3292
+ if (rule.test(normalized, opts)) {
3158
3293
  const agent_message = `Denied by ${CITE2}: ${rule.description} (rule \`${rule.id}\`). Use /git-staging; never discard human hunks or push protected branches from the agent.`;
3159
3294
  return {
3160
3295
  permission: "deny",
@@ -3168,6 +3303,18 @@ function evaluateShellCommand(command) {
3168
3303
  }
3169
3304
 
3170
3305
  // src/commands/guard.ts
3306
+ var execFileAsync3 = promisify4(execFile4);
3307
+ async function detectCurrentBranch() {
3308
+ try {
3309
+ const { stdout } = await execFileAsync3("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
3310
+ encoding: "utf8"
3311
+ });
3312
+ const branch = stdout.trim();
3313
+ return branch && branch !== "HEAD" ? branch : void 0;
3314
+ } catch {
3315
+ return void 0;
3316
+ }
3317
+ }
3171
3318
  var guardCommand = defineCommand7({
3172
3319
  meta: {
3173
3320
  name: "guard",
@@ -3196,7 +3343,8 @@ var guardCommand = defineCommand7({
3196
3343
  const payload = await readStdinJson();
3197
3344
  command = typeof payload.command === "string" ? payload.command : "";
3198
3345
  }
3199
- const result = evaluateShellCommand(command);
3346
+ const currentBranch = await detectCurrentBranch();
3347
+ const result = evaluateShellCommand(command, { currentBranch });
3200
3348
  console.log(JSON.stringify(result));
3201
3349
  }
3202
3350
  }),
@@ -3224,7 +3372,7 @@ var guardCommand = defineCommand7({
3224
3372
  continue: true,
3225
3373
  user_message: secretsAdviseMessage(hits),
3226
3374
  agent_message: secretsAdviseMessage(hits),
3227
- hits
3375
+ hits: hits.map((h) => ({ patternId: h.patternId }))
3228
3376
  })
3229
3377
  );
3230
3378
  }
@@ -4299,12 +4447,16 @@ import path30 from "path";
4299
4447
  import { defineCommand as defineCommand12 } from "citty";
4300
4448
 
4301
4449
  // src/invariants/monitors-untriaged.ts
4302
- import { execFile as execFile3 } from "child_process";
4303
- import { readFile as readFile14, readdir as readdir3, stat } from "fs/promises";
4450
+ import { execFile as execFile5 } from "child_process";
4451
+ import { readFile as readFile14, readdir as readdir3, stat as stat2 } from "fs/promises";
4304
4452
  import path29 from "path";
4305
- import { promisify as promisify3 } from "util";
4306
- var execFileAsync2 = promisify3(execFile3);
4307
- var TRIAGE_HEADING_RE = /^#{2,6}\s+.*\b(triage|follow-?up plan|residuals plan)\b/im;
4453
+ import { promisify as promisify5 } from "util";
4454
+
4455
+ // src/invariants/triage-heading.ts
4456
+ var TRIAGE_HEADING_RE = /^#{2,6}\s+(?:Triage note|Follow-?up plan|Residuals plan)\b/im;
4457
+
4458
+ // src/invariants/monitors-untriaged.ts
4459
+ var execFileAsync4 = promisify5(execFile5);
4308
4460
  var CITE4 = "agent-kit monitors --untriaged (ADR 2026-07-27_plan-review-triage-untriaged-not-mtime; never newest-mtime-wins)";
4309
4461
  function hasOpenGaps(content) {
4310
4462
  if (/###\s+Still open[^\n]*\n+(?:\s*\n)*(?:None\.|none\.|\*None\*)/i.test(content)) {
@@ -4330,7 +4482,7 @@ async function listMonitorFiles(memoryDir) {
4330
4482
  async function gitFreshMonitorNames(rootDir) {
4331
4483
  const names = /* @__PURE__ */ new Set();
4332
4484
  try {
4333
- const { stdout } = await execFileAsync2(
4485
+ const { stdout } = await execFileAsync4(
4334
4486
  "git",
4335
4487
  ["status", "--porcelain", "--", ".cursor/memory"],
4336
4488
  { cwd: rootDir, maxBuffer: 2 * 1024 * 1024 }
@@ -4369,7 +4521,7 @@ async function selectUntriagedMonitors(rootDir) {
4369
4521
  for (const name of allNames) {
4370
4522
  const abs = path29.join(memoryDir, name);
4371
4523
  try {
4372
- const [content, st] = await Promise.all([readFile14(abs, "utf8"), stat(abs)]);
4524
+ const [content, st] = await Promise.all([readFile14(abs, "utf8"), stat2(abs)]);
4373
4525
  byName.set(name, { content, mtimeMs: st.mtimeMs });
4374
4526
  } catch {
4375
4527
  }
@@ -5176,10 +5328,10 @@ var statusCommand = defineCommand15({
5176
5328
  import { defineCommand as defineCommand16 } from "citty";
5177
5329
 
5178
5330
  // src/lifecycle/check-updates.ts
5179
- import { execFile as execFile4 } from "child_process";
5331
+ import { execFile as execFile6 } from "child_process";
5180
5332
  import path37 from "path";
5181
- import { promisify as promisify4 } from "util";
5182
- var execFileAsync3 = promisify4(execFile4);
5333
+ import { promisify as promisify6 } from "util";
5334
+ var execFileAsync5 = promisify6(execFile6);
5183
5335
  var SEMVER_CORE = /^v?(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/i;
5184
5336
  var FACTORY_URL_MARKERS = ["agent-kit-dev"];
5185
5337
  var FACTORY_REFS = /* @__PURE__ */ new Set(["staging", "homologacao", "develop", "dev"]);
@@ -5238,7 +5390,7 @@ function pickLatestSemverTag(lsRemoteStdout) {
5238
5390
  }
5239
5391
  async function fetchLatestPublicVersion(registryUrl = DEFAULT_REGISTRY_URL) {
5240
5392
  assertSafeRegistrySource(registryUrl, "main");
5241
- const { stdout } = await execFileAsync3("git", ["ls-remote", "--tags", "--", registryUrl], {
5393
+ const { stdout } = await execFileAsync5("git", ["ls-remote", "--tags", "--", registryUrl], {
5242
5394
  env: gitEnv2(),
5243
5395
  timeout: 2e4
5244
5396
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dadado/agent-kit-cli",
3
- "version": "4.8.2",
3
+ "version": "4.8.3",
4
4
  "description": "Agent Kit CLI: HITL framework install and tooling for AI-assisted IDEs (rules, skills, plan/handoff, context).",
5
5
  "type": "module",
6
6
  "bin": {