@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.
- package/dashboard/dashboard.html +95 -22
- package/dashboard/lib/guards.mjs +19 -1
- package/dashboard/lib/semantic-model.mjs +38 -23
- package/dashboard/lib/triage-heading.mjs +20 -0
- package/dashboard/start-broadcast.mjs +23 -23
- package/dashboard/start.mjs +22 -28
- package/dist/index.js +176 -24
- package/package.json +1 -1
package/dashboard/dashboard.html
CHANGED
|
@@ -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
|
-
*
|
|
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
|
|
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
|
|
5643
|
-
|
|
5644
|
-
|
|
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
|
-
|
|
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 =
|
|
5830
|
+
const current = resolveFlightLogCurrent(d) || '';
|
|
5786
5831
|
const past = Array.isArray(fl.past)
|
|
5787
|
-
? fl.past
|
|
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
|
|
5791
|
-
|
|
5792
|
-
|
|
5793
|
-
|
|
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
|
|
6089
|
-
flightLogFingerprint(
|
|
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;
|
package/dashboard/lib/guards.mjs
CHANGED
|
@@ -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() !== ""
|
|
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
|
|
580
|
-
* or bulk FR CTAs).
|
|
581
|
-
*
|
|
582
|
-
*
|
|
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(
|
|
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(
|
|
588
|
+
if (!Array.isArray(reportOrAttentionItems) || reportOrAttentionItems.length === 0) return [];
|
|
592
589
|
const out = [];
|
|
593
|
-
for (const item of
|
|
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
|
-
|
|
1379
|
-
|
|
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(
|
|
1382
|
-
/\b(hard.?stop|quota\s+pause)\b/i.test(
|
|
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(
|
|
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(
|
|
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(
|
|
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 {
|
|
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
|
-
"
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
"
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
const
|
|
114
|
-
const
|
|
115
|
-
const
|
|
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(
|
|
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
|
}
|
package/dashboard/start.mjs
CHANGED
|
@@ -15,17 +15,18 @@
|
|
|
15
15
|
* Foreground serve for debugging remains: `npm run start:dashboard`.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import {
|
|
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
|
-
|
|
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
|
-
"
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
"
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
157
|
-
const
|
|
158
|
-
const
|
|
159
|
-
const
|
|
160
|
-
const
|
|
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 {
|
|
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
|
|
2308
|
+
import { execFile as execFile3 } from "child_process";
|
|
2244
2309
|
import path14 from "path";
|
|
2245
|
-
import { promisify as
|
|
2246
|
-
var exec =
|
|
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 --
|
|
3123
|
-
test: (cmd) =>
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
4306
|
-
|
|
4307
|
-
|
|
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
|
|
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"),
|
|
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
|
|
5331
|
+
import { execFile as execFile6 } from "child_process";
|
|
5180
5332
|
import path37 from "path";
|
|
5181
|
-
import { promisify as
|
|
5182
|
-
var
|
|
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
|
|
5393
|
+
const { stdout } = await execFileAsync5("git", ["ls-remote", "--tags", "--", registryUrl], {
|
|
5242
5394
|
env: gitEnv2(),
|
|
5243
5395
|
timeout: 2e4
|
|
5244
5396
|
});
|
package/package.json
CHANGED