@dadado/agent-kit-cli 4.7.2 → 4.8.2
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-data.mjs +813 -0
- package/dashboard/dashboard.html +6882 -0
- package/dashboard/lib/guards.mjs +917 -0
- package/dashboard/lib/live-refresh.mjs +147 -0
- package/dashboard/lib/semantic-model.d.mts +34 -0
- package/dashboard/lib/semantic-model.mjs +3581 -0
- package/dashboard/logo-cursor.svg +10 -0
- package/dashboard/logo.svg +1 -0
- package/dashboard/serve.mjs +550 -0
- package/dashboard/start-broadcast.mjs +231 -0
- package/dashboard/start.mjs +292 -0
- package/dist/index.js +1782 -285
- package/package.json +5 -3
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { defineCommand as
|
|
4
|
+
import { defineCommand as defineCommand18, runMain } from "citty";
|
|
5
5
|
|
|
6
6
|
// src/commands/add.ts
|
|
7
7
|
import { defineCommand } from "citty";
|
|
@@ -974,6 +974,22 @@ var L0_ARTIFACTS = [
|
|
|
974
974
|
source: ".cursor/commands/start-project.md",
|
|
975
975
|
target: ".cursor/commands/start-project.md"
|
|
976
976
|
},
|
|
977
|
+
{
|
|
978
|
+
source: ".cursor/commands/backlog-add.md",
|
|
979
|
+
target: ".cursor/commands/backlog-add.md"
|
|
980
|
+
},
|
|
981
|
+
{
|
|
982
|
+
source: ".cursor/commands/backlog-edit.md",
|
|
983
|
+
target: ".cursor/commands/backlog-edit.md"
|
|
984
|
+
},
|
|
985
|
+
{
|
|
986
|
+
source: ".cursor/commands/backlog-delete.md",
|
|
987
|
+
target: ".cursor/commands/backlog-delete.md"
|
|
988
|
+
},
|
|
989
|
+
{
|
|
990
|
+
source: ".cursor/commands/backlog-cancel.md",
|
|
991
|
+
target: ".cursor/commands/backlog-cancel.md"
|
|
992
|
+
},
|
|
977
993
|
{
|
|
978
994
|
source: ".cursor/commands/agent-kit-onboard.md",
|
|
979
995
|
target: ".cursor/commands/agent-kit-onboard.md"
|
|
@@ -986,6 +1002,14 @@ var L0_ARTIFACTS = [
|
|
|
986
1002
|
source: ".cursor/commands/run-plan.md",
|
|
987
1003
|
target: ".cursor/commands/run-plan.md"
|
|
988
1004
|
},
|
|
1005
|
+
{
|
|
1006
|
+
source: ".cursor/commands/hotfix.md",
|
|
1007
|
+
target: ".cursor/commands/hotfix.md"
|
|
1008
|
+
},
|
|
1009
|
+
{
|
|
1010
|
+
source: ".cursor/commands/run-plan-all.md",
|
|
1011
|
+
target: ".cursor/commands/run-plan-all.md"
|
|
1012
|
+
},
|
|
989
1013
|
// Deprecated aliases of /run-plan (kept so existing references keep working)
|
|
990
1014
|
{
|
|
991
1015
|
source: ".cursor/commands/run-plan-loop.md",
|
|
@@ -1014,6 +1038,10 @@ var L0_ARTIFACTS = [
|
|
|
1014
1038
|
source: ".cursor/commands/plan-review-triage.md",
|
|
1015
1039
|
target: ".cursor/commands/plan-review-triage.md"
|
|
1016
1040
|
},
|
|
1041
|
+
{
|
|
1042
|
+
source: ".cursor/commands/field-report-resolve.md",
|
|
1043
|
+
target: ".cursor/commands/field-report-resolve.md"
|
|
1044
|
+
},
|
|
1017
1045
|
// Context (templates + example config; private config.json is not L0)
|
|
1018
1046
|
{
|
|
1019
1047
|
source: ".cursor/context/templates/plan-external-review-prompt.md",
|
|
@@ -1047,28 +1075,52 @@ var L0_ARTIFACTS = [
|
|
|
1047
1075
|
source: ".cursor/context/config.example.json",
|
|
1048
1076
|
target: ".cursor/context/config.example.json"
|
|
1049
1077
|
},
|
|
1050
|
-
// Scripts (canonical
|
|
1078
|
+
// Scripts (canonical launchers; consumers never receive repo-root scripts/)
|
|
1051
1079
|
{
|
|
1052
1080
|
source: ".cursor/scripts/plan-external-review.sh",
|
|
1053
1081
|
target: ".cursor/scripts/plan-external-review.sh"
|
|
1054
1082
|
},
|
|
1083
|
+
{
|
|
1084
|
+
source: ".cursor/scripts/field-report-cadence-bump.sh",
|
|
1085
|
+
target: ".cursor/scripts/field-report-cadence-bump.sh"
|
|
1086
|
+
},
|
|
1087
|
+
{
|
|
1088
|
+
source: ".cursor/scripts/run-plan-all-consolidate.sh",
|
|
1089
|
+
target: ".cursor/scripts/run-plan-all-consolidate.sh"
|
|
1090
|
+
},
|
|
1055
1091
|
// Secrets gate (structural)
|
|
1056
1092
|
{
|
|
1057
1093
|
source: ".cursor/hooks/pre-commit/check-secrets.sh",
|
|
1058
1094
|
target: ".cursor/hooks/pre-commit/check-secrets.sh"
|
|
1059
1095
|
},
|
|
1060
|
-
// Native Cursor agent hooks (
|
|
1096
|
+
// Native Cursor agent hooks (thin adapters -> agent-kit CLI)
|
|
1061
1097
|
{
|
|
1062
1098
|
source: ".cursor/hooks.json",
|
|
1063
1099
|
target: ".cursor/hooks.json"
|
|
1064
1100
|
},
|
|
1065
1101
|
{
|
|
1066
|
-
source: ".cursor/hooks/agent/
|
|
1067
|
-
target: ".cursor/hooks/agent/
|
|
1102
|
+
source: ".cursor/hooks/agent/resolve-agent-kit.sh",
|
|
1103
|
+
target: ".cursor/hooks/agent/resolve-agent-kit.sh"
|
|
1104
|
+
},
|
|
1105
|
+
{
|
|
1106
|
+
source: ".cursor/hooks/agent/session-start.sh",
|
|
1107
|
+
target: ".cursor/hooks/agent/session-start.sh"
|
|
1108
|
+
},
|
|
1109
|
+
{
|
|
1110
|
+
source: ".cursor/hooks/agent/pre-compact.sh",
|
|
1111
|
+
target: ".cursor/hooks/agent/pre-compact.sh"
|
|
1112
|
+
},
|
|
1113
|
+
{
|
|
1114
|
+
source: ".cursor/hooks/agent/guard-shell.sh",
|
|
1115
|
+
target: ".cursor/hooks/agent/guard-shell.sh"
|
|
1068
1116
|
},
|
|
1069
1117
|
{
|
|
1070
|
-
source: ".cursor/hooks/agent/
|
|
1071
|
-
target: ".cursor/hooks/agent/
|
|
1118
|
+
source: ".cursor/hooks/agent/after-edit-schema.sh",
|
|
1119
|
+
target: ".cursor/hooks/agent/after-edit-schema.sh"
|
|
1120
|
+
},
|
|
1121
|
+
{
|
|
1122
|
+
source: ".cursor/hooks/agent/secrets-prompt.sh",
|
|
1123
|
+
target: ".cursor/hooks/agent/secrets-prompt.sh"
|
|
1072
1124
|
},
|
|
1073
1125
|
// Git spine docs at project root (not a nested agent-kit/ copy)
|
|
1074
1126
|
{
|
|
@@ -1354,12 +1406,40 @@ var contributeCommand = defineCommand2({
|
|
|
1354
1406
|
}
|
|
1355
1407
|
});
|
|
1356
1408
|
|
|
1409
|
+
// src/commands/dashboard-broadcast.ts
|
|
1410
|
+
import { spawn as spawn2 } from "child_process";
|
|
1411
|
+
import { access as access3 } from "fs/promises";
|
|
1412
|
+
import path10 from "path";
|
|
1413
|
+
import { defineCommand as defineCommand4 } from "citty";
|
|
1414
|
+
|
|
1357
1415
|
// src/commands/dashboard.ts
|
|
1358
|
-
import { spawn } from "child_process";
|
|
1416
|
+
import { execFileSync, spawn } from "child_process";
|
|
1359
1417
|
import { access as access2 } from "fs/promises";
|
|
1360
1418
|
import path9 from "path";
|
|
1419
|
+
import { fileURLToPath } from "url";
|
|
1361
1420
|
import { defineCommand as defineCommand3 } from "citty";
|
|
1362
|
-
|
|
1421
|
+
function bundledDashboardCandidates(filename, moduleUrl = import.meta.url) {
|
|
1422
|
+
const here = path9.dirname(fileURLToPath(moduleUrl));
|
|
1423
|
+
return [
|
|
1424
|
+
// Published / built: <pkg>/dist/index.js -> <pkg>/dashboard/
|
|
1425
|
+
path9.join(here, "..", "dashboard", filename),
|
|
1426
|
+
// Dev source: packages/cli/src/commands/*.ts -> packages/cli/dashboard/ (sync copy)
|
|
1427
|
+
path9.join(here, "..", "..", "dashboard", filename),
|
|
1428
|
+
// From src/commands: packages/dashboard/ (unused layout); from dist: two levels above package
|
|
1429
|
+
path9.join(here, "..", "..", "..", "dashboard", filename)
|
|
1430
|
+
];
|
|
1431
|
+
}
|
|
1432
|
+
async function firstExisting(candidates) {
|
|
1433
|
+
for (const candidate2 of candidates) {
|
|
1434
|
+
try {
|
|
1435
|
+
await access2(candidate2);
|
|
1436
|
+
return path9.resolve(candidate2);
|
|
1437
|
+
} catch {
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
return null;
|
|
1441
|
+
}
|
|
1442
|
+
async function findDashboardStart(cwd, env = process.env, options = {}) {
|
|
1363
1443
|
let dir = path9.resolve(cwd);
|
|
1364
1444
|
for (; ; ) {
|
|
1365
1445
|
const candidate2 = path9.join(dir, "dashboard", "start.mjs");
|
|
@@ -1369,9 +1449,40 @@ async function findDashboardStart(cwd) {
|
|
|
1369
1449
|
} catch {
|
|
1370
1450
|
}
|
|
1371
1451
|
const parent = path9.dirname(dir);
|
|
1372
|
-
if (parent === dir)
|
|
1452
|
+
if (parent === dir) break;
|
|
1373
1453
|
dir = parent;
|
|
1374
1454
|
}
|
|
1455
|
+
for (const key of ["MISSION_CONTROL_KIT_ROOT", "AGENT_KIT_HOME"]) {
|
|
1456
|
+
const base = env[key];
|
|
1457
|
+
if (!base || typeof base !== "string" || !base.trim()) continue;
|
|
1458
|
+
const candidate2 = path9.join(path9.resolve(base.trim()), "dashboard", "start.mjs");
|
|
1459
|
+
try {
|
|
1460
|
+
await access2(candidate2);
|
|
1461
|
+
return candidate2;
|
|
1462
|
+
} catch {
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
const sibling = path9.join(path9.resolve(cwd), "..", "agent-kit", "dashboard", "start.mjs");
|
|
1466
|
+
try {
|
|
1467
|
+
await access2(sibling);
|
|
1468
|
+
return path9.resolve(sibling);
|
|
1469
|
+
} catch {
|
|
1470
|
+
}
|
|
1471
|
+
return firstExisting(
|
|
1472
|
+
bundledDashboardCandidates("start.mjs", options.moduleUrl ?? import.meta.url)
|
|
1473
|
+
);
|
|
1474
|
+
}
|
|
1475
|
+
function resolveDashboardSnapshotRoot(cwd) {
|
|
1476
|
+
const abs = path9.resolve(cwd);
|
|
1477
|
+
try {
|
|
1478
|
+
const top = execFileSync("git", ["-C", abs, "rev-parse", "--show-toplevel"], {
|
|
1479
|
+
encoding: "utf8",
|
|
1480
|
+
timeout: 5e3
|
|
1481
|
+
}).trim();
|
|
1482
|
+
if (top) return path9.resolve(top);
|
|
1483
|
+
} catch {
|
|
1484
|
+
}
|
|
1485
|
+
return abs;
|
|
1375
1486
|
}
|
|
1376
1487
|
function runStartScript(startPath, env) {
|
|
1377
1488
|
return new Promise((resolve, reject) => {
|
|
@@ -1387,13 +1498,13 @@ function runStartScript(startPath, env) {
|
|
|
1387
1498
|
var dashboardCommand = defineCommand3({
|
|
1388
1499
|
meta: {
|
|
1389
1500
|
name: "dashboard",
|
|
1390
|
-
description: "Start Mission Control
|
|
1501
|
+
description: "Start Mission Control for this workspace (stable per-root port) and open the panel URL."
|
|
1391
1502
|
},
|
|
1392
1503
|
args: {
|
|
1393
1504
|
cwd: {
|
|
1394
1505
|
type: "string",
|
|
1395
1506
|
default: process.cwd(),
|
|
1396
|
-
description: "
|
|
1507
|
+
description: "Workspace to snapshot (git root preferred); also searched upward for dashboard/start.mjs"
|
|
1397
1508
|
},
|
|
1398
1509
|
"no-open": {
|
|
1399
1510
|
type: "boolean",
|
|
@@ -1402,27 +1513,122 @@ var dashboardCommand = defineCommand3({
|
|
|
1402
1513
|
}
|
|
1403
1514
|
},
|
|
1404
1515
|
async run({ args }) {
|
|
1516
|
+
const snapshotRoot = resolveDashboardSnapshotRoot(args.cwd);
|
|
1405
1517
|
const startPath = await findDashboardStart(args.cwd);
|
|
1406
1518
|
if (!startPath) {
|
|
1407
1519
|
logger.error(
|
|
1408
|
-
"No dashboard/start.mjs found.
|
|
1520
|
+
"No dashboard/start.mjs found. After a CLI publish that ships dashboard/ (Path C), reinstall @dadado/agent-kit-cli. Or set MISSION_CONTROL_KIT_ROOT / AGENT_KIT_HOME to an agent-kit checkout, place a sibling ../agent-kit tree, or run from that kit tree."
|
|
1409
1521
|
);
|
|
1410
1522
|
process.exitCode = 1;
|
|
1411
1523
|
return;
|
|
1412
1524
|
}
|
|
1413
1525
|
const env = { ...process.env };
|
|
1526
|
+
env.MISSION_CONTROL_REPO_ROOT = snapshotRoot;
|
|
1414
1527
|
if (args["no-open"]) env.MISSION_CONTROL_NO_OPEN = "1";
|
|
1415
1528
|
const code = await runStartScript(startPath, env);
|
|
1416
1529
|
if (code !== 0) process.exitCode = code;
|
|
1417
1530
|
}
|
|
1418
1531
|
});
|
|
1419
1532
|
|
|
1533
|
+
// src/commands/dashboard-broadcast.ts
|
|
1534
|
+
async function findDashboardBroadcastStart(cwd, env = process.env, options = {}) {
|
|
1535
|
+
let dir = path10.resolve(cwd);
|
|
1536
|
+
for (; ; ) {
|
|
1537
|
+
const candidate2 = path10.join(dir, "dashboard", "start-broadcast.mjs");
|
|
1538
|
+
try {
|
|
1539
|
+
await access3(candidate2);
|
|
1540
|
+
return candidate2;
|
|
1541
|
+
} catch {
|
|
1542
|
+
}
|
|
1543
|
+
const parent = path10.dirname(dir);
|
|
1544
|
+
if (parent === dir) break;
|
|
1545
|
+
dir = parent;
|
|
1546
|
+
}
|
|
1547
|
+
for (const key of ["MISSION_CONTROL_KIT_ROOT", "AGENT_KIT_HOME"]) {
|
|
1548
|
+
const base = env[key];
|
|
1549
|
+
if (!base || typeof base !== "string" || !base.trim()) continue;
|
|
1550
|
+
const candidate2 = path10.join(path10.resolve(base.trim()), "dashboard", "start-broadcast.mjs");
|
|
1551
|
+
try {
|
|
1552
|
+
await access3(candidate2);
|
|
1553
|
+
return candidate2;
|
|
1554
|
+
} catch {
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
const sibling = path10.join(
|
|
1558
|
+
path10.resolve(cwd),
|
|
1559
|
+
"..",
|
|
1560
|
+
"agent-kit",
|
|
1561
|
+
"dashboard",
|
|
1562
|
+
"start-broadcast.mjs"
|
|
1563
|
+
);
|
|
1564
|
+
try {
|
|
1565
|
+
await access3(sibling);
|
|
1566
|
+
return path10.resolve(sibling);
|
|
1567
|
+
} catch {
|
|
1568
|
+
}
|
|
1569
|
+
for (const candidate2 of bundledDashboardCandidates(
|
|
1570
|
+
"start-broadcast.mjs",
|
|
1571
|
+
options.moduleUrl ?? import.meta.url
|
|
1572
|
+
)) {
|
|
1573
|
+
try {
|
|
1574
|
+
await access3(candidate2);
|
|
1575
|
+
return path10.resolve(candidate2);
|
|
1576
|
+
} catch {
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
return null;
|
|
1580
|
+
}
|
|
1581
|
+
function runStartScript2(startPath, env) {
|
|
1582
|
+
return new Promise((resolve, reject) => {
|
|
1583
|
+
const child = spawn2(process.execPath, [startPath], {
|
|
1584
|
+
cwd: path10.dirname(path10.dirname(startPath)),
|
|
1585
|
+
env,
|
|
1586
|
+
stdio: "inherit"
|
|
1587
|
+
});
|
|
1588
|
+
child.on("error", reject);
|
|
1589
|
+
child.on("close", (code) => resolve(code ?? 1));
|
|
1590
|
+
});
|
|
1591
|
+
}
|
|
1592
|
+
var dashboardBroadcastCommand = defineCommand4({
|
|
1593
|
+
meta: {
|
|
1594
|
+
name: "dashboard-broadcast",
|
|
1595
|
+
description: "Opt-in LAN Mission Control (token-gated). Terminal counterpart to /dashboard-broadcast. Does not change loopback /dashboard."
|
|
1596
|
+
},
|
|
1597
|
+
args: {
|
|
1598
|
+
cwd: {
|
|
1599
|
+
type: "string",
|
|
1600
|
+
default: process.cwd(),
|
|
1601
|
+
description: "Directory to search upward for dashboard/start-broadcast.mjs"
|
|
1602
|
+
},
|
|
1603
|
+
"no-open": {
|
|
1604
|
+
type: "boolean",
|
|
1605
|
+
default: false,
|
|
1606
|
+
description: "Do not open a browser; only ensure the server is up and print LAN URL + token"
|
|
1607
|
+
}
|
|
1608
|
+
},
|
|
1609
|
+
async run({ args }) {
|
|
1610
|
+
const startPath = await findDashboardBroadcastStart(args.cwd);
|
|
1611
|
+
if (!startPath) {
|
|
1612
|
+
const loopback = await findDashboardStart(args.cwd);
|
|
1613
|
+
logger.error(
|
|
1614
|
+
loopback ? "No dashboard/start-broadcast.mjs found beside dashboard/start.mjs. Update the agent-kit tree or reinstall a CLI that ships dashboard/, then retry." : "No dashboard/start-broadcast.mjs found. After a CLI publish that ships dashboard/ (Path C), reinstall @dadado/agent-kit-cli. Or run from an agent-kit tree that includes dashboard/."
|
|
1615
|
+
);
|
|
1616
|
+
process.exitCode = 1;
|
|
1617
|
+
return;
|
|
1618
|
+
}
|
|
1619
|
+
const env = { ...process.env };
|
|
1620
|
+
if (args["no-open"]) env.MISSION_CONTROL_NO_OPEN = "1";
|
|
1621
|
+
const code = await runStartScript2(startPath, env);
|
|
1622
|
+
if (code !== 0) process.exitCode = code;
|
|
1623
|
+
}
|
|
1624
|
+
});
|
|
1625
|
+
|
|
1420
1626
|
// src/commands/diff.ts
|
|
1421
|
-
import { defineCommand as
|
|
1627
|
+
import { defineCommand as defineCommand5 } from "citty";
|
|
1422
1628
|
|
|
1423
1629
|
// src/lifecycle/diff.ts
|
|
1424
1630
|
import { readFile as readFile5 } from "fs/promises";
|
|
1425
|
-
import
|
|
1631
|
+
import path11 from "path";
|
|
1426
1632
|
async function readIfExists(abs) {
|
|
1427
1633
|
try {
|
|
1428
1634
|
return await readFile5(abs, "utf8");
|
|
@@ -1431,7 +1637,7 @@ async function readIfExists(abs) {
|
|
|
1431
1637
|
}
|
|
1432
1638
|
}
|
|
1433
1639
|
async function comparePair(registryRoot, projectRoot, sourceRel, targetRel, protectedGlobs) {
|
|
1434
|
-
const posixTarget = targetRel.split(
|
|
1640
|
+
const posixTarget = targetRel.split(path11.sep).join("/");
|
|
1435
1641
|
if (isProtectedPath(posixTarget, protectedGlobs)) {
|
|
1436
1642
|
return { path: posixTarget, status: "protected" };
|
|
1437
1643
|
}
|
|
@@ -1461,7 +1667,7 @@ async function diffAgainstRegistry(registryRoot, projectRoot, manifest) {
|
|
|
1461
1667
|
const entries = [];
|
|
1462
1668
|
const seen = /* @__PURE__ */ new Set();
|
|
1463
1669
|
const pushUnique = async (sourceRel, targetRel) => {
|
|
1464
|
-
const key = targetRel.split(
|
|
1670
|
+
const key = targetRel.split(path11.sep).join("/");
|
|
1465
1671
|
if (seen.has(key)) return;
|
|
1466
1672
|
seen.add(key);
|
|
1467
1673
|
entries.push(
|
|
@@ -1488,8 +1694,8 @@ async function diffAgainstRegistry(registryRoot, projectRoot, manifest) {
|
|
|
1488
1694
|
continue;
|
|
1489
1695
|
}
|
|
1490
1696
|
const category = skill.path.includes("/core/") ? "core" : "community";
|
|
1491
|
-
const sourceRel =
|
|
1492
|
-
const targetRel =
|
|
1697
|
+
const sourceRel = path11.posix.join(skill.path, "SKILL.md");
|
|
1698
|
+
const targetRel = path11.posix.join(".cursor", "skills", category, skill.id, "SKILL.md");
|
|
1493
1699
|
await pushUnique(sourceRel, targetRel);
|
|
1494
1700
|
}
|
|
1495
1701
|
}
|
|
@@ -1508,7 +1714,7 @@ function summarizeDiff(entries) {
|
|
|
1508
1714
|
}
|
|
1509
1715
|
|
|
1510
1716
|
// src/commands/diff.ts
|
|
1511
|
-
var diffCommand =
|
|
1717
|
+
var diffCommand = defineCommand5({
|
|
1512
1718
|
meta: {
|
|
1513
1719
|
name: "diff",
|
|
1514
1720
|
description: "Compare installed kit artifacts to the registry (respects L3 protected)."
|
|
@@ -1557,8 +1763,86 @@ var diffCommand = defineCommand4({
|
|
|
1557
1763
|
});
|
|
1558
1764
|
|
|
1559
1765
|
// src/commands/doctor.ts
|
|
1560
|
-
import
|
|
1561
|
-
import { defineCommand as
|
|
1766
|
+
import path22 from "path";
|
|
1767
|
+
import { defineCommand as defineCommand6 } from "citty";
|
|
1768
|
+
|
|
1769
|
+
// src/invariants/hooks-health.ts
|
|
1770
|
+
import { access as access4, readFile as readFile6 } from "fs/promises";
|
|
1771
|
+
import path12 from "path";
|
|
1772
|
+
var EXPECTED_EVENTS = [
|
|
1773
|
+
"sessionStart",
|
|
1774
|
+
"preCompact",
|
|
1775
|
+
"beforeShellExecution",
|
|
1776
|
+
"afterFileEdit",
|
|
1777
|
+
"beforeSubmitPrompt"
|
|
1778
|
+
];
|
|
1779
|
+
async function exists(p) {
|
|
1780
|
+
try {
|
|
1781
|
+
await access4(p);
|
|
1782
|
+
return true;
|
|
1783
|
+
} catch {
|
|
1784
|
+
return false;
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
async function assessHooksHealth(rootDir) {
|
|
1788
|
+
const root = path12.resolve(rootDir);
|
|
1789
|
+
const hooksJsonPath = ".cursor/hooks.json";
|
|
1790
|
+
const hooksJsonAbs = path12.join(root, hooksJsonPath);
|
|
1791
|
+
const reasons = [];
|
|
1792
|
+
const wiredEvents = [];
|
|
1793
|
+
if (!await exists(hooksJsonAbs)) {
|
|
1794
|
+
return {
|
|
1795
|
+
status: "missing",
|
|
1796
|
+
reasons: ["`.cursor/hooks.json` not found"],
|
|
1797
|
+
hooksJsonPath,
|
|
1798
|
+
expectedEvents: [...EXPECTED_EVENTS],
|
|
1799
|
+
wiredEvents
|
|
1800
|
+
};
|
|
1801
|
+
}
|
|
1802
|
+
let parsed;
|
|
1803
|
+
try {
|
|
1804
|
+
parsed = JSON.parse(await readFile6(hooksJsonAbs, "utf8"));
|
|
1805
|
+
} catch {
|
|
1806
|
+
return {
|
|
1807
|
+
status: "degraded",
|
|
1808
|
+
reasons: ["`.cursor/hooks.json` is not valid JSON"],
|
|
1809
|
+
hooksJsonPath,
|
|
1810
|
+
expectedEvents: [...EXPECTED_EVENTS],
|
|
1811
|
+
wiredEvents
|
|
1812
|
+
};
|
|
1813
|
+
}
|
|
1814
|
+
const hooks = parsed.hooks ?? {};
|
|
1815
|
+
for (const event of EXPECTED_EVENTS) {
|
|
1816
|
+
const list = hooks[event];
|
|
1817
|
+
if (Array.isArray(list) && list.length > 0) {
|
|
1818
|
+
wiredEvents.push(event);
|
|
1819
|
+
for (const entry of list) {
|
|
1820
|
+
if (!entry || typeof entry !== "object") continue;
|
|
1821
|
+
const command = String(entry.command ?? "");
|
|
1822
|
+
if (command.endsWith(".py") || command.includes("python")) {
|
|
1823
|
+
reasons.push(`${event} still points at a Python script (${command})`);
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
} else {
|
|
1827
|
+
reasons.push(`missing hook event: ${event}`);
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
const resolveLib = path12.join(root, ".cursor", "hooks", "agent", "resolve-agent-kit.sh");
|
|
1831
|
+
if (!await exists(resolveLib)) {
|
|
1832
|
+
reasons.push("missing `.cursor/hooks/agent/resolve-agent-kit.sh` (thin adapter resolver)");
|
|
1833
|
+
}
|
|
1834
|
+
if (Array.isArray(hooks.stop) && hooks.stop.length > 0) {
|
|
1835
|
+
reasons.push("`stop` hook is registered (forbidden; remove it)");
|
|
1836
|
+
}
|
|
1837
|
+
const status = reasons.length === 0 && wiredEvents.length === EXPECTED_EVENTS.length ? "active" : "degraded";
|
|
1838
|
+
return {
|
|
1839
|
+
status,
|
|
1840
|
+
reasons,
|
|
1841
|
+
hooksJsonPath,
|
|
1842
|
+
expectedEvents: [...EXPECTED_EVENTS],
|
|
1843
|
+
wiredEvents
|
|
1844
|
+
};
|
|
1845
|
+
}
|
|
1562
1846
|
|
|
1563
1847
|
// src/scanner/readiness.ts
|
|
1564
1848
|
import { createHash as createHash2 } from "crypto";
|
|
@@ -1807,12 +2091,12 @@ function createReadinessReport(scan, options) {
|
|
|
1807
2091
|
}
|
|
1808
2092
|
|
|
1809
2093
|
// src/scanner/safe-fixes.ts
|
|
1810
|
-
import { readFile as
|
|
1811
|
-
import
|
|
2094
|
+
import { readFile as readFile9, writeFile as writeFile3 } from "fs/promises";
|
|
2095
|
+
import path20 from "path";
|
|
1812
2096
|
|
|
1813
2097
|
// src/scanner/detect-repository.ts
|
|
1814
|
-
import { readFile as
|
|
1815
|
-
import
|
|
2098
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
2099
|
+
import path13 from "path";
|
|
1816
2100
|
var CONTEXT_PATHS = [
|
|
1817
2101
|
["README.md", "README"],
|
|
1818
2102
|
["README", "README"],
|
|
@@ -1831,7 +2115,7 @@ var CONTEXT_PATHS = [
|
|
|
1831
2115
|
async function existingEvidence(rootDir, candidates) {
|
|
1832
2116
|
const evidence = await Promise.all(
|
|
1833
2117
|
candidates.map(
|
|
1834
|
-
async ([relativePath, label]) => await fileExists(
|
|
2118
|
+
async ([relativePath, label]) => await fileExists(path13.join(rootDir, relativePath)) ? { source: "file", value: `${relativePath}:${label}` } : void 0
|
|
1835
2119
|
)
|
|
1836
2120
|
);
|
|
1837
2121
|
return evidence.flatMap((item) => item ? [item] : []);
|
|
@@ -1852,7 +2136,7 @@ async function detectContext(rootDir) {
|
|
|
1852
2136
|
async function detectPurpose(rootDir, stack) {
|
|
1853
2137
|
const entries = await listDirectory(rootDir);
|
|
1854
2138
|
const lowerEntries = entries.map((entry) => entry.toLowerCase());
|
|
1855
|
-
const packageJson = await readJson(
|
|
2139
|
+
const packageJson = await readJson(path13.join(rootDir, "package.json"));
|
|
1856
2140
|
const categories = [];
|
|
1857
2141
|
const evidence = [];
|
|
1858
2142
|
const add = (category, value2) => {
|
|
@@ -1892,16 +2176,16 @@ async function detectPurpose(rootDir, stack) {
|
|
|
1892
2176
|
}
|
|
1893
2177
|
async function detectAgentKit(rootDir) {
|
|
1894
2178
|
const manifestRelativePath = ".cursor/agent-kit.json";
|
|
1895
|
-
const manifestPath =
|
|
2179
|
+
const manifestPath = path13.join(rootDir, manifestRelativePath);
|
|
1896
2180
|
const installed = await fileExists(manifestPath);
|
|
1897
2181
|
const manifest = installed ? await readJson(manifestPath) : null;
|
|
1898
2182
|
return {
|
|
1899
2183
|
installed,
|
|
1900
2184
|
manifestPath: installed ? manifestRelativePath : void 0,
|
|
1901
2185
|
version: manifest?.version,
|
|
1902
|
-
hasPlans: await fileExists(
|
|
1903
|
-
hasHandoff: await fileExists(
|
|
1904
|
-
hasMemory: await fileExists(
|
|
2186
|
+
hasPlans: await fileExists(path13.join(rootDir, ".cursor/plans")),
|
|
2187
|
+
hasHandoff: await fileExists(path13.join(rootDir, ".cursor/HANDOFF.md")),
|
|
2188
|
+
hasMemory: await fileExists(path13.join(rootDir, ".cursor/memory"))
|
|
1905
2189
|
};
|
|
1906
2190
|
}
|
|
1907
2191
|
var REQUIRED_SECRET_PATTERNS = [
|
|
@@ -1915,9 +2199,9 @@ var REQUIRED_SECRET_PATTERNS = [
|
|
|
1915
2199
|
"*service-account*.json"
|
|
1916
2200
|
];
|
|
1917
2201
|
async function detectSafety(rootDir, trackedFiles) {
|
|
1918
|
-
const gitignorePath =
|
|
2202
|
+
const gitignorePath = path13.join(rootDir, ".gitignore");
|
|
1919
2203
|
const hasGitignore = await fileExists(gitignorePath);
|
|
1920
|
-
const gitignore = hasGitignore ? await
|
|
2204
|
+
const gitignore = hasGitignore ? await readFile7(gitignorePath, "utf8") : "";
|
|
1921
2205
|
const lines = gitignore.split(/\r?\n/).map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
|
|
1922
2206
|
const ignoredSecretPatterns = REQUIRED_SECRET_PATTERNS.filter(
|
|
1923
2207
|
(pattern) => lines.includes(pattern)
|
|
@@ -1926,7 +2210,7 @@ async function detectSafety(rootDir, trackedFiles) {
|
|
|
1926
2210
|
(file) => /(^|\/)(\.env(\..+)?|.*\.(key|pem|p12|pfx)|.*credentials.*\.json)$/i.test(file)
|
|
1927
2211
|
);
|
|
1928
2212
|
const hookPaths = [".husky", ".git/hooks/pre-commit", "git-hooks/pre-commit"];
|
|
1929
|
-
const hasHooks = (await Promise.all(hookPaths.map((item) => fileExists(
|
|
2213
|
+
const hasHooks = (await Promise.all(hookPaths.map((item) => fileExists(path13.join(rootDir, item))))).some(Boolean);
|
|
1930
2214
|
const guardCandidates = [
|
|
1931
2215
|
".husky/pre-commit",
|
|
1932
2216
|
".husky/pre-push",
|
|
@@ -1935,7 +2219,7 @@ async function detectSafety(rootDir, trackedFiles) {
|
|
|
1935
2219
|
];
|
|
1936
2220
|
const guardContents = await Promise.all(
|
|
1937
2221
|
guardCandidates.map(
|
|
1938
|
-
async (item) => await fileExists(
|
|
2222
|
+
async (item) => await fileExists(path13.join(rootDir, item)) ? readFile7(path13.join(rootDir, item), "utf8") : ""
|
|
1939
2223
|
)
|
|
1940
2224
|
);
|
|
1941
2225
|
return {
|
|
@@ -1953,11 +2237,11 @@ async function detectSafety(rootDir, trackedFiles) {
|
|
|
1953
2237
|
}
|
|
1954
2238
|
|
|
1955
2239
|
// src/scanner/scan.ts
|
|
1956
|
-
import
|
|
2240
|
+
import path19 from "path";
|
|
1957
2241
|
|
|
1958
2242
|
// src/scanner/detect-git.ts
|
|
1959
2243
|
import { execFile as execFile2 } from "child_process";
|
|
1960
|
-
import
|
|
2244
|
+
import path14 from "path";
|
|
1961
2245
|
import { promisify as promisify2 } from "util";
|
|
1962
2246
|
var exec = promisify2(execFile2);
|
|
1963
2247
|
function remoteHostname(remoteUrl) {
|
|
@@ -1981,7 +2265,7 @@ function sanitizeRemoteUrl(remoteUrl) {
|
|
|
1981
2265
|
}
|
|
1982
2266
|
async function detectProvider(rootDir, remoteUrl) {
|
|
1983
2267
|
const configuration = await readJson(
|
|
1984
|
-
|
|
2268
|
+
path14.join(rootDir, ".cursor", "agent-kit.config.json")
|
|
1985
2269
|
);
|
|
1986
2270
|
const configuredProvider = configuration?.git?.provider;
|
|
1987
2271
|
if (configuredProvider) {
|
|
@@ -2038,7 +2322,7 @@ async function detectProvider(rootDir, remoteUrl) {
|
|
|
2038
2322
|
evidence: remoteEvidence
|
|
2039
2323
|
};
|
|
2040
2324
|
}
|
|
2041
|
-
if (await fileExists(
|
|
2325
|
+
if (await fileExists(path14.join(rootDir, ".gitlab-ci.yml"))) {
|
|
2042
2326
|
return {
|
|
2043
2327
|
provider: "gitlab",
|
|
2044
2328
|
providerKind: "gitlab-self-hosted",
|
|
@@ -2127,11 +2411,11 @@ async function detectGit(rootDir) {
|
|
|
2127
2411
|
}
|
|
2128
2412
|
|
|
2129
2413
|
// src/scanner/detect-ide.ts
|
|
2130
|
-
import
|
|
2414
|
+
import path15 from "path";
|
|
2131
2415
|
async function detectIde(rootDir) {
|
|
2132
|
-
const hasCursor = await fileExists(
|
|
2133
|
-
const hasVSCode = await fileExists(
|
|
2134
|
-
const hasWindsurf = await fileExists(
|
|
2416
|
+
const hasCursor = await fileExists(path15.join(rootDir, ".cursor"));
|
|
2417
|
+
const hasVSCode = await fileExists(path15.join(rootDir, ".vscode"));
|
|
2418
|
+
const hasWindsurf = await fileExists(path15.join(rootDir, ".windsurfrules"));
|
|
2135
2419
|
if (hasCursor) return { ide: "cursor", plan: "cursor-pro" };
|
|
2136
2420
|
if (hasVSCode) return { ide: "vscode", plan: "vscode-pro" };
|
|
2137
2421
|
if (hasWindsurf) return { ide: "windsurf", plan: "windsurf" };
|
|
@@ -2139,7 +2423,7 @@ async function detectIde(rootDir) {
|
|
|
2139
2423
|
}
|
|
2140
2424
|
|
|
2141
2425
|
// src/scanner/detect-infra.ts
|
|
2142
|
-
import
|
|
2426
|
+
import path16 from "path";
|
|
2143
2427
|
|
|
2144
2428
|
// src/types.ts
|
|
2145
2429
|
var CI_PLATFORM_FILES = {
|
|
@@ -2168,12 +2452,12 @@ var PM_TOOL_LABELS = {
|
|
|
2168
2452
|
|
|
2169
2453
|
// src/scanner/detect-infra.ts
|
|
2170
2454
|
async function detectInfra(rootDir) {
|
|
2171
|
-
const docker = await fileExists(
|
|
2172
|
-
const kubernetes = await fileExists(
|
|
2455
|
+
const docker = await fileExists(path16.join(rootDir, "Dockerfile")) || await fileExists(path16.join(rootDir, "docker-compose.yml")) || await fileExists(path16.join(rootDir, "docker-compose.yaml"));
|
|
2456
|
+
const kubernetes = await fileExists(path16.join(rootDir, "k8s")) || await fileExists(path16.join(rootDir, "kubernetes"));
|
|
2173
2457
|
let ci = "none";
|
|
2174
2458
|
const ciFiles = [];
|
|
2175
2459
|
for (const [platform, filePath] of Object.entries(CI_PLATFORM_FILES)) {
|
|
2176
|
-
if (await fileExists(
|
|
2460
|
+
if (await fileExists(path16.join(rootDir, filePath))) {
|
|
2177
2461
|
if (ci === "none") ci = platform;
|
|
2178
2462
|
ciFiles.push(filePath);
|
|
2179
2463
|
}
|
|
@@ -2198,30 +2482,30 @@ async function detectInfra(rootDir) {
|
|
|
2198
2482
|
];
|
|
2199
2483
|
const infrastructureFiles = (await Promise.all(
|
|
2200
2484
|
infrastructureCandidates.map(
|
|
2201
|
-
async (file) => await fileExists(
|
|
2485
|
+
async (file) => await fileExists(path16.join(rootDir, file)) ? file : void 0
|
|
2202
2486
|
)
|
|
2203
2487
|
)).filter((file) => file !== void 0);
|
|
2204
2488
|
const deploymentFiles = (await Promise.all(
|
|
2205
2489
|
deploymentCandidates.map(
|
|
2206
|
-
async (file) => await fileExists(
|
|
2490
|
+
async (file) => await fileExists(path16.join(rootDir, file)) ? file : void 0
|
|
2207
2491
|
)
|
|
2208
2492
|
)).filter((file) => file !== void 0);
|
|
2209
2493
|
return { docker, kubernetes, ci, ciFiles, infrastructureFiles, deploymentFiles };
|
|
2210
2494
|
}
|
|
2211
2495
|
|
|
2212
2496
|
// src/scanner/detect-services.ts
|
|
2213
|
-
import { readFile as
|
|
2214
|
-
import
|
|
2497
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
2498
|
+
import path17 from "path";
|
|
2215
2499
|
async function detectProjectManagement(rootDir) {
|
|
2216
2500
|
const tools = [];
|
|
2217
2501
|
const mcpConfigPaths = [
|
|
2218
|
-
|
|
2219
|
-
|
|
2502
|
+
path17.join(rootDir, ".cursor", "mcp.json"),
|
|
2503
|
+
path17.join(rootDir, "mcp.json")
|
|
2220
2504
|
];
|
|
2221
2505
|
for (const configPath of mcpConfigPaths) {
|
|
2222
2506
|
if (!await fileExists(configPath)) continue;
|
|
2223
2507
|
try {
|
|
2224
|
-
const raw = await
|
|
2508
|
+
const raw = await readFile8(configPath, "utf8");
|
|
2225
2509
|
const lower = raw.toLowerCase();
|
|
2226
2510
|
if (lower.includes("clickup")) tools.push("clickup");
|
|
2227
2511
|
if (lower.includes("jira") || lower.includes("atlassian")) tools.push("jira");
|
|
@@ -2232,20 +2516,20 @@ async function detectProjectManagement(rootDir) {
|
|
|
2232
2516
|
} catch {
|
|
2233
2517
|
}
|
|
2234
2518
|
}
|
|
2235
|
-
if (await fileExists(
|
|
2519
|
+
if (await fileExists(path17.join(rootDir, ".github", "ISSUE_TEMPLATE"))) {
|
|
2236
2520
|
tools.push("github-issues");
|
|
2237
2521
|
}
|
|
2238
|
-
if (await fileExists(
|
|
2522
|
+
if (await fileExists(path17.join(rootDir, ".github", "projects"))) {
|
|
2239
2523
|
tools.push("github-projects");
|
|
2240
2524
|
}
|
|
2241
2525
|
return [...new Set(tools)];
|
|
2242
2526
|
}
|
|
2243
2527
|
async function detectServices(rootDir) {
|
|
2244
|
-
const hasPrisma = await fileExists(
|
|
2245
|
-
const hasSequelize = await fileExists(
|
|
2246
|
-
const hasDrizzle = await fileExists(
|
|
2247
|
-
const hasKnex = await fileExists(
|
|
2248
|
-
const hasTypeorm = await fileExists(
|
|
2528
|
+
const hasPrisma = await fileExists(path17.join(rootDir, "prisma/schema.prisma"));
|
|
2529
|
+
const hasSequelize = await fileExists(path17.join(rootDir, "sequelize"));
|
|
2530
|
+
const hasDrizzle = await fileExists(path17.join(rootDir, "drizzle.config.ts"));
|
|
2531
|
+
const hasKnex = await fileExists(path17.join(rootDir, "knexfile.ts"));
|
|
2532
|
+
const hasTypeorm = await fileExists(path17.join(rootDir, "ormconfig.json"));
|
|
2249
2533
|
const database = hasPrisma || hasSequelize || hasDrizzle || hasKnex || hasTypeorm ? "postgresql" : void 0;
|
|
2250
2534
|
const orm = hasPrisma ? "prisma" : hasDrizzle ? "drizzle" : hasSequelize ? "sequelize" : hasKnex ? "knex" : hasTypeorm ? "typeorm" : void 0;
|
|
2251
2535
|
const projectManagement = await detectProjectManagement(rootDir);
|
|
@@ -2257,7 +2541,7 @@ async function detectServices(rootDir) {
|
|
|
2257
2541
|
}
|
|
2258
2542
|
|
|
2259
2543
|
// src/scanner/detect-stack.ts
|
|
2260
|
-
import
|
|
2544
|
+
import path18 from "path";
|
|
2261
2545
|
var PROJECT_MARKERS = [
|
|
2262
2546
|
"package.json",
|
|
2263
2547
|
"requirements.txt",
|
|
@@ -2286,7 +2570,7 @@ async function detectPackageManager(rootDir, packageJson) {
|
|
|
2286
2570
|
};
|
|
2287
2571
|
}
|
|
2288
2572
|
for (const [lockfile, packageManager] of LOCKFILES) {
|
|
2289
|
-
if (await fileExists(
|
|
2573
|
+
if (await fileExists(path18.join(rootDir, lockfile))) {
|
|
2290
2574
|
return {
|
|
2291
2575
|
packageManager,
|
|
2292
2576
|
evidence: [{ source: "file", value: lockfile }]
|
|
@@ -2303,27 +2587,27 @@ function commandsForScripts(scripts, packageManager) {
|
|
|
2303
2587
|
return { testCommands, validationCommands };
|
|
2304
2588
|
}
|
|
2305
2589
|
async function detectStack(rootDir) {
|
|
2306
|
-
const hasAnyProjectMarker = (await Promise.all(PROJECT_MARKERS.map((item) => fileExists(
|
|
2307
|
-
const hasPackageJson = await fileExists(
|
|
2590
|
+
const hasAnyProjectMarker = (await Promise.all(PROJECT_MARKERS.map((item) => fileExists(path18.join(rootDir, item))))).some(Boolean);
|
|
2591
|
+
const hasPackageJson = await fileExists(path18.join(rootDir, "package.json"));
|
|
2308
2592
|
if (hasPackageJson) {
|
|
2309
|
-
const packageJson = await readJson(
|
|
2593
|
+
const packageJson = await readJson(path18.join(rootDir, "package.json")) ?? {};
|
|
2310
2594
|
const scripts = packageJson.scripts ?? {};
|
|
2311
2595
|
const packageManager = await detectPackageManager(rootDir, packageJson);
|
|
2312
2596
|
const commands = commandsForScripts(scripts, packageManager.packageManager);
|
|
2313
|
-
const hasNextConfig = await fileExists(
|
|
2314
|
-
const hasNestConfig = await fileExists(
|
|
2597
|
+
const hasNextConfig = await fileExists(path18.join(rootDir, "next.config.js")) || await fileExists(path18.join(rootDir, "next.config.mjs")) || await fileExists(path18.join(rootDir, "next.config.ts"));
|
|
2598
|
+
const hasNestConfig = await fileExists(path18.join(rootDir, "nest-cli.json"));
|
|
2315
2599
|
return {
|
|
2316
2600
|
language: "node",
|
|
2317
2601
|
framework: hasNextConfig ? "nextjs" : hasNestConfig ? "nestjs" : "node",
|
|
2318
2602
|
packageManager: packageManager.packageManager,
|
|
2319
2603
|
packageManagerEvidence: packageManager.evidence,
|
|
2320
2604
|
scripts,
|
|
2321
|
-
workspaces: packageJson.workspaces !== void 0 || await fileExists(
|
|
2605
|
+
workspaces: packageJson.workspaces !== void 0 || await fileExists(path18.join(rootDir, "pnpm-workspace.yaml")),
|
|
2322
2606
|
...commands,
|
|
2323
2607
|
hasProjectFiles: hasAnyProjectMarker
|
|
2324
2608
|
};
|
|
2325
2609
|
}
|
|
2326
|
-
if (await fileExists(
|
|
2610
|
+
if (await fileExists(path18.join(rootDir, "pyproject.toml"))) {
|
|
2327
2611
|
return {
|
|
2328
2612
|
language: "python",
|
|
2329
2613
|
framework: "python",
|
|
@@ -2333,7 +2617,7 @@ async function detectStack(rootDir) {
|
|
|
2333
2617
|
hasProjectFiles: hasAnyProjectMarker
|
|
2334
2618
|
};
|
|
2335
2619
|
}
|
|
2336
|
-
if (await fileExists(
|
|
2620
|
+
if (await fileExists(path18.join(rootDir, "go.mod"))) {
|
|
2337
2621
|
return {
|
|
2338
2622
|
language: "go",
|
|
2339
2623
|
framework: "go",
|
|
@@ -2343,7 +2627,7 @@ async function detectStack(rootDir) {
|
|
|
2343
2627
|
hasProjectFiles: hasAnyProjectMarker
|
|
2344
2628
|
};
|
|
2345
2629
|
}
|
|
2346
|
-
if (await fileExists(
|
|
2630
|
+
if (await fileExists(path18.join(rootDir, "Cargo.toml"))) {
|
|
2347
2631
|
return {
|
|
2348
2632
|
language: "rust",
|
|
2349
2633
|
framework: "rust",
|
|
@@ -2353,7 +2637,7 @@ async function detectStack(rootDir) {
|
|
|
2353
2637
|
hasProjectFiles: hasAnyProjectMarker
|
|
2354
2638
|
};
|
|
2355
2639
|
}
|
|
2356
|
-
if (await fileExists(
|
|
2640
|
+
if (await fileExists(path18.join(rootDir, "composer.json"))) {
|
|
2357
2641
|
return {
|
|
2358
2642
|
language: "php",
|
|
2359
2643
|
framework: "php",
|
|
@@ -2386,7 +2670,7 @@ function isGreenfieldByEntries(entries) {
|
|
|
2386
2670
|
return meaningful.length === 0;
|
|
2387
2671
|
}
|
|
2388
2672
|
async function runScanner(rootDir) {
|
|
2389
|
-
const normalizedRoot =
|
|
2673
|
+
const normalizedRoot = path19.resolve(rootDir);
|
|
2390
2674
|
const entries = await listDirectory(normalizedRoot);
|
|
2391
2675
|
const stack = await detectStack(normalizedRoot);
|
|
2392
2676
|
const purpose = await detectPurpose(normalizedRoot, stack);
|
|
@@ -2537,10 +2821,13 @@ function preferenceDefaults(onboarding, onboarded) {
|
|
|
2537
2821
|
enabled: false,
|
|
2538
2822
|
backend: "claude",
|
|
2539
2823
|
autoRemediate: false,
|
|
2540
|
-
offerOnExhausted: true
|
|
2824
|
+
offerOnExhausted: true,
|
|
2825
|
+
mode: "paste",
|
|
2826
|
+
midBatchAudits: false,
|
|
2827
|
+
preflight: "off"
|
|
2541
2828
|
},
|
|
2542
2829
|
autoHandoff: false,
|
|
2543
|
-
|
|
2830
|
+
agentPersona: {
|
|
2544
2831
|
default: "autopilot",
|
|
2545
2832
|
modes: {
|
|
2546
2833
|
"continue-plan": "autopilot",
|
|
@@ -2587,21 +2874,21 @@ async function executeSafeReadinessFixes(rootDir, options) {
|
|
|
2587
2874
|
});
|
|
2588
2875
|
const changes = [];
|
|
2589
2876
|
for (const relativePath of ESSENTIAL_DIRECTORIES) {
|
|
2590
|
-
const absolutePath =
|
|
2591
|
-
const
|
|
2592
|
-
if (!
|
|
2877
|
+
const absolutePath = path20.join(beforeScan.rootDir, relativePath);
|
|
2878
|
+
const exists2 = await fileExists(absolutePath);
|
|
2879
|
+
if (!exists2 && !dryRun) await ensureDir(absolutePath);
|
|
2593
2880
|
recordChange(
|
|
2594
2881
|
changes,
|
|
2595
2882
|
"ensure-agent-kit-directory",
|
|
2596
2883
|
relativePath,
|
|
2597
|
-
!
|
|
2884
|
+
!exists2,
|
|
2598
2885
|
dryRun,
|
|
2599
|
-
relativeEvidence(relativePath,
|
|
2886
|
+
relativeEvidence(relativePath, exists2 ? "already exists" : "missing directory")
|
|
2600
2887
|
);
|
|
2601
2888
|
}
|
|
2602
2889
|
const gitignoreRelativePath = ".gitignore";
|
|
2603
|
-
const gitignorePath =
|
|
2604
|
-
const existingGitignore = await fileExists(gitignorePath) ? await
|
|
2890
|
+
const gitignorePath = path20.join(beforeScan.rootDir, gitignoreRelativePath);
|
|
2891
|
+
const existingGitignore = await fileExists(gitignorePath) ? await readFile9(gitignorePath, "utf8") : "";
|
|
2605
2892
|
const mergedGitignore = mergeSecretIgnores(existingGitignore);
|
|
2606
2893
|
const gitignoreChanged = mergedGitignore !== existingGitignore;
|
|
2607
2894
|
if (gitignoreChanged && !dryRun) await writeFile3(gitignorePath, mergedGitignore, "utf8");
|
|
@@ -2616,7 +2903,7 @@ async function executeSafeReadinessFixes(rootDir, options) {
|
|
|
2616
2903
|
gitignoreChanged ? "required secret patterns are missing" : "required patterns are present"
|
|
2617
2904
|
)
|
|
2618
2905
|
);
|
|
2619
|
-
const profilePath =
|
|
2906
|
+
const profilePath = path20.join(beforeScan.rootDir, PROFILE_RELATIVE_PATH);
|
|
2620
2907
|
const existingProfile = await readJson(profilePath) ?? {};
|
|
2621
2908
|
const desiredProfile = createProfile(beforeScan, before, generatedAt);
|
|
2622
2909
|
const mergedProfile = mergeMissing(existingProfile, desiredProfile);
|
|
@@ -2638,7 +2925,7 @@ async function executeSafeReadinessFixes(rootDir, options) {
|
|
|
2638
2925
|
generatorVersion: options.generatorVersion,
|
|
2639
2926
|
generatedAt
|
|
2640
2927
|
});
|
|
2641
|
-
const contextConfigPath =
|
|
2928
|
+
const contextConfigPath = path20.join(beforeScan.rootDir, CONTEXT_CONFIG_RELATIVE_PATH);
|
|
2642
2929
|
const existingContextConfig = await readJson(contextConfigPath) ?? {};
|
|
2643
2930
|
const onboarding = reconcileOnboardingState(evidenceReport, existingContextConfig, generatedAt);
|
|
2644
2931
|
const defaults = preferenceDefaults(onboarding, existingContextConfig.onboarded);
|
|
@@ -2667,24 +2954,25 @@ async function executeSafeReadinessFixes(rootDir, options) {
|
|
|
2667
2954
|
}
|
|
2668
2955
|
|
|
2669
2956
|
// src/scanner/snapshot.ts
|
|
2670
|
-
import
|
|
2957
|
+
import path21 from "path";
|
|
2671
2958
|
var READINESS_SNAPSHOT_RELATIVE_PATH = ".cursor/context/readiness.json";
|
|
2672
2959
|
async function writeReadinessSnapshot(rootDir, report) {
|
|
2673
|
-
const snapshotPath =
|
|
2960
|
+
const snapshotPath = path21.join(rootDir, READINESS_SNAPSHOT_RELATIVE_PATH);
|
|
2674
2961
|
await writeJson(snapshotPath, report);
|
|
2675
2962
|
return snapshotPath;
|
|
2676
2963
|
}
|
|
2677
2964
|
|
|
2678
2965
|
// src/commands/doctor.ts
|
|
2679
2966
|
async function runDoctor(cwd, options = {}) {
|
|
2680
|
-
const rootDir =
|
|
2967
|
+
const rootDir = path22.resolve(cwd);
|
|
2968
|
+
const hooks = await assessHooksHealth(rootDir);
|
|
2681
2969
|
if (options.fixSafe) {
|
|
2682
2970
|
const execution = await executeSafeReadinessFixes(rootDir, {
|
|
2683
2971
|
generatorVersion: KIT_VERSION,
|
|
2684
2972
|
generatedAt: options.generatedAt
|
|
2685
2973
|
});
|
|
2686
2974
|
await writeReadinessSnapshot(rootDir, execution.after);
|
|
2687
|
-
return { report: execution.after, safeChanges: execution.changes };
|
|
2975
|
+
return { report: execution.after, safeChanges: execution.changes, hooks };
|
|
2688
2976
|
}
|
|
2689
2977
|
const scan = await runScanner(rootDir);
|
|
2690
2978
|
const report = createReadinessReport(scan, {
|
|
@@ -2692,7 +2980,7 @@ async function runDoctor(cwd, options = {}) {
|
|
|
2692
2980
|
generatedAt: options.generatedAt
|
|
2693
2981
|
});
|
|
2694
2982
|
await writeReadinessSnapshot(rootDir, report);
|
|
2695
|
-
return { report, safeChanges: [] };
|
|
2983
|
+
return { report, safeChanges: [], hooks };
|
|
2696
2984
|
}
|
|
2697
2985
|
function printDoctorSummary(result) {
|
|
2698
2986
|
const { summary, pendingActions } = result.report;
|
|
@@ -2704,11 +2992,17 @@ function printDoctorSummary(result) {
|
|
|
2704
2992
|
);
|
|
2705
2993
|
console.log(` safe fixes applied: ${fixed}`);
|
|
2706
2994
|
console.log(` pending actions: ${pendingActions.length}`);
|
|
2995
|
+
console.log(`hooks: ${result.hooks.status}`);
|
|
2996
|
+
if (result.hooks.reasons.length > 0) {
|
|
2997
|
+
for (const reason of result.hooks.reasons.slice(0, 5)) {
|
|
2998
|
+
console.log(` - ${reason}`);
|
|
2999
|
+
}
|
|
3000
|
+
}
|
|
2707
3001
|
console.log(
|
|
2708
3002
|
nextAction ? `Next: ${nextAction.recommendation}` : "Next: repository readiness checks are complete"
|
|
2709
3003
|
);
|
|
2710
3004
|
}
|
|
2711
|
-
var doctorCommand =
|
|
3005
|
+
var doctorCommand = defineCommand6({
|
|
2712
3006
|
meta: {
|
|
2713
3007
|
name: "doctor",
|
|
2714
3008
|
description: "Diagnose repository readiness and optionally apply safe local fixes."
|
|
@@ -2739,11 +3033,210 @@ var doctorCommand = defineCommand5({
|
|
|
2739
3033
|
}
|
|
2740
3034
|
});
|
|
2741
3035
|
|
|
3036
|
+
// src/commands/guard.ts
|
|
3037
|
+
import { defineCommand as defineCommand7 } from "citty";
|
|
3038
|
+
|
|
3039
|
+
// src/hooks/read-stdin-json.ts
|
|
3040
|
+
async function readStdinJson() {
|
|
3041
|
+
const chunks = [];
|
|
3042
|
+
for await (const chunk of process.stdin) {
|
|
3043
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
|
|
3044
|
+
}
|
|
3045
|
+
const raw = Buffer.concat(chunks).toString("utf8").trim();
|
|
3046
|
+
if (!raw) return {};
|
|
3047
|
+
try {
|
|
3048
|
+
return JSON.parse(raw);
|
|
3049
|
+
} catch {
|
|
3050
|
+
return {};
|
|
3051
|
+
}
|
|
3052
|
+
}
|
|
3053
|
+
|
|
3054
|
+
// src/invariants/secrets-scan.ts
|
|
3055
|
+
var CITE = "agent-kit guard prompt (docs/cursor-native-audit.md)";
|
|
3056
|
+
var SECRET_PATTERNS2 = [
|
|
3057
|
+
{
|
|
3058
|
+
id: "json-secret-kv",
|
|
3059
|
+
re: /"(password|apiKey|api_key|secret|token|auth)"\s*:\s*"[^"]{12,}"/i
|
|
3060
|
+
},
|
|
3061
|
+
{
|
|
3062
|
+
id: "env-assignment",
|
|
3063
|
+
re: /\b(?:API_KEY|SECRET|PASSWORD|TOKEN|ACCESS_KEY|PRIVATE_KEY)\s*=\s*['"]?[^\s'"]{12,}/i
|
|
3064
|
+
},
|
|
3065
|
+
{
|
|
3066
|
+
id: "aws-access-key",
|
|
3067
|
+
re: /\bAKIA[0-9A-Z]{16}\b/
|
|
3068
|
+
},
|
|
3069
|
+
{
|
|
3070
|
+
id: "github-pat",
|
|
3071
|
+
re: /\bghp_[A-Za-z0-9_]{36,}\b/
|
|
3072
|
+
},
|
|
3073
|
+
{
|
|
3074
|
+
id: "openai-sk",
|
|
3075
|
+
re: /\bsk-[A-Za-z0-9]{20,}\b/
|
|
3076
|
+
}
|
|
3077
|
+
];
|
|
3078
|
+
function excerptAround(text, index, len) {
|
|
3079
|
+
const start = Math.max(0, index - 8);
|
|
3080
|
+
const end = Math.min(text.length, index + len + 8);
|
|
3081
|
+
return text.slice(start, end).replace(/\s+/g, " ");
|
|
3082
|
+
}
|
|
3083
|
+
function scanTextForSecrets(text) {
|
|
3084
|
+
if (!text) return [];
|
|
3085
|
+
const hits = [];
|
|
3086
|
+
for (const { id, re } of SECRET_PATTERNS2) {
|
|
3087
|
+
const flags = re.flags.includes("g") ? re.flags : `${re.flags}g`;
|
|
3088
|
+
const global = new RegExp(re.source, flags);
|
|
3089
|
+
let match = global.exec(text);
|
|
3090
|
+
while (match) {
|
|
3091
|
+
hits.push({
|
|
3092
|
+
patternId: id,
|
|
3093
|
+
excerpt: excerptAround(text, match.index, match[0].length)
|
|
3094
|
+
});
|
|
3095
|
+
if (!global.global) break;
|
|
3096
|
+
match = global.exec(text);
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
return hits;
|
|
3100
|
+
}
|
|
3101
|
+
function secretsAdviseMessage(hits) {
|
|
3102
|
+
const ids = [...new Set(hits.map((h) => h.patternId))].join(", ");
|
|
3103
|
+
return `Possible secret pattern(s) in prompt (${ids}). Cite: ${CITE}. Remove live credentials before submitting; use env vars or a secrets store.`;
|
|
3104
|
+
}
|
|
3105
|
+
|
|
3106
|
+
// src/invariants/shell-guard.ts
|
|
3107
|
+
var CITE2 = "agent-kit guard shell (ADR 2026-07-29_cli-invariants-thin-hook-adapters)";
|
|
3108
|
+
function normalizeShellCommand(command) {
|
|
3109
|
+
return command.replace(/\s+/g, " ").trim();
|
|
3110
|
+
}
|
|
3111
|
+
function shellInvocationHeads(command) {
|
|
3112
|
+
const normalized = normalizeShellCommand(command);
|
|
3113
|
+
if (!normalized) return [];
|
|
3114
|
+
return normalized.split(/(?:&&|\|\||[;|])/).map((part) => part.trim().replace(/^(?:\w+=\S+\s+)*/, "")).filter(Boolean);
|
|
3115
|
+
}
|
|
3116
|
+
function anyHeadMatches(command, re) {
|
|
3117
|
+
return shellInvocationHeads(command).some((head) => re.test(head));
|
|
3118
|
+
}
|
|
3119
|
+
var SHELL_DENY_RULES = [
|
|
3120
|
+
{
|
|
3121
|
+
id: "git-checkout-path",
|
|
3122
|
+
description: "git checkout -- <paths> discards working-tree edits",
|
|
3123
|
+
test: (cmd) => anyHeadMatches(cmd, /^(?:[\w./-]+\/)?git\s+checkout\s+--(?:\s|$)/)
|
|
3124
|
+
},
|
|
3125
|
+
{
|
|
3126
|
+
id: "git-restore",
|
|
3127
|
+
description: "git restore discards working-tree edits",
|
|
3128
|
+
test: (cmd) => anyHeadMatches(cmd, /^(?:[\w./-]+\/)?git\s+restore\b/)
|
|
3129
|
+
},
|
|
3130
|
+
{
|
|
3131
|
+
id: "git-reset-hard",
|
|
3132
|
+
description: "git reset --hard destroys uncommitted work",
|
|
3133
|
+
test: (cmd) => anyHeadMatches(cmd, /^(?:[\w./-]+\/)?git\s+reset\b.*--hard\b/)
|
|
3134
|
+
},
|
|
3135
|
+
{
|
|
3136
|
+
id: "git-clean-fd",
|
|
3137
|
+
description: "git clean -fd removes untracked files",
|
|
3138
|
+
test: (cmd) => shellInvocationHeads(cmd).some(
|
|
3139
|
+
(head) => /^(?:[\w./-]+\/)?git\s+clean\b/.test(head) && /(?:^|\s)-(?:[a-z]*f[a-z]*d|[a-z]*d[a-z]*f)(?:\s|$)/.test(head)
|
|
3140
|
+
)
|
|
3141
|
+
},
|
|
3142
|
+
{
|
|
3143
|
+
id: "git-push-main",
|
|
3144
|
+
description: "direct push to main/master/prod bypasses staging",
|
|
3145
|
+
test: (cmd) => shellInvocationHeads(cmd).some((head) => {
|
|
3146
|
+
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);
|
|
3148
|
+
})
|
|
3149
|
+
}
|
|
3150
|
+
];
|
|
3151
|
+
function evaluateShellCommand(command) {
|
|
3152
|
+
const normalized = normalizeShellCommand(command);
|
|
3153
|
+
if (!normalized) {
|
|
3154
|
+
return { permission: "allow" };
|
|
3155
|
+
}
|
|
3156
|
+
for (const rule of SHELL_DENY_RULES) {
|
|
3157
|
+
if (rule.test(normalized)) {
|
|
3158
|
+
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
|
+
return {
|
|
3160
|
+
permission: "deny",
|
|
3161
|
+
rule: rule.id,
|
|
3162
|
+
agent_message,
|
|
3163
|
+
user_message: agent_message
|
|
3164
|
+
};
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3167
|
+
return { permission: "allow" };
|
|
3168
|
+
}
|
|
3169
|
+
|
|
3170
|
+
// src/commands/guard.ts
|
|
3171
|
+
var guardCommand = defineCommand7({
|
|
3172
|
+
meta: {
|
|
3173
|
+
name: "guard",
|
|
3174
|
+
description: "Mechanizable deny/annotate guards (shell, prompt). Hooks are thin adapters."
|
|
3175
|
+
},
|
|
3176
|
+
subCommands: {
|
|
3177
|
+
shell: defineCommand7({
|
|
3178
|
+
meta: {
|
|
3179
|
+
name: "shell",
|
|
3180
|
+
description: "Evaluate a shell command against the destructive deny-list"
|
|
3181
|
+
},
|
|
3182
|
+
args: {
|
|
3183
|
+
json: {
|
|
3184
|
+
type: "boolean",
|
|
3185
|
+
default: true,
|
|
3186
|
+
description: "Print Cursor beforeShellExecution JSON (default)"
|
|
3187
|
+
},
|
|
3188
|
+
command: {
|
|
3189
|
+
type: "string",
|
|
3190
|
+
description: "Command string (otherwise read from stdin JSON.command)"
|
|
3191
|
+
}
|
|
3192
|
+
},
|
|
3193
|
+
async run({ args }) {
|
|
3194
|
+
let command = typeof args.command === "string" ? args.command : "";
|
|
3195
|
+
if (!command) {
|
|
3196
|
+
const payload = await readStdinJson();
|
|
3197
|
+
command = typeof payload.command === "string" ? payload.command : "";
|
|
3198
|
+
}
|
|
3199
|
+
const result = evaluateShellCommand(command);
|
|
3200
|
+
console.log(JSON.stringify(result));
|
|
3201
|
+
}
|
|
3202
|
+
}),
|
|
3203
|
+
prompt: defineCommand7({
|
|
3204
|
+
meta: {
|
|
3205
|
+
name: "prompt",
|
|
3206
|
+
description: "Scan prompt text for secret patterns (advisory; fail-open at hook)"
|
|
3207
|
+
},
|
|
3208
|
+
args: {
|
|
3209
|
+
json: {
|
|
3210
|
+
type: "boolean",
|
|
3211
|
+
default: true
|
|
3212
|
+
}
|
|
3213
|
+
},
|
|
3214
|
+
async run() {
|
|
3215
|
+
const payload = await readStdinJson();
|
|
3216
|
+
const text = typeof payload.prompt === "string" && payload.prompt || typeof payload.text === "string" && payload.text || "";
|
|
3217
|
+
const hits = scanTextForSecrets(text);
|
|
3218
|
+
if (hits.length === 0) {
|
|
3219
|
+
console.log(JSON.stringify({ continue: true, hits: [] }));
|
|
3220
|
+
return;
|
|
3221
|
+
}
|
|
3222
|
+
console.log(
|
|
3223
|
+
JSON.stringify({
|
|
3224
|
+
continue: true,
|
|
3225
|
+
user_message: secretsAdviseMessage(hits),
|
|
3226
|
+
agent_message: secretsAdviseMessage(hits),
|
|
3227
|
+
hits
|
|
3228
|
+
})
|
|
3229
|
+
);
|
|
3230
|
+
}
|
|
3231
|
+
})
|
|
3232
|
+
}
|
|
3233
|
+
});
|
|
3234
|
+
|
|
2742
3235
|
// src/commands/handoff.ts
|
|
2743
|
-
import { spawn as
|
|
2744
|
-
import { readFile as
|
|
2745
|
-
import
|
|
2746
|
-
import { defineCommand as
|
|
3236
|
+
import { spawn as spawn3 } from "child_process";
|
|
3237
|
+
import { readFile as readFile10, readdir as readdir2, writeFile as writeFile4 } from "fs/promises";
|
|
3238
|
+
import path23 from "path";
|
|
3239
|
+
import { defineCommand as defineCommand8 } from "citty";
|
|
2747
3240
|
function parsePlanFrontmatter(raw) {
|
|
2748
3241
|
const match = raw.match(/^---\n([\s\S]*?)\n---/);
|
|
2749
3242
|
if (!match?.[1]) return null;
|
|
@@ -2762,19 +3255,19 @@ async function findActivePlan(plansDir) {
|
|
|
2762
3255
|
if (!await fileExists(plansDir)) return null;
|
|
2763
3256
|
const files = (await readdir2(plansDir)).filter((f) => f.endsWith(".plan.md")).sort().reverse();
|
|
2764
3257
|
for (const file of files) {
|
|
2765
|
-
const raw = await
|
|
3258
|
+
const raw = await readFile10(path23.join(plansDir, file), "utf8");
|
|
2766
3259
|
const fm = parsePlanFrontmatter(raw);
|
|
2767
3260
|
if (fm?.todos?.some((t) => t.status !== "completed" && t.status !== "cancelled")) {
|
|
2768
3261
|
return { file, raw };
|
|
2769
3262
|
}
|
|
2770
3263
|
}
|
|
2771
|
-
return files[0] ? { file: files[0], raw: await
|
|
3264
|
+
return files[0] ? { file: files[0], raw: await readFile10(path23.join(plansDir, files[0]), "utf8") } : null;
|
|
2772
3265
|
}
|
|
2773
3266
|
function now() {
|
|
2774
3267
|
return (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 16);
|
|
2775
3268
|
}
|
|
2776
3269
|
async function loadProfile(rootDir) {
|
|
2777
|
-
const configPath =
|
|
3270
|
+
const configPath = path23.join(rootDir, ".cursor", "agent-kit.config.json");
|
|
2778
3271
|
try {
|
|
2779
3272
|
return await readJson(configPath);
|
|
2780
3273
|
} catch {
|
|
@@ -2852,7 +3345,7 @@ function printV3Guidance() {
|
|
|
2852
3345
|
}
|
|
2853
3346
|
function runCursorHandoff(scriptPath, cwd) {
|
|
2854
3347
|
return new Promise((resolve, reject) => {
|
|
2855
|
-
const child =
|
|
3348
|
+
const child = spawn3("sh", [scriptPath, "handoff"], {
|
|
2856
3349
|
cwd,
|
|
2857
3350
|
stdio: "inherit"
|
|
2858
3351
|
});
|
|
@@ -2860,7 +3353,7 @@ function runCursorHandoff(scriptPath, cwd) {
|
|
|
2860
3353
|
child.on("close", (code) => resolve(code ?? 1));
|
|
2861
3354
|
});
|
|
2862
3355
|
}
|
|
2863
|
-
var handoffCommand =
|
|
3356
|
+
var handoffCommand = defineCommand8({
|
|
2864
3357
|
meta: {
|
|
2865
3358
|
name: "handoff",
|
|
2866
3359
|
description: "Write .cursor/HANDOFF.md from the active Cursor plan, or run ./cursor-handoff handoff when no plan exists."
|
|
@@ -2874,13 +3367,13 @@ var handoffCommand = defineCommand6({
|
|
|
2874
3367
|
},
|
|
2875
3368
|
async run({ args }) {
|
|
2876
3369
|
const profile = await loadProfile(args.cwd);
|
|
2877
|
-
const plansDir =
|
|
2878
|
-
const handoffPath =
|
|
3370
|
+
const plansDir = path23.join(args.cwd, ".cursor", "plans");
|
|
3371
|
+
const handoffPath = path23.join(args.cwd, ".cursor", "HANDOFF.md");
|
|
2879
3372
|
const plan = await findActivePlan(plansDir);
|
|
2880
3373
|
if (plan) {
|
|
2881
3374
|
const fm = parsePlanFrontmatter(plan.raw);
|
|
2882
3375
|
if (fm) {
|
|
2883
|
-
await ensureDir(
|
|
3376
|
+
await ensureDir(path23.join(args.cwd, ".cursor"));
|
|
2884
3377
|
const content = buildHandoff(plan.file, fm, profile);
|
|
2885
3378
|
await writeFile4(handoffPath, content, "utf8");
|
|
2886
3379
|
logger.success("HANDOFF.md updated: .cursor/HANDOFF.md");
|
|
@@ -2900,7 +3393,7 @@ var handoffCommand = defineCommand6({
|
|
|
2900
3393
|
}
|
|
2901
3394
|
logger.warn(`Plan ${plan.file} without valid frontmatter; trying legacy flow.`);
|
|
2902
3395
|
}
|
|
2903
|
-
const scriptPath =
|
|
3396
|
+
const scriptPath = path23.join(args.cwd, "cursor-handoff");
|
|
2904
3397
|
if (!await fileExists(scriptPath)) {
|
|
2905
3398
|
printV3Guidance();
|
|
2906
3399
|
return;
|
|
@@ -2924,28 +3417,399 @@ var handoffCommand = defineCommand6({
|
|
|
2924
3417
|
}
|
|
2925
3418
|
});
|
|
2926
3419
|
|
|
2927
|
-
// src/commands/
|
|
2928
|
-
import
|
|
2929
|
-
import { defineCommand as
|
|
3420
|
+
// src/commands/hook.ts
|
|
3421
|
+
import path25 from "path";
|
|
3422
|
+
import { defineCommand as defineCommand9 } from "citty";
|
|
2930
3423
|
|
|
2931
|
-
// src/
|
|
3424
|
+
// src/hooks/pre-compact.ts
|
|
3425
|
+
function buildPreCompactUserMessage(payload = {}) {
|
|
3426
|
+
const pct = payload.context_usage_percent;
|
|
3427
|
+
const trigger = payload.trigger || "auto";
|
|
3428
|
+
const pctTxt = pct !== void 0 && pct !== null ? `~${pct}%` : "high";
|
|
3429
|
+
const msg = `Context compacting (${trigger}, usage ${pctTxt}). Update \`.cursor/HANDOFF.md\` and open a new chat with \`/continue-plan\` so the next agent starts fresh.`;
|
|
3430
|
+
return { user_message: msg };
|
|
3431
|
+
}
|
|
3432
|
+
|
|
3433
|
+
// src/hooks/session-start.ts
|
|
3434
|
+
import { spawn as spawn4 } from "child_process";
|
|
3435
|
+
import { access as access5, readFile as readFile11 } from "fs/promises";
|
|
2932
3436
|
import path24 from "path";
|
|
2933
|
-
import { defineCommand as defineCommand7 } from "citty";
|
|
2934
3437
|
|
|
2935
|
-
// src/
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
3438
|
+
// src/invariants/handoff-schema.ts
|
|
3439
|
+
var MACHINE_LIST_CHECKS = [
|
|
3440
|
+
{
|
|
3441
|
+
heading: "Backlog plans",
|
|
3442
|
+
fieldLabels: ["Backlog plans", "Backlog"],
|
|
3443
|
+
code: "heading-without-field-backlog"
|
|
3444
|
+
},
|
|
3445
|
+
{
|
|
3446
|
+
heading: "Parked plans",
|
|
3447
|
+
fieldLabels: ["Parked plans"],
|
|
3448
|
+
code: "heading-without-field-parked"
|
|
3449
|
+
},
|
|
3450
|
+
{
|
|
3451
|
+
heading: "Run queue",
|
|
3452
|
+
fieldLabels: ["Run queue"],
|
|
3453
|
+
code: "heading-without-field-run-queue"
|
|
3454
|
+
}
|
|
3455
|
+
];
|
|
3456
|
+
var CITE3 = "agent-kit validate handoff (see .cursor/context/templates/handoff.md)";
|
|
3457
|
+
function validateHandoffText(text) {
|
|
3458
|
+
if (!text.trim()) return [];
|
|
3459
|
+
const warnings = [];
|
|
3460
|
+
for (const check2 of MACHINE_LIST_CHECKS) {
|
|
3461
|
+
const headingRe = new RegExp(`^##\\s+${check2.heading}\\s*$`, "m");
|
|
3462
|
+
if (!headingRe.test(text)) continue;
|
|
3463
|
+
const hasField = check2.fieldLabels.some(
|
|
3464
|
+
(label) => new RegExp(`^- \\*\\*${label}:\\*\\*`, "m").test(text)
|
|
3465
|
+
);
|
|
3466
|
+
if (!hasField) {
|
|
3467
|
+
warnings.push({
|
|
3468
|
+
code: check2.code,
|
|
3469
|
+
message: `\`## ${check2.heading}\` found without \`- **${check2.fieldLabels[0]}:**\` (Mission Control will miss this list; rewrite as a field bullet).`,
|
|
3470
|
+
cite: CITE3
|
|
3471
|
+
});
|
|
3472
|
+
}
|
|
3473
|
+
}
|
|
3474
|
+
return warnings;
|
|
3475
|
+
}
|
|
3476
|
+
|
|
3477
|
+
// src/hooks/hard-rules.ts
|
|
3478
|
+
var HARD_RULES = `# Agent Kit session hard rules (manual mode default)
|
|
3479
|
+
|
|
3480
|
+
1. **One phase per chat.** Finish the current phase (or one to-do if the phase is huge), update \`.cursor/HANDOFF.md\`, then STOP and ask the user before starting the next phase.
|
|
3481
|
+
2. **Do not burn the window.** Never run an entire multi-phase plan in one conversation unless the user explicitly ran \`/run-plan\` (or a deprecated alias \`/run-plan-loop\` / \`/run-plan-orchestrated\`).
|
|
3482
|
+
3. **Context questions are not optional.** If the user asks about context / contexto / window size, run the context-guardian protocol: warn, offer handoff, do NOT dismiss with "it's fine" and keep coding.
|
|
3483
|
+
4. **Read HANDOFF first** when resuming. Do not restart the plan from scratch.
|
|
3484
|
+
5. **Git:** suggest \`/git-staging\` after a phase with a diff; never \`/git-prod\` without explicit confirmation.
|
|
3485
|
+
6. **HITL slash commands win.** When waiting for confirmation on \`/git-staging\` or \`/git-prod\` (or similar), do not divert to continue-plan / phase-boundary chatter; stay on that routine until the user answers.
|
|
3486
|
+
7. **\`/start-project\` is plan bootstrap, not execute.** Broad Intake Review first, then two gates: (A) single composite question (with active plan: backlog+write / park+write / modify / cancel; without: write / modify / cancel) \u2014 approve/write the plan file only, (B) approve the first unit. Goal text in the same message is NOT permission to edit product files. Never "create plan and start Phase 1" in one turn. If HANDOFF already has an active plan, disposition is merged into Gate A composite options; never park silently. Gates use Ask questions per \`.cursor/rules/hitl-ask-questions.mdc\`. Fallback: one numbered list per message.
|
|
3487
|
+
8. **\`/continue-plan\` waits for yes.** Summarize next \`[to-do-id]\`, then stop until the user confirms before editing.
|
|
3488
|
+
9. **\`/run-plan-all\` is a pure orchestrator.** After the confirm queue Ask, dispatch one Task subagent per plan (run the \`/run-plan\` tick contract inside it); never implement to-dos, run tests, or write changelogs in the orchestrator window.
|
|
3489
|
+
10. **Backlog CRUD never activates.** \`/backlog-add\` enqueues (Broad Intake + write Ask + plan file + HANDOFF Backlog) without park, activate, or Gate B. \`/backlog-edit\` / \`/backlog-delete\` / \`/backlog-cancel\` require Ask confirm before mutate; delete archives from Backlog, cancel is soft in place. No Field Report cards for routine backlog CRUD.
|
|
3490
|
+
11. **HANDOFF machine fields are bullet fields, not \`##\` headings.** Mission Control parses \`- **Plan:**\`, \`- **Backlog plans:**\`, \`- **Parked plans:**\`, \`- **Run queue:**\` (etc.). Canonical Plan: \`- **Plan:** \\\`name.plan.md\\\`\` or \`none\`. Nested backlog/parked rows: \`- \\\`other.plan.md\\\`\`. Never invent \`## Backlog plans\` / \`## Parked plans\` / \`## Run queue\` headings in place of those fields (Checklist / Current mission go empty or idle).`;
|
|
3491
|
+
var DOGFOOD_INBOX_HINT = `## Dogfood inbox
|
|
3492
|
+
|
|
3493
|
+
Unprocessed files are listed under \`dogfood/README.md\` (### Unprocessed Files). Follow the ingest ritual there (detect \u2192 analyze \u2192 memory WRITE \u2192 triage). Do not auto-start analysis unless the user asks.`;
|
|
3494
|
+
var UPDATE_CHECK_NUDGE = `## Agent Kit update available
|
|
3495
|
+
|
|
3496
|
+
Installed **v{installed}**; latest public **v{latest}**.
|
|
3497
|
+
|
|
3498
|
+
This is an advisory only (no files were changed). To apply, run \`/update\` and confirm via Ask questions. Bare \`agent-kit update\` is an explicit operator invoke, not a background job.`;
|
|
3499
|
+
|
|
3500
|
+
// src/hooks/session-start.ts
|
|
3501
|
+
var NONE_PLACEHOLDERS = /* @__PURE__ */ new Set(["none", "n/a", "empty", "nil"]);
|
|
3502
|
+
async function readTextLimited(filePath, limit = 60) {
|
|
3503
|
+
try {
|
|
3504
|
+
const lines = (await readFile11(filePath, "utf8")).split(/\r?\n/);
|
|
3505
|
+
return lines.slice(0, limit).join("\n").trim();
|
|
3506
|
+
} catch {
|
|
3507
|
+
return "";
|
|
3508
|
+
}
|
|
3509
|
+
}
|
|
3510
|
+
async function readFull(filePath) {
|
|
3511
|
+
try {
|
|
3512
|
+
return await readFile11(filePath, "utf8");
|
|
3513
|
+
} catch {
|
|
3514
|
+
return "";
|
|
3515
|
+
}
|
|
3516
|
+
}
|
|
3517
|
+
async function fileExists2(p) {
|
|
3518
|
+
try {
|
|
3519
|
+
await access5(p);
|
|
3520
|
+
return true;
|
|
3521
|
+
} catch {
|
|
3522
|
+
return false;
|
|
3523
|
+
}
|
|
3524
|
+
}
|
|
3525
|
+
function parseUnprocessedDogfoodItems(readmeText) {
|
|
3526
|
+
const items = [];
|
|
3527
|
+
let inSection = false;
|
|
3528
|
+
for (const line of readmeText.split(/\r?\n/)) {
|
|
3529
|
+
if (line.startsWith("### Unprocessed Files")) {
|
|
3530
|
+
inSection = true;
|
|
3531
|
+
continue;
|
|
3532
|
+
}
|
|
3533
|
+
if (!inSection) continue;
|
|
3534
|
+
if (line.startsWith("### ")) break;
|
|
3535
|
+
const stripped = line.trim();
|
|
3536
|
+
if (!stripped.startsWith("- ")) continue;
|
|
3537
|
+
const body = stripped.slice(2).trim();
|
|
3538
|
+
const normalized = body.toLowerCase().replace(/[*_]/g, "").trim();
|
|
3539
|
+
if (!normalized || NONE_PLACEHOLDERS.has(normalized)) continue;
|
|
3540
|
+
items.push(body);
|
|
3541
|
+
}
|
|
3542
|
+
return items;
|
|
3543
|
+
}
|
|
3544
|
+
async function l0Present(root) {
|
|
3545
|
+
const cursor = path24.join(root, ".cursor");
|
|
3546
|
+
return await fileExists2(path24.join(cursor, "agent-kit.json")) || await fileExists2(path24.join(cursor, "commands", "agent-kit-onboard.md")) || await fileExists2(path24.join(cursor, "commands", "start-project.md"));
|
|
3547
|
+
}
|
|
3548
|
+
function checkLabelAndRecommendation(check2) {
|
|
3549
|
+
const checkId = check2.id;
|
|
3550
|
+
if (typeof checkId !== "string" || !checkId) return null;
|
|
3551
|
+
const actions = check2.actions;
|
|
3552
|
+
if (Array.isArray(actions)) {
|
|
3553
|
+
for (const action2 of actions) {
|
|
3554
|
+
if (!action2 || typeof action2 !== "object") continue;
|
|
3555
|
+
const a = action2;
|
|
3556
|
+
if (typeof a.id === "string" && typeof a.recommendation === "string") {
|
|
3557
|
+
return [a.id, a.recommendation];
|
|
3558
|
+
}
|
|
3559
|
+
}
|
|
3560
|
+
}
|
|
3561
|
+
const title = check2.title;
|
|
3562
|
+
if (typeof title === "string" && title) return [checkId, title];
|
|
3563
|
+
return [checkId, "Resolve this readiness check"];
|
|
3564
|
+
}
|
|
3565
|
+
function unresolvedReadinessChecks(data) {
|
|
3566
|
+
const essential = [];
|
|
3567
|
+
const nonessential = [];
|
|
3568
|
+
const pillars = data.pillars;
|
|
3569
|
+
if (!Array.isArray(pillars)) return { essential, nonessential };
|
|
3570
|
+
for (const pillar2 of pillars) {
|
|
3571
|
+
if (!pillar2 || typeof pillar2 !== "object") continue;
|
|
3572
|
+
const checks = pillar2.checks;
|
|
3573
|
+
if (!Array.isArray(checks)) continue;
|
|
3574
|
+
for (const check2 of checks) {
|
|
3575
|
+
if (!check2 || typeof check2 !== "object") continue;
|
|
3576
|
+
const c = check2;
|
|
3577
|
+
if (c.status === "ready") continue;
|
|
3578
|
+
if (c.essential === true) essential.push(c);
|
|
3579
|
+
else nonessential.push(c);
|
|
3580
|
+
}
|
|
3581
|
+
}
|
|
3582
|
+
return { essential, nonessential };
|
|
3583
|
+
}
|
|
3584
|
+
async function readinessSection(root) {
|
|
3585
|
+
const snapshotPath = path24.join(root, ".cursor", "context", "readiness.json");
|
|
3586
|
+
let data;
|
|
3587
|
+
try {
|
|
3588
|
+
data = JSON.parse(await readFile11(snapshotPath, "utf8"));
|
|
3589
|
+
} catch {
|
|
3590
|
+
return null;
|
|
3591
|
+
}
|
|
3592
|
+
const { essential, nonessential } = unresolvedReadinessChecks(data);
|
|
3593
|
+
if (essential[0]) {
|
|
3594
|
+
const labeled = checkLabelAndRecommendation(essential[0]);
|
|
3595
|
+
if (!labeled) return null;
|
|
3596
|
+
const [actionId, recommendation] = labeled;
|
|
3597
|
+
return `## Repository readiness
|
|
3598
|
+
|
|
3599
|
+
Unresolved essential check: \`${actionId}\`. ${recommendation} Run \`/agent-kit-onboard\` before \`/start-project\`. An active plan or HANDOFF remains the current work and is not replaced.`;
|
|
3600
|
+
}
|
|
3601
|
+
if (nonessential[0]) {
|
|
3602
|
+
const labeled = checkLabelAndRecommendation(nonessential[0]);
|
|
3603
|
+
if (!labeled) return null;
|
|
3604
|
+
const [actionId, recommendation] = labeled;
|
|
3605
|
+
return `## Repository readiness
|
|
3606
|
+
|
|
3607
|
+
Optional readiness item: \`${actionId}\`. ${recommendation} This does not block \`/start-project\` or active plan work. Resume later with \`/agent-kit-onboard\` if useful.`;
|
|
3608
|
+
}
|
|
3609
|
+
const actions = data.pendingActions;
|
|
3610
|
+
if (!Array.isArray(actions) || !actions[0] || typeof actions[0] !== "object") return null;
|
|
3611
|
+
const first = actions[0];
|
|
3612
|
+
if (typeof first.id !== "string" || typeof first.recommendation !== "string") return null;
|
|
3613
|
+
return `## Repository readiness
|
|
3614
|
+
|
|
3615
|
+
Optional readiness item: \`${first.id}\`. ${first.recommendation} This does not block \`/start-project\` or active plan work. Resume later with \`/agent-kit-onboard\` if useful.`;
|
|
3616
|
+
}
|
|
3617
|
+
async function dogfoodInboxSection(root) {
|
|
3618
|
+
const dogfoodDir = path24.join(root, "dogfood");
|
|
3619
|
+
if (!await fileExists2(dogfoodDir)) return null;
|
|
3620
|
+
const readme = path24.join(dogfoodDir, "README.md");
|
|
3621
|
+
if (!await fileExists2(readme)) return null;
|
|
3622
|
+
try {
|
|
3623
|
+
const text = await readFile11(readme, "utf8");
|
|
3624
|
+
if (!parseUnprocessedDogfoodItems(text).length) return null;
|
|
3625
|
+
return DOGFOOD_INBOX_HINT;
|
|
3626
|
+
} catch {
|
|
3627
|
+
return null;
|
|
3628
|
+
}
|
|
3629
|
+
}
|
|
3630
|
+
async function loadUpdateCheckPrefs(root) {
|
|
3631
|
+
try {
|
|
3632
|
+
const data = JSON.parse(
|
|
3633
|
+
await readFile11(path24.join(root, ".cursor", "context", "config.json"), "utf8")
|
|
3634
|
+
);
|
|
3635
|
+
const uc = data.updateCheck;
|
|
3636
|
+
if (!uc || typeof uc !== "object" || uc.enabled !== true) {
|
|
3637
|
+
return null;
|
|
3638
|
+
}
|
|
3639
|
+
return uc;
|
|
3640
|
+
} catch {
|
|
3641
|
+
return null;
|
|
3642
|
+
}
|
|
3643
|
+
}
|
|
3644
|
+
function runUpdateCheckJson(root) {
|
|
3645
|
+
return new Promise((resolve) => {
|
|
3646
|
+
const child = spawn4(
|
|
3647
|
+
process.execPath,
|
|
3648
|
+
[
|
|
3649
|
+
process.argv[1] ?? "",
|
|
3650
|
+
"update",
|
|
3651
|
+
"--check",
|
|
3652
|
+
"--json",
|
|
3653
|
+
"--respect-prefs",
|
|
3654
|
+
"--stamp",
|
|
3655
|
+
"--cwd",
|
|
3656
|
+
root
|
|
3657
|
+
],
|
|
3658
|
+
{ stdio: ["ignore", "pipe", "ignore"], timeout: 12e3 }
|
|
3659
|
+
);
|
|
3660
|
+
let out = "";
|
|
3661
|
+
child.stdout?.on("data", (chunk) => {
|
|
3662
|
+
out += chunk.toString("utf8");
|
|
3663
|
+
});
|
|
3664
|
+
child.on("error", () => resolve(null));
|
|
3665
|
+
child.on("close", () => {
|
|
3666
|
+
try {
|
|
3667
|
+
const parsed = JSON.parse(out.trim());
|
|
3668
|
+
resolve(parsed && typeof parsed === "object" ? parsed : null);
|
|
3669
|
+
} catch {
|
|
3670
|
+
resolve(null);
|
|
3671
|
+
}
|
|
3672
|
+
});
|
|
3673
|
+
});
|
|
3674
|
+
}
|
|
3675
|
+
async function updateCheckSection(root) {
|
|
3676
|
+
if (await loadUpdateCheckPrefs(root) === null) return null;
|
|
3677
|
+
const result = await new Promise((resolve) => {
|
|
3678
|
+
const child = spawn4(
|
|
3679
|
+
"agent-kit",
|
|
3680
|
+
["update", "--check", "--json", "--respect-prefs", "--stamp", "--cwd", root],
|
|
3681
|
+
{ stdio: ["ignore", "pipe", "ignore"], timeout: 12e3, shell: false }
|
|
3682
|
+
);
|
|
3683
|
+
let out = "";
|
|
3684
|
+
child.stdout?.on("data", (chunk) => {
|
|
3685
|
+
out += chunk.toString("utf8");
|
|
3686
|
+
});
|
|
3687
|
+
child.on("error", () => {
|
|
3688
|
+
void runUpdateCheckJson(root).then(resolve);
|
|
3689
|
+
});
|
|
3690
|
+
child.on("close", (code) => {
|
|
3691
|
+
if (code !== 0 && !out.trim()) {
|
|
3692
|
+
void runUpdateCheckJson(root).then(resolve);
|
|
3693
|
+
return;
|
|
3694
|
+
}
|
|
3695
|
+
try {
|
|
3696
|
+
resolve(JSON.parse(out.trim()));
|
|
3697
|
+
} catch {
|
|
3698
|
+
void runUpdateCheckJson(root).then(resolve);
|
|
3699
|
+
}
|
|
3700
|
+
});
|
|
3701
|
+
});
|
|
3702
|
+
if (!result || result.status !== "update-available") return null;
|
|
3703
|
+
if (result.applyRecommended === true) return null;
|
|
3704
|
+
const installed = String(result.installedVersion ?? "?");
|
|
3705
|
+
const latest = String(result.latestVersion ?? "?");
|
|
3706
|
+
return UPDATE_CHECK_NUDGE.replace("{installed}", installed).replace("{latest}", latest);
|
|
3707
|
+
}
|
|
3708
|
+
async function buildSessionStartAdditionalContext(rootDir, _payload = {}) {
|
|
3709
|
+
const root = path24.resolve(rootDir);
|
|
3710
|
+
const handoffPath = path24.join(root, ".cursor", "HANDOFF.md");
|
|
3711
|
+
const handoffFull = await readFull(handoffPath);
|
|
3712
|
+
const handoff = await readTextLimited(handoffPath);
|
|
3713
|
+
const parts = [HARD_RULES];
|
|
3714
|
+
if (await l0Present(root)) {
|
|
3715
|
+
const readiness = await readinessSection(root);
|
|
3716
|
+
if (readiness) parts.push(readiness);
|
|
3717
|
+
}
|
|
3718
|
+
const dogfood = await dogfoodInboxSection(root);
|
|
3719
|
+
if (dogfood) parts.push(dogfood);
|
|
3720
|
+
const updateNudge = await updateCheckSection(root);
|
|
3721
|
+
if (updateNudge) parts.push(updateNudge);
|
|
3722
|
+
const formatWarnings = validateHandoffText(handoffFull);
|
|
3723
|
+
if (formatWarnings.length) {
|
|
3724
|
+
const bullet = formatWarnings.map((w) => `- ${w.message}`).join("\n");
|
|
3725
|
+
parts.push(
|
|
3726
|
+
`## HANDOFF format warning (Mission Control)
|
|
3727
|
+
|
|
3728
|
+
${bullet}
|
|
3729
|
+
|
|
3730
|
+
Rewrite machine lists as \`- **Field:**\` bullets before trusting Checklist / Current mission.`
|
|
3731
|
+
);
|
|
3732
|
+
}
|
|
3733
|
+
if (handoff) {
|
|
3734
|
+
parts.push(`## Current HANDOFF.md (excerpt)
|
|
3735
|
+
|
|
3736
|
+
${handoff}`);
|
|
3737
|
+
} else {
|
|
3738
|
+
parts.push(
|
|
3739
|
+
"## HANDOFF.md\n\nNo handoff file yet. If starting work, create a plan with to-dos first (`/start-project`)."
|
|
3740
|
+
);
|
|
3741
|
+
}
|
|
3742
|
+
return { additional_context: parts.join("\n\n") };
|
|
3743
|
+
}
|
|
3744
|
+
function resolveSessionRoot(payload, cwd = process.cwd()) {
|
|
3745
|
+
const roots = payload.workspace_roots;
|
|
3746
|
+
if (Array.isArray(roots) && typeof roots[0] === "string" && roots[0]) {
|
|
3747
|
+
return roots[0];
|
|
3748
|
+
}
|
|
3749
|
+
return cwd;
|
|
3750
|
+
}
|
|
3751
|
+
|
|
3752
|
+
// src/commands/hook.ts
|
|
3753
|
+
var hookCommand = defineCommand9({
|
|
3754
|
+
meta: {
|
|
3755
|
+
name: "hook",
|
|
3756
|
+
description: "Cursor hook adapters (session-start, pre-compact). CLI is SoT; thin hooks shell out here."
|
|
3757
|
+
},
|
|
3758
|
+
subCommands: {
|
|
3759
|
+
"session-start": defineCommand9({
|
|
3760
|
+
meta: {
|
|
3761
|
+
name: "session-start",
|
|
3762
|
+
description: "Emit sessionStart additional_context JSON (stdin: Cursor payload)"
|
|
3763
|
+
},
|
|
3764
|
+
args: {
|
|
3765
|
+
cwd: {
|
|
3766
|
+
type: "string",
|
|
3767
|
+
default: process.cwd()
|
|
3768
|
+
}
|
|
3769
|
+
},
|
|
3770
|
+
async run({ args }) {
|
|
3771
|
+
const payload = await readStdinJson();
|
|
3772
|
+
const cwd = typeof args.cwd === "string" ? args.cwd : process.cwd();
|
|
3773
|
+
const root = resolveSessionRoot(payload, path25.resolve(cwd));
|
|
3774
|
+
const out = await buildSessionStartAdditionalContext(root, payload);
|
|
3775
|
+
console.log(JSON.stringify(out));
|
|
3776
|
+
}
|
|
3777
|
+
}),
|
|
3778
|
+
"pre-compact": defineCommand9({
|
|
3779
|
+
meta: {
|
|
3780
|
+
name: "pre-compact",
|
|
3781
|
+
description: "Emit preCompact user_message JSON (stdin: Cursor payload)"
|
|
3782
|
+
},
|
|
3783
|
+
async run() {
|
|
3784
|
+
const payload = await readStdinJson();
|
|
3785
|
+
console.log(JSON.stringify(buildPreCompactUserMessage(payload)));
|
|
3786
|
+
}
|
|
3787
|
+
})
|
|
3788
|
+
}
|
|
3789
|
+
});
|
|
3790
|
+
|
|
3791
|
+
// src/commands/init.ts
|
|
3792
|
+
import { intro, outro } from "@clack/prompts";
|
|
3793
|
+
import { defineCommand as defineCommand11 } from "citty";
|
|
3794
|
+
|
|
3795
|
+
// src/commands/install.ts
|
|
3796
|
+
import path28 from "path";
|
|
3797
|
+
import { defineCommand as defineCommand10 } from "citty";
|
|
3798
|
+
|
|
3799
|
+
// src/generator/personalization.ts
|
|
3800
|
+
import { readFile as readFile12, writeFile as writeFile5 } from "fs/promises";
|
|
3801
|
+
import path26 from "path";
|
|
3802
|
+
var PERSONALIZATION_CONTRACT_VERSION = 1;
|
|
3803
|
+
var CONTEXT_PATH = ".cursor/project-context.md";
|
|
3804
|
+
var AGENTS_PATH = "AGENTS.md";
|
|
3805
|
+
var RESULT_PATH = ".cursor/context/personalization.json";
|
|
3806
|
+
function uniqueEvidence(evidence) {
|
|
3807
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3808
|
+
return evidence.filter((item) => {
|
|
3809
|
+
const key = `${item.source}:${item.value}`;
|
|
3810
|
+
if (seen.has(key)) return false;
|
|
3811
|
+
seen.add(key);
|
|
3812
|
+
return true;
|
|
2949
3813
|
});
|
|
2950
3814
|
}
|
|
2951
3815
|
function purposeEvidence(profile) {
|
|
@@ -3085,7 +3949,7 @@ function renderProjectContext(profile) {
|
|
|
3085
3949
|
`;
|
|
3086
3950
|
}
|
|
3087
3951
|
async function createOwnedFile(rootDir, relativePath, content, evidence) {
|
|
3088
|
-
const target =
|
|
3952
|
+
const target = path26.join(rootDir, relativePath);
|
|
3089
3953
|
if (await fileExists(target)) {
|
|
3090
3954
|
return {
|
|
3091
3955
|
kind: "file",
|
|
@@ -3095,7 +3959,7 @@ async function createOwnedFile(rootDir, relativePath, content, evidence) {
|
|
|
3095
3959
|
evidence
|
|
3096
3960
|
};
|
|
3097
3961
|
}
|
|
3098
|
-
await ensureDir(
|
|
3962
|
+
await ensureDir(path26.dirname(target));
|
|
3099
3963
|
await writeFile5(target, content, "utf8");
|
|
3100
3964
|
return {
|
|
3101
3965
|
kind: "file",
|
|
@@ -3112,7 +3976,7 @@ async function packTargets(registryRoot, packId) {
|
|
|
3112
3976
|
async function existingTargets(projectRoot, targets) {
|
|
3113
3977
|
const checks = await Promise.all(
|
|
3114
3978
|
targets.map(
|
|
3115
|
-
async (target) => await fileExists(
|
|
3979
|
+
async (target) => await fileExists(path26.join(projectRoot, target)) ? target : null
|
|
3116
3980
|
)
|
|
3117
3981
|
);
|
|
3118
3982
|
return checks.filter((target) => target !== null);
|
|
@@ -3134,14 +3998,14 @@ async function applyPersonalization(input) {
|
|
|
3134
3998
|
componentResults.push({ ...item, status: "unavailable" });
|
|
3135
3999
|
continue;
|
|
3136
4000
|
}
|
|
3137
|
-
const target =
|
|
4001
|
+
const target = path26.posix.join(
|
|
3138
4002
|
".cursor",
|
|
3139
4003
|
"skills",
|
|
3140
4004
|
skill.path.includes("/core/") ? "core" : "community",
|
|
3141
4005
|
skill.id,
|
|
3142
4006
|
"SKILL.md"
|
|
3143
4007
|
);
|
|
3144
|
-
if (await fileExists(
|
|
4008
|
+
if (await fileExists(path26.join(input.rootDir, target))) {
|
|
3145
4009
|
componentResults.push({ ...item, status: "skipped-customized", path: target });
|
|
3146
4010
|
protectedPaths.add(target);
|
|
3147
4011
|
continue;
|
|
@@ -3196,7 +4060,7 @@ async function applyPersonalization(input) {
|
|
|
3196
4060
|
items: [...fileResults, ...componentResults],
|
|
3197
4061
|
protectedPaths: [...protectedPaths].sort()
|
|
3198
4062
|
};
|
|
3199
|
-
await writeJson(
|
|
4063
|
+
await writeJson(path26.join(input.rootDir, RESULT_PATH), result);
|
|
3200
4064
|
return {
|
|
3201
4065
|
result,
|
|
3202
4066
|
manifest: {
|
|
@@ -3214,26 +4078,26 @@ async function applyPersonalization(input) {
|
|
|
3214
4078
|
};
|
|
3215
4079
|
}
|
|
3216
4080
|
async function readRepositoryProfile(rootDir) {
|
|
3217
|
-
const target =
|
|
4081
|
+
const target = path26.join(rootDir, ".cursor/agent-kit.config.json");
|
|
3218
4082
|
if (!await fileExists(target)) return null;
|
|
3219
|
-
return JSON.parse(await
|
|
4083
|
+
return JSON.parse(await readFile12(target, "utf8"));
|
|
3220
4084
|
}
|
|
3221
4085
|
|
|
3222
4086
|
// src/lifecycle/onboard-migration.ts
|
|
3223
4087
|
import { createHash as createHash3 } from "crypto";
|
|
3224
|
-
import { readFile as
|
|
3225
|
-
import
|
|
4088
|
+
import { readFile as readFile13, unlink } from "fs/promises";
|
|
4089
|
+
import path27 from "path";
|
|
3226
4090
|
var LEGACY_ONBOARD_PATH = ".cursor/commands/onboard.md";
|
|
3227
4091
|
var NAMESPACED_ONBOARD_PATH = ".cursor/commands/agent-kit-onboard.md";
|
|
3228
4092
|
var MANAGED_LEGACY_HASHES = /* @__PURE__ */ new Set([
|
|
3229
4093
|
"b274a68941813f19b185893cb7c5561dff027f53270890029992f208e24992fe"
|
|
3230
4094
|
]);
|
|
3231
4095
|
async function migrateLegacyOnboardCommand(projectRoot, managedHashes = MANAGED_LEGACY_HASHES) {
|
|
3232
|
-
const legacyPath =
|
|
4096
|
+
const legacyPath = path27.join(projectRoot, LEGACY_ONBOARD_PATH);
|
|
3233
4097
|
if (!await fileExists(legacyPath)) return "absent";
|
|
3234
|
-
const namespacedPath =
|
|
4098
|
+
const namespacedPath = path27.join(projectRoot, NAMESPACED_ONBOARD_PATH);
|
|
3235
4099
|
if (!await fileExists(namespacedPath)) return "preserved-customized";
|
|
3236
|
-
const content = await
|
|
4100
|
+
const content = await readFile13(legacyPath);
|
|
3237
4101
|
const hash = createHash3("sha256").update(content).digest("hex");
|
|
3238
4102
|
if (!managedHashes.has(hash)) return "preserved-customized";
|
|
3239
4103
|
await unlink(legacyPath);
|
|
@@ -3302,7 +4166,7 @@ function printReadinessNarrative(result) {
|
|
|
3302
4166
|
);
|
|
3303
4167
|
}
|
|
3304
4168
|
async function performInstall(options) {
|
|
3305
|
-
const projectRoot =
|
|
4169
|
+
const projectRoot = path28.resolve(options.cwd);
|
|
3306
4170
|
const packs = parsePackList(options.pack);
|
|
3307
4171
|
const existing = await loadAgentKitManifest(projectRoot);
|
|
3308
4172
|
const registry = await resolveRegistryFromCli({
|
|
@@ -3356,7 +4220,7 @@ async function performInstall(options) {
|
|
|
3356
4220
|
safeChanges: readinessExecution.changes
|
|
3357
4221
|
};
|
|
3358
4222
|
}
|
|
3359
|
-
var installCommand =
|
|
4223
|
+
var installCommand = defineCommand10({
|
|
3360
4224
|
meta: {
|
|
3361
4225
|
name: "install",
|
|
3362
4226
|
description: "Bootstrap L0 (+ optional packs) from the registry and write agent-kit.json."
|
|
@@ -3378,7 +4242,7 @@ var installCommand = defineCommand7({
|
|
|
3378
4242
|
...REGISTRY_CLI_ARGS
|
|
3379
4243
|
},
|
|
3380
4244
|
async run({ args }) {
|
|
3381
|
-
const projectRoot =
|
|
4245
|
+
const projectRoot = path28.resolve(args.cwd);
|
|
3382
4246
|
logger.info(`Installing into: ${projectRoot}`);
|
|
3383
4247
|
const packs = parsePackList(args.pack);
|
|
3384
4248
|
for (const id of packs) {
|
|
@@ -3406,7 +4270,7 @@ var installCommand = defineCommand7({
|
|
|
3406
4270
|
async function runInitCompatibility(cwd, installer = performInstall) {
|
|
3407
4271
|
return installer({ cwd });
|
|
3408
4272
|
}
|
|
3409
|
-
var initCommand =
|
|
4273
|
+
var initCommand = defineCommand11({
|
|
3410
4274
|
meta: {
|
|
3411
4275
|
name: "init",
|
|
3412
4276
|
description: "Guided compatibility entry point for install and repository readiness."
|
|
@@ -3430,16 +4294,186 @@ var initCommand = defineCommand8({
|
|
|
3430
4294
|
}
|
|
3431
4295
|
});
|
|
3432
4296
|
|
|
3433
|
-
// src/commands/
|
|
4297
|
+
// src/commands/monitors.ts
|
|
4298
|
+
import path30 from "path";
|
|
4299
|
+
import { defineCommand as defineCommand12 } from "citty";
|
|
4300
|
+
|
|
4301
|
+
// 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";
|
|
3434
4304
|
import path29 from "path";
|
|
3435
|
-
import {
|
|
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;
|
|
4308
|
+
var CITE4 = "agent-kit monitors --untriaged (ADR 2026-07-27_plan-review-triage-untriaged-not-mtime; never newest-mtime-wins)";
|
|
4309
|
+
function hasOpenGaps(content) {
|
|
4310
|
+
if (/###\s+Still open[^\n]*\n+(?:\s*\n)*(?:None\.|none\.|\*None\*)/i.test(content)) {
|
|
4311
|
+
return false;
|
|
4312
|
+
}
|
|
4313
|
+
if (/###\s+Still open/i.test(content) && !/###\s+Still open[^\n]*\n+(?:\s*\n)*(?:None\.|none\.)/i.test(content)) {
|
|
4314
|
+
const m = content.match(/###\s+Still open[^\n]*\n([\s\S]*?)(?=\n### |\n## |$)/i);
|
|
4315
|
+
if (m?.[1]?.trim() && !/^(none\.?|\*none\*)$/i.test(m[1].trim())) {
|
|
4316
|
+
return true;
|
|
4317
|
+
}
|
|
4318
|
+
}
|
|
4319
|
+
if (/^#{2,6}\s+.*\bResidual items?\b/im.test(content)) return true;
|
|
4320
|
+
return false;
|
|
4321
|
+
}
|
|
4322
|
+
async function listMonitorFiles(memoryDir) {
|
|
4323
|
+
try {
|
|
4324
|
+
const names = await readdir3(memoryDir);
|
|
4325
|
+
return names.filter((n) => n.startsWith("plan-monitor-") && n.endsWith(".md")).sort();
|
|
4326
|
+
} catch {
|
|
4327
|
+
return [];
|
|
4328
|
+
}
|
|
4329
|
+
}
|
|
4330
|
+
async function gitFreshMonitorNames(rootDir) {
|
|
4331
|
+
const names = /* @__PURE__ */ new Set();
|
|
4332
|
+
try {
|
|
4333
|
+
const { stdout } = await execFileAsync2(
|
|
4334
|
+
"git",
|
|
4335
|
+
["status", "--porcelain", "--", ".cursor/memory"],
|
|
4336
|
+
{ cwd: rootDir, maxBuffer: 2 * 1024 * 1024 }
|
|
4337
|
+
);
|
|
4338
|
+
for (const line of stdout.split("\n")) {
|
|
4339
|
+
if (!line.trim()) continue;
|
|
4340
|
+
const file = line.slice(3).trim().replace(/^.* -> /, "");
|
|
4341
|
+
const base = path29.basename(file);
|
|
4342
|
+
if (base.startsWith("plan-monitor-") && base.endsWith(".md")) {
|
|
4343
|
+
names.add(base);
|
|
4344
|
+
}
|
|
4345
|
+
}
|
|
4346
|
+
} catch {
|
|
4347
|
+
}
|
|
4348
|
+
return names;
|
|
4349
|
+
}
|
|
4350
|
+
function extractHandoffPlanSlugs(handoff) {
|
|
4351
|
+
const slugs = /* @__PURE__ */ new Set();
|
|
4352
|
+
for (const m of handoff.matchAll(/`([a-z0-9][a-z0-9._-]*)\.plan\.md`/gi)) {
|
|
4353
|
+
if (m[1]) slugs.add(m[1].toLowerCase());
|
|
4354
|
+
}
|
|
4355
|
+
for (const m of handoff.matchAll(/plan-monitor-([a-z0-9][a-z0-9._-]*)\.md/gi)) {
|
|
4356
|
+
if (m[1]) slugs.add(m[1].toLowerCase());
|
|
4357
|
+
}
|
|
4358
|
+
return slugs;
|
|
4359
|
+
}
|
|
4360
|
+
function monitorSlugFromName(fileName) {
|
|
4361
|
+
return fileName.replace(/^plan-monitor-/, "").replace(/\.md$/, "").toLowerCase();
|
|
4362
|
+
}
|
|
4363
|
+
async function selectUntriagedMonitors(rootDir) {
|
|
4364
|
+
const root = path29.resolve(rootDir);
|
|
4365
|
+
const memoryDir = path29.join(root, ".cursor", "memory");
|
|
4366
|
+
const allNames = await listMonitorFiles(memoryDir);
|
|
4367
|
+
const selectionOrder = ["git-fresh", "handoff-aligned", "untriaged-scan"];
|
|
4368
|
+
const byName = /* @__PURE__ */ new Map();
|
|
4369
|
+
for (const name of allNames) {
|
|
4370
|
+
const abs = path29.join(memoryDir, name);
|
|
4371
|
+
try {
|
|
4372
|
+
const [content, st] = await Promise.all([readFile14(abs, "utf8"), stat(abs)]);
|
|
4373
|
+
byName.set(name, { content, mtimeMs: st.mtimeMs });
|
|
4374
|
+
} catch {
|
|
4375
|
+
}
|
|
4376
|
+
}
|
|
4377
|
+
const untriaged = (name) => {
|
|
4378
|
+
const row = byName.get(name);
|
|
4379
|
+
return !!row && !TRIAGE_HEADING_RE.test(row.content);
|
|
4380
|
+
};
|
|
4381
|
+
const gitFresh = await gitFreshMonitorNames(root);
|
|
4382
|
+
const gitFreshSet = [...gitFresh].filter(untriaged).sort();
|
|
4383
|
+
let handoff = "";
|
|
4384
|
+
try {
|
|
4385
|
+
handoff = await readFile14(path29.join(root, ".cursor", "HANDOFF.md"), "utf8");
|
|
4386
|
+
} catch {
|
|
4387
|
+
handoff = "";
|
|
4388
|
+
}
|
|
4389
|
+
const handoffSlugs = extractHandoffPlanSlugs(handoff);
|
|
4390
|
+
const handoffAligned = allNames.filter((n) => untriaged(n) && handoffSlugs.has(monitorSlugFromName(n))).sort();
|
|
4391
|
+
const scanAll = allNames.filter(untriaged);
|
|
4392
|
+
let chosen;
|
|
4393
|
+
if (gitFreshSet.length > 0) {
|
|
4394
|
+
chosen = { names: gitFreshSet, bucket: "git-fresh" };
|
|
4395
|
+
} else if (handoffAligned.length > 0) {
|
|
4396
|
+
chosen = { names: handoffAligned, bucket: "handoff-aligned" };
|
|
4397
|
+
} else {
|
|
4398
|
+
chosen = { names: scanAll, bucket: "untriaged-scan" };
|
|
4399
|
+
}
|
|
4400
|
+
const entries = [];
|
|
4401
|
+
for (const name of chosen.names) {
|
|
4402
|
+
const row = byName.get(name);
|
|
4403
|
+
if (!row) continue;
|
|
4404
|
+
entries.push({
|
|
4405
|
+
path: path29.join(memoryDir, name),
|
|
4406
|
+
relativePath: path29.relative(root, path29.join(memoryDir, name)).split(path29.sep).join("/"),
|
|
4407
|
+
mtimeMs: row.mtimeMs,
|
|
4408
|
+
hasTriageHeading: false,
|
|
4409
|
+
hasOpenGaps: hasOpenGaps(row.content),
|
|
4410
|
+
selectionBucket: chosen.bucket
|
|
4411
|
+
});
|
|
4412
|
+
}
|
|
4413
|
+
entries.sort((a, b) => {
|
|
4414
|
+
if (a.hasOpenGaps !== b.hasOpenGaps) return a.hasOpenGaps ? -1 : 1;
|
|
4415
|
+
return b.mtimeMs - a.mtimeMs;
|
|
4416
|
+
});
|
|
4417
|
+
return {
|
|
4418
|
+
selectionOrder: [...selectionOrder],
|
|
4419
|
+
monitors: entries,
|
|
4420
|
+
cite: CITE4
|
|
4421
|
+
};
|
|
4422
|
+
}
|
|
4423
|
+
|
|
4424
|
+
// src/commands/monitors.ts
|
|
4425
|
+
var monitorsCommand = defineCommand12({
|
|
4426
|
+
meta: {
|
|
4427
|
+
name: "monitors",
|
|
4428
|
+
description: "Plan-monitor selection helpers (untriaged SoT for /plan-review-triage)"
|
|
4429
|
+
},
|
|
4430
|
+
args: {
|
|
4431
|
+
cwd: {
|
|
4432
|
+
type: "string",
|
|
4433
|
+
default: process.cwd()
|
|
4434
|
+
},
|
|
4435
|
+
untriaged: {
|
|
4436
|
+
type: "boolean",
|
|
4437
|
+
default: false,
|
|
4438
|
+
description: "Select untriaged monitors (never newest-mtime-wins alone)"
|
|
4439
|
+
},
|
|
4440
|
+
json: {
|
|
4441
|
+
type: "boolean",
|
|
4442
|
+
default: false,
|
|
4443
|
+
description: "Machine-readable JSON"
|
|
4444
|
+
}
|
|
4445
|
+
},
|
|
4446
|
+
async run({ args }) {
|
|
4447
|
+
if (!args.untriaged) {
|
|
4448
|
+
console.error("Usage: agent-kit monitors --untriaged [--json] [--cwd <dir>]");
|
|
4449
|
+
process.exitCode = 2;
|
|
4450
|
+
return;
|
|
4451
|
+
}
|
|
4452
|
+
const result = await selectUntriagedMonitors(path30.resolve(args.cwd));
|
|
4453
|
+
if (args.json) {
|
|
4454
|
+
console.log(JSON.stringify(result, null, 2));
|
|
4455
|
+
return;
|
|
4456
|
+
}
|
|
4457
|
+
if (result.monitors.length === 0) {
|
|
4458
|
+
console.log("No untriaged plan-monitor files.");
|
|
4459
|
+
return;
|
|
4460
|
+
}
|
|
4461
|
+
for (const m of result.monitors) {
|
|
4462
|
+
console.log(m.relativePath);
|
|
4463
|
+
}
|
|
4464
|
+
}
|
|
4465
|
+
});
|
|
4466
|
+
|
|
4467
|
+
// src/commands/run-plan.ts
|
|
4468
|
+
import path35 from "path";
|
|
4469
|
+
import { defineCommand as defineCommand13 } from "citty";
|
|
3436
4470
|
|
|
3437
4471
|
// src/plan-loop/backends.ts
|
|
3438
|
-
import { execFileSync, spawn as
|
|
4472
|
+
import { execFileSync as execFileSync2, spawn as spawn5 } from "child_process";
|
|
3439
4473
|
import { createWriteStream } from "fs";
|
|
3440
4474
|
async function which(bin) {
|
|
3441
4475
|
try {
|
|
3442
|
-
const out =
|
|
4476
|
+
const out = execFileSync2("which", [bin], { encoding: "utf8" }).trim();
|
|
3443
4477
|
return out || null;
|
|
3444
4478
|
} catch {
|
|
3445
4479
|
return null;
|
|
@@ -3448,7 +4482,7 @@ async function which(bin) {
|
|
|
3448
4482
|
function spawnLogged(command, args, logPath) {
|
|
3449
4483
|
return new Promise((resolve, reject) => {
|
|
3450
4484
|
const out = createWriteStream(logPath, { flags: "w" });
|
|
3451
|
-
const child =
|
|
4485
|
+
const child = spawn5(command, args, {
|
|
3452
4486
|
stdio: ["ignore", "pipe", "pipe"]
|
|
3453
4487
|
});
|
|
3454
4488
|
const onData = (chunk) => {
|
|
@@ -3516,14 +4550,14 @@ function listBackendIds() {
|
|
|
3516
4550
|
}
|
|
3517
4551
|
|
|
3518
4552
|
// src/plan-loop/run-loop.ts
|
|
3519
|
-
import { mkdir as mkdir4, readFile as
|
|
3520
|
-
import
|
|
4553
|
+
import { mkdir as mkdir4, readFile as readFile17, rm, unlink as unlink2 } from "fs/promises";
|
|
4554
|
+
import path34 from "path";
|
|
3521
4555
|
|
|
3522
4556
|
// src/plan-loop/external-review.ts
|
|
3523
|
-
import { spawn as
|
|
3524
|
-
import
|
|
3525
|
-
var CANONICAL_REL =
|
|
3526
|
-
var FALLBACK_REL =
|
|
4557
|
+
import { spawn as spawn6 } from "child_process";
|
|
4558
|
+
import path31 from "path";
|
|
4559
|
+
var CANONICAL_REL = path31.join(".cursor", "scripts", "plan-external-review.sh");
|
|
4560
|
+
var FALLBACK_REL = path31.join("scripts", "plan-external-review.sh");
|
|
3527
4561
|
function isPlanExhaustedReason(reason) {
|
|
3528
4562
|
const r = reason.trim().toLowerCase();
|
|
3529
4563
|
if (!r) return false;
|
|
@@ -3541,12 +4575,12 @@ function shouldArmExternalPlanReview(input) {
|
|
|
3541
4575
|
return false;
|
|
3542
4576
|
}
|
|
3543
4577
|
async function armExternalPlanReview(root, options = {}) {
|
|
3544
|
-
const spawnFn = options.spawnFn ??
|
|
4578
|
+
const spawnFn = options.spawnFn ?? spawn6;
|
|
3545
4579
|
const existsFn = options.existsFn ?? fileExists;
|
|
3546
4580
|
const log = options.log ?? ((line) => console.log(line));
|
|
3547
4581
|
const force = options.force === true;
|
|
3548
|
-
const canonicalPath =
|
|
3549
|
-
const fallbackPath =
|
|
4582
|
+
const canonicalPath = path31.join(root, CANONICAL_REL);
|
|
4583
|
+
const fallbackPath = path31.join(root, FALLBACK_REL);
|
|
3550
4584
|
let scriptPath = null;
|
|
3551
4585
|
let scriptRel = CANONICAL_REL;
|
|
3552
4586
|
if (await existsFn(canonicalPath)) {
|
|
@@ -3563,11 +4597,11 @@ async function armExternalPlanReview(root, options = {}) {
|
|
|
3563
4597
|
return { invoked: false, exitCode: null, output: "" };
|
|
3564
4598
|
}
|
|
3565
4599
|
log("Plan exhausted: arming optional external plan review (opt-in handled by script)...");
|
|
3566
|
-
const args = force ? [scriptPath, "--force"] : [scriptPath];
|
|
4600
|
+
const args = force ? [scriptPath, "--force", "--print"] : [scriptPath, "--print"];
|
|
3567
4601
|
return new Promise((resolve) => {
|
|
3568
4602
|
const child = spawnFn("bash", args, {
|
|
3569
4603
|
cwd: root,
|
|
3570
|
-
env: process.env
|
|
4604
|
+
env: { ...process.env, AGENT_KIT_HEADLESS: "1" }
|
|
3571
4605
|
});
|
|
3572
4606
|
let output = "";
|
|
3573
4607
|
const append = (chunk) => {
|
|
@@ -3598,9 +4632,105 @@ async function armExternalPlanReview(root, options = {}) {
|
|
|
3598
4632
|
});
|
|
3599
4633
|
}
|
|
3600
4634
|
|
|
4635
|
+
// src/plan-loop/persona-banners.ts
|
|
4636
|
+
import path32 from "path";
|
|
4637
|
+
import {
|
|
4638
|
+
blue,
|
|
4639
|
+
cyan as cyan2,
|
|
4640
|
+
gray,
|
|
4641
|
+
green as green2,
|
|
4642
|
+
lightGray,
|
|
4643
|
+
lightGreen,
|
|
4644
|
+
magenta,
|
|
4645
|
+
red as red2,
|
|
4646
|
+
white,
|
|
4647
|
+
yellow as yellow2
|
|
4648
|
+
} from "kolorist";
|
|
4649
|
+
var CLI_RUN_PLAN_MODE = "cli-run-plan";
|
|
4650
|
+
var DEFAULT_CLI_PERSONA_ID = "ghost-runner";
|
|
4651
|
+
var COLORS = {
|
|
4652
|
+
white,
|
|
4653
|
+
gray,
|
|
4654
|
+
green: green2,
|
|
4655
|
+
cyan: cyan2,
|
|
4656
|
+
magenta,
|
|
4657
|
+
yellow: yellow2,
|
|
4658
|
+
red: red2,
|
|
4659
|
+
blue,
|
|
4660
|
+
"light-green": lightGreen,
|
|
4661
|
+
lightgreen: lightGreen,
|
|
4662
|
+
"light-gray": lightGray,
|
|
4663
|
+
lightgray: lightGray
|
|
4664
|
+
};
|
|
4665
|
+
function resolveColor(name, fallback) {
|
|
4666
|
+
if (!name) return fallback;
|
|
4667
|
+
return COLORS[name.toLowerCase()] ?? fallback;
|
|
4668
|
+
}
|
|
4669
|
+
async function resolveCliPersonaId(root) {
|
|
4670
|
+
try {
|
|
4671
|
+
const cfg = await readJson(
|
|
4672
|
+
path32.join(root, ".cursor", "context", "config.json")
|
|
4673
|
+
);
|
|
4674
|
+
const modes = cfg?.agentPersona?.modes ?? cfg?.workspaceSkin?.modes;
|
|
4675
|
+
const id = modes?.[CLI_RUN_PLAN_MODE];
|
|
4676
|
+
if (typeof id === "string" && id.trim()) return id.trim();
|
|
4677
|
+
} catch {
|
|
4678
|
+
}
|
|
4679
|
+
return DEFAULT_CLI_PERSONA_ID;
|
|
4680
|
+
}
|
|
4681
|
+
async function loadPersonaPack(root, personaId) {
|
|
4682
|
+
try {
|
|
4683
|
+
const personaPath = path32.join(root, "registry", "personas", "core", personaId, "persona.json");
|
|
4684
|
+
const pack = await readJson(personaPath);
|
|
4685
|
+
if (!pack || typeof pack.id !== "string") return null;
|
|
4686
|
+
return pack;
|
|
4687
|
+
} catch {
|
|
4688
|
+
return null;
|
|
4689
|
+
}
|
|
4690
|
+
}
|
|
4691
|
+
async function loadCliRunPlanPersona(root) {
|
|
4692
|
+
const id = await resolveCliPersonaId(root);
|
|
4693
|
+
return loadPersonaPack(root, id);
|
|
4694
|
+
}
|
|
4695
|
+
function createPersonaBannerPrinter(persona) {
|
|
4696
|
+
if (!persona?.cliBanners) return null;
|
|
4697
|
+
const banners = persona.cliBanners;
|
|
4698
|
+
if (!banners.tickStart && !banners.tickEnd && !banners.phaseComplete) return null;
|
|
4699
|
+
const primary = resolveColor(persona.ansiPalette?.primary, white);
|
|
4700
|
+
const secondary = resolveColor(persona.ansiPalette?.secondary, gray);
|
|
4701
|
+
const accent = resolveColor(persona.ansiPalette?.accent, green2);
|
|
4702
|
+
return {
|
|
4703
|
+
tickStart(detail) {
|
|
4704
|
+
if (banners.tickStart) {
|
|
4705
|
+
console.log(`${accent(banners.tickStart)} ${primary(detail)}`);
|
|
4706
|
+
} else {
|
|
4707
|
+
console.log(primary(detail));
|
|
4708
|
+
}
|
|
4709
|
+
},
|
|
4710
|
+
tickEnd(detail) {
|
|
4711
|
+
if (!banners.tickEnd) return;
|
|
4712
|
+
const line = detail ? `${banners.tickEnd} ${detail}` : banners.tickEnd;
|
|
4713
|
+
console.log(secondary(line));
|
|
4714
|
+
},
|
|
4715
|
+
phaseComplete(detail) {
|
|
4716
|
+
if (!banners.phaseComplete) return;
|
|
4717
|
+
const line = detail ? `${banners.phaseComplete} ${detail}` : banners.phaseComplete;
|
|
4718
|
+
console.log(accent(line));
|
|
4719
|
+
},
|
|
4720
|
+
sleep(seconds) {
|
|
4721
|
+
console.log(secondary(`sleep ${seconds}s`));
|
|
4722
|
+
},
|
|
4723
|
+
stop(reason) {
|
|
4724
|
+
if (banners.tickEnd) {
|
|
4725
|
+
console.log(secondary(`${banners.tickEnd} ${reason}`));
|
|
4726
|
+
}
|
|
4727
|
+
}
|
|
4728
|
+
};
|
|
4729
|
+
}
|
|
4730
|
+
|
|
3601
4731
|
// src/plan-loop/plan-state.ts
|
|
3602
|
-
import { readFile as
|
|
3603
|
-
import
|
|
4732
|
+
import { readFile as readFile15, readdir as readdir4 } from "fs/promises";
|
|
4733
|
+
import path33 from "path";
|
|
3604
4734
|
function countPendingTodos(raw) {
|
|
3605
4735
|
const lines = raw.split(/\r?\n/);
|
|
3606
4736
|
let inFront = 0;
|
|
@@ -3627,15 +4757,15 @@ function countPendingTodos(raw) {
|
|
|
3627
4757
|
}
|
|
3628
4758
|
async function findActivePlanFile(plansDir) {
|
|
3629
4759
|
if (!await fileExists(plansDir)) return null;
|
|
3630
|
-
const files = (await
|
|
3631
|
-
return files[0] ?
|
|
4760
|
+
const files = (await readdir4(plansDir)).filter((f) => f.endsWith(".plan.md")).sort();
|
|
4761
|
+
return files[0] ? path33.join(plansDir, files[0]) : null;
|
|
3632
4762
|
}
|
|
3633
4763
|
async function readPlan(planPath) {
|
|
3634
|
-
return
|
|
4764
|
+
return readFile15(planPath, "utf8");
|
|
3635
4765
|
}
|
|
3636
4766
|
|
|
3637
4767
|
// src/plan-loop/sentinel.ts
|
|
3638
|
-
import { readFile as
|
|
4768
|
+
import { readFile as readFile16 } from "fs/promises";
|
|
3639
4769
|
var SENTINEL_RE = /LOOP_TICK_RESULT:\s*(continue|stop(?:\s*[—\-].*)?)/i;
|
|
3640
4770
|
function takeFromText(text) {
|
|
3641
4771
|
if (!text) return null;
|
|
@@ -3682,7 +4812,7 @@ function parseSentinelFromLog(content) {
|
|
|
3682
4812
|
}
|
|
3683
4813
|
async function parseSentinelFromLogFile(logPath) {
|
|
3684
4814
|
try {
|
|
3685
|
-
const content = await
|
|
4815
|
+
const content = await readFile16(logPath, "utf8");
|
|
3686
4816
|
return parseSentinelFromLog(content);
|
|
3687
4817
|
} catch {
|
|
3688
4818
|
return { kind: "missing" };
|
|
@@ -3694,101 +4824,6 @@ function formatSentinelLine(sentinel) {
|
|
|
3694
4824
|
return "";
|
|
3695
4825
|
}
|
|
3696
4826
|
|
|
3697
|
-
// src/plan-loop/skin-banners.ts
|
|
3698
|
-
import path27 from "path";
|
|
3699
|
-
import {
|
|
3700
|
-
blue,
|
|
3701
|
-
cyan as cyan2,
|
|
3702
|
-
gray,
|
|
3703
|
-
green as green2,
|
|
3704
|
-
lightGray,
|
|
3705
|
-
lightGreen,
|
|
3706
|
-
magenta,
|
|
3707
|
-
red as red2,
|
|
3708
|
-
white,
|
|
3709
|
-
yellow as yellow2
|
|
3710
|
-
} from "kolorist";
|
|
3711
|
-
var CLI_RUN_PLAN_MODE = "cli-run-plan";
|
|
3712
|
-
var DEFAULT_CLI_SKIN_ID = "ghost-runner";
|
|
3713
|
-
var COLORS = {
|
|
3714
|
-
white,
|
|
3715
|
-
gray,
|
|
3716
|
-
green: green2,
|
|
3717
|
-
cyan: cyan2,
|
|
3718
|
-
magenta,
|
|
3719
|
-
yellow: yellow2,
|
|
3720
|
-
red: red2,
|
|
3721
|
-
blue,
|
|
3722
|
-
"light-green": lightGreen,
|
|
3723
|
-
lightgreen: lightGreen,
|
|
3724
|
-
"light-gray": lightGray,
|
|
3725
|
-
lightgray: lightGray
|
|
3726
|
-
};
|
|
3727
|
-
function resolveColor(name, fallback) {
|
|
3728
|
-
if (!name) return fallback;
|
|
3729
|
-
return COLORS[name.toLowerCase()] ?? fallback;
|
|
3730
|
-
}
|
|
3731
|
-
async function resolveCliSkinId(root) {
|
|
3732
|
-
try {
|
|
3733
|
-
const cfg = await readJson(
|
|
3734
|
-
path27.join(root, ".cursor", "context", "config.json")
|
|
3735
|
-
);
|
|
3736
|
-
const id = cfg?.workspaceSkin?.modes?.[CLI_RUN_PLAN_MODE];
|
|
3737
|
-
if (typeof id === "string" && id.trim()) return id.trim();
|
|
3738
|
-
} catch {
|
|
3739
|
-
}
|
|
3740
|
-
return DEFAULT_CLI_SKIN_ID;
|
|
3741
|
-
}
|
|
3742
|
-
async function loadSkinPack(root, skinId) {
|
|
3743
|
-
try {
|
|
3744
|
-
const skinPath = path27.join(root, "registry", "skins", "core", skinId, "skin.json");
|
|
3745
|
-
const pack = await readJson(skinPath);
|
|
3746
|
-
if (!pack || typeof pack.id !== "string") return null;
|
|
3747
|
-
return pack;
|
|
3748
|
-
} catch {
|
|
3749
|
-
return null;
|
|
3750
|
-
}
|
|
3751
|
-
}
|
|
3752
|
-
async function loadCliRunPlanSkin(root) {
|
|
3753
|
-
const id = await resolveCliSkinId(root);
|
|
3754
|
-
return loadSkinPack(root, id);
|
|
3755
|
-
}
|
|
3756
|
-
function createSkinBannerPrinter(skin) {
|
|
3757
|
-
if (!skin?.cliBanners) return null;
|
|
3758
|
-
const banners = skin.cliBanners;
|
|
3759
|
-
if (!banners.tickStart && !banners.tickEnd && !banners.phaseComplete) return null;
|
|
3760
|
-
const primary = resolveColor(skin.ansiPalette?.primary, white);
|
|
3761
|
-
const secondary = resolveColor(skin.ansiPalette?.secondary, gray);
|
|
3762
|
-
const accent = resolveColor(skin.ansiPalette?.accent, green2);
|
|
3763
|
-
return {
|
|
3764
|
-
tickStart(detail) {
|
|
3765
|
-
if (banners.tickStart) {
|
|
3766
|
-
console.log(`${accent(banners.tickStart)} ${primary(detail)}`);
|
|
3767
|
-
} else {
|
|
3768
|
-
console.log(primary(detail));
|
|
3769
|
-
}
|
|
3770
|
-
},
|
|
3771
|
-
tickEnd(detail) {
|
|
3772
|
-
if (!banners.tickEnd) return;
|
|
3773
|
-
const line = detail ? `${banners.tickEnd} ${detail}` : banners.tickEnd;
|
|
3774
|
-
console.log(secondary(line));
|
|
3775
|
-
},
|
|
3776
|
-
phaseComplete(detail) {
|
|
3777
|
-
if (!banners.phaseComplete) return;
|
|
3778
|
-
const line = detail ? `${banners.phaseComplete} ${detail}` : banners.phaseComplete;
|
|
3779
|
-
console.log(accent(line));
|
|
3780
|
-
},
|
|
3781
|
-
sleep(seconds) {
|
|
3782
|
-
console.log(secondary(`sleep ${seconds}s`));
|
|
3783
|
-
},
|
|
3784
|
-
stop(reason) {
|
|
3785
|
-
if (banners.tickEnd) {
|
|
3786
|
-
console.log(secondary(`${banners.tickEnd} ${reason}`));
|
|
3787
|
-
}
|
|
3788
|
-
}
|
|
3789
|
-
};
|
|
3790
|
-
}
|
|
3791
|
-
|
|
3792
4827
|
// src/plan-loop/run-loop.ts
|
|
3793
4828
|
var TICK_PROMPT = '/run-plan - single tick from the headless runner (agent-kit run-plan). Read .cursor/HANDOFF.md and the active plan in .cursor/plans/. Mark the next to-do as in_progress in the frontmatter, execute ONLY that to-do, mark completed and update HANDOFF. If there is a commitable diff: run /git-staging without asking for confirmation. NEVER /git-prod. Do NOT re-arm an internal Loop skill - the external runner starts the next agent. End the response with exactly one line: "LOOP_TICK_RESULT: continue" if implementable to-dos remain, or "LOOP_TICK_RESULT: stop - <reason>" (plan exhausted, external blocker, or human decision needed).';
|
|
3794
4829
|
function stamp() {
|
|
@@ -3800,9 +4835,9 @@ function sleep(ms) {
|
|
|
3800
4835
|
return new Promise((r) => setTimeout(r, ms));
|
|
3801
4836
|
}
|
|
3802
4837
|
async function runPlanLoop(opts) {
|
|
3803
|
-
const plansDir =
|
|
3804
|
-
const stopFile =
|
|
3805
|
-
const logDir =
|
|
4838
|
+
const plansDir = path34.join(opts.root, ".cursor", "plans");
|
|
4839
|
+
const stopFile = path34.join(opts.root, ".cursor", "loop.stop");
|
|
4840
|
+
const logDir = path34.join(opts.root, ".cursor", "loop-logs");
|
|
3806
4841
|
const planPath = await findActivePlanFile(plansDir);
|
|
3807
4842
|
if (!planPath) {
|
|
3808
4843
|
logger.error("No active plan in .cursor/plans/");
|
|
@@ -3821,13 +4856,13 @@ async function runPlanLoop(opts) {
|
|
|
3821
4856
|
};
|
|
3822
4857
|
process.on("SIGINT", onSigInt);
|
|
3823
4858
|
try {
|
|
3824
|
-
const
|
|
3825
|
-
const banners =
|
|
3826
|
-
console.log(`Active plan: ${
|
|
4859
|
+
const persona = await loadCliRunPlanPersona(opts.root);
|
|
4860
|
+
const banners = createPersonaBannerPrinter(persona);
|
|
4861
|
+
console.log(`Active plan: ${path34.basename(planPath)}`);
|
|
3827
4862
|
console.log(`Pending to-dos: ${await pending()} | max ticks: ${opts.maxTicks}`);
|
|
3828
4863
|
console.log(`Backend: ${opts.backend.id}`);
|
|
3829
|
-
if (
|
|
3830
|
-
console.log(`CLI
|
|
4864
|
+
if (persona) {
|
|
4865
|
+
console.log(`CLI persona: ${persona.displayName ?? persona.id}`);
|
|
3831
4866
|
}
|
|
3832
4867
|
if (opts.dryRun) {
|
|
3833
4868
|
console.log("--dry-run: no agent will be started. Tick prompt:");
|
|
@@ -3866,8 +4901,8 @@ async function runPlanLoop(opts) {
|
|
|
3866
4901
|
planExhausted = true;
|
|
3867
4902
|
break;
|
|
3868
4903
|
}
|
|
3869
|
-
const logPath =
|
|
3870
|
-
const relLog =
|
|
4904
|
+
const logPath = path34.join(logDir, `tick-${stamp()}.log`);
|
|
4905
|
+
const relLog = path34.relative(opts.root, logPath);
|
|
3871
4906
|
console.log("");
|
|
3872
4907
|
const tickLine = `=== tick ${tick}/${opts.maxTicks} - pending: ${before} - log: ${relLog} ===`;
|
|
3873
4908
|
if (banners) banners.tickStart(tickLine);
|
|
@@ -3886,7 +4921,7 @@ async function runPlanLoop(opts) {
|
|
|
3886
4921
|
return 1;
|
|
3887
4922
|
}
|
|
3888
4923
|
try {
|
|
3889
|
-
const logText = await
|
|
4924
|
+
const logText = await readFile17(logPath, "utf8");
|
|
3890
4925
|
if (logText.includes("Too many MCP tools")) {
|
|
3891
4926
|
const msg = "Too many MCP tools for the headless model - disable servers (cursor-agent mcp disable <id>) and run again.";
|
|
3892
4927
|
if (banners) banners.stop(msg);
|
|
@@ -3943,7 +4978,7 @@ async function runPlanLoop(opts) {
|
|
|
3943
4978
|
const finishDetail = `after ${tick} tick(s); pending: ${pendingNow}`;
|
|
3944
4979
|
if (banners) banners.phaseComplete(finishDetail);
|
|
3945
4980
|
console.log(
|
|
3946
|
-
`Loop finished after ${tick} tick(s). Pending now: ${pendingNow}. Logs in ${
|
|
4981
|
+
`Loop finished after ${tick} tick(s). Pending now: ${pendingNow}. Logs in ${path34.relative(opts.root, logDir)}/`
|
|
3947
4982
|
);
|
|
3948
4983
|
if (planExhausted || shouldArmExternalPlanReview({ pending: pendingNow, stopReason })) {
|
|
3949
4984
|
await armExternalPlanReview(opts.root);
|
|
@@ -3955,7 +4990,7 @@ async function runPlanLoop(opts) {
|
|
|
3955
4990
|
}
|
|
3956
4991
|
|
|
3957
4992
|
// src/commands/run-plan.ts
|
|
3958
|
-
var runPlanCommand =
|
|
4993
|
+
var runPlanCommand = defineCommand13({
|
|
3959
4994
|
meta: {
|
|
3960
4995
|
name: "run-plan",
|
|
3961
4996
|
description: "Headless continuous plan runner: one fresh agent per tick (LOOP_TICK_RESULT contract). Never git-prod."
|
|
@@ -4014,7 +5049,7 @@ var runPlanCommand = defineCommand9({
|
|
|
4014
5049
|
return;
|
|
4015
5050
|
}
|
|
4016
5051
|
const code = await runPlanLoop({
|
|
4017
|
-
root:
|
|
5052
|
+
root: path35.resolve(args.cwd),
|
|
4018
5053
|
maxTicks,
|
|
4019
5054
|
sleepSeconds,
|
|
4020
5055
|
model: args.model ? String(args.model) : void 0,
|
|
@@ -4026,8 +5061,8 @@ var runPlanCommand = defineCommand9({
|
|
|
4026
5061
|
});
|
|
4027
5062
|
|
|
4028
5063
|
// src/commands/scan.ts
|
|
4029
|
-
import { defineCommand as
|
|
4030
|
-
var scanCommand =
|
|
5064
|
+
import { defineCommand as defineCommand14 } from "citty";
|
|
5065
|
+
var scanCommand = defineCommand14({
|
|
4031
5066
|
meta: {
|
|
4032
5067
|
name: "scan",
|
|
4033
5068
|
description: "Scan the current repository and print detected profile."
|
|
@@ -4048,8 +5083,8 @@ var scanCommand = defineCommand10({
|
|
|
4048
5083
|
});
|
|
4049
5084
|
|
|
4050
5085
|
// src/commands/status.ts
|
|
4051
|
-
import
|
|
4052
|
-
import { defineCommand as
|
|
5086
|
+
import path36 from "path";
|
|
5087
|
+
import { defineCommand as defineCommand15 } from "citty";
|
|
4053
5088
|
function profileStatus(profile) {
|
|
4054
5089
|
if (!profile) return { origin: "none", evidence: [], profile: null };
|
|
4055
5090
|
if ("detection" in profile && profile.detection && typeof profile.detection === "object") {
|
|
@@ -4062,7 +5097,7 @@ function profileStatus(profile) {
|
|
|
4062
5097
|
}
|
|
4063
5098
|
return { origin: "legacy-wizard", evidence: [], profile };
|
|
4064
5099
|
}
|
|
4065
|
-
var statusCommand =
|
|
5100
|
+
var statusCommand = defineCommand15({
|
|
4066
5101
|
meta: {
|
|
4067
5102
|
name: "status",
|
|
4068
5103
|
description: "Show Agent Kit distribution status (manifest + optional wizard profile)."
|
|
@@ -4079,11 +5114,11 @@ var statusCommand = defineCommand11({
|
|
|
4079
5114
|
}
|
|
4080
5115
|
},
|
|
4081
5116
|
async run({ args }) {
|
|
4082
|
-
const rootDir =
|
|
5117
|
+
const rootDir = path36.resolve(args.cwd);
|
|
4083
5118
|
const [manifest, rawProfile, scan] = await Promise.all([
|
|
4084
5119
|
loadAgentKitManifest(rootDir),
|
|
4085
5120
|
readJson(
|
|
4086
|
-
|
|
5121
|
+
path36.join(rootDir, ".cursor", "agent-kit.config.json")
|
|
4087
5122
|
),
|
|
4088
5123
|
runScanner(rootDir)
|
|
4089
5124
|
]);
|
|
@@ -4138,20 +5173,297 @@ var statusCommand = defineCommand11({
|
|
|
4138
5173
|
});
|
|
4139
5174
|
|
|
4140
5175
|
// src/commands/update.ts
|
|
4141
|
-
import { defineCommand as
|
|
4142
|
-
|
|
5176
|
+
import { defineCommand as defineCommand16 } from "citty";
|
|
5177
|
+
|
|
5178
|
+
// src/lifecycle/check-updates.ts
|
|
5179
|
+
import { execFile as execFile4 } from "child_process";
|
|
5180
|
+
import path37 from "path";
|
|
5181
|
+
import { promisify as promisify4 } from "util";
|
|
5182
|
+
var execFileAsync3 = promisify4(execFile4);
|
|
5183
|
+
var SEMVER_CORE = /^v?(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/i;
|
|
5184
|
+
var FACTORY_URL_MARKERS = ["agent-kit-dev"];
|
|
5185
|
+
var FACTORY_REFS = /* @__PURE__ */ new Set(["staging", "homologacao", "develop", "dev"]);
|
|
5186
|
+
var DEFAULT_UPDATE_CHECK = {
|
|
5187
|
+
enabled: false,
|
|
5188
|
+
intervalDays: 7,
|
|
5189
|
+
lastCheckedAt: null
|
|
5190
|
+
};
|
|
5191
|
+
function gitEnv2() {
|
|
5192
|
+
return { ...process.env, GIT_ALLOW_PROTOCOL: "https" };
|
|
5193
|
+
}
|
|
5194
|
+
function compareSemver(a, b) {
|
|
5195
|
+
const pa = parseSemverParts(a);
|
|
5196
|
+
const pb = parseSemverParts(b);
|
|
5197
|
+
if (!pa || !pb) {
|
|
5198
|
+
throw new Error(`Invalid semver for compare: "${a}" vs "${b}"`);
|
|
5199
|
+
}
|
|
5200
|
+
for (let i = 0; i < 3; i++) {
|
|
5201
|
+
const d = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
5202
|
+
if (d !== 0) return d < 0 ? -1 : 1;
|
|
5203
|
+
}
|
|
5204
|
+
return 0;
|
|
5205
|
+
}
|
|
5206
|
+
function parseSemverParts(version) {
|
|
5207
|
+
const m = SEMVER_CORE.exec(version.trim());
|
|
5208
|
+
if (!m) return null;
|
|
5209
|
+
return [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
5210
|
+
}
|
|
5211
|
+
function normalizeSemver(version) {
|
|
5212
|
+
const parts = parseSemverParts(version);
|
|
5213
|
+
if (!parts) return null;
|
|
5214
|
+
return `${parts[0]}.${parts[1]}.${parts[2]}`;
|
|
5215
|
+
}
|
|
5216
|
+
function isFactoryOrDevRegistry(url, ref) {
|
|
5217
|
+
if (url) {
|
|
5218
|
+
const lower = url.toLowerCase();
|
|
5219
|
+
if (FACTORY_URL_MARKERS.some((m) => lower.includes(m))) return true;
|
|
5220
|
+
}
|
|
5221
|
+
if (ref && FACTORY_REFS.has(ref.toLowerCase())) return true;
|
|
5222
|
+
return false;
|
|
5223
|
+
}
|
|
5224
|
+
function pickLatestSemverTag(lsRemoteStdout) {
|
|
5225
|
+
let best = null;
|
|
5226
|
+
for (const line of lsRemoteStdout.split("\n")) {
|
|
5227
|
+
const trimmed = line.trim();
|
|
5228
|
+
if (!trimmed) continue;
|
|
5229
|
+
const ref = trimmed.split(/\s+/)[1];
|
|
5230
|
+
if (!ref?.startsWith("refs/tags/")) continue;
|
|
5231
|
+
if (ref.endsWith("^{}")) continue;
|
|
5232
|
+
const tag = ref.slice("refs/tags/".length);
|
|
5233
|
+
const norm = normalizeSemver(tag);
|
|
5234
|
+
if (!norm) continue;
|
|
5235
|
+
if (!best || compareSemver(norm, best) > 0) best = norm;
|
|
5236
|
+
}
|
|
5237
|
+
return best;
|
|
5238
|
+
}
|
|
5239
|
+
async function fetchLatestPublicVersion(registryUrl = DEFAULT_REGISTRY_URL) {
|
|
5240
|
+
assertSafeRegistrySource(registryUrl, "main");
|
|
5241
|
+
const { stdout } = await execFileAsync3("git", ["ls-remote", "--tags", "--", registryUrl], {
|
|
5242
|
+
env: gitEnv2(),
|
|
5243
|
+
timeout: 2e4
|
|
5244
|
+
});
|
|
5245
|
+
return pickLatestSemverTag(stdout);
|
|
5246
|
+
}
|
|
5247
|
+
function readUpdateCheckPrefs(config) {
|
|
5248
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) {
|
|
5249
|
+
return { ...DEFAULT_UPDATE_CHECK };
|
|
5250
|
+
}
|
|
5251
|
+
const raw = config.updateCheck;
|
|
5252
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
5253
|
+
return { ...DEFAULT_UPDATE_CHECK };
|
|
5254
|
+
}
|
|
5255
|
+
const uc = raw;
|
|
5256
|
+
const intervalDays = typeof uc.intervalDays === "number" && Number.isInteger(uc.intervalDays) && uc.intervalDays >= 1 ? uc.intervalDays : DEFAULT_UPDATE_CHECK.intervalDays;
|
|
5257
|
+
return {
|
|
5258
|
+
enabled: uc.enabled === true,
|
|
5259
|
+
intervalDays,
|
|
5260
|
+
lastCheckedAt: typeof uc.lastCheckedAt === "string" ? uc.lastCheckedAt : null
|
|
5261
|
+
};
|
|
5262
|
+
}
|
|
5263
|
+
function intervalElapsed(lastCheckedAt, intervalDays) {
|
|
5264
|
+
if (!lastCheckedAt) return true;
|
|
5265
|
+
const last = Date.parse(lastCheckedAt);
|
|
5266
|
+
if (Number.isNaN(last)) return true;
|
|
5267
|
+
const ms = intervalDays * 24 * 60 * 60 * 1e3;
|
|
5268
|
+
return Date.now() - last >= ms;
|
|
5269
|
+
}
|
|
5270
|
+
async function loadContextConfig(cwd) {
|
|
5271
|
+
const configPath = path37.join(cwd, ".cursor", "context", "config.json");
|
|
5272
|
+
return readJson(configPath);
|
|
5273
|
+
}
|
|
5274
|
+
async function stampLastCheckedAt(cwd) {
|
|
5275
|
+
const configPath = path37.join(cwd, ".cursor", "context", "config.json");
|
|
5276
|
+
const existing = await loadContextConfig(cwd) ?? {};
|
|
5277
|
+
const prev = existing.updateCheck && typeof existing.updateCheck === "object" ? { ...existing.updateCheck } : {};
|
|
5278
|
+
existing.updateCheck = {
|
|
5279
|
+
...DEFAULT_UPDATE_CHECK,
|
|
5280
|
+
...prev,
|
|
5281
|
+
lastCheckedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5282
|
+
};
|
|
5283
|
+
await writeJson(configPath, existing);
|
|
5284
|
+
}
|
|
5285
|
+
async function checkForUpdates(cwd, options = {}) {
|
|
5286
|
+
const manifest = await loadAgentKitManifest(cwd);
|
|
5287
|
+
if (!manifest) {
|
|
5288
|
+
return {
|
|
5289
|
+
status: "skipped-no-manifest",
|
|
5290
|
+
installedVersion: null,
|
|
5291
|
+
latestVersion: null,
|
|
5292
|
+
registryUrl: null,
|
|
5293
|
+
registryRef: null,
|
|
5294
|
+
applyRecommended: false,
|
|
5295
|
+
message: "No .cursor/agent-kit.json \u2014 run agent-kit install first."
|
|
5296
|
+
};
|
|
5297
|
+
}
|
|
5298
|
+
const registryUrl = manifest.registry?.url ?? null;
|
|
5299
|
+
const registryRef = manifest.registry?.ref ?? null;
|
|
5300
|
+
if (isFactoryOrDevRegistry(registryUrl, registryRef)) {
|
|
5301
|
+
return {
|
|
5302
|
+
status: "skipped-factory",
|
|
5303
|
+
installedVersion: manifest.version,
|
|
5304
|
+
latestVersion: null,
|
|
5305
|
+
registryUrl,
|
|
5306
|
+
registryRef,
|
|
5307
|
+
applyRecommended: false,
|
|
5308
|
+
message: "Factory/dev registry detected \u2014 skipping public update check. Use /update only with explicit consumer intent."
|
|
5309
|
+
};
|
|
5310
|
+
}
|
|
5311
|
+
if (options.respectPrefs) {
|
|
5312
|
+
const config = await loadContextConfig(cwd);
|
|
5313
|
+
const prefs = readUpdateCheckPrefs(config);
|
|
5314
|
+
if (!prefs.enabled) {
|
|
5315
|
+
return {
|
|
5316
|
+
status: "skipped-disabled",
|
|
5317
|
+
installedVersion: manifest.version,
|
|
5318
|
+
latestVersion: null,
|
|
5319
|
+
registryUrl,
|
|
5320
|
+
registryRef,
|
|
5321
|
+
applyRecommended: false,
|
|
5322
|
+
message: "updateCheck.enabled is false (opt-in). Set true in .cursor/context/config.json to nudge."
|
|
5323
|
+
};
|
|
5324
|
+
}
|
|
5325
|
+
if (!intervalElapsed(prefs.lastCheckedAt, prefs.intervalDays)) {
|
|
5326
|
+
return {
|
|
5327
|
+
status: "skipped-interval",
|
|
5328
|
+
installedVersion: manifest.version,
|
|
5329
|
+
latestVersion: null,
|
|
5330
|
+
registryUrl,
|
|
5331
|
+
registryRef,
|
|
5332
|
+
applyRecommended: false,
|
|
5333
|
+
message: `Within updateCheck.intervalDays (${prefs.intervalDays}); last check ${prefs.lastCheckedAt}.`
|
|
5334
|
+
};
|
|
5335
|
+
}
|
|
5336
|
+
}
|
|
5337
|
+
let latest;
|
|
5338
|
+
try {
|
|
5339
|
+
latest = options.latestVersion !== void 0 ? options.latestVersion : await fetchLatestPublicVersion(options.publicRegistryUrl ?? DEFAULT_REGISTRY_URL);
|
|
5340
|
+
} catch (err) {
|
|
5341
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5342
|
+
return {
|
|
5343
|
+
status: "error",
|
|
5344
|
+
installedVersion: manifest.version,
|
|
5345
|
+
latestVersion: null,
|
|
5346
|
+
registryUrl,
|
|
5347
|
+
registryRef,
|
|
5348
|
+
applyRecommended: false,
|
|
5349
|
+
message: `Failed to fetch public tags: ${msg}`
|
|
5350
|
+
};
|
|
5351
|
+
}
|
|
5352
|
+
if (options.stamp) {
|
|
5353
|
+
try {
|
|
5354
|
+
await stampLastCheckedAt(cwd);
|
|
5355
|
+
} catch {
|
|
5356
|
+
}
|
|
5357
|
+
}
|
|
5358
|
+
const installed = normalizeSemver(manifest.version);
|
|
5359
|
+
if (!installed) {
|
|
5360
|
+
return {
|
|
5361
|
+
status: "error",
|
|
5362
|
+
installedVersion: manifest.version,
|
|
5363
|
+
latestVersion: latest,
|
|
5364
|
+
registryUrl,
|
|
5365
|
+
registryRef,
|
|
5366
|
+
applyRecommended: false,
|
|
5367
|
+
message: `Installed version "${manifest.version}" is not valid semver.`
|
|
5368
|
+
};
|
|
5369
|
+
}
|
|
5370
|
+
if (!latest) {
|
|
5371
|
+
return {
|
|
5372
|
+
status: "error",
|
|
5373
|
+
installedVersion: installed,
|
|
5374
|
+
latestVersion: null,
|
|
5375
|
+
registryUrl,
|
|
5376
|
+
registryRef,
|
|
5377
|
+
applyRecommended: false,
|
|
5378
|
+
message: "No semver tags found on the public registry."
|
|
5379
|
+
};
|
|
5380
|
+
}
|
|
5381
|
+
const cmp = compareSemver(installed, latest);
|
|
5382
|
+
if (cmp === 0) {
|
|
5383
|
+
return {
|
|
5384
|
+
status: "up-to-date",
|
|
5385
|
+
installedVersion: installed,
|
|
5386
|
+
latestVersion: latest,
|
|
5387
|
+
registryUrl,
|
|
5388
|
+
registryRef,
|
|
5389
|
+
applyRecommended: false,
|
|
5390
|
+
message: `Installed v${installed} matches latest public v${latest}.`
|
|
5391
|
+
};
|
|
5392
|
+
}
|
|
5393
|
+
if (cmp < 0) {
|
|
5394
|
+
return {
|
|
5395
|
+
status: "update-available",
|
|
5396
|
+
installedVersion: installed,
|
|
5397
|
+
latestVersion: latest,
|
|
5398
|
+
registryUrl,
|
|
5399
|
+
registryRef,
|
|
5400
|
+
applyRecommended: false,
|
|
5401
|
+
message: `Update available: v${installed} \u2192 v${latest}. Run /update (Ask confirm) to apply; never silent.`
|
|
5402
|
+
};
|
|
5403
|
+
}
|
|
5404
|
+
return {
|
|
5405
|
+
status: "ahead-of-public",
|
|
5406
|
+
installedVersion: installed,
|
|
5407
|
+
latestVersion: latest,
|
|
5408
|
+
registryUrl,
|
|
5409
|
+
registryRef,
|
|
5410
|
+
applyRecommended: false,
|
|
5411
|
+
message: `Installed v${installed} is ahead of latest public v${latest} (dev/local build).`
|
|
5412
|
+
};
|
|
5413
|
+
}
|
|
5414
|
+
|
|
5415
|
+
// src/commands/update.ts
|
|
5416
|
+
var updateCommand = defineCommand16({
|
|
4143
5417
|
meta: {
|
|
4144
5418
|
name: "update",
|
|
4145
|
-
description: "Re-apply L0/packs/skills from the registry; never overwrites L3 protected paths."
|
|
5419
|
+
description: "Re-apply L0/packs/skills from the registry; never overwrites L3 protected paths. Use --check for notify-only."
|
|
4146
5420
|
},
|
|
4147
5421
|
args: {
|
|
4148
5422
|
cwd: {
|
|
4149
5423
|
type: "string",
|
|
4150
5424
|
default: process.cwd()
|
|
4151
5425
|
},
|
|
5426
|
+
check: {
|
|
5427
|
+
type: "boolean",
|
|
5428
|
+
description: "Check-only: compare installed version to latest public tag; never apply L0 writes",
|
|
5429
|
+
default: false
|
|
5430
|
+
},
|
|
5431
|
+
json: {
|
|
5432
|
+
type: "boolean",
|
|
5433
|
+
description: "Print machine-readable JSON (with --check)",
|
|
5434
|
+
default: false
|
|
5435
|
+
},
|
|
5436
|
+
"respect-prefs": {
|
|
5437
|
+
type: "boolean",
|
|
5438
|
+
description: "Honor updateCheck.enabled and intervalDays from .cursor/context/config.json (hooks)",
|
|
5439
|
+
default: false
|
|
5440
|
+
},
|
|
5441
|
+
stamp: {
|
|
5442
|
+
type: "boolean",
|
|
5443
|
+
description: "Persist updateCheck.lastCheckedAt after a network check",
|
|
5444
|
+
default: false
|
|
5445
|
+
},
|
|
4152
5446
|
...REGISTRY_CLI_ARGS
|
|
4153
5447
|
},
|
|
4154
5448
|
async run({ args }) {
|
|
5449
|
+
if (args.check) {
|
|
5450
|
+
const result = await checkForUpdates(args.cwd, {
|
|
5451
|
+
respectPrefs: Boolean(args["respect-prefs"]),
|
|
5452
|
+
stamp: Boolean(args.stamp),
|
|
5453
|
+
publicRegistryUrl: typeof args.url === "string" && args.url ? args.url : void 0
|
|
5454
|
+
});
|
|
5455
|
+
if (args.json) {
|
|
5456
|
+
console.log(JSON.stringify(result, null, 2));
|
|
5457
|
+
} else {
|
|
5458
|
+
const line = `[${result.status}] ${result.message}`;
|
|
5459
|
+
if (result.status === "error") logger.error(line);
|
|
5460
|
+
else if (result.status === "update-available") logger.warn(line);
|
|
5461
|
+
else if (result.status.startsWith("skipped-")) logger.warn(line);
|
|
5462
|
+
else logger.info(line);
|
|
5463
|
+
}
|
|
5464
|
+
if (result.status === "error") process.exitCode = 2;
|
|
5465
|
+
return;
|
|
5466
|
+
}
|
|
4155
5467
|
const existing = await loadAgentKitManifest(args.cwd);
|
|
4156
5468
|
if (!existing) {
|
|
4157
5469
|
logger.warn("No .cursor/agent-kit.json \u2014 run agent-kit install first.");
|
|
@@ -4183,8 +5495,188 @@ var updateCommand = defineCommand12({
|
|
|
4183
5495
|
}
|
|
4184
5496
|
});
|
|
4185
5497
|
|
|
5498
|
+
// src/commands/validate.ts
|
|
5499
|
+
import { readFile as readFile18 } from "fs/promises";
|
|
5500
|
+
import path38 from "path";
|
|
5501
|
+
import { defineCommand as defineCommand17 } from "citty";
|
|
5502
|
+
|
|
5503
|
+
// src/invariants/plan-schema.ts
|
|
5504
|
+
var CITE5 = "agent-kit validate plan (.cursor/context/templates/plan.md)";
|
|
5505
|
+
function validatePlanFrontmatterText(text) {
|
|
5506
|
+
const warnings = [];
|
|
5507
|
+
const match = text.match(/^---\n([\s\S]*?)\n---/);
|
|
5508
|
+
if (!match?.[1]) {
|
|
5509
|
+
warnings.push({
|
|
5510
|
+
code: "missing-frontmatter",
|
|
5511
|
+
message: "Plan file has no YAML frontmatter block.",
|
|
5512
|
+
cite: CITE5
|
|
5513
|
+
});
|
|
5514
|
+
return warnings;
|
|
5515
|
+
}
|
|
5516
|
+
const block = match[1];
|
|
5517
|
+
if (!/^todos:\s*$/m.test(block) && !/^todos:\s*\[/m.test(block)) {
|
|
5518
|
+
if (!/^todos:/m.test(block)) {
|
|
5519
|
+
warnings.push({
|
|
5520
|
+
code: "missing-todos",
|
|
5521
|
+
message: "Plan frontmatter has no `todos:` key.",
|
|
5522
|
+
cite: CITE5
|
|
5523
|
+
});
|
|
5524
|
+
}
|
|
5525
|
+
}
|
|
5526
|
+
if (!/^name:\s*\S+/m.test(block)) {
|
|
5527
|
+
warnings.push({
|
|
5528
|
+
code: "missing-name",
|
|
5529
|
+
message: "Plan frontmatter has no `name:` key.",
|
|
5530
|
+
cite: CITE5
|
|
5531
|
+
});
|
|
5532
|
+
}
|
|
5533
|
+
const hasTodoItem = /^- id:\s*\S+/m.test(block);
|
|
5534
|
+
if (/^todos:/m.test(block) && !hasTodoItem && !/^todos:\s*\[\s*\]/m.test(block)) {
|
|
5535
|
+
warnings.push({
|
|
5536
|
+
code: "empty-todos",
|
|
5537
|
+
message: "Plan frontmatter `todos:` has no `- id:` items.",
|
|
5538
|
+
cite: CITE5
|
|
5539
|
+
});
|
|
5540
|
+
}
|
|
5541
|
+
return warnings;
|
|
5542
|
+
}
|
|
5543
|
+
|
|
5544
|
+
// src/commands/validate.ts
|
|
5545
|
+
async function resolveEditedPath(cwd, explicit) {
|
|
5546
|
+
if (explicit) {
|
|
5547
|
+
const filePath2 = path38.resolve(cwd, explicit);
|
|
5548
|
+
try {
|
|
5549
|
+
return { filePath: filePath2, content: await readFile18(filePath2, "utf8") };
|
|
5550
|
+
} catch {
|
|
5551
|
+
return null;
|
|
5552
|
+
}
|
|
5553
|
+
}
|
|
5554
|
+
const payload = await readStdinJson();
|
|
5555
|
+
const rel = typeof payload.file_path === "string" && payload.file_path || typeof payload.path === "string" && payload.path || typeof payload.file === "string" && payload.file || "";
|
|
5556
|
+
if (!rel) return null;
|
|
5557
|
+
const filePath = path38.isAbsolute(rel) ? rel : path38.resolve(cwd, rel);
|
|
5558
|
+
try {
|
|
5559
|
+
return { filePath, content: await readFile18(filePath, "utf8") };
|
|
5560
|
+
} catch {
|
|
5561
|
+
return null;
|
|
5562
|
+
}
|
|
5563
|
+
}
|
|
5564
|
+
function isHandoffPath(filePath) {
|
|
5565
|
+
return filePath.replace(/\\/g, "/").endsWith(".cursor/HANDOFF.md");
|
|
5566
|
+
}
|
|
5567
|
+
function isPlanPath(filePath) {
|
|
5568
|
+
const norm = filePath.replace(/\\/g, "/");
|
|
5569
|
+
return norm.includes("/.cursor/plans/") && norm.endsWith(".plan.md");
|
|
5570
|
+
}
|
|
5571
|
+
var validateCommand = defineCommand17({
|
|
5572
|
+
meta: {
|
|
5573
|
+
name: "validate",
|
|
5574
|
+
description: "Advisory validators for HANDOFF / plan frontmatter (afterFileEdit adapter)"
|
|
5575
|
+
},
|
|
5576
|
+
subCommands: {
|
|
5577
|
+
handoff: defineCommand17({
|
|
5578
|
+
meta: { name: "handoff", description: "Validate HANDOFF machine fields" },
|
|
5579
|
+
args: {
|
|
5580
|
+
cwd: { type: "string", default: process.cwd() },
|
|
5581
|
+
file: { type: "string", description: "Path to HANDOFF.md" },
|
|
5582
|
+
json: { type: "boolean", default: true }
|
|
5583
|
+
},
|
|
5584
|
+
async run({ args }) {
|
|
5585
|
+
const cwd = typeof args.cwd === "string" ? args.cwd : process.cwd();
|
|
5586
|
+
const fileArg = typeof args.file === "string" ? args.file : void 0;
|
|
5587
|
+
const filePath = fileArg ? path38.resolve(cwd, fileArg) : path38.join(path38.resolve(cwd), ".cursor", "HANDOFF.md");
|
|
5588
|
+
let content = "";
|
|
5589
|
+
try {
|
|
5590
|
+
content = await readFile18(filePath, "utf8");
|
|
5591
|
+
} catch {
|
|
5592
|
+
console.log(JSON.stringify({ ok: true, warnings: [], note: "file missing" }));
|
|
5593
|
+
return;
|
|
5594
|
+
}
|
|
5595
|
+
const warnings = validateHandoffText(content);
|
|
5596
|
+
console.log(JSON.stringify({ ok: warnings.length === 0, warnings }));
|
|
5597
|
+
}
|
|
5598
|
+
}),
|
|
5599
|
+
plan: defineCommand17({
|
|
5600
|
+
meta: { name: "plan", description: "Validate plan frontmatter" },
|
|
5601
|
+
args: {
|
|
5602
|
+
cwd: { type: "string", default: process.cwd() },
|
|
5603
|
+
file: { type: "string", description: "Path to *.plan.md" },
|
|
5604
|
+
json: { type: "boolean", default: true }
|
|
5605
|
+
},
|
|
5606
|
+
async run({ args }) {
|
|
5607
|
+
const cwd = typeof args.cwd === "string" ? args.cwd : process.cwd();
|
|
5608
|
+
const fileArg = typeof args.file === "string" ? args.file : void 0;
|
|
5609
|
+
if (!fileArg) {
|
|
5610
|
+
console.log(JSON.stringify({ ok: false, warnings: [{ message: "file required" }] }));
|
|
5611
|
+
process.exitCode = 2;
|
|
5612
|
+
return;
|
|
5613
|
+
}
|
|
5614
|
+
const filePath = path38.resolve(cwd, fileArg);
|
|
5615
|
+
let content = "";
|
|
5616
|
+
try {
|
|
5617
|
+
content = await readFile18(filePath, "utf8");
|
|
5618
|
+
} catch {
|
|
5619
|
+
console.log(JSON.stringify({ ok: true, warnings: [], note: "file missing" }));
|
|
5620
|
+
return;
|
|
5621
|
+
}
|
|
5622
|
+
const warnings = validatePlanFrontmatterText(content);
|
|
5623
|
+
console.log(JSON.stringify({ ok: warnings.length === 0, warnings }));
|
|
5624
|
+
}
|
|
5625
|
+
}),
|
|
5626
|
+
"after-edit": defineCommand17({
|
|
5627
|
+
meta: {
|
|
5628
|
+
name: "after-edit",
|
|
5629
|
+
description: "Advisory afterFileEdit: annotate HANDOFF/plan issues (never block)"
|
|
5630
|
+
},
|
|
5631
|
+
args: {
|
|
5632
|
+
cwd: { type: "string", default: process.cwd() }
|
|
5633
|
+
},
|
|
5634
|
+
async run({ args }) {
|
|
5635
|
+
const cwd = typeof args.cwd === "string" ? args.cwd : process.cwd();
|
|
5636
|
+
const resolved = await resolveEditedPath(path38.resolve(cwd));
|
|
5637
|
+
if (!resolved) {
|
|
5638
|
+
console.log(JSON.stringify({}));
|
|
5639
|
+
return;
|
|
5640
|
+
}
|
|
5641
|
+
const { filePath, content } = resolved;
|
|
5642
|
+
if (isHandoffPath(filePath)) {
|
|
5643
|
+
const warnings = validateHandoffText(content);
|
|
5644
|
+
if (!warnings.length) {
|
|
5645
|
+
console.log(JSON.stringify({}));
|
|
5646
|
+
return;
|
|
5647
|
+
}
|
|
5648
|
+
const msg = warnings.map((w) => w.message).join(" ");
|
|
5649
|
+
console.log(
|
|
5650
|
+
JSON.stringify({
|
|
5651
|
+
user_message: msg,
|
|
5652
|
+
agent_message: `${msg} Cite: agent-kit validate handoff.`
|
|
5653
|
+
})
|
|
5654
|
+
);
|
|
5655
|
+
return;
|
|
5656
|
+
}
|
|
5657
|
+
if (isPlanPath(filePath)) {
|
|
5658
|
+
const warnings = validatePlanFrontmatterText(content);
|
|
5659
|
+
if (!warnings.length) {
|
|
5660
|
+
console.log(JSON.stringify({}));
|
|
5661
|
+
return;
|
|
5662
|
+
}
|
|
5663
|
+
const msg = warnings.map((w) => w.message).join(" ");
|
|
5664
|
+
console.log(
|
|
5665
|
+
JSON.stringify({
|
|
5666
|
+
user_message: msg,
|
|
5667
|
+
agent_message: `${msg} Cite: agent-kit validate plan.`
|
|
5668
|
+
})
|
|
5669
|
+
);
|
|
5670
|
+
return;
|
|
5671
|
+
}
|
|
5672
|
+
console.log(JSON.stringify({}));
|
|
5673
|
+
}
|
|
5674
|
+
})
|
|
5675
|
+
}
|
|
5676
|
+
});
|
|
5677
|
+
|
|
4186
5678
|
// src/index.ts
|
|
4187
|
-
var main =
|
|
5679
|
+
var main = defineCommand18({
|
|
4188
5680
|
meta: {
|
|
4189
5681
|
name: "agent-kit",
|
|
4190
5682
|
description: "HITL framework for AI-assisted IDEs"
|
|
@@ -4201,7 +5693,12 @@ var main = defineCommand13({
|
|
|
4201
5693
|
contribute: contributeCommand,
|
|
4202
5694
|
handoff: handoffCommand,
|
|
4203
5695
|
"run-plan": runPlanCommand,
|
|
4204
|
-
dashboard: dashboardCommand
|
|
5696
|
+
dashboard: dashboardCommand,
|
|
5697
|
+
"dashboard-broadcast": dashboardBroadcastCommand,
|
|
5698
|
+
hook: hookCommand,
|
|
5699
|
+
guard: guardCommand,
|
|
5700
|
+
monitors: monitorsCommand,
|
|
5701
|
+
validate: validateCommand
|
|
4205
5702
|
}
|
|
4206
5703
|
});
|
|
4207
5704
|
runMain(main);
|