@dadado/agent-kit-cli 6.1.0 → 6.2.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.
Files changed (53) hide show
  1. package/dashboard/README.md +3 -1
  2. package/dashboard/dashboard-data.mjs +13 -2
  3. package/dashboard/dashboard.html +95 -2
  4. package/dashboard/lib/gate-exit-tickets.d.mts +45 -0
  5. package/dashboard/lib/gate-exit-tickets.mjs +233 -0
  6. package/dashboard/lib/semantic-model.d.mts +21 -0
  7. package/dashboard/lib/semantic-model.mjs +47 -0
  8. package/dist/{add-YTCDLPUS.js → add-Y2426TGU.js} +3 -3
  9. package/dist/{chunk-PCCQUWE5.js → chunk-3LLXDHIU.js} +1 -1
  10. package/dist/{chunk-YP4OOK4T.js → chunk-BEKQ6PVD.js} +2 -2
  11. package/dist/{chunk-3DGTPONS.js → chunk-C2SJDYAM.js} +1 -1
  12. package/dist/{chunk-C26I6HTO.js → chunk-C4Q77GGJ.js} +0 -1
  13. package/dist/{chunk-DRND2KYV.js → chunk-CER2APV2.js} +1 -1
  14. package/dist/{chunk-BBVBYQJO.js → chunk-CWDXNBEU.js} +2 -2
  15. package/dist/{chunk-U5F6RJN4.js → chunk-GO3K752H.js} +4 -1
  16. package/dist/chunk-GUQYNUV4.js +61 -0
  17. package/dist/{chunk-TMFOLRU2.js → chunk-IR7FFPUU.js} +8 -8
  18. package/dist/{chunk-ESNAFDYT.js → chunk-JEI6HACW.js} +44 -1
  19. package/dist/chunk-KVY425NM.js +519 -0
  20. package/dist/{chunk-WUWRGOUV.js → chunk-L5V3W3DY.js} +13 -1
  21. package/dist/chunk-MO63KZBP.js +516 -0
  22. package/dist/chunk-MUTTNRX4.js +53 -0
  23. package/dist/{chunk-DBPNGZNH.js → chunk-NSSXSZLL.js} +13 -2
  24. package/dist/chunk-QTBTKEXK.js +77 -0
  25. package/dist/{chunk-PPKX55GI.js → chunk-SPYIP76F.js} +1 -1
  26. package/dist/{chunk-DBXDV5O7.js → chunk-TGNFGMPV.js} +2 -2
  27. package/dist/{chunk-2FQX2O4I.js → chunk-TW4BF5OE.js} +2 -2
  28. package/dist/{chunk-GN4SAZF2.js → chunk-VO73OL5V.js} +2 -2
  29. package/dist/{chunk-VTOUQAVR.js → chunk-X45O2AIE.js} +9 -1
  30. package/dist/{chunk-HFUH3HVS.js → chunk-XDCCYBCJ.js} +7 -1
  31. package/dist/{chunk-TPCCSWK6.js → chunk-YRFSTHR5.js} +30 -526
  32. package/dist/{chunk-3XKVQS2K.js → chunk-Z7ABX5QI.js} +13 -9
  33. package/dist/{contribute-A7NCWTUM.js → contribute-TVST3CJO.js} +4 -4
  34. package/dist/countdown-QXJD6TDB.js +479 -0
  35. package/dist/{dashboard-YSNUGBFY.js → dashboard-YNCFISNO.js} +1 -1
  36. package/dist/{dashboard-broadcast-M4CMW7RX.js → dashboard-broadcast-NK7PU72L.js} +3 -2
  37. package/dist/{diff-4MDU5SLE.js → diff-MECE5RZB.js} +5 -5
  38. package/dist/{doctor-SS6Y2T6H.js → doctor-VLZNXJNA.js} +9 -9
  39. package/dist/ground-control-NMYYIVCV.js +434 -0
  40. package/dist/{handoff-MK5JG3J5.js → handoff-5AGE2D3T.js} +3 -2
  41. package/dist/{hook-D2WHBKRN.js → hook-XCZWX462.js} +7 -6
  42. package/dist/index.js +33 -23
  43. package/dist/{init-GVVBA2BM.js → init-HKMRU2V4.js} +12 -12
  44. package/dist/{install-R4HPI7VK.js → install-X2J6KWZ5.js} +12 -12
  45. package/dist/{mission-control-LJACEVVN.js → mission-control-MHQU73XC.js} +2 -2
  46. package/dist/{plan-index-V7T5MKL6.js → plan-index-BNLUU6T5.js} +3 -2
  47. package/dist/{run-YV6XTFZY.js → run-R66G276Q.js} +63 -15
  48. package/dist/{run-plan-KT443FO6.js → run-plan-24Z5K4P3.js} +17 -10
  49. package/dist/{scan-K6TQGV4S.js → scan-UX4HARMJ.js} +1 -1
  50. package/dist/{setup-global-AT4DD3MG.js → setup-global-ADJFMOZX.js} +6 -6
  51. package/dist/{status-MPJHI5RP.js → status-UB6NL3Q3.js} +8 -8
  52. package/dist/{update-P62637ZI.js → update-PFBFY7GT.js} +9 -9
  53. package/package.json +1 -1
@@ -31,7 +31,9 @@ A host page can replace the copy-to-clipboard default by defining
31
31
  every action is one `{ kind: "copy", text, subject, destination, toastMessage }` record. The web
32
32
  dashboard keeps the copy-only default. Contract, CSP readiness counts and the innerHTML sink
33
33
  audit: `docs/mission-control-embedding.md`. The snapshot shape is versioned by `dashboardDataVersion`
34
- and published as `docs/contracts/mission-control-snapshot.schema.json`.
34
+ and published as `docs/contracts/mission-control-snapshot.schema.json`. The Flight Log lists pending
35
+ operator tickets (gate exits awaiting triage) read-only from `gateExit`
36
+ (`lib/gate-exit-tickets.mjs`, the no-fetch mirror of `agent-kit ground-control status`).
35
37
 
36
38
  ## Git tab and DevOps tab
37
39
 
@@ -7,6 +7,7 @@
7
7
  import { execFileSync } from "node:child_process";
8
8
  import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
9
9
  import { dirname, join, resolve } from "node:path";
10
+ import { collectGateExitTickets } from "./lib/gate-exit-tickets.mjs";
10
11
  import {
11
12
  allowlistConfig,
12
13
  MAX_STRING,
@@ -132,7 +133,7 @@ function redactTerminalOutput(text) {
132
133
 
133
134
  const SNAPSHOT = {
134
135
  _schema: {
135
- version: "1.5.0",
136
+ version: "1.6.0",
136
137
  description: "Mission Control dashboard data model",
137
138
  fields: {
138
139
  generatedAt: "ISO-8601 timestamp of snapshot generation",
@@ -151,6 +152,8 @@ const SNAPSHOT = {
151
152
  "Active Cursor terminal sessions with metadata, output line count, and capped lastOutput",
152
153
  runLogs:
153
154
  "Newest headless agent-kit run / tick logs from <state root>/loop-logs (AGENT_KIT_STATE_ROOT: .cursor by default, or .agent-kit; no ~/.cursor dependency): format, session id, capped last agent text, result, LOOP_TICK_RESULT line, HITL gate ids and reply stamps (1.5.0)",
155
+ gateExit:
156
+ "Pending operator tickets (read-only, never fetches): {marker: absent|ok|invalid, base, tickets[] (stamp, headline, askId, labels, trigger, openedAt, url, branch, branchState unmerged|merged|none, inMarker), skipped[] (latest launcher skip records), truncated} from .cursor/context/gate-exit.json and gate-exit/<stamp> branches; same contract as agent-kit ground-control status (1.6.0)",
154
157
  processes:
155
158
  "Running process snapshots (node, serve.mjs, git operations) with elapsed time and a generated narration per process",
156
159
  skills: "Available skills discovered in .cursor/skills/",
@@ -159,7 +162,7 @@ const SNAPSHOT = {
159
162
  },
160
163
  },
161
164
  generatedAt: new Date().toISOString(),
162
- dashboardDataVersion: "1.5.0",
165
+ dashboardDataVersion: "1.6.0",
163
166
  plans: [],
164
167
  system: {
165
168
  repoRoot: ROOT,
@@ -171,6 +174,7 @@ const SNAPSHOT = {
171
174
  git: {},
172
175
  terminals: [],
173
176
  runLogs: [],
177
+ gateExit: { marker: "absent", base: "staging", tickets: [], skipped: [], truncated: false },
174
178
  processes: [],
175
179
  skills: [],
176
180
  health: { status: "ok", checks: [] },
@@ -779,6 +783,12 @@ if (withinSnapshotBudget(200)) {
779
783
  SNAPSHOT.runLogs = collectRunLogs(ROOT, { redact: redactTerminalOutput });
780
784
  }
781
785
 
786
+ // 7c. Pending operator tickets (gate exits): marker + gate-exit/<stamp> branches, read-only and
787
+ // without a fetch (the CLI's `ground-control status` fetches; a refresh must not).
788
+ if (withinSnapshotBudget(200)) {
789
+ SNAPSHOT.gateExit = collectGateExitTickets(ROOT, { redact: redactTerminalOutput });
790
+ }
791
+
782
792
  // 8. Config
783
793
  const configPath = join(ROOT, ".cursor", "context", "config.json");
784
794
  if (existsSync(configPath)) {
@@ -1372,6 +1382,7 @@ function readPreviousInventory() {
1372
1382
  flightLogLedger: collectFlightLogLedger(),
1373
1383
  cadenceLedger: collectCadenceLedger(),
1374
1384
  cadenceConfig: collectCadenceConfig(),
1385
+ gateExitTickets: SNAPSHOT.gateExit?.tickets || [],
1375
1386
  });
1376
1387
  persistMissionTimingLedger(missionControl.timingLedger);
1377
1388
  persistFlightLogLedger(missionControl.flightLogLedger);
@@ -3397,6 +3397,23 @@ html[data-monitor-density="comfortable"] .live-activity-feed .monitor-row .monit
3397
3397
  gap: 6px;
3398
3398
  margin-bottom: 4px;
3399
3399
  }
3400
+ /* Pending operator tickets lane (read-only; prompt chrome via flightLogKindClassName). */
3401
+ .flight-log-tickets {
3402
+ display: flex;
3403
+ flex-direction: column;
3404
+ gap: 6px;
3405
+ margin-bottom: 4px;
3406
+ }
3407
+ .flight-log-card-ticket {
3408
+ cursor: default;
3409
+ }
3410
+ .flight-log-card-code {
3411
+ margin-top: 6px;
3412
+ font-family: var(--mc-font-mono);
3413
+ font-size: 11px;
3414
+ color: var(--text-secondary);
3415
+ word-break: break-all;
3416
+ }
3400
3417
  .flight-log-card {
3401
3418
  display: block;
3402
3419
  width: 100%;
@@ -6499,6 +6516,13 @@ function snapshotPrevData(d) {
6499
6516
  quietOpenTriages: Array.isArray(d.missionControl.flightLog.quietOpenTriages)
6500
6517
  ? d.missionControl.flightLog.quietOpenTriages.map((i) => i?.id || i?.sourcePath || '')
6501
6518
  : [],
6519
+ tickets: Array.isArray(d.missionControl.flightLog.tickets)
6520
+ ? d.missionControl.flightLog.tickets.map((t) => ({
6521
+ stamp: t?.stamp || '',
6522
+ branchState: t?.branchState || '',
6523
+ text: t?.text || '',
6524
+ }))
6525
+ : [],
6502
6526
  }
6503
6527
  : null,
6504
6528
  };
@@ -7445,6 +7469,52 @@ function renderFlightLogQuietOpenTriageCard(item, idx) {
7445
7469
  `;
7446
7470
  }
7447
7471
 
7472
+ /**
7473
+ * Pending operator ticket card (gate exit awaiting triage). Read-only: no button,
7474
+ * no handler; prompt chrome from flightLogKindClassName('prompt'). Triage stays in
7475
+ * the CLI (`agent-kit ground-control`) or the `ticket` Ask; the card shows the command.
7476
+ * @param {{ id?: string, stamp?: string, title?: string, text?: string, askId?: string|null, branch?: string|null, branchState?: string, url?: string|null, triage?: string, sourcePath?: string }} ticket
7477
+ * @param {number} idx
7478
+ */
7479
+ function renderFlightLogTicketCard(ticket, idx) {
7480
+ const stamp = typeof ticket?.stamp === 'string' ? ticket.stamp.trim() : '';
7481
+ const text = typeof ticket?.text === 'string' ? ticket.text.trim() : '';
7482
+ if (!stamp || !text) return '';
7483
+ const title =
7484
+ typeof ticket?.title === 'string' && ticket.title.trim() ? ticket.title.trim() : 'Pending ticket';
7485
+ const state =
7486
+ ticket?.branchState === 'merged'
7487
+ ? 'merged, ready to accept'
7488
+ : ticket?.branchState === 'unmerged'
7489
+ ? 'awaiting merge'
7490
+ : 'no gate-exit branch';
7491
+ const metaBits = [];
7492
+ if (typeof ticket?.askId === 'string' && ticket.askId) metaBits.push(`Ask ${escapeHtml(ticket.askId)}`);
7493
+ metaBits.push(
7494
+ escapeHtml(typeof ticket?.branch === 'string' && ticket.branch ? `${ticket.branch} (${state})` : state),
7495
+ );
7496
+ if (typeof ticket?.url === 'string' && /^https?:\/\//.test(ticket.url)) metaBits.push(escapeHtml(ticket.url));
7497
+ const triage = typeof ticket?.triage === 'string' && ticket.triage.trim() ? ticket.triage.trim() : '';
7498
+ const aria = `${title} ${stamp}: ${text}`;
7499
+ const kindClass = flightLogKindClassName('prompt');
7500
+ return `
7501
+ <div class="flight-log-card flight-log-card-current flight-log-card-ticket ${kindClass}" role="listitem" tabindex="0" data-focus-key="flight-log-ticket-${idx}" data-flight-log-kind="prompt" data-ticket-stamp="${escapeAttr(stamp)}" aria-label="${escapeAttr(aria)}">
7502
+ <span class="flight-log-card-label">${escapeHtml(title)} · ${escapeHtml(stamp)}</span>
7503
+ <div class="flight-log-card-text">${escapeHtml(text)}</div>
7504
+ <div class="flight-log-card-meta">${metaBits.join(' · ')}</div>
7505
+ ${triage ? `<div class="flight-log-card-code">${escapeHtml(triage)}</div>` : ''}
7506
+ </div>
7507
+ `;
7508
+ }
7509
+
7510
+ /** Pending ticket rows on the Flight Log (stamp + text required, as the card renders). */
7511
+ function flightLogTicketEntries(fl) {
7512
+ if (!fl || !Array.isArray(fl.tickets)) return [];
7513
+ return fl.tickets.filter(
7514
+ (t) => t && typeof t.stamp === 'string' && t.stamp.trim() && typeof t.text === 'string' && t.text.trim(),
7515
+ );
7516
+ }
7517
+
7448
7518
  /**
7449
7519
  * Resolve Live Gaps text for Flight Log render + fingerprint (same fallback chain).
7450
7520
  * Prefer fl.current; with Plan:none, fall back to now.gaps then system.handoff.gaps.
@@ -7537,6 +7607,16 @@ function renderAttentionPanel(d, attentionChanged) {
7537
7607
  .filter(Boolean)
7538
7608
  .join('');
7539
7609
  const hasOpenTriages = openTriageCards.length > 0;
7610
+ // Pending operator tickets render above every branch: a ticket never falls through to All clear.
7611
+ const tickets = flightLogTicketEntries(fl);
7612
+ const ticketCards = tickets
7613
+ .map((t, idx) => renderFlightLogTicketCard(t, idx))
7614
+ .filter(Boolean)
7615
+ .join('');
7616
+ const hasTickets = ticketCards.length > 0;
7617
+ const ticketsBlock = hasTickets
7618
+ ? `<div class="flight-log-tickets" role="list" aria-label="Pending operator tickets (Ground Control)">${ticketCards}</div>`
7619
+ : '';
7540
7620
 
7541
7621
  let body;
7542
7622
  const hasCurrent = Boolean(current);
@@ -7546,6 +7626,8 @@ function renderAttentionPanel(d, attentionChanged) {
7546
7626
  if (!hasCurrent && !hasPast && !hasWarnings) {
7547
7627
  if (hasOpenTriages) {
7548
7628
  body = `<div class="flight-log-stack" role="list" aria-label="Reviews awaiting triage">${openTriageCards}</div>`;
7629
+ } else if (hasTickets) {
7630
+ body = '';
7549
7631
  } else {
7550
7632
  body = renderEmptyStateCta({
7551
7633
  headline: 'All clear',
@@ -7578,8 +7660,9 @@ function renderAttentionPanel(d, attentionChanged) {
7578
7660
  </div>`;
7579
7661
  body = `${warningsBlock}<div class="flight-log-stack" role="list">${currentCard}${pastCards}</div>`;
7580
7662
  }
7663
+ body = `${ticketsBlock}${body}`;
7581
7664
 
7582
- const countLabel = hasCurrent
7665
+ const baseCountLabel = hasCurrent
7583
7666
  ? hasPast
7584
7667
  ? `1 · ${past.length} earlier${hasWarnings ? ` · ${warnings.length} warn` : ''}`
7585
7668
  : hasWarnings
@@ -7592,6 +7675,12 @@ function renderAttentionPanel(d, attentionChanged) {
7592
7675
  : hasOpenTriages
7593
7676
  ? `${quietOpenTriages.length} review`
7594
7677
  : 'Clear';
7678
+ const ticketCountLabel = hasTickets ? `${tickets.length} ticket${tickets.length === 1 ? '' : 's'}` : '';
7679
+ const countLabel = !ticketCountLabel
7680
+ ? baseCountLabel
7681
+ : baseCountLabel === 'Clear'
7682
+ ? ticketCountLabel
7683
+ : `${ticketCountLabel} · ${baseCountLabel}`;
7595
7684
 
7596
7685
  return `
7597
7686
  <div class="card${attentionChanged ? ' card-flash' : ''}" id="attention-panel">
@@ -7695,9 +7784,13 @@ function flightLogFingerprint(d) {
7695
7784
  )
7696
7785
  .join('\n')
7697
7786
  : '';
7787
+ // Pending tickets: stamp + merge state + headline, so a merge or a new note re-renders.
7788
+ const tickets = flightLogTicketEntries(fl)
7789
+ .map((t) => `${t.stamp}|${t.branchState || ''}|${t.text}`)
7790
+ .join('\n');
7698
7791
  // Busy-outside-plan header chip must re-render the card on flip.
7699
7792
  const busy = (d.missionControl?.now ?? d.now)?.busyOutsidePlan?.active === true;
7700
- return `${current}\0${past}\0${warnings}\0${openTriages}\0${busy ? 'busy' : ''}`;
7793
+ return `${current}\0${past}\0${warnings}\0${openTriages}\0${tickets}\0${busy ? 'busy' : ''}`;
7701
7794
  }
7702
7795
 
7703
7796
  // Step progress bar: one segment per plan step (completed / current / remaining).
@@ -0,0 +1,45 @@
1
+ /** Ambient types for dashboard/lib/gate-exit-tickets.mjs (consumed by CLI TypeScript). */
2
+
3
+ export const GATE_EXIT_BRANCH_PREFIX: "gate-exit/";
4
+ export const GATE_EXIT_MARKER_RELPATH: ".cursor/context/gate-exit.json";
5
+ export const GATE_EXIT_NOTE_DIR: ".cursor/gate-exit";
6
+ export const GATE_EXIT_DEFAULT_BASE: "staging";
7
+ export const GATE_EXIT_STAMP_RE: RegExp;
8
+ export const MAX_GATE_EXIT_TICKETS: 10;
9
+ export const MAX_GATE_EXIT_SKIPPED: 5;
10
+ export const MAX_GATE_EXIT_TEXT: 300;
11
+
12
+ export type GitRunner = (args: string[]) => { code: number; stdout: string };
13
+
14
+ export interface GateExitTicketRow {
15
+ stamp: string;
16
+ headline: string | null;
17
+ askId: string | null;
18
+ labels: string[];
19
+ trigger: "gate" | "stopped-by-operator" | "failure" | null;
20
+ openedAt: string | null;
21
+ url: string | null;
22
+ branch: string | null;
23
+ branchState: "unmerged" | "merged" | "none";
24
+ inMarker: boolean;
25
+ }
26
+
27
+ export interface GateExitTickets {
28
+ marker: "absent" | "ok" | "invalid";
29
+ base: string;
30
+ tickets: GateExitTicketRow[];
31
+ skipped: string[];
32
+ truncated: boolean;
33
+ }
34
+
35
+ export function makeGitRunner(root: string, timeoutMs?: number): GitRunner;
36
+ export function readGateExitMarkerSync(
37
+ root: string,
38
+ ):
39
+ | { kind: "absent" }
40
+ | { kind: "invalid" }
41
+ | { kind: "ok"; pending: Record<string, unknown>[]; skipped: string[] };
42
+ export function collectGateExitTickets(
43
+ root: string,
44
+ opts?: { git?: GitRunner; redact?: (s: string) => string; max?: number },
45
+ ): GateExitTickets;
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Pending operator tickets for Mission Control (ADR 2026-10-08
3
+ * countdown-coming-home-generic-lane point 3: "Mission Control shows pending
4
+ * tickets read-only in the Flight Log").
5
+ *
6
+ * Mirrors the read half of `groundControlStatus`
7
+ * (packages/cli/src/commands/ground-control.ts) on the same contract: the
8
+ * gitignored marker `.cursor/context/gate-exit.json`, `gate-exit/<UTC stamp>`
9
+ * branches, the note at `.cursor/gate-exit/<stamp>.md` (first line = ticket
10
+ * headline), and the same merged test (branch tip is an ancestor of the base,
11
+ * or the base holds the note blob byte for byte). A parity test pins this file
12
+ * to the CLI (packages/cli/src/dashboard/gate-exit-tickets.test.ts).
13
+ *
14
+ * Unlike `agent-kit ground-control status` this never fetches: the dashboard
15
+ * re-snapshots on every refresh, so it shows the last fetched state. It never
16
+ * writes, never throws, and costs one `git for-each-ref` when there is no
17
+ * marker and no gate-exit branch.
18
+ */
19
+ import { execFileSync } from "node:child_process";
20
+ import { readFileSync } from "node:fs";
21
+ import { join } from "node:path";
22
+
23
+ export const GATE_EXIT_BRANCH_PREFIX = "gate-exit/";
24
+ export const GATE_EXIT_MARKER_RELPATH = ".cursor/context/gate-exit.json";
25
+ export const GATE_EXIT_NOTE_DIR = ".cursor/gate-exit";
26
+ export const GATE_EXIT_DEFAULT_BASE = "staging";
27
+ export const GATE_EXIT_STAMP_RE = /^\d{8}T\d{6}Z$/;
28
+ export const MAX_GATE_EXIT_TICKETS = 10;
29
+ export const MAX_GATE_EXIT_SKIPPED = 5;
30
+ export const MAX_GATE_EXIT_TEXT = 300;
31
+
32
+ const SAFE_REF_RE = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/;
33
+ const TRIGGERS = new Set(["gate", "stopped-by-operator", "failure", "review"]);
34
+
35
+ const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
36
+
37
+ function cap(text, max = MAX_GATE_EXIT_TEXT) {
38
+ const s = String(text ?? "").trim();
39
+ return s.length > max ? `${s.slice(0, max - 1)}…` : s;
40
+ }
41
+
42
+ function isSafeBaseRef(ref) {
43
+ return (
44
+ typeof ref === "string" &&
45
+ SAFE_REF_RE.test(ref) &&
46
+ !ref.includes("..") &&
47
+ !ref.endsWith("/") &&
48
+ !ref.endsWith(".lock")
49
+ );
50
+ }
51
+
52
+ /** Synchronous git runner: `{ code, stdout }`, never throws. */
53
+ export function makeGitRunner(root, timeoutMs = 5000) {
54
+ return (args) => {
55
+ try {
56
+ const stdout = execFileSync("git", args, {
57
+ cwd: root,
58
+ encoding: "utf-8",
59
+ timeout: timeoutMs,
60
+ stdio: ["ignore", "pipe", "ignore"],
61
+ });
62
+ return { code: 0, stdout };
63
+ } catch (err) {
64
+ const status = isRecord(err) && typeof err.status === "number" ? err.status : 1;
65
+ return { code: status || 1, stdout: "" };
66
+ }
67
+ };
68
+ }
69
+
70
+ /** Marker read with the CLI's validation (`readGateExitMarker`). */
71
+ export function readGateExitMarkerSync(root) {
72
+ let raw;
73
+ try {
74
+ raw = readFileSync(join(root, GATE_EXIT_MARKER_RELPATH), "utf8");
75
+ } catch (err) {
76
+ if (isRecord(err) && err.code === "ENOENT") return { kind: "absent" };
77
+ return { kind: "invalid" };
78
+ }
79
+ try {
80
+ const parsed = JSON.parse(raw);
81
+ if (!isRecord(parsed) || parsed.schema !== 1) return { kind: "invalid" };
82
+ if (!Array.isArray(parsed.pending) || !Array.isArray(parsed.skipped)) {
83
+ return { kind: "invalid" };
84
+ }
85
+ const pending = parsed.pending.filter(
86
+ (e) => isRecord(e) && typeof e.stamp === "string" && GATE_EXIT_STAMP_RE.test(e.stamp),
87
+ );
88
+ return { kind: "ok", pending, skipped: parsed.skipped.map(String) };
89
+ } catch {
90
+ return { kind: "invalid" };
91
+ }
92
+ }
93
+
94
+ /** Base ref chain of `resolveGateExitRefs`: origin/<base>, origin/HEAD, local <base>, HEAD. */
95
+ function resolveBaseRef(git, hasOrigin, requested) {
96
+ let base = isSafeBaseRef(requested) ? requested : GATE_EXIT_DEFAULT_BASE;
97
+ const verify = (ref) => git(["rev-parse", "--verify", "--quiet", `${ref}^{commit}`]).code === 0;
98
+ if (hasOrigin) {
99
+ if (verify(`refs/remotes/origin/${base}`))
100
+ return { base, baseRef: `refs/remotes/origin/${base}` };
101
+ const head = git(["symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"]);
102
+ const fallback = head.stdout.trim().replace(/^origin\//, "");
103
+ if (head.code === 0 && isSafeBaseRef(fallback)) {
104
+ base = fallback;
105
+ return { base, baseRef: `refs/remotes/origin/${fallback}` };
106
+ }
107
+ return { base, baseRef: "HEAD" };
108
+ }
109
+ if (verify(`refs/heads/${base}`)) return { base, baseRef: `refs/heads/${base}` };
110
+ return { base, baseRef: "HEAD" };
111
+ }
112
+
113
+ function blobId(git, spec) {
114
+ const r = git(["rev-parse", "--verify", "--quiet", spec]);
115
+ const id = r.stdout.trim();
116
+ return r.code === 0 && id ? id : null;
117
+ }
118
+
119
+ function notePath(stamp) {
120
+ return `${GATE_EXIT_NOTE_DIR}/${stamp}.md`;
121
+ }
122
+
123
+ function isMerged(git, branchRef, baseRef, stamp) {
124
+ if (git(["merge-base", "--is-ancestor", branchRef, baseRef]).code === 0) return true;
125
+ const onBranch = blobId(git, `${branchRef}:${notePath(stamp)}`);
126
+ return onBranch !== null && onBranch === blobId(git, `${baseRef}:${notePath(stamp)}`);
127
+ }
128
+
129
+ function readHeadline(git, ref, stamp) {
130
+ const shown = git(["show", `${ref}:${notePath(stamp)}`]);
131
+ if (shown.code !== 0) return null;
132
+ const first = shown.stdout.split("\n")[0]?.trim() ?? "";
133
+ return first || null;
134
+ }
135
+
136
+ function stampOfBranch(short) {
137
+ const i = short.lastIndexOf(GATE_EXIT_BRANCH_PREFIX);
138
+ if (i < 0) return null;
139
+ const stamp = short.slice(i + GATE_EXIT_BRANCH_PREFIX.length);
140
+ return GATE_EXIT_STAMP_RE.test(stamp) ? stamp : null;
141
+ }
142
+
143
+ function shortRef(ref) {
144
+ return ref.replace(/^refs\/remotes\//, "").replace(/^refs\/heads\//, "");
145
+ }
146
+
147
+ function emptyResult(marker, base = GATE_EXIT_DEFAULT_BASE) {
148
+ return { marker, base, tickets: [], skipped: [], truncated: false };
149
+ }
150
+
151
+ /**
152
+ * Pending operator tickets: marker entries plus unmerged gate-exit branches
153
+ * (remote-tracking and local) with no entry. A merged branch with no entry is
154
+ * history and is hidden, as in `ground-control status`.
155
+ * @param {string} root
156
+ * @param {{ git?: (args: string[]) => { code: number, stdout: string }, redact?: (s: string) => string, max?: number }} [opts]
157
+ */
158
+ export function collectGateExitTickets(root, opts = {}) {
159
+ const redact = typeof opts.redact === "function" ? opts.redact : (s) => s;
160
+ const max = Number.isInteger(opts.max) && opts.max > 0 ? opts.max : MAX_GATE_EXIT_TICKETS;
161
+ const git = opts.git ?? makeGitRunner(root);
162
+ const marker = readGateExitMarkerSync(root);
163
+ const entries = marker.kind === "ok" ? marker.pending : [];
164
+ const skipped =
165
+ marker.kind === "ok" ? marker.skipped.slice(-MAX_GATE_EXIT_SKIPPED).map((s) => cap(s, 80)) : [];
166
+
167
+ const listed = git([
168
+ "for-each-ref",
169
+ "--format=%(refname)",
170
+ `refs/remotes/origin/${GATE_EXIT_BRANCH_PREFIX}`,
171
+ `refs/heads/${GATE_EXIT_BRANCH_PREFIX}`,
172
+ ]);
173
+ const branchRefs = listed.code === 0 ? listed.stdout.split("\n").filter((l) => l.trim()) : [];
174
+ if (entries.length === 0 && branchRefs.length === 0) {
175
+ return { ...emptyResult(marker.kind), skipped };
176
+ }
177
+
178
+ const hasOrigin = git(["remote", "get-url", "origin"]).code === 0;
179
+ const defaultRefs = resolveBaseRef(git, hasOrigin, GATE_EXIT_DEFAULT_BASE);
180
+ // Same scopes as the CLI: with an origin, remote-tracking + local heads; without, local heads.
181
+ const remoteScope = `refs/remotes/origin/${GATE_EXIT_BRANCH_PREFIX}`;
182
+ const present = new Set(branchRefs);
183
+ const stamps = new Set(entries.map((e) => e.stamp));
184
+ for (const ref of branchRefs) {
185
+ if (!hasOrigin && ref.startsWith(remoteScope)) continue;
186
+ const stamp = stampOfBranch(shortRef(ref));
187
+ if (stamp) stamps.add(stamp);
188
+ }
189
+
190
+ const tickets = [];
191
+ const ordered = [...stamps].sort();
192
+ for (const stamp of ordered) {
193
+ const entry = entries.find((e) => e.stamp === stamp);
194
+ const remote = `refs/remotes/origin/${GATE_EXIT_BRANCH_PREFIX}${stamp}`;
195
+ const local = `refs/heads/${GATE_EXIT_BRANCH_PREFIX}${stamp}`;
196
+ const branchRef = hasOrigin && present.has(remote) ? remote : present.has(local) ? local : null;
197
+ const refs =
198
+ entry && isSafeBaseRef(entry.base) && entry.base !== defaultRefs.base
199
+ ? resolveBaseRef(git, hasOrigin, entry.base)
200
+ : defaultRefs;
201
+ let branchState = "none";
202
+ if (branchRef) {
203
+ branchState = isMerged(git, branchRef, refs.baseRef, stamp) ? "merged" : "unmerged";
204
+ }
205
+ if (!entry && branchState === "merged") continue;
206
+ const headline =
207
+ (branchRef ? readHeadline(git, branchRef, stamp) : null) ??
208
+ readHeadline(git, refs.baseRef, stamp);
209
+ const url = typeof entry?.url === "string" && /^https?:\/\//.test(entry.url) ? entry.url : null;
210
+ tickets.push({
211
+ stamp,
212
+ headline: headline ? cap(redact(headline)) : null,
213
+ askId: typeof entry?.askId === "string" ? cap(entry.askId, 64) : null,
214
+ labels: Array.isArray(entry?.labels)
215
+ ? entry.labels.slice(0, 8).map((l) => cap(redact(String(l)), 80))
216
+ : [],
217
+ trigger: TRIGGERS.has(entry?.trigger) ? entry.trigger : null,
218
+ openedAt: typeof entry?.openedAt === "string" ? cap(entry.openedAt, 40) : null,
219
+ url: url ? cap(redact(url), 300) : null,
220
+ branch: branchRef ? shortRef(branchRef) : null,
221
+ branchState,
222
+ inMarker: Boolean(entry),
223
+ });
224
+ }
225
+
226
+ return {
227
+ marker: marker.kind,
228
+ base: defaultRefs.base,
229
+ tickets: tickets.slice(0, max),
230
+ skipped,
231
+ truncated: tickets.length > max,
232
+ };
233
+ }
@@ -32,3 +32,24 @@ export type HandoffMarkdown = {
32
32
  };
33
33
 
34
34
  export function parseHandoffMarkdown(content: string): HandoffMarkdown | null;
35
+
36
+ /** Flight Log pending operator tickets lane (read-only rows). */
37
+ export type FlightLogTicket = {
38
+ id: string;
39
+ stamp: string;
40
+ title: string;
41
+ text: string;
42
+ askId: string | null;
43
+ branch: string | null;
44
+ branchState: "unmerged" | "merged" | "none";
45
+ url: string | null;
46
+ triage: string;
47
+ sourcePath: string;
48
+ };
49
+
50
+ export const FLIGHT_LOG_TICKETS_CAP: 5;
51
+
52
+ export function buildFlightLogTickets(
53
+ tickets: Record<string, unknown>[] | null | undefined,
54
+ opts?: { cap?: number },
55
+ ): FlightLogTicket[];
@@ -643,6 +643,51 @@ export function listFlightLogQuietOpenTriages(reportOrAttentionItems, opts = {})
643
643
  return out;
644
644
  }
645
645
 
646
+ /** Cap pending operator tickets on the Flight Log tickets lane. */
647
+ export const FLIGHT_LOG_TICKETS_CAP = 5;
648
+
649
+ /**
650
+ * Pending operator tickets for the Flight Log (read-only; prompt chrome).
651
+ * Input rows come from `collectGateExitTickets` (dashboard/lib/gate-exit-tickets.mjs,
652
+ * the read half of `agent-kit ground-control status`). No action: triage stays
653
+ * in the CLI (`ground-control accept|discard <stamp>`) or the `ticket` Ask.
654
+ * @param {object[]|null|undefined} tickets
655
+ * @param {{ cap?: number }} [opts]
656
+ * @returns {{ id: string, stamp: string, title: string, text: string, askId: string|null, branch: string|null, branchState: string, url: string|null, triage: string, sourcePath: string }[]}
657
+ */
658
+ export function buildFlightLogTickets(tickets, opts = {}) {
659
+ const cap =
660
+ typeof opts.cap === "number" && opts.cap > 0 ? Math.floor(opts.cap) : FLIGHT_LOG_TICKETS_CAP;
661
+ if (!Array.isArray(tickets)) return [];
662
+ const out = [];
663
+ for (const t of tickets) {
664
+ if (!t || typeof t.stamp !== "string" || !/^\d{8}T\d{6}Z$/.test(t.stamp)) continue;
665
+ const headline =
666
+ typeof t.headline === "string" && t.headline.trim()
667
+ ? t.headline.replace(/^#\s*/, "").trim()
668
+ : `Gate exit ${t.stamp} (note not readable)`;
669
+ const branchState =
670
+ t.branchState === "merged" || t.branchState === "unmerged" ? t.branchState : "none";
671
+ out.push({
672
+ id: `flight-log-ticket:${t.stamp}`,
673
+ stamp: t.stamp,
674
+ title: "Pending ticket",
675
+ text: truncateStr(headline, MAX_SEMANTIC_LABEL),
676
+ askId: typeof t.askId === "string" && t.askId ? t.askId : null,
677
+ branch: typeof t.branch === "string" && t.branch ? t.branch : null,
678
+ branchState,
679
+ url: typeof t.url === "string" && /^https?:\/\//.test(t.url) ? t.url : null,
680
+ triage:
681
+ branchState === "merged"
682
+ ? `agent-kit ground-control accept ${t.stamp}`
683
+ : `agent-kit ground-control accept|discard ${t.stamp}`,
684
+ sourcePath: t.inMarker ? ".cursor/context/gate-exit.json" : t.branch || "gate-exit/",
685
+ });
686
+ if (out.length >= cap) break;
687
+ }
688
+ return out;
689
+ }
690
+
646
691
  /** Relative path of the gitignored Field Report activity cadence ledger. */
647
692
  export const FIELD_REPORT_CADENCE_LEDGER_REL = ".cursor/context/field-report-cadence.json";
648
693
 
@@ -3790,6 +3835,7 @@ export function buildMissionControlView({
3790
3835
  flightLogLedger = null,
3791
3836
  cadenceLedger = null,
3792
3837
  cadenceConfig = null,
3838
+ gateExitTickets = [],
3793
3839
  nowMs = Date.now(),
3794
3840
  } = {}) {
3795
3841
  const nowBase = buildCurrentExecution(plans, handoff);
@@ -3814,6 +3860,7 @@ export function buildMissionControlView({
3814
3860
  },
3815
3861
  );
3816
3862
  flightLog.warnings = buildFlightLogWarnings(handoff);
3863
+ flightLog.tickets = buildFlightLogTickets(gateExitTickets);
3817
3864
  const classifiedPlans = enrichPlans(plans, handoff);
3818
3865
  const planEvents = formatPlanHandoffActivity({ now, handoff, plans });
3819
3866
  const inventoryEvents = formatInventoryActivity(
@@ -5,7 +5,7 @@ import {
5
5
  import {
6
6
  REGISTRY_CLI_ARGS,
7
7
  resolveRegistryFromCli
8
- } from "./chunk-3DGTPONS.js";
8
+ } from "./chunk-C2SJDYAM.js";
9
9
  import {
10
10
  logger
11
11
  } from "./chunk-ZB43YMHW.js";
@@ -22,11 +22,11 @@ import {
22
22
  resolveProtectedGlobs,
23
23
  saveManifest,
24
24
  upsertIdList
25
- } from "./chunk-DBPNGZNH.js";
25
+ } from "./chunk-NSSXSZLL.js";
26
26
  import {
27
27
  KIT_VERSION
28
28
  } from "./chunk-7HDQEK6T.js";
29
- import "./chunk-C26I6HTO.js";
29
+ import "./chunk-C4Q77GGJ.js";
30
30
  import "./chunk-5B4DVO6C.js";
31
31
 
32
32
  // src/commands/add.ts
@@ -2,7 +2,7 @@ import {
2
2
  REPO_ROOT_ENV,
3
3
  bundledDashboardCandidates,
4
4
  findDashboardStart
5
- } from "./chunk-HFUH3HVS.js";
5
+ } from "./chunk-XDCCYBCJ.js";
6
6
  import {
7
7
  HELMET_ACCENT,
8
8
  HELMET_FILL,
@@ -5,7 +5,7 @@ import {
5
5
  RESOLVE_AGENT_KIT_REL,
6
6
  SESSION_START_HOOK_MARKER,
7
7
  discoverInstalledCommands
8
- } from "./chunk-PPKX55GI.js";
8
+ } from "./chunk-SPYIP76F.js";
9
9
  import {
10
10
  findOnPath
11
11
  } from "./chunk-WGLPZXNU.js";
@@ -18,7 +18,7 @@ import {
18
18
  REQUIRED_SECRET_PATTERNS,
19
19
  detectGitDirty,
20
20
  runScanner
21
- } from "./chunk-U5F6RJN4.js";
21
+ } from "./chunk-GO3K752H.js";
22
22
  import {
23
23
  CONTEXT_CONFIG_REL,
24
24
  contextConfigPath
@@ -5,7 +5,7 @@ import {
5
5
  DEFAULT_REGISTRY_REF,
6
6
  DEFAULT_REGISTRY_URL,
7
7
  resolveRegistryRoot
8
- } from "./chunk-DBPNGZNH.js";
8
+ } from "./chunk-NSSXSZLL.js";
9
9
 
10
10
  // src/lifecycle/resolve-cli.ts
11
11
  async function resolveRegistryFromCli(options) {
@@ -23,7 +23,6 @@ function runnerStatePaths(root, env = process.env) {
23
23
  }
24
24
  async function ensureLoopLogsDir(paths) {
25
25
  await mkdir(paths.loopLogsDir, { recursive: true });
26
- if (paths.stateDir === ".cursor") return;
27
26
  const ignore = path.join(paths.loopLogsDir, ".gitignore");
28
27
  try {
29
28
  await writeFile(ignore, "*\n", { flag: "wx" });