jorgex-stack 1.0.29 → 1.0.31
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/README.md +17 -9
- package/dist/cli.js +75 -123
- package/package.json +1 -1
- package/stack/agents/README.md +2 -2
- package/stack/agents/orchestrator.md +1 -205
- package/stack/agents/test-analyzer.md +25 -64
- package/stack/agents/tester.md +52 -27
- package/stack/skills/agent-delegation/SKILL.md +5 -5
- package/stack/skills/orchestrator/SKILL.md +228 -0
- package/stack/skills/tdd/SKILL.md +54 -72
- package/stack/skills/tdd/mocking.md +22 -47
- package/stack/skills/tdd/tests.md +42 -44
- package/stack/skills/to-prd/SKILL.md +10 -4
- package/stack/skills/work-lifecycle/references/plan-template.md +10 -0
- package/stack/system-prompt/AGENTS.md +5 -3
- package/upstreams.json +2 -1
- package/stack/modes/programmatic/agent-delegation.addendum.md +0 -11
package/README.md
CHANGED
|
@@ -8,16 +8,24 @@ Portable multi-agent harness: one configuration source — 15 agents, 17 skills,
|
|
|
8
8
|
|
|
9
9
|
Install and run via npm without cloning the repository:
|
|
10
10
|
|
|
11
|
+
```bash
|
|
12
|
+
# First installation
|
|
13
|
+
pnpm dlx jorgex-stack install
|
|
14
|
+
|
|
15
|
+
# Already installed: apply the latest published stack while keeping the existing model selection
|
|
16
|
+
pnpm dlx jorgex-stack sync
|
|
11
17
|
```
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
pnpm dlx jorgex-stack
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
pnpm dlx jorgex-stack
|
|
20
|
-
pnpm dlx jorgex-stack
|
|
18
|
+
|
|
19
|
+
Other important commands:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pnpm dlx jorgex-stack doctor # check Engram, config drift, hooks and keys
|
|
23
|
+
pnpm dlx jorgex-stack models # change models by runtime, tier or subagent
|
|
24
|
+
pnpm dlx jorgex-stack update --check # report available stack, Engram and skill updates
|
|
25
|
+
pnpm dlx jorgex-stack update # interactively review and apply available updates
|
|
26
|
+
pnpm dlx jorgex-stack restore --list # list automatic backups
|
|
27
|
+
pnpm dlx jorgex-stack restore <id> # restore one backup
|
|
28
|
+
pnpm dlx jorgex-stack uninstall # remove managed files; keep Engram data intact
|
|
21
29
|
```
|
|
22
30
|
|
|
23
31
|
For development from a clone, run the same commands through `pnpm cli <command>` (see [Development](#development)).
|
package/dist/cli.js
CHANGED
|
@@ -5,7 +5,7 @@ import * as p6 from "@clack/prompts";
|
|
|
5
5
|
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
6
6
|
|
|
7
7
|
// src/install.ts
|
|
8
|
-
import
|
|
8
|
+
import fs13 from "fs";
|
|
9
9
|
import path19 from "path";
|
|
10
10
|
import * as p from "@clack/prompts";
|
|
11
11
|
|
|
@@ -546,8 +546,6 @@ var opencodeAdapter = {
|
|
|
546
546
|
${deny}`);
|
|
547
547
|
}
|
|
548
548
|
if (!agent.spawn) lines.push(" task: deny");
|
|
549
|
-
lines.push("tools:");
|
|
550
|
-
lines.push(` write: ${agent.readonly ? "false" : "true"}`);
|
|
551
549
|
}
|
|
552
550
|
return [
|
|
553
551
|
{
|
|
@@ -803,16 +801,7 @@ description: ${yamlString2(agent.description)}
|
|
|
803
801
|
keep-coding-instructions: true
|
|
804
802
|
---
|
|
805
803
|
${agent.body}`;
|
|
806
|
-
|
|
807
|
-
const skill = `---
|
|
808
|
-
name: ${agent.name}
|
|
809
|
-
description: ${yamlString2(skillDescription)}
|
|
810
|
-
---
|
|
811
|
-
${agent.body}`;
|
|
812
|
-
return [
|
|
813
|
-
{ file: `${agent.name}.md`, content: style, kind: "output-style" },
|
|
814
|
-
{ file: `${agent.name}/SKILL.md`, content: skill, kind: "skill" }
|
|
815
|
-
];
|
|
804
|
+
return [{ file: `${agent.name}.md`, content: style, kind: "output-style" }];
|
|
816
805
|
}
|
|
817
806
|
const lines = [`name: ${agent.name}`, `description: ${yamlString2(agent.description)}`];
|
|
818
807
|
const tools = toolsFor(agent);
|
|
@@ -995,16 +984,7 @@ var codexAdapter = {
|
|
|
995
984
|
`# Uso: codex --profile ${agent.name}`,
|
|
996
985
|
`developer_instructions = ${tomlMultiline(agent.body)}`
|
|
997
986
|
];
|
|
998
|
-
|
|
999
|
-
const skill = `---
|
|
1000
|
-
name: ${agent.name}
|
|
1001
|
-
description: ${JSON.stringify(skillDescription)}
|
|
1002
|
-
---
|
|
1003
|
-
${agent.body}`;
|
|
1004
|
-
return [
|
|
1005
|
-
{ file: `${agent.name}.config.toml`, content: profileLines.join("\n") + "\n", kind: "profile" },
|
|
1006
|
-
{ file: `${agent.name}/SKILL.md`, content: skill, kind: "skill" }
|
|
1007
|
-
];
|
|
987
|
+
return [{ file: `${agent.name}.config.toml`, content: profileLines.join("\n") + "\n", kind: "profile" }];
|
|
1008
988
|
}
|
|
1009
989
|
const lines = [
|
|
1010
990
|
`name = ${tomlString(agent.name)}`,
|
|
@@ -1379,13 +1359,6 @@ import path13 from "path";
|
|
|
1379
1359
|
var PROGRAMMATIC_ROOT = ["modes", "programmatic"];
|
|
1380
1360
|
var PROGRAMMATIC_MARKER = "<!-- jorgex:programmatic-mode -->";
|
|
1381
1361
|
var LEGACY_RESULT_CONTRACT_SECTION = /\n?##\s+Result contract[\s\S]*$/;
|
|
1382
|
-
var LEGACY_SKILL_DELEGATION_SECTION = /\n?##\s+Formato obligatorio[\s\S]*$/;
|
|
1383
|
-
var LEGACY_DELEGATION_LINE = /- For each `→ \[agent\]: \.\.\.` line, launch the corresponding specialist\./;
|
|
1384
|
-
var LEGACY_PROGRAMMATIC_PHRASES = [
|
|
1385
|
-
[/\bResult contract\b/g, "strict JSON handoff"],
|
|
1386
|
-
[/Status \/ Delegations \/ Risks/g, "status, delegations, and risks"],
|
|
1387
|
-
[LEGACY_DELEGATION_LINE, "- Process the JSON `delegations[]` array and launch the corresponding specialist."]
|
|
1388
|
-
];
|
|
1389
1362
|
var normalize = (value) => value.replace(/\r\n/g, "\n");
|
|
1390
1363
|
function loadProgrammaticAddendum(stackDir, fileName) {
|
|
1391
1364
|
return normalize(fs9.readFileSync(path13.join(stackDir, ...PROGRAMMATIC_ROOT, fileName), "utf8")).trim();
|
|
@@ -1399,10 +1372,7 @@ ${addendum}
|
|
|
1399
1372
|
`;
|
|
1400
1373
|
}
|
|
1401
1374
|
function stripLegacyResultContract(body) {
|
|
1402
|
-
return
|
|
1403
|
-
(text2, [pattern, replacement]) => text2.replace(pattern, replacement),
|
|
1404
|
-
normalize(body).replace(LEGACY_RESULT_CONTRACT_SECTION, "")
|
|
1405
|
-
).trimEnd();
|
|
1375
|
+
return normalize(body).replace(LEGACY_RESULT_CONTRACT_SECTION, "").replace(/\bResult contract\b/g, "strict JSON handoff").trimEnd();
|
|
1406
1376
|
}
|
|
1407
1377
|
function concurrencyRule(concurrency) {
|
|
1408
1378
|
if (concurrency === "parallel") {
|
|
@@ -1422,22 +1392,13 @@ function composeProgrammaticSystemPrompt(stackDir, content, mode) {
|
|
|
1422
1392
|
}
|
|
1423
1393
|
function composeProgrammaticAgentBody(stackDir, agent, mode, concurrency) {
|
|
1424
1394
|
if (mode !== "programmatic") return normalize(agent.body);
|
|
1425
|
-
const fileName = agent.mode === "primary" ? "orchestrator.addendum.md" : "subagent.addendum.md";
|
|
1426
|
-
let addendum = loadProgrammaticAddendum(stackDir, fileName);
|
|
1427
1395
|
if (agent.mode === "primary") {
|
|
1428
|
-
|
|
1396
|
+
const addendum2 = loadProgrammaticAddendum(stackDir, "orchestrator.addendum.md").replace("{{CONCURRENCY_RULE}}", concurrencyRule(concurrency ?? "serial"));
|
|
1397
|
+
return appendAddendum(agent.body, addendum2);
|
|
1429
1398
|
}
|
|
1399
|
+
const addendum = loadProgrammaticAddendum(stackDir, "subagent.addendum.md");
|
|
1430
1400
|
return appendAddendum(stripLegacyResultContract(agent.body), addendum);
|
|
1431
1401
|
}
|
|
1432
|
-
function composeProgrammaticSkillBody(stackDir, skillPath, content, mode) {
|
|
1433
|
-
if (mode !== "programmatic") return normalize(content);
|
|
1434
|
-
if (skillPath !== path13.join("agent-delegation", "SKILL.md")) return normalize(content);
|
|
1435
|
-
const base = normalize(content).replace(
|
|
1436
|
-
"Las delegaciones van **en tu output final**, en el formato de abajo. El orquestador las lee y decide a qui\xE9n invocar.",
|
|
1437
|
-
"Las delegaciones van como strings en el JSON final `delegations[]`. El orquestador las lee y decide a qui\xE9n invocar."
|
|
1438
|
-
).replace(LEGACY_SKILL_DELEGATION_SECTION, "").trimEnd();
|
|
1439
|
-
return appendAddendum(base, loadProgrammaticAddendum(stackDir, "agent-delegation.addendum.md"));
|
|
1440
|
-
}
|
|
1441
1402
|
|
|
1442
1403
|
// src/components/system-prompt.ts
|
|
1443
1404
|
var normalize2 = (s) => s.replace(/\r\n/g, "\n");
|
|
@@ -1461,12 +1422,11 @@ function planSystemPrompt(adapter, ctx) {
|
|
|
1461
1422
|
// src/components/agents.ts
|
|
1462
1423
|
import path15 from "path";
|
|
1463
1424
|
function planAgents(adapter, ctx) {
|
|
1464
|
-
const { agentsDir, commandsDir, outputStylesDir,
|
|
1425
|
+
const { agentsDir, commandsDir, outputStylesDir, profilesDir, scriptsDir } = adapter.paths(ctx.configDir);
|
|
1465
1426
|
const dirFor = {
|
|
1466
1427
|
agent: agentsDir,
|
|
1467
1428
|
command: commandsDir,
|
|
1468
1429
|
"output-style": outputStylesDir,
|
|
1469
|
-
skill: skillsDir,
|
|
1470
1430
|
profile: profilesDir
|
|
1471
1431
|
};
|
|
1472
1432
|
const scriptsBase = scriptsDir.replace(/\\/g, "/");
|
|
@@ -1489,43 +1449,35 @@ function planAgents(adapter, ctx) {
|
|
|
1489
1449
|
|
|
1490
1450
|
// src/components/skills.ts
|
|
1491
1451
|
import path16 from "path";
|
|
1492
|
-
import fs11 from "fs";
|
|
1493
1452
|
function planSkills(adapter, ctx) {
|
|
1494
1453
|
const { skillsDir } = adapter.paths(ctx.configDir);
|
|
1495
1454
|
const source = path16.join(ctx.stackDir, "skills");
|
|
1496
1455
|
return listFilesRecursive(source).map((file) => {
|
|
1497
1456
|
const relative = path16.relative(source, file);
|
|
1498
|
-
if (relative === path16.join("agent-delegation", "SKILL.md") && ctx.mode === "programmatic") {
|
|
1499
|
-
return {
|
|
1500
|
-
kind: "write",
|
|
1501
|
-
target: path16.join(skillsDir, relative),
|
|
1502
|
-
content: composeProgrammaticSkillBody(ctx.stackDir, relative, fs11.readFileSync(file, "utf8"), ctx.mode)
|
|
1503
|
-
};
|
|
1504
|
-
}
|
|
1505
1457
|
return { kind: "copy", source: file, target: path16.join(skillsDir, relative) };
|
|
1506
1458
|
});
|
|
1507
1459
|
}
|
|
1508
1460
|
|
|
1509
1461
|
// src/components/commands.ts
|
|
1510
1462
|
import path17 from "path";
|
|
1511
|
-
import
|
|
1463
|
+
import fs11 from "fs";
|
|
1512
1464
|
function planCommands(adapter, ctx) {
|
|
1513
1465
|
const { commandsDir } = adapter.paths(ctx.configDir);
|
|
1514
1466
|
const source = path17.join(ctx.stackDir, "commands");
|
|
1515
|
-
if (!
|
|
1467
|
+
if (!fs11.existsSync(source)) return [];
|
|
1516
1468
|
const commandFiles = [
|
|
1517
1469
|
...listMarkdownFiles(source),
|
|
1518
1470
|
...listMarkdownFiles(path17.join(source, adapter.id))
|
|
1519
1471
|
];
|
|
1520
1472
|
return commandFiles.map(({ file, fullPath }) => {
|
|
1521
|
-
const raw =
|
|
1473
|
+
const raw = fs11.readFileSync(fullPath, "utf8").replace(/\r\n/g, "\n");
|
|
1522
1474
|
const rendered = adapter.renderCommand(file, raw);
|
|
1523
1475
|
return { kind: "write", target: path17.join(commandsDir, rendered.file), content: rendered.content };
|
|
1524
1476
|
});
|
|
1525
1477
|
}
|
|
1526
1478
|
function listMarkdownFiles(dir) {
|
|
1527
|
-
if (!
|
|
1528
|
-
return
|
|
1479
|
+
if (!fs11.existsSync(dir)) return [];
|
|
1480
|
+
return fs11.readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".md")).map((entry) => ({ file: entry.name, fullPath: path17.join(dir, entry.name) }));
|
|
1529
1481
|
}
|
|
1530
1482
|
|
|
1531
1483
|
// src/components/hooks.ts
|
|
@@ -1540,22 +1492,22 @@ function planMcp(adapter, ctx) {
|
|
|
1540
1492
|
|
|
1541
1493
|
// src/components/plugins.ts
|
|
1542
1494
|
import path18 from "path";
|
|
1543
|
-
import
|
|
1495
|
+
import fs12 from "fs";
|
|
1544
1496
|
function planPlugins(adapter, ctx) {
|
|
1545
1497
|
const { pluginsDir } = adapter.paths(ctx.configDir);
|
|
1546
1498
|
if (pluginsDir === null) return [];
|
|
1547
1499
|
const source = path18.join(ctx.stackDir, "plugins", adapter.id);
|
|
1548
|
-
if (!
|
|
1500
|
+
if (!fs12.existsSync(source)) return [];
|
|
1549
1501
|
return listFilesRecursive(source).filter((f) => f.endsWith(".ts")).map((sourceFile) => {
|
|
1550
1502
|
const target = path18.join(pluginsDir, path18.relative(source, sourceFile));
|
|
1551
|
-
const raw =
|
|
1503
|
+
const raw = fs12.readFileSync(sourceFile, "utf8");
|
|
1552
1504
|
let content = raw;
|
|
1553
1505
|
if (content.includes('"{{ENGRAM_BIN}}"')) {
|
|
1554
1506
|
content = content.replace(/"\{\{ENGRAM_BIN\}\}"/g, JSON.stringify(ctx.engramBin ?? "engram"));
|
|
1555
1507
|
}
|
|
1556
1508
|
if (content.includes('"{{ENGRAM_PROTOCOL}}"')) {
|
|
1557
1509
|
const protocol = stripLeadingHtmlComments(
|
|
1558
|
-
|
|
1510
|
+
fs12.readFileSync(path18.join(ctx.stackDir, "system-prompt", "engram-protocol.md"), "utf8").replace(/\r\n/g, "\n")
|
|
1559
1511
|
);
|
|
1560
1512
|
content = content.replace(/"\{\{ENGRAM_PROTOCOL\}\}"/g, JSON.stringify(protocol));
|
|
1561
1513
|
}
|
|
@@ -1602,7 +1554,7 @@ function diffPlan(plan) {
|
|
|
1602
1554
|
if (current === null) return { action, status: "create" };
|
|
1603
1555
|
return { action, status: current === action.content ? "unchanged" : "update" };
|
|
1604
1556
|
}
|
|
1605
|
-
if (!
|
|
1557
|
+
if (!fs13.existsSync(action.target)) return { action, status: "create" };
|
|
1606
1558
|
return { action, status: sameFileContent(action.source, action.target) ? "unchanged" : "update" };
|
|
1607
1559
|
});
|
|
1608
1560
|
}
|
|
@@ -1734,7 +1686,7 @@ async function runInstall(opts) {
|
|
|
1734
1686
|
applyChanges(changes);
|
|
1735
1687
|
const pruneRoot = useManifest ? HOME : path19.dirname(configDir);
|
|
1736
1688
|
for (const orphan of orphans) {
|
|
1737
|
-
|
|
1689
|
+
fs13.rmSync(orphan, { force: true });
|
|
1738
1690
|
pruneEmptyDirs(orphan, pruneRoot);
|
|
1739
1691
|
}
|
|
1740
1692
|
const verifyCtx = { ...ctx, warnings: [] };
|
|
@@ -1757,7 +1709,7 @@ async function runInstall(opts) {
|
|
|
1757
1709
|
}
|
|
1758
1710
|
|
|
1759
1711
|
// src/uninstall.ts
|
|
1760
|
-
import
|
|
1712
|
+
import fs14 from "fs";
|
|
1761
1713
|
import path20 from "path";
|
|
1762
1714
|
import * as p2 from "@clack/prompts";
|
|
1763
1715
|
async function runUninstall(opts) {
|
|
@@ -1812,7 +1764,7 @@ async function runUninstall(opts) {
|
|
|
1812
1764
|
const pruneRoot = usingRealConfig ? HOME : path20.dirname(configDir);
|
|
1813
1765
|
const planTargets = [
|
|
1814
1766
|
.../* @__PURE__ */ new Set([...buildPlan(adapter, ctx).map((a) => path20.resolve(a.target)), ...prevOwned.map((t) => path20.resolve(t))])
|
|
1815
|
-
].filter((t) => !mergedTargets.has(t) &&
|
|
1767
|
+
].filter((t) => !mergedTargets.has(t) && fs14.existsSync(t));
|
|
1816
1768
|
const deleteTargets = planTargets.filter(
|
|
1817
1769
|
(t) => !retained.has(t) && !(ctx.preserveEngram && path20.basename(t) === "engram.ts") && isContainedIn(t, pruneRoot)
|
|
1818
1770
|
);
|
|
@@ -1822,18 +1774,18 @@ async function runUninstall(opts) {
|
|
|
1822
1774
|
if (sharedKept > 0) p2.log.info(`${sharedKept} archivos se conservan: otros runtimes instalados los siguen usando.`);
|
|
1823
1775
|
if (opts.dryRun) continue;
|
|
1824
1776
|
const backup = createBackup(
|
|
1825
|
-
[...deleteTargets, ...unmerge.map((a) => a.target).filter((t) =>
|
|
1777
|
+
[...deleteTargets, ...unmerge.map((a) => a.target).filter((t) => fs14.existsSync(t))],
|
|
1826
1778
|
`uninstall-${id}`
|
|
1827
1779
|
);
|
|
1828
1780
|
if (backup) p2.log.info(`Backup: ${backup.id} (${backup.files.length} archivos)`);
|
|
1829
1781
|
for (const target of deleteTargets) {
|
|
1830
|
-
|
|
1782
|
+
fs14.rmSync(target, { force: true });
|
|
1831
1783
|
pruneEmptyDirs(target, pruneRoot);
|
|
1832
1784
|
}
|
|
1833
1785
|
for (const action of unmerge) {
|
|
1834
1786
|
if (action.kind !== "write") continue;
|
|
1835
1787
|
if (action.content.trim() === "") {
|
|
1836
|
-
|
|
1788
|
+
fs14.rmSync(action.target, { force: true });
|
|
1837
1789
|
} else {
|
|
1838
1790
|
writeText(action.target, action.content);
|
|
1839
1791
|
}
|
|
@@ -1847,7 +1799,7 @@ async function runUninstall(opts) {
|
|
|
1847
1799
|
|
|
1848
1800
|
// src/doctor.ts
|
|
1849
1801
|
import path21 from "path";
|
|
1850
|
-
import
|
|
1802
|
+
import fs15 from "fs";
|
|
1851
1803
|
import * as p3 from "@clack/prompts";
|
|
1852
1804
|
function engramVersion(bin) {
|
|
1853
1805
|
const out = runDetectedBin(bin, ["--version"], 5e3);
|
|
@@ -1880,11 +1832,11 @@ async function runDoctor() {
|
|
|
1880
1832
|
}
|
|
1881
1833
|
const engramDataDir = process.env.ENGRAM_DATA_DIR ?? path21.join(HOME, ".engram");
|
|
1882
1834
|
const engramDb = path21.join(engramDataDir, "engram.db");
|
|
1883
|
-
if (
|
|
1884
|
-
const sizeMb = (
|
|
1835
|
+
if (fs15.existsSync(engramDb)) {
|
|
1836
|
+
const sizeMb = (fs15.statSync(engramDb).size / 1024 / 1024).toFixed(1);
|
|
1885
1837
|
p3.log.info(`Engram DB: ${engramDb} (${sizeMb} MB de memorias \u2014 el stack no la toca JAM\xC1S).`);
|
|
1886
1838
|
}
|
|
1887
|
-
if (!
|
|
1839
|
+
if (!fs15.existsSync(modelMapFile())) p3.log.info("model-map: a\xFAn no creado (se crea en el primer install o con 'models').");
|
|
1888
1840
|
const manifest = readManifest();
|
|
1889
1841
|
const modePreference = loadInstallModePreference();
|
|
1890
1842
|
const current = collectAllCurrentTargets(modePreference);
|
|
@@ -1923,10 +1875,10 @@ async function runDoctor() {
|
|
|
1923
1875
|
p3.log.warn(`${adapter.name}: ${orphans.length} archivos hu\xE9rfanos de versiones previas \u2192 ejecuta 'sync'.`);
|
|
1924
1876
|
problems++;
|
|
1925
1877
|
}
|
|
1926
|
-
if (adapter.id === "codex" &&
|
|
1878
|
+
if (adapter.id === "codex" && fs15.existsSync(path21.join(detection.configDir, "hooks.json"))) {
|
|
1927
1879
|
p3.log.info("Codex: recuerda que los hooks requieren aprobaci\xF3n manual \u2014 verifica con /hooks dentro de codex.");
|
|
1928
1880
|
}
|
|
1929
|
-
if (adapter.id === "codex" &&
|
|
1881
|
+
if (adapter.id === "codex" && fs15.existsSync(path21.join(detection.configDir, "AGENTS.override.md"))) {
|
|
1930
1882
|
p3.log.warn(
|
|
1931
1883
|
"Codex: existe ~/.codex/AGENTS.override.md \u2014 tiene prioridad ABSOLUTA y tapa el AGENTS.md gestionado por el stack."
|
|
1932
1884
|
);
|
|
@@ -1940,14 +1892,14 @@ async function runDoctor() {
|
|
|
1940
1892
|
}
|
|
1941
1893
|
|
|
1942
1894
|
// src/update.ts
|
|
1943
|
-
import
|
|
1895
|
+
import fs18 from "fs";
|
|
1944
1896
|
import path24 from "path";
|
|
1945
1897
|
import os3 from "os";
|
|
1946
1898
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
1947
1899
|
import * as p4 from "@clack/prompts";
|
|
1948
1900
|
|
|
1949
1901
|
// src/lib/github.ts
|
|
1950
|
-
import
|
|
1902
|
+
import fs16 from "fs";
|
|
1951
1903
|
import path22 from "path";
|
|
1952
1904
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
1953
1905
|
import os2 from "os";
|
|
@@ -2022,7 +1974,7 @@ function validateExtractedTree(destDir) {
|
|
|
2022
1974
|
const walk = (dir) => {
|
|
2023
1975
|
let entries;
|
|
2024
1976
|
try {
|
|
2025
|
-
entries =
|
|
1977
|
+
entries = fs16.readdirSync(dir, { withFileTypes: true });
|
|
2026
1978
|
} catch {
|
|
2027
1979
|
return false;
|
|
2028
1980
|
}
|
|
@@ -2030,7 +1982,7 @@ function validateExtractedTree(destDir) {
|
|
|
2030
1982
|
const full = path22.join(dir, entry.name);
|
|
2031
1983
|
let stat;
|
|
2032
1984
|
try {
|
|
2033
|
-
stat =
|
|
1985
|
+
stat = fs16.lstatSync(full);
|
|
2034
1986
|
} catch {
|
|
2035
1987
|
return false;
|
|
2036
1988
|
}
|
|
@@ -2047,14 +1999,14 @@ function validateExtractedTree(destDir) {
|
|
|
2047
1999
|
function resolveTarBin() {
|
|
2048
2000
|
if (process.platform !== "win32") return "tar";
|
|
2049
2001
|
const winTar = path22.join(process.env["SystemRoot"] ?? "C:\\Windows", "System32", "tar.exe");
|
|
2050
|
-
return
|
|
2002
|
+
return fs16.existsSync(winTar) ? winTar : "tar";
|
|
2051
2003
|
}
|
|
2052
2004
|
async function downloadRepoTarball(repo, sha, destDir, validateSubdir) {
|
|
2053
2005
|
const url = `https://codeload.github.com/${repo}/tar.gz/${sha}`;
|
|
2054
2006
|
const tmp = path22.join(os2.tmpdir(), `jorgex-tarball-${Date.now()}.tar.gz`);
|
|
2055
2007
|
const fail = (reason) => {
|
|
2056
2008
|
try {
|
|
2057
|
-
|
|
2009
|
+
fs16.rmSync(destDir, { recursive: true, force: true });
|
|
2058
2010
|
} catch {
|
|
2059
2011
|
}
|
|
2060
2012
|
return { ok: false, reason };
|
|
@@ -2073,10 +2025,10 @@ async function downloadRepoTarball(repo, sha, destDir, validateSubdir) {
|
|
|
2073
2025
|
if (!res.body) return fail("respuesta HTTP sin cuerpo");
|
|
2074
2026
|
await pipeline(
|
|
2075
2027
|
Readable.fromWeb(res.body),
|
|
2076
|
-
|
|
2028
|
+
fs16.createWriteStream(tmp)
|
|
2077
2029
|
);
|
|
2078
|
-
|
|
2079
|
-
|
|
2030
|
+
fs16.rmSync(destDir, { recursive: true, force: true });
|
|
2031
|
+
fs16.mkdirSync(destDir, { recursive: true });
|
|
2080
2032
|
try {
|
|
2081
2033
|
execFileSync2(resolveTarBin(), ["-xzf", tmp, "--strip-components=1", "-C", destDir], { stdio: "pipe" });
|
|
2082
2034
|
} catch (err) {
|
|
@@ -2089,30 +2041,30 @@ async function downloadRepoTarball(repo, sha, destDir, validateSubdir) {
|
|
|
2089
2041
|
if (validateRoot !== resolvedDest && !isContainedIn(validateRoot, resolvedDest)) {
|
|
2090
2042
|
return fail(`la ruta de validaci\xF3n "${validateSubdir}" escapa del destino`);
|
|
2091
2043
|
}
|
|
2092
|
-
if (
|
|
2044
|
+
if (fs16.existsSync(validateRoot) && !validateExtractedTree(validateRoot)) {
|
|
2093
2045
|
return fail("el \xE1rbol extra\xEDdo contiene symlinks o rutas fuera del destino");
|
|
2094
2046
|
}
|
|
2095
|
-
const validated =
|
|
2047
|
+
const validated = fs16.existsSync(validateRoot);
|
|
2096
2048
|
return { ok: true, validated };
|
|
2097
2049
|
} catch (err) {
|
|
2098
2050
|
const timedOut = err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError");
|
|
2099
2051
|
return fail(timedOut ? "timeout de descarga (120s)" : err instanceof Error ? `fallo de red: ${err.message}` : "error desconocido");
|
|
2100
2052
|
} finally {
|
|
2101
2053
|
try {
|
|
2102
|
-
|
|
2054
|
+
fs16.rmSync(tmp, { force: true });
|
|
2103
2055
|
} catch {
|
|
2104
2056
|
}
|
|
2105
2057
|
}
|
|
2106
2058
|
}
|
|
2107
2059
|
|
|
2108
2060
|
// src/lib/skill-update.ts
|
|
2109
|
-
import
|
|
2061
|
+
import fs17 from "fs";
|
|
2110
2062
|
import path23 from "path";
|
|
2111
2063
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
2112
2064
|
var PROTECTED_SKILLS = /* @__PURE__ */ new Set(["agent-delegation", "work-lifecycle", "xreview"]);
|
|
2113
2065
|
function sameTextContentNormalized(a, b) {
|
|
2114
|
-
const ba =
|
|
2115
|
-
const bb =
|
|
2066
|
+
const ba = fs17.readFileSync(a);
|
|
2067
|
+
const bb = fs17.readFileSync(b);
|
|
2116
2068
|
if (ba.equals(bb)) return true;
|
|
2117
2069
|
const sa = ba.toString("utf8").replace(/\r\n/g, "\n");
|
|
2118
2070
|
const sb = bb.toString("utf8").replace(/\r\n/g, "\n");
|
|
@@ -2194,7 +2146,7 @@ function replaceSkill(name, upstreamSkillDir, newCommit, opts) {
|
|
|
2194
2146
|
throw new Error(`La skill "${name}" es propia del stack y no se actualiza desde upstream.`);
|
|
2195
2147
|
}
|
|
2196
2148
|
const upstreamsFile = upstreamsFilePath ?? path23.join(path23.dirname(stackRoot()), "upstreams.json");
|
|
2197
|
-
const raw =
|
|
2149
|
+
const raw = fs17.readFileSync(upstreamsFile, "utf8");
|
|
2198
2150
|
const data = JSON.parse(raw);
|
|
2199
2151
|
const skillEntry = data?.skills?.[name];
|
|
2200
2152
|
if (!skillEntry) {
|
|
@@ -2213,7 +2165,7 @@ function replaceSkill(name, upstreamSkillDir, newCommit, opts) {
|
|
|
2213
2165
|
try {
|
|
2214
2166
|
const upstreamFiles = listFilesRecursive(upstreamSkillDir);
|
|
2215
2167
|
for (const src of upstreamFiles) {
|
|
2216
|
-
const st =
|
|
2168
|
+
const st = fs17.lstatSync(src);
|
|
2217
2169
|
if (st.isSymbolicLink()) {
|
|
2218
2170
|
throw new Error(`Symlink rechazado en upstream de skill "${name}": ${src}`);
|
|
2219
2171
|
}
|
|
@@ -2223,14 +2175,14 @@ function replaceSkill(name, upstreamSkillDir, newCommit, opts) {
|
|
|
2223
2175
|
copyFile(src, dest);
|
|
2224
2176
|
}
|
|
2225
2177
|
const oldDir = `${localSkillDir}.old-${process.pid}`;
|
|
2226
|
-
if (
|
|
2227
|
-
|
|
2178
|
+
if (fs17.existsSync(localSkillDir)) {
|
|
2179
|
+
fs17.renameSync(localSkillDir, oldDir);
|
|
2228
2180
|
}
|
|
2229
|
-
|
|
2230
|
-
|
|
2181
|
+
fs17.renameSync(stagingDir, localSkillDir);
|
|
2182
|
+
fs17.rmSync(oldDir, { recursive: true, force: true });
|
|
2231
2183
|
} catch (err) {
|
|
2232
2184
|
try {
|
|
2233
|
-
|
|
2185
|
+
fs17.rmSync(stagingDir, { recursive: true, force: true });
|
|
2234
2186
|
} catch {
|
|
2235
2187
|
}
|
|
2236
2188
|
throw err;
|
|
@@ -2245,7 +2197,7 @@ function rateLimitHint(prefix) {
|
|
|
2245
2197
|
}
|
|
2246
2198
|
function loadUpstreams() {
|
|
2247
2199
|
const file = path24.join(path24.dirname(stackRoot()), "upstreams.json");
|
|
2248
|
-
return JSON.parse(
|
|
2200
|
+
return JSON.parse(fs18.readFileSync(file, "utf8"));
|
|
2249
2201
|
}
|
|
2250
2202
|
function skillsToScan(maintainer, upstreams) {
|
|
2251
2203
|
return maintainer ? Object.keys(upstreams.skills) : [];
|
|
@@ -2358,7 +2310,7 @@ function isEngramRunning() {
|
|
|
2358
2310
|
}
|
|
2359
2311
|
}
|
|
2360
2312
|
function isGitClone(projectRoot = path24.dirname(stackRoot())) {
|
|
2361
|
-
return
|
|
2313
|
+
return fs18.existsSync(path24.join(projectRoot, ".git"));
|
|
2362
2314
|
}
|
|
2363
2315
|
var STACK_METHOD_CLONE = "git pull + pnpm install + pnpm build";
|
|
2364
2316
|
function resolvePnpm() {
|
|
@@ -2368,7 +2320,7 @@ function resolvePnpm() {
|
|
|
2368
2320
|
}
|
|
2369
2321
|
function cleanupTmp(dir) {
|
|
2370
2322
|
try {
|
|
2371
|
-
|
|
2323
|
+
fs18.rmSync(dir, { recursive: true, force: true });
|
|
2372
2324
|
} catch {
|
|
2373
2325
|
}
|
|
2374
2326
|
}
|
|
@@ -2390,7 +2342,7 @@ function updateStackGlobal() {
|
|
|
2390
2342
|
execFileSync4(pnpm, ["add", "-g", "jorgex-stack@latest"], { stdio: "inherit" });
|
|
2391
2343
|
}
|
|
2392
2344
|
async function downloadSkillToTemp(repo, sha, skillPath) {
|
|
2393
|
-
const root =
|
|
2345
|
+
const root = fs18.mkdtempSync(path24.join(os3.tmpdir(), "jorgex-skill-"));
|
|
2394
2346
|
try {
|
|
2395
2347
|
const result = await downloadRepoTarball(repo, sha, root, skillPath);
|
|
2396
2348
|
if (!result.ok) {
|
|
@@ -2403,10 +2355,10 @@ async function downloadSkillToTemp(repo, sha, skillPath) {
|
|
|
2403
2355
|
cleanupTmp(root);
|
|
2404
2356
|
return { error: `la ruta "${skillPath}" escapa del directorio temporal` };
|
|
2405
2357
|
}
|
|
2406
|
-
if (
|
|
2358
|
+
if (fs18.existsSync(sub)) return { dir: sub, root };
|
|
2407
2359
|
const lastSeg = skillPath.split("/").pop();
|
|
2408
2360
|
const sub2 = path24.resolve(path24.join(root, lastSeg));
|
|
2409
|
-
if (isContainedIn(sub2, root) &&
|
|
2361
|
+
if (isContainedIn(sub2, root) && fs18.existsSync(sub2)) {
|
|
2410
2362
|
if (!validateExtractedTree(sub2)) {
|
|
2411
2363
|
cleanupTmp(root);
|
|
2412
2364
|
return { error: `el sub\xE1rbol "${lastSeg}" contiene symlinks o rutas fuera del destino` };
|
|
@@ -2425,11 +2377,11 @@ async function downloadSkillToTemp(repo, sha, skillPath) {
|
|
|
2425
2377
|
function pruneEngramDbBackups() {
|
|
2426
2378
|
try {
|
|
2427
2379
|
const dir = dataDir();
|
|
2428
|
-
if (!
|
|
2429
|
-
const backups =
|
|
2380
|
+
if (!fs18.existsSync(dir)) return;
|
|
2381
|
+
const backups = fs18.readdirSync(dir).filter((f) => f.startsWith("engram-db-backup-") && f.endsWith(".db")).map((f) => ({ name: f, mtime: fs18.statSync(path24.join(dir, f)).mtimeMs })).sort((a, b) => b.mtime - a.mtime);
|
|
2430
2382
|
for (const old of backups.slice(3)) {
|
|
2431
2383
|
try {
|
|
2432
|
-
|
|
2384
|
+
fs18.rmSync(path24.join(dir, old.name));
|
|
2433
2385
|
} catch {
|
|
2434
2386
|
}
|
|
2435
2387
|
}
|
|
@@ -2437,7 +2389,7 @@ function pruneEngramDbBackups() {
|
|
|
2437
2389
|
}
|
|
2438
2390
|
}
|
|
2439
2391
|
function rotateLockedBinary(binPath, sweepRoot = HOME) {
|
|
2440
|
-
if (!
|
|
2392
|
+
if (!fs18.existsSync(binPath)) return null;
|
|
2441
2393
|
const dir = path24.dirname(binPath);
|
|
2442
2394
|
const base = path24.basename(binPath);
|
|
2443
2395
|
const escapedBase = base.replace(/[.*+?^$()|[\]{}\\]/g, "\\$&");
|
|
@@ -2445,10 +2397,10 @@ function rotateLockedBinary(binPath, sweepRoot = HOME) {
|
|
|
2445
2397
|
const resolvedDir = path24.resolve(dir);
|
|
2446
2398
|
if (resolvedDir === path24.resolve(sweepRoot) || isContainedIn(resolvedDir, sweepRoot)) {
|
|
2447
2399
|
try {
|
|
2448
|
-
for (const entry of
|
|
2400
|
+
for (const entry of fs18.readdirSync(dir)) {
|
|
2449
2401
|
if (oldPattern.test(entry)) {
|
|
2450
2402
|
try {
|
|
2451
|
-
|
|
2403
|
+
fs18.rmSync(path24.join(dir, entry), { force: true });
|
|
2452
2404
|
} catch {
|
|
2453
2405
|
}
|
|
2454
2406
|
}
|
|
@@ -2457,7 +2409,7 @@ function rotateLockedBinary(binPath, sweepRoot = HOME) {
|
|
|
2457
2409
|
}
|
|
2458
2410
|
}
|
|
2459
2411
|
const rotated = path24.join(dir, `${base}.old-${Date.now()}`);
|
|
2460
|
-
|
|
2412
|
+
fs18.renameSync(binPath, rotated);
|
|
2461
2413
|
return rotated;
|
|
2462
2414
|
}
|
|
2463
2415
|
async function updateEngram(engramRepo, latestVersion) {
|
|
@@ -2468,7 +2420,7 @@ async function updateEngram(engramRepo, latestVersion) {
|
|
|
2468
2420
|
}
|
|
2469
2421
|
const engramDataDir = process.env.ENGRAM_DATA_DIR ?? path24.join(HOME, ".engram");
|
|
2470
2422
|
const engramDb = path24.join(engramDataDir, "engram.db");
|
|
2471
|
-
if (
|
|
2423
|
+
if (fs18.existsSync(engramDb)) {
|
|
2472
2424
|
const doBackup = await p4.confirm({
|
|
2473
2425
|
message: `\xBFHacer backup de la DB de Engram antes de actualizar? (${engramDb})`,
|
|
2474
2426
|
initialValue: true
|
|
@@ -2477,8 +2429,8 @@ async function updateEngram(engramRepo, latestVersion) {
|
|
|
2477
2429
|
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
2478
2430
|
const dest = path24.join(dataDir(), `engram-db-backup-${ts}.db`);
|
|
2479
2431
|
try {
|
|
2480
|
-
if (!
|
|
2481
|
-
|
|
2432
|
+
if (!fs18.existsSync(dataDir())) fs18.mkdirSync(dataDir(), { recursive: true });
|
|
2433
|
+
fs18.copyFileSync(engramDb, dest);
|
|
2482
2434
|
p4.log.success(`DB respaldada en ${dest} (la DB original NO se modifica jam\xE1s).`);
|
|
2483
2435
|
pruneEngramDbBackups();
|
|
2484
2436
|
} catch (err) {
|
|
@@ -2528,10 +2480,10 @@ async function updateEngram(engramRepo, latestVersion) {
|
|
|
2528
2480
|
["install", `github.com/Gentleman-Programming/engram/cmd/engram@v${latestVersion}`],
|
|
2529
2481
|
{ stdio: "inherit" }
|
|
2530
2482
|
);
|
|
2531
|
-
const rollbackOk = resolveEngramRollback({ installOk: true, rotated, bin, binExists:
|
|
2483
|
+
const rollbackOk = resolveEngramRollback({ installOk: true, rotated, bin, binExists: fs18.existsSync(bin ?? "") });
|
|
2532
2484
|
if (rollbackOk.action === "restore") {
|
|
2533
2485
|
try {
|
|
2534
|
-
|
|
2486
|
+
fs18.renameSync(rotated, bin);
|
|
2535
2487
|
p4.log.warn(rollbackOk.messages.onRestore);
|
|
2536
2488
|
} catch {
|
|
2537
2489
|
p4.log.warn(rollbackOk.messages.onRenameFail);
|
|
@@ -2539,10 +2491,10 @@ async function updateEngram(engramRepo, latestVersion) {
|
|
|
2539
2491
|
}
|
|
2540
2492
|
return true;
|
|
2541
2493
|
} catch (err) {
|
|
2542
|
-
const rollbackFail = resolveEngramRollback({ installOk: false, rotated, bin, binExists:
|
|
2494
|
+
const rollbackFail = resolveEngramRollback({ installOk: false, rotated, bin, binExists: fs18.existsSync(bin ?? "") });
|
|
2543
2495
|
if (rollbackFail.action === "restore") {
|
|
2544
2496
|
try {
|
|
2545
|
-
|
|
2497
|
+
fs18.renameSync(rotated, bin);
|
|
2546
2498
|
p4.log.info(rollbackFail.messages.onRestore);
|
|
2547
2499
|
} catch {
|
|
2548
2500
|
p4.log.error(rollbackFail.messages.onRenameFail);
|
|
@@ -3063,7 +3015,7 @@ function cancelled() {
|
|
|
3063
3015
|
}
|
|
3064
3016
|
|
|
3065
3017
|
// src/lib/release.ts
|
|
3066
|
-
import
|
|
3018
|
+
import fs19 from "fs";
|
|
3067
3019
|
import path26 from "path";
|
|
3068
3020
|
import { execFileSync as execFileSync5 } from "child_process";
|
|
3069
3021
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
@@ -3071,7 +3023,7 @@ function findPackageJson() {
|
|
|
3071
3023
|
let dir = path26.dirname(fileURLToPath2(import.meta.url));
|
|
3072
3024
|
for (let i = 0; i < 6; i++) {
|
|
3073
3025
|
const candidate = path26.join(dir, "package.json");
|
|
3074
|
-
if (
|
|
3026
|
+
if (fs19.existsSync(candidate)) return candidate;
|
|
3075
3027
|
dir = path26.dirname(dir);
|
|
3076
3028
|
}
|
|
3077
3029
|
throw new Error("No se encontr\xF3 package.json cerca del CLI.");
|
|
@@ -3081,7 +3033,7 @@ function readPackageVersion() {
|
|
|
3081
3033
|
}
|
|
3082
3034
|
function readPackageMetadata() {
|
|
3083
3035
|
const packageJson = findPackageJson();
|
|
3084
|
-
const raw =
|
|
3036
|
+
const raw = fs19.readFileSync(packageJson, "utf8");
|
|
3085
3037
|
const parsed = JSON.parse(raw);
|
|
3086
3038
|
const name = typeof parsed.name === "string" ? parsed.name.trim() : "";
|
|
3087
3039
|
const version = typeof parsed.version === "string" ? parsed.version.trim() : "";
|
package/package.json
CHANGED
package/stack/agents/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Agentes canónicos
|
|
2
2
|
|
|
3
|
-
Una sola fuente por agente. El instalador los traduce al formato de cada runtime (PRD §6): Markdown+frontmatter para Claude Code y OpenCode, TOML para Codex.
|
|
3
|
+
Una sola fuente por agente. El instalador los traduce al formato de cada runtime (PRD §6): Markdown+frontmatter para Claude Code y OpenCode, TOML para Codex. El workflow completo del orchestrator vive únicamente en `skills/orchestrator/SKILL.md`; el agente primary es un wrapper corto que obliga a cargar esa skill.
|
|
4
4
|
|
|
5
5
|
## Frontmatter canónico
|
|
6
6
|
|
|
@@ -19,7 +19,7 @@ Una sola fuente por agente. El instalador los traduce al formato de cada runtime
|
|
|
19
19
|
| Canónico | Claude Code | Codex | OpenCode |
|
|
20
20
|
|---|---|---|---|
|
|
21
21
|
| `tier` | alias `fable`/`opus`/`sonnet`/`haiku` según model-map | `model` + `model_reasoning_effort` | `provider/model` del model-map |
|
|
22
|
-
| `readonly: true` | `tools: Read, Grep, Glob` (+Bash si aplica) | `sandbox_mode = "read-only"` | `
|
|
22
|
+
| `readonly: true` | `tools: Read, Grep, Glob` (+Bash si aplica) | `sandbox_mode = "read-only"` | `permission: { edit: deny }` |
|
|
23
23
|
| `bash: git-read` | `Bash` completo en tools — la restricción a git read es solo de prompt (el frontmatter de Claude Code no tiene esa granularidad) | con `readonly: true` el sandbox read-only impide escrituras; con `readonly: false` la restricción es solo de prompt | `permission.bash: { "git diff*": allow, "git log*": allow }` (única aplicación real) |
|
|
24
24
|
| `bash: none` | sin `Bash` en tools | sandbox read-only | `permission: { bash: deny }` |
|
|
25
25
|
| `spawn: false` | sin tool `Agent`/`Task` | n/a | `permission: { task: deny }` |
|