@lmzhen/dsh-evolution-core 0.1.0-rc.61 → 0.1.0-rc.62

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/lib/index.js CHANGED
@@ -935,7 +935,13 @@ Quality bar:
935
935
  - Prefer verbatim flags, paths, and APIs from the source. Never invent them.
936
936
  - Keep it tight: ~100 lines simple, ~200 complex.
937
937
  - No router/index/hub skills that only point at other skills.
938
- - References go in \`references/\`, templates in \`templates/\`.`;
938
+ - References go in \`references/\`, templates in \`templates/\`.
939
+
940
+ Learn workflow (when the user asks you to learn a reusable skill, or you decide to turn a source/request into one):
941
+ 1. Gather every source named (files, URLs, "what we just did", pasted notes) with the tools you already have — and treat prose after a source as authoring requirements, not noise.
942
+ 2. Apply every requirement and constraint from the request to the SKILL.md you author.
943
+ 3. Author exactly ONE SKILL.md and save it with \`skill_manage\` (action=create); non-trivial scripts go under \`scripts/\`.
944
+ 4. When done, tell the user the skill name, its category, and a one-line summary of what it captured.`;
939
945
  //#endregion
940
946
  //#region lib/types/learn-prompt.js
941
947
  /**
@@ -1372,59 +1378,103 @@ var MemoryStore = class {
1372
1378
  };
1373
1379
  }
1374
1380
  async add(target, facts) {
1375
- const refusal = await this.oversizedRefusal(target);
1376
- if (refusal) return refusal;
1377
- if (await this.detectDrift(target)) {
1378
- const backup = await this.backupFile(target);
1379
- return {
1380
- ok: false,
1381
- message: `External drift detected in memory file.${backup ? ` A backup was saved to ${basename(backup)}.` : ""} Resolve the drift before retrying.`,
1382
- entries: [],
1383
- chars: 0,
1384
- limit: this.limitFor(target)
1385
- };
1386
- }
1387
- const content = facts.trim();
1388
- if (!content) return {
1381
+ if (!facts.trim()) return {
1389
1382
  ok: false,
1390
1383
  message: "Content cannot be empty.",
1391
1384
  entries: [],
1392
1385
  chars: 0,
1393
1386
  limit: this.limitFor(target)
1394
1387
  };
1388
+ const path = fileFor(this.root, target);
1389
+ let outcome;
1390
+ await transactIo(this.io, path, async (current) => {
1391
+ const core = await this.addCore(target, facts, current ?? "");
1392
+ outcome = core.result;
1393
+ return core.write ?? current ?? "";
1394
+ });
1395
+ return outcome;
1396
+ }
1397
+ /**
1398
+ * Single-entry add inside the transaction: shared checks (oversized,
1399
+ * drift, threat) and the content computation. `raw` is the locked view
1400
+ * (`current`) — never a second IO read. `write: null` means "no change".
1401
+ */
1402
+ async addCore(target, facts, raw) {
1403
+ const content = facts.trim();
1404
+ if (!content) return {
1405
+ result: this.failure(target, "Content cannot be empty.", []),
1406
+ write: null
1407
+ };
1408
+ const refusal = await this.oversizedRefusal(target);
1409
+ if (refusal) return {
1410
+ result: refusal,
1411
+ write: null
1412
+ };
1413
+ if (this.driftFromRaw(target, raw)) {
1414
+ const backup = await this.backupFile(target);
1415
+ return {
1416
+ result: {
1417
+ ok: false,
1418
+ message: `External drift detected in memory file.${backup ? ` A backup was saved to ${basename(backup)}.` : ""} Resolve the drift before retrying.`,
1419
+ entries: [],
1420
+ chars: 0,
1421
+ limit: this.limitFor(target)
1422
+ },
1423
+ write: null
1424
+ };
1425
+ }
1395
1426
  const threat = scanMemoryThreats(content);
1396
1427
  if (threat) return {
1397
- ok: false,
1398
- message: threat,
1399
- entries: [],
1400
- chars: 0,
1401
- limit: this.limitFor(target)
1428
+ result: {
1429
+ ok: false,
1430
+ message: threat,
1431
+ entries: [],
1432
+ chars: 0,
1433
+ limit: this.limitFor(target)
1434
+ },
1435
+ write: null
1402
1436
  };
1403
- const entries = await this.read(target);
1437
+ const entries = [...new Set(normalizeEntries(raw))];
1404
1438
  if (entries.some((entry) => stripDatePrefix(entry) === content)) {
1405
1439
  this.resetFailures();
1406
1440
  return {
1407
- ok: true,
1408
- message: `Entry already exists (no duplicate added).${this.storageHint(target, entries.join(ENTRY_DELIMITER).length)}`,
1409
- entries,
1410
- chars: entries.join(ENTRY_DELIMITER).length,
1411
- limit: this.limitFor(target)
1441
+ result: {
1442
+ ok: true,
1443
+ message: `Entry already exists (no duplicate added).${this.storageHint(target, entries.join(ENTRY_DELIMITER).length)}`,
1444
+ entries,
1445
+ chars: entries.join(ENTRY_DELIMITER).length,
1446
+ limit: this.limitFor(target)
1447
+ },
1448
+ write: null
1412
1449
  };
1413
1450
  }
1414
1451
  const next = [...entries, this.addDatePrefix ? `## ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}\n${content}` : content];
1415
1452
  const total = next.join(ENTRY_DELIMITER).length;
1416
1453
  const addLimit = this.limitFor(target);
1417
- if (addLimit > 0 && total > addLimit) return this.failure(target, `Adding this entry would exceed the ${addLimit} char limit. Consolidate or remove stale entries, then retry.`, entries);
1418
- await this.write(target, next);
1454
+ if (addLimit > 0 && total > addLimit) return {
1455
+ result: this.failure(target, `Adding this entry would exceed the ${addLimit} char limit. Consolidate or remove stale entries, then retry.`, entries),
1456
+ write: null
1457
+ };
1419
1458
  this.resetFailures();
1420
1459
  return {
1421
- ok: true,
1422
- message: `Entry added.${this.storageHint(target, total)}`,
1423
- entries: next,
1424
- chars: total,
1425
- limit: this.limitFor(target)
1460
+ result: {
1461
+ ok: true,
1462
+ message: `Entry added.${this.storageHint(target, total)}`,
1463
+ entries: next,
1464
+ chars: total,
1465
+ limit: this.limitFor(target)
1466
+ },
1467
+ write: render(next)
1426
1468
  };
1427
1469
  }
1470
+ /** Canonical-form drift check derived from the locked view (same formula as `detectDrift`, no second read). */
1471
+ driftFromRaw(target, raw) {
1472
+ if (raw.trim() === "") return false;
1473
+ const entries = normalizeEntries(raw);
1474
+ const limit = this.limitFor(target);
1475
+ if (limit > 0 && entries.some((entry) => entry.length > limit)) return true;
1476
+ return render(entries) !== raw;
1477
+ }
1428
1478
  async applyBatch(target, operations) {
1429
1479
  if (operations.length === 0) return {
1430
1480
  ok: false,
@@ -1433,95 +1483,138 @@ var MemoryStore = class {
1433
1483
  chars: 0,
1434
1484
  limit: this.limitFor(target)
1435
1485
  };
1486
+ const path = fileFor(this.root, target);
1487
+ let outcome;
1488
+ await transactIo(this.io, path, async (current) => {
1489
+ const core = await this.applyBatchCore(target, operations, current ?? "");
1490
+ outcome = core.result;
1491
+ return core.write ?? current ?? "";
1492
+ });
1493
+ return outcome;
1494
+ }
1495
+ /** Batch RMW inside the transaction. `write: null` = failure/no-op, disk untouched. */
1496
+ async applyBatchCore(target, operations, raw) {
1436
1497
  const refusal = await this.oversizedRefusal(target);
1437
- if (refusal) return refusal;
1438
- if (await this.detectDrift(target)) {
1498
+ if (refusal) return {
1499
+ result: refusal,
1500
+ write: null
1501
+ };
1502
+ if (this.driftFromRaw(target, raw)) {
1439
1503
  const backup = await this.backupFile(target);
1440
1504
  return {
1441
- ok: false,
1442
- message: `External drift detected in memory file.${backup ? ` A backup was saved to ${basename(backup)}.` : ""} Resolve the drift before retrying.`,
1443
- entries: [],
1444
- chars: 0,
1445
- limit: this.limitFor(target)
1505
+ result: {
1506
+ ok: false,
1507
+ message: `External drift detected in memory file.${backup ? ` A backup was saved to ${basename(backup)}.` : ""} Resolve the drift before retrying.`,
1508
+ entries: [],
1509
+ chars: 0,
1510
+ limit: this.limitFor(target)
1511
+ },
1512
+ write: null
1446
1513
  };
1447
1514
  }
1448
- const entries = await this.read(target);
1515
+ const entries = [...new Set(normalizeEntries(raw))];
1449
1516
  const working = [...entries];
1450
1517
  for (const [index, op] of operations.entries()) {
1451
1518
  const position = index + 1;
1452
1519
  if (op.action === "add") {
1453
1520
  const body = (op.facts ?? "").trim();
1454
1521
  if (!body) return {
1455
- ok: false,
1456
- message: `Operation ${position} (add): facts is required. No operations were applied.${previewEntries(entries)}`,
1457
- entries,
1458
- chars: entries.join(ENTRY_DELIMITER).length,
1459
- limit: this.limitFor(target)
1522
+ result: {
1523
+ ok: false,
1524
+ message: `Operation ${position} (add): facts is required. No operations were applied.${previewEntries(entries)}`,
1525
+ entries,
1526
+ chars: entries.join(ENTRY_DELIMITER).length,
1527
+ limit: this.limitFor(target)
1528
+ },
1529
+ write: null
1460
1530
  };
1461
1531
  const threat = scanMemoryThreats(body);
1462
1532
  if (threat) return {
1463
- ok: false,
1464
- message: `Operation ${position}: ${threat}`,
1465
- entries,
1466
- chars: entries.join(ENTRY_DELIMITER).length,
1467
- limit: this.limitFor(target)
1533
+ result: {
1534
+ ok: false,
1535
+ message: `Operation ${position}: ${threat}`,
1536
+ entries,
1537
+ chars: entries.join(ENTRY_DELIMITER).length,
1538
+ limit: this.limitFor(target)
1539
+ },
1540
+ write: null
1468
1541
  };
1469
1542
  if (!working.some((entry) => stripDatePrefix(entry) === body)) working.push(this.addDatePrefix ? `## ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}\n${body}` : body);
1470
1543
  continue;
1471
1544
  }
1472
1545
  const needle = (op.old_text ?? "").trim();
1473
1546
  if (!needle) return {
1474
- ok: false,
1475
- message: `Operation ${position} (${op.action}): old_text is required. No operations were applied.${previewEntries(entries)}`,
1476
- entries,
1477
- chars: entries.join(ENTRY_DELIMITER).length,
1478
- limit: this.limitFor(target)
1547
+ result: {
1548
+ ok: false,
1549
+ message: `Operation ${position} (${op.action}): old_text is required. No operations were applied.${previewEntries(entries)}`,
1550
+ entries,
1551
+ chars: entries.join(ENTRY_DELIMITER).length,
1552
+ limit: this.limitFor(target)
1553
+ },
1554
+ write: null
1479
1555
  };
1480
1556
  const matches = working.map((entry, matchIndex) => ({
1481
1557
  entry,
1482
1558
  matchIndex
1483
1559
  })).filter(({ entry }) => entry.includes(needle));
1484
- if (matches.length === 0) return this.failure(target, `Operation ${position}: no entry matching "${needle}" found. No operations were applied.`, entries);
1560
+ if (matches.length === 0) return {
1561
+ result: this.failure(target, `Operation ${position}: no entry matching "${needle}" found. No operations were applied.`, entries),
1562
+ write: null
1563
+ };
1485
1564
  if (new Set(matches.map((m) => m.entry)).size > 1) return {
1486
- ok: false,
1487
- message: `Operation ${position}: "${needle}" matched multiple distinct entries. No operations were applied.${previewEntries(entries)}`,
1488
- entries,
1489
- chars: entries.join(ENTRY_DELIMITER).length,
1490
- limit: this.limitFor(target)
1565
+ result: {
1566
+ ok: false,
1567
+ message: `Operation ${position}: "${needle}" matched multiple distinct entries. No operations were applied.${previewEntries(entries)}`,
1568
+ entries,
1569
+ chars: entries.join(ENTRY_DELIMITER).length,
1570
+ limit: this.limitFor(target)
1571
+ },
1572
+ write: null
1491
1573
  };
1492
1574
  const matchIndex = matches[0]?.matchIndex ?? -1;
1493
1575
  if (op.action === "remove") working.splice(matchIndex, 1);
1494
1576
  else {
1495
1577
  const body = (op.facts ?? "").trim();
1496
1578
  if (!body) return {
1497
- ok: false,
1498
- message: `Operation ${position} (replace): facts is required.${previewEntries(entries)}`,
1499
- entries,
1500
- chars: entries.join(ENTRY_DELIMITER).length,
1501
- limit: this.limitFor(target)
1579
+ result: {
1580
+ ok: false,
1581
+ message: `Operation ${position} (replace): facts is required.${previewEntries(entries)}`,
1582
+ entries,
1583
+ chars: entries.join(ENTRY_DELIMITER).length,
1584
+ limit: this.limitFor(target)
1585
+ },
1586
+ write: null
1502
1587
  };
1503
1588
  const threat = scanMemoryThreats(body);
1504
1589
  if (threat) return {
1505
- ok: false,
1506
- message: `Operation ${position}: ${threat}`,
1507
- entries,
1508
- chars: entries.join(ENTRY_DELIMITER).length,
1509
- limit: this.limitFor(target)
1590
+ result: {
1591
+ ok: false,
1592
+ message: `Operation ${position}: ${threat}`,
1593
+ entries,
1594
+ chars: entries.join(ENTRY_DELIMITER).length,
1595
+ limit: this.limitFor(target)
1596
+ },
1597
+ write: null
1510
1598
  };
1511
1599
  working[matchIndex] = body;
1512
1600
  }
1513
1601
  }
1514
1602
  const total = working.join(ENTRY_DELIMITER).length;
1515
1603
  const batchLimit = this.limitFor(target);
1516
- if (batchLimit > 0 && total > batchLimit) return this.failure(target, `Batch result (${total} chars) exceeds the ${batchLimit} limit. Remove or shorten more entries in the same batch.`, entries);
1517
- await this.write(target, working);
1604
+ if (batchLimit > 0 && total > batchLimit) return {
1605
+ result: this.failure(target, `Batch result (${total} chars) exceeds the ${batchLimit} limit. Remove or shorten more entries in the same batch.`, entries),
1606
+ write: null
1607
+ };
1518
1608
  this.resetFailures();
1519
1609
  return {
1520
- ok: true,
1521
- message: `Applied ${operations.length} operation(s).${this.storageHint(target, total)}`,
1522
- entries: working,
1523
- chars: total,
1524
- limit: this.limitFor(target)
1610
+ result: {
1611
+ ok: true,
1612
+ message: `Applied ${operations.length} operation(s).${this.storageHint(target, total)}`,
1613
+ entries: working,
1614
+ chars: total,
1615
+ limit: this.limitFor(target)
1616
+ },
1617
+ write: render(working)
1525
1618
  };
1526
1619
  }
1527
1620
  async renderContext() {
@@ -72,7 +72,17 @@ export declare class MemoryStore {
72
72
  */
73
73
  private oversizedRefusal;
74
74
  add(target: MemoryTarget, facts: string): Promise<MemoryApplyResult>;
75
+ /**
76
+ * Single-entry add inside the transaction: shared checks (oversized,
77
+ * drift, threat) and the content computation. `raw` is the locked view
78
+ * (`current`) — never a second IO read. `write: null` means "no change".
79
+ */
80
+ private addCore;
81
+ /** Canonical-form drift check derived from the locked view (same formula as `detectDrift`, no second read). */
82
+ private driftFromRaw;
75
83
  applyBatch(target: MemoryTarget, operations: MemoryOperation[]): Promise<MemoryApplyResult>;
84
+ /** Batch RMW inside the transaction. `write: null` = failure/no-op, disk untouched. */
85
+ private applyBatchCore;
76
86
  renderContext(): Promise<string>;
77
87
  /**
78
88
  * Detect on-disk drift: true when the file is not in the canonical
@@ -28,5 +28,5 @@ export interface PromptBundle {
28
28
  }
29
29
  export declare const PROMPT_BUNDLE: PromptBundle;
30
30
  export declare function verifyPromptBundle(bundle?: PromptBundle): boolean;
31
- export declare const DSH_AUTHORING_STANDARDS = "Follow the Hermes skill-authoring standards, translated to DSH tools.\n\nFrontmatter:\n- name: lowercase-hyphenated, <=64 chars, no spaces.\n- description: ONE sentence, <=60 characters, ends with a period. State the capability, not the implementation. No marketing words. Do NOT repeat the skill name. Count the characters before saving. If the description contains a colon, wrap the whole value in double quotes.\n- version: 0.1.0\n- author: always the literal value \"Hermes\". NEVER fill it from the environment, git config, or any identity you can probe \u2014 an environment-derived name is a privacy leak the user never opted into (skills get shared and published), and the skill names itself as Hermes.\n- platforms: declare [macos], [linux], and/or [windows] only when the skill is genuinely OS-bound (osascript/apt/systemctl => the matching OS; /proc, signal.SIGKILL => linux; fcntl/termios => POSIX). Prefer fixing it cross-platform first (tempdir, pathlib, pure-Node); omit the field for portable skills.\n- metadata.hermes.tags: a few Capitalized, Relevant, Tags.\n- metadata.hermes.related_skills: [a, b] \u2014 name sibling skills this one builds on or is referenced by (optional; feeds the quality references factor).\n\nBody section order (omit only when empty):\n1. \"# <Human Title>\" then a 2-3 sentence intro: what it does, what it does NOT do, key dependency stance.\n2. \"## When to Use\" \u2014 concrete trigger phrases.\n3. \"## Prerequisites\" \u2014 exact env vars, install steps, credentials.\n4. \"## How to Run\" \u2014 canonical invocation framed through DSH tools.\n5. \"## Quick Reference\" \u2014 flat command/endpoint list.\n6. \"## Procedure\" \u2014 numbered steps with copy-paste-exact commands.\n7. \"## Pitfalls\" \u2014 known limits and rate limits.\n8. \"## Verification\" \u2014 one check proving the skill worked.\n\nDSH-tool framing:\n- Reference DSH tools by name in backticks: `bash`, `str_replace_editor`, `write`, `skill`, `skill_manage`, `memory`.\n- Do not name wrapped shell utilities when a DSH tool already covers them.\n- Larger scripts belong under `scripts/` (written with `skill_manage write_file`) and are referenced from SKILL.md by relative path.\n\nQuality bar:\n- Prefer verbatim flags, paths, and APIs from the source. Never invent them.\n- Keep it tight: ~100 lines simple, ~200 complex.\n- No router/index/hub skills that only point at other skills.\n- References go in `references/`, templates in `templates/`.";
31
+ export declare const DSH_AUTHORING_STANDARDS = "Follow the Hermes skill-authoring standards, translated to DSH tools.\n\nFrontmatter:\n- name: lowercase-hyphenated, <=64 chars, no spaces.\n- description: ONE sentence, <=60 characters, ends with a period. State the capability, not the implementation. No marketing words. Do NOT repeat the skill name. Count the characters before saving. If the description contains a colon, wrap the whole value in double quotes.\n- version: 0.1.0\n- author: always the literal value \"Hermes\". NEVER fill it from the environment, git config, or any identity you can probe \u2014 an environment-derived name is a privacy leak the user never opted into (skills get shared and published), and the skill names itself as Hermes.\n- platforms: declare [macos], [linux], and/or [windows] only when the skill is genuinely OS-bound (osascript/apt/systemctl => the matching OS; /proc, signal.SIGKILL => linux; fcntl/termios => POSIX). Prefer fixing it cross-platform first (tempdir, pathlib, pure-Node); omit the field for portable skills.\n- metadata.hermes.tags: a few Capitalized, Relevant, Tags.\n- metadata.hermes.related_skills: [a, b] \u2014 name sibling skills this one builds on or is referenced by (optional; feeds the quality references factor).\n\nBody section order (omit only when empty):\n1. \"# <Human Title>\" then a 2-3 sentence intro: what it does, what it does NOT do, key dependency stance.\n2. \"## When to Use\" \u2014 concrete trigger phrases.\n3. \"## Prerequisites\" \u2014 exact env vars, install steps, credentials.\n4. \"## How to Run\" \u2014 canonical invocation framed through DSH tools.\n5. \"## Quick Reference\" \u2014 flat command/endpoint list.\n6. \"## Procedure\" \u2014 numbered steps with copy-paste-exact commands.\n7. \"## Pitfalls\" \u2014 known limits and rate limits.\n8. \"## Verification\" \u2014 one check proving the skill worked.\n\nDSH-tool framing:\n- Reference DSH tools by name in backticks: `bash`, `str_replace_editor`, `write`, `skill`, `skill_manage`, `memory`.\n- Do not name wrapped shell utilities when a DSH tool already covers them.\n- Larger scripts belong under `scripts/` (written with `skill_manage write_file`) and are referenced from SKILL.md by relative path.\n\nQuality bar:\n- Prefer verbatim flags, paths, and APIs from the source. Never invent them.\n- Keep it tight: ~100 lines simple, ~200 complex.\n- No router/index/hub skills that only point at other skills.\n- References go in `references/`, templates in `templates/`.\n\nLearn workflow (when the user asks you to learn a reusable skill, or you decide to turn a source/request into one):\n1. Gather every source named (files, URLs, \"what we just did\", pasted notes) with the tools you already have \u2014 and treat prose after a source as authoring requirements, not noise.\n2. Apply every requirement and constraint from the request to the SKILL.md you author.\n3. Author exactly ONE SKILL.md and save it with `skill_manage` (action=create); non-trivial scripts go under `scripts/`.\n4. When done, tell the user the skill name, its category, and a one-line summary of what it captured.";
32
32
  //# sourceMappingURL=prompts.d.ts.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lmzhen/dsh-evolution-core",
3
3
  "description": "Shared stores, prompts, signals and lifecycle logic for the dsh-evolution plugin family (community build)",
4
- "version": "0.1.0-rc.61",
4
+ "version": "0.1.0-rc.62",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },