facult 2.13.9 → 2.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -8
- package/assets/packs/facult-operating-model/agents/evolution-planner/agent.toml +3 -0
- package/assets/packs/facult-operating-model/agents/integration-auditor/agent.toml +8 -1
- package/assets/packs/facult-operating-model/agents/scope-promoter/agent.toml +8 -1
- package/assets/packs/facult-operating-model/agents/writeback-curator/agent.toml +2 -0
- package/assets/packs/facult-operating-model/instructions/INTEGRATION.md +44 -0
- package/assets/packs/facult-operating-model/instructions/PROJECT_CAPABILITY.md +35 -0
- package/assets/packs/facult-operating-model/instructions/WORK_UNITS.md +48 -0
- package/assets/packs/facult-operating-model/skills/capability-evolution/SKILL.md +25 -6
- package/assets/packs/facult-operating-model/skills/project-operating-layer-design/SKILL.md +33 -0
- package/assets/packs/facult-operating-model/snippets/global/core/feedback-loops.md +1 -0
- package/assets/packs/facult-operating-model/snippets/global/core/work-units.md +2 -1
- package/assets/packs/facult-operating-model/{AGENTS.global.md → snippets/templates/agents-global.md} +6 -0
- package/docs/README.md +4 -0
- package/docs/assets/fclt-capability-loop.png +0 -0
- package/docs/built-in-pack.md +23 -2
- package/docs/codex-plugin.md +57 -0
- package/docs/concepts.md +4 -1
- package/docs/pack-upgrades.md +73 -0
- package/docs/reference.md +2 -2
- package/docs/roadmap.md +4 -3
- package/docs/work-units.md +96 -0
- package/package.json +5 -1
- package/plugins/fclt/.codex-plugin/plugin.json +31 -0
- package/plugins/fclt/.mcp.json +11 -0
- package/plugins/fclt/scripts/fclt-mcp.js +320 -0
- package/plugins/fclt/skills/fclt-capability-review/SKILL.md +51 -0
- package/plugins/fclt/skills/fclt-evolution/SKILL.md +65 -0
- package/plugins/fclt/skills/fclt-setup/SKILL.md +65 -0
- package/plugins/fclt/skills/fclt-writeback/SKILL.md +57 -0
- package/src/agents.ts +1 -0
- package/src/builtin-assets.ts +1 -1
- package/src/builtin.ts +22 -0
- package/src/doctor.ts +6 -2
- package/src/global-docs.ts +6 -2
- package/src/remote.ts +252 -10
package/src/remote.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
2
3
|
import { mkdir, readdir, readFile, rm } from "node:fs/promises";
|
|
3
4
|
import { homedir } from "node:os";
|
|
4
5
|
import {
|
|
@@ -10,7 +11,10 @@ import {
|
|
|
10
11
|
resolve,
|
|
11
12
|
} from "node:path";
|
|
12
13
|
import { isCancel, multiselect, select, text } from "@clack/prompts";
|
|
13
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
builtinOperatingModelInstallRelPath,
|
|
16
|
+
facultBuiltinPackRoot,
|
|
17
|
+
} from "./builtin";
|
|
14
18
|
import { parseCliContextArgs, resolveCliContextRoot } from "./cli-context";
|
|
15
19
|
import {
|
|
16
20
|
renderBullets,
|
|
@@ -21,7 +25,11 @@ import {
|
|
|
21
25
|
renderTable,
|
|
22
26
|
} from "./cli-ui";
|
|
23
27
|
import { buildIndex } from "./index-builder";
|
|
24
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
facultRootDir,
|
|
30
|
+
projectRootFromAiRoot,
|
|
31
|
+
readFacultConfig,
|
|
32
|
+
} from "./paths";
|
|
25
33
|
import {
|
|
26
34
|
assertManifestIntegrity,
|
|
27
35
|
assertManifestSignature,
|
|
@@ -130,6 +138,7 @@ interface InstallResult {
|
|
|
130
138
|
sourceTrustLevel: SourceTrustLevel;
|
|
131
139
|
dryRun: boolean;
|
|
132
140
|
changedPaths: string[];
|
|
141
|
+
skippedPaths?: string[];
|
|
133
142
|
}
|
|
134
143
|
|
|
135
144
|
interface UpdateCheckResult {
|
|
@@ -1298,41 +1307,250 @@ async function listFilesRecursive(rootDir: string): Promise<string[]> {
|
|
|
1298
1307
|
return out.sort();
|
|
1299
1308
|
}
|
|
1300
1309
|
|
|
1310
|
+
interface BuiltinPackManifest {
|
|
1311
|
+
version: 1;
|
|
1312
|
+
pack: string;
|
|
1313
|
+
updatedAt: string;
|
|
1314
|
+
files: Record<string, { sha256: string }>;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
function sha256Text(value: string): string {
|
|
1318
|
+
return createHash("sha256").update(value).digest("hex");
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
function builtinPackManifestPath(rootDir: string): string {
|
|
1322
|
+
return join(rootDir, ".facult", "packs", "facult-operating-model.json");
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
async function readBuiltinPackManifest(
|
|
1326
|
+
rootDir: string
|
|
1327
|
+
): Promise<BuiltinPackManifest | null> {
|
|
1328
|
+
const pathValue = builtinPackManifestPath(rootDir);
|
|
1329
|
+
if (!(await pathExists(pathValue))) {
|
|
1330
|
+
return null;
|
|
1331
|
+
}
|
|
1332
|
+
try {
|
|
1333
|
+
const parsed = JSON.parse(await Bun.file(pathValue).text());
|
|
1334
|
+
if (
|
|
1335
|
+
parsed?.version === 1 &&
|
|
1336
|
+
parsed.pack === "facult-operating-model" &&
|
|
1337
|
+
isPlainObject(parsed.files)
|
|
1338
|
+
) {
|
|
1339
|
+
return parsed as BuiltinPackManifest;
|
|
1340
|
+
}
|
|
1341
|
+
} catch {
|
|
1342
|
+
return null;
|
|
1343
|
+
}
|
|
1344
|
+
return null;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
function serializeBuiltinPackManifest(manifest: BuiltinPackManifest): string {
|
|
1348
|
+
return `${JSON.stringify(manifest, null, 2)}\n`;
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
const OPERATING_MODEL_SNIPPET_FRAME = `## Working mode
|
|
1352
|
+
|
|
1353
|
+
<!-- fclty:global/baseline -->
|
|
1354
|
+
<!-- /fclty:global/baseline -->
|
|
1355
|
+
|
|
1356
|
+
<!-- fclty:global/core/work-units -->
|
|
1357
|
+
<!-- /fclty:global/core/work-units -->
|
|
1358
|
+
|
|
1359
|
+
<!-- fclty:global/core/feedback-loops -->
|
|
1360
|
+
<!-- /fclty:global/core/feedback-loops -->
|
|
1361
|
+
|
|
1362
|
+
<!-- fclty:global/core/verification -->
|
|
1363
|
+
<!-- /fclty:global/core/verification -->
|
|
1364
|
+
|
|
1365
|
+
<!-- fclty:global/core/writeback -->
|
|
1366
|
+
<!-- /fclty:global/core/writeback -->
|
|
1367
|
+
|
|
1368
|
+
## Shared instruction sources
|
|
1369
|
+
|
|
1370
|
+
- For work-unit definition and scope clarification, read \${refs.work_units}.
|
|
1371
|
+
- For identifying, improving, and validating feedback loops, read \${refs.feedback_loops}.
|
|
1372
|
+
- For verification and anti-false-positive checks, read \${refs.verification}.
|
|
1373
|
+
- For checking integration boundaries, read \${refs.integration}.
|
|
1374
|
+
- For learning, decisions, and writeback, read \${refs.learning_writeback}.
|
|
1375
|
+
- For capability evolution, proposal kinds, and \`facult ai\` workflow, read \${refs.evolution}.
|
|
1376
|
+
- For deciding whether something belongs in global or project scope, read \${refs.project_capability}.
|
|
1377
|
+
- Add private language, coding, or writing refs in local config only when they belong to the user's own operating layer.
|
|
1378
|
+
`;
|
|
1379
|
+
|
|
1380
|
+
function appendOperatingModelFrame(seedText: string): string {
|
|
1381
|
+
const normalized = seedText.trimEnd();
|
|
1382
|
+
if (normalized.includes("<!-- fclty:global/baseline -->")) {
|
|
1383
|
+
return `${normalized}\n`;
|
|
1384
|
+
}
|
|
1385
|
+
return `${normalized}\n\n## Facult Operating Model\n\n${OPERATING_MODEL_SNIPPET_FRAME}`;
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
async function firstExistingFileText(
|
|
1389
|
+
candidates: string[]
|
|
1390
|
+
): Promise<string | null> {
|
|
1391
|
+
for (const candidate of candidates) {
|
|
1392
|
+
if (await pathExists(candidate)) {
|
|
1393
|
+
return await Bun.file(candidate).text();
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
return null;
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
async function seedAgentsGlobalText(args: {
|
|
1400
|
+
rootDir: string;
|
|
1401
|
+
homeDir?: string;
|
|
1402
|
+
fallbackText: string;
|
|
1403
|
+
}): Promise<{ text: string; seededFromExisting: boolean }> {
|
|
1404
|
+
const home = args.homeDir ?? homedir();
|
|
1405
|
+
const projectRoot = projectRootFromAiRoot(args.rootDir, home);
|
|
1406
|
+
const seedText = await firstExistingFileText(
|
|
1407
|
+
projectRoot
|
|
1408
|
+
? [
|
|
1409
|
+
join(projectRoot, "AGENTS.md"),
|
|
1410
|
+
join(projectRoot, "CLAUDE.md"),
|
|
1411
|
+
join(projectRoot, ".codex", "AGENTS.md"),
|
|
1412
|
+
join(projectRoot, ".claude", "CLAUDE.md"),
|
|
1413
|
+
]
|
|
1414
|
+
: [
|
|
1415
|
+
join(home, ".codex", "AGENTS.md"),
|
|
1416
|
+
join(home, ".claude", "CLAUDE.md"),
|
|
1417
|
+
join(home, ".cursor", "AGENTS.md"),
|
|
1418
|
+
]
|
|
1419
|
+
);
|
|
1420
|
+
if (!seedText?.trim()) {
|
|
1421
|
+
return { text: args.fallbackText, seededFromExisting: false };
|
|
1422
|
+
}
|
|
1423
|
+
return {
|
|
1424
|
+
text: appendOperatingModelFrame(seedText),
|
|
1425
|
+
seededFromExisting: true,
|
|
1426
|
+
};
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1301
1429
|
async function scaffoldBuiltinOperatingModelPack(args: {
|
|
1302
1430
|
rootDir: string;
|
|
1303
1431
|
homeDir?: string;
|
|
1304
1432
|
dryRun?: boolean;
|
|
1305
1433
|
force?: boolean;
|
|
1434
|
+
update?: boolean;
|
|
1306
1435
|
installedAs?: string;
|
|
1307
1436
|
}): Promise<InstallResult> {
|
|
1308
1437
|
const rootDir = resolve(args.rootDir);
|
|
1309
1438
|
const packRoot = facultBuiltinPackRoot("facult-operating-model");
|
|
1310
1439
|
const files = await listFilesRecursive(packRoot);
|
|
1311
1440
|
const changedPaths: string[] = [];
|
|
1441
|
+
const skippedPaths: string[] = [];
|
|
1442
|
+
const existingManifest = await readBuiltinPackManifest(rootDir);
|
|
1443
|
+
const manifestFiles: BuiltinPackManifest["files"] = {
|
|
1444
|
+
...(existingManifest?.files ?? {}),
|
|
1445
|
+
};
|
|
1312
1446
|
|
|
1313
1447
|
for (const sourcePath of files) {
|
|
1314
1448
|
const relPath = relative(packRoot, sourcePath);
|
|
1315
1449
|
if (!relPath || relPath.startsWith("..")) {
|
|
1316
1450
|
continue;
|
|
1317
1451
|
}
|
|
1318
|
-
const
|
|
1319
|
-
const
|
|
1320
|
-
|
|
1452
|
+
const targetRelPath = builtinOperatingModelInstallRelPath(relPath);
|
|
1453
|
+
const targetPath = join(rootDir, targetRelPath);
|
|
1454
|
+
const rawSourceText = await Bun.file(sourcePath).text();
|
|
1455
|
+
const targetExists = await pathExists(targetPath);
|
|
1456
|
+
const seed =
|
|
1457
|
+
targetRelPath === "AGENTS.global.md" && !targetExists
|
|
1458
|
+
? await seedAgentsGlobalText({
|
|
1459
|
+
rootDir,
|
|
1460
|
+
homeDir: args.homeDir,
|
|
1461
|
+
fallbackText: rawSourceText,
|
|
1462
|
+
})
|
|
1463
|
+
: null;
|
|
1464
|
+
const sourceText = seed?.text ?? rawSourceText;
|
|
1465
|
+
const trackInManifest = !seed?.seededFromExisting;
|
|
1466
|
+
const sourceHash = sha256Text(sourceText);
|
|
1467
|
+
let shouldWrite = !targetExists || Boolean(args.force);
|
|
1468
|
+
|
|
1469
|
+
if (targetExists && !shouldWrite) {
|
|
1470
|
+
const targetText = await Bun.file(targetPath).text();
|
|
1471
|
+
const targetHash = sha256Text(targetText);
|
|
1472
|
+
if (targetHash === sourceHash) {
|
|
1473
|
+
if (trackInManifest) {
|
|
1474
|
+
manifestFiles[targetRelPath] = { sha256: sourceHash };
|
|
1475
|
+
}
|
|
1476
|
+
} else if (
|
|
1477
|
+
args.update &&
|
|
1478
|
+
existingManifest?.files[targetRelPath]?.sha256 === targetHash
|
|
1479
|
+
) {
|
|
1480
|
+
shouldWrite = true;
|
|
1481
|
+
} else if (args.update) {
|
|
1482
|
+
skippedPaths.push(targetPath);
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
if (!shouldWrite) {
|
|
1321
1487
|
continue;
|
|
1322
1488
|
}
|
|
1323
1489
|
changedPaths.push(targetPath);
|
|
1490
|
+
if (trackInManifest) {
|
|
1491
|
+
manifestFiles[targetRelPath] = { sha256: sourceHash };
|
|
1492
|
+
} else {
|
|
1493
|
+
delete manifestFiles[targetRelPath];
|
|
1494
|
+
}
|
|
1324
1495
|
if (!args.dryRun) {
|
|
1325
1496
|
await mkdir(dirname(targetPath), { recursive: true });
|
|
1326
|
-
await Bun.write(targetPath,
|
|
1497
|
+
await Bun.write(targetPath, sourceText);
|
|
1327
1498
|
}
|
|
1328
1499
|
}
|
|
1329
1500
|
|
|
1330
1501
|
const configPath = join(rootDir, "config.toml");
|
|
1331
|
-
|
|
1502
|
+
const configRelPath = "config.toml";
|
|
1503
|
+
const configText = "version = 1\n";
|
|
1504
|
+
const configHash = sha256Text(configText);
|
|
1505
|
+
const configExists = await pathExists(configPath);
|
|
1506
|
+
let shouldWriteConfig = !configExists || Boolean(args.force);
|
|
1507
|
+
if (configExists && !shouldWriteConfig) {
|
|
1508
|
+
const targetText = await Bun.file(configPath).text();
|
|
1509
|
+
const targetHash = sha256Text(targetText);
|
|
1510
|
+
if (targetHash === configHash) {
|
|
1511
|
+
manifestFiles[configRelPath] = { sha256: configHash };
|
|
1512
|
+
} else if (
|
|
1513
|
+
args.update &&
|
|
1514
|
+
existingManifest?.files[configRelPath]?.sha256 === targetHash
|
|
1515
|
+
) {
|
|
1516
|
+
shouldWriteConfig = true;
|
|
1517
|
+
} else if (args.update) {
|
|
1518
|
+
skippedPaths.push(configPath);
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
if (shouldWriteConfig) {
|
|
1332
1522
|
changedPaths.push(configPath);
|
|
1523
|
+
manifestFiles[configRelPath] = { sha256: configHash };
|
|
1333
1524
|
if (!args.dryRun) {
|
|
1334
1525
|
await mkdir(dirname(configPath), { recursive: true });
|
|
1335
|
-
await Bun.write(configPath,
|
|
1526
|
+
await Bun.write(configPath, configText);
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
const manifestPath = builtinPackManifestPath(rootDir);
|
|
1531
|
+
const sortedManifestFiles = Object.fromEntries(
|
|
1532
|
+
Object.entries(manifestFiles).sort(([a], [b]) => a.localeCompare(b))
|
|
1533
|
+
);
|
|
1534
|
+
const stableManifest = serializeBuiltinPackManifest({
|
|
1535
|
+
version: 1,
|
|
1536
|
+
pack: "facult-operating-model",
|
|
1537
|
+
updatedAt: existingManifest?.updatedAt ?? "",
|
|
1538
|
+
files: sortedManifestFiles,
|
|
1539
|
+
});
|
|
1540
|
+
const existingManifestText = (await pathExists(manifestPath))
|
|
1541
|
+
? await Bun.file(manifestPath).text()
|
|
1542
|
+
: null;
|
|
1543
|
+
if (existingManifestText !== stableManifest) {
|
|
1544
|
+
const nextManifest = serializeBuiltinPackManifest({
|
|
1545
|
+
version: 1,
|
|
1546
|
+
pack: "facult-operating-model",
|
|
1547
|
+
updatedAt: new Date().toISOString(),
|
|
1548
|
+
files: sortedManifestFiles,
|
|
1549
|
+
});
|
|
1550
|
+
changedPaths.push(manifestPath);
|
|
1551
|
+
if (!args.dryRun) {
|
|
1552
|
+
await mkdir(dirname(manifestPath), { recursive: true });
|
|
1553
|
+
await Bun.write(manifestPath, nextManifest);
|
|
1336
1554
|
}
|
|
1337
1555
|
}
|
|
1338
1556
|
|
|
@@ -1352,6 +1570,7 @@ async function scaffoldBuiltinOperatingModelPack(args: {
|
|
|
1352
1570
|
sourceTrustLevel: "trusted",
|
|
1353
1571
|
dryRun: Boolean(args.dryRun),
|
|
1354
1572
|
changedPaths: uniqueSorted(changedPaths),
|
|
1573
|
+
skippedPaths: uniqueSorted(skippedPaths),
|
|
1355
1574
|
};
|
|
1356
1575
|
}
|
|
1357
1576
|
|
|
@@ -1360,6 +1579,7 @@ async function scaffoldBuiltinProjectAiPack(args: {
|
|
|
1360
1579
|
homeDir?: string;
|
|
1361
1580
|
dryRun?: boolean;
|
|
1362
1581
|
force?: boolean;
|
|
1582
|
+
update?: boolean;
|
|
1363
1583
|
}): Promise<InstallResult> {
|
|
1364
1584
|
const cwd = resolve(args.cwd ?? process.cwd());
|
|
1365
1585
|
return await scaffoldBuiltinOperatingModelPack({
|
|
@@ -1367,6 +1587,7 @@ async function scaffoldBuiltinProjectAiPack(args: {
|
|
|
1367
1587
|
homeDir: args.homeDir,
|
|
1368
1588
|
dryRun: args.dryRun,
|
|
1369
1589
|
force: args.force,
|
|
1590
|
+
update: args.update,
|
|
1370
1591
|
installedAs: "project-ai",
|
|
1371
1592
|
});
|
|
1372
1593
|
}
|
|
@@ -2815,9 +3036,11 @@ function printTemplatesHelp() {
|
|
|
2815
3036
|
),
|
|
2816
3037
|
renderCode("fclt templates init agents [--force] [--dry-run]"),
|
|
2817
3038
|
renderCode(
|
|
2818
|
-
"fclt templates init operating-model [--global|--project|--root PATH] [--force] [--dry-run]"
|
|
3039
|
+
"fclt templates init operating-model [--global|--project|--root PATH] [--update] [--force] [--dry-run]"
|
|
3040
|
+
),
|
|
3041
|
+
renderCode(
|
|
3042
|
+
"fclt templates init project-ai [--update] [--force] [--dry-run]"
|
|
2819
3043
|
),
|
|
2820
|
-
renderCode("fclt templates init project-ai [--force] [--dry-run]"),
|
|
2821
3044
|
renderCode(
|
|
2822
3045
|
"fclt templates init automation <template-id> [--scope global|project|wide] [--name <name>] [--project-root <path>] [--cwds <path1,path2>] [--rrule <RRULE>] [--status PAUSED|ACTIVE] [--yes] [--dry-run]"
|
|
2823
3046
|
),
|
|
@@ -3322,6 +3545,7 @@ export async function templatesCommand(
|
|
|
3322
3545
|
}
|
|
3323
3546
|
const dryRun = args.includes("--dry-run");
|
|
3324
3547
|
const force = args.includes("--force");
|
|
3548
|
+
const update = args.includes("--update");
|
|
3325
3549
|
const json = args.includes("--json");
|
|
3326
3550
|
const parsedArgs = parseTemplateInitArgs(args);
|
|
3327
3551
|
const positional = parsedArgs.positional;
|
|
@@ -3333,6 +3557,7 @@ export async function templatesCommand(
|
|
|
3333
3557
|
homeDir: ctx.homeDir,
|
|
3334
3558
|
dryRun,
|
|
3335
3559
|
force,
|
|
3560
|
+
update,
|
|
3336
3561
|
});
|
|
3337
3562
|
if (json) {
|
|
3338
3563
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -3348,6 +3573,14 @@ export async function templatesCommand(
|
|
|
3348
3573
|
title: "Changed Paths",
|
|
3349
3574
|
lines: renderBullets(result.changedPaths),
|
|
3350
3575
|
},
|
|
3576
|
+
...(result.skippedPaths?.length
|
|
3577
|
+
? [
|
|
3578
|
+
{
|
|
3579
|
+
title: "Skipped Local Edits",
|
|
3580
|
+
lines: renderBullets(result.skippedPaths),
|
|
3581
|
+
},
|
|
3582
|
+
]
|
|
3583
|
+
: []),
|
|
3351
3584
|
],
|
|
3352
3585
|
})
|
|
3353
3586
|
);
|
|
@@ -3378,6 +3611,7 @@ export async function templatesCommand(
|
|
|
3378
3611
|
homeDir: ctx.homeDir,
|
|
3379
3612
|
dryRun,
|
|
3380
3613
|
force,
|
|
3614
|
+
update,
|
|
3381
3615
|
});
|
|
3382
3616
|
if (json) {
|
|
3383
3617
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -3393,6 +3627,14 @@ export async function templatesCommand(
|
|
|
3393
3627
|
title: "Changed Paths",
|
|
3394
3628
|
lines: renderBullets(result.changedPaths),
|
|
3395
3629
|
},
|
|
3630
|
+
...(result.skippedPaths?.length
|
|
3631
|
+
? [
|
|
3632
|
+
{
|
|
3633
|
+
title: "Skipped Local Edits",
|
|
3634
|
+
lines: renderBullets(result.skippedPaths),
|
|
3635
|
+
},
|
|
3636
|
+
]
|
|
3637
|
+
: []),
|
|
3396
3638
|
],
|
|
3397
3639
|
})
|
|
3398
3640
|
);
|