@lmzhen/dsh-evolution-core 0.30.36 → 0.30.38
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 +3776 -2300
- package/lib/types/evolution-events.d.ts +6 -2
- package/lib/types/evolution-events.js +6 -0
- package/lib/types/extract-plan.js +17 -11
- package/lib/types/extract-prompt.d.ts +1 -5
- package/lib/types/extract-prompt.js +2 -0
- package/lib/types/extract-request.d.ts +32 -0
- package/lib/types/extract-request.js +155 -0
- package/lib/types/index.d.ts +4 -0
- package/lib/types/index.js +4 -0
- package/lib/types/mutation-storage-adapter.d.ts +14 -0
- package/lib/types/mutation-storage-adapter.js +107 -0
- package/lib/types/mutation-unit.d.ts +164 -0
- package/lib/types/mutation-unit.js +509 -0
- package/lib/types/prompts.d.ts +3 -3
- package/lib/types/prompts.js +8 -8
- package/lib/types/proposal-view.d.ts +4 -0
- package/lib/types/proposal-view.js +3 -1
- package/lib/types/skill-health.d.ts +5 -0
- package/lib/types/skill-health.js +3 -0
- package/lib/types/skill-history.d.ts +2 -0
- package/lib/types/skill-history.js +3 -0
- package/lib/types/skill-lint.d.ts +2 -0
- package/lib/types/skill-lint.js +13 -2
- package/lib/types/skill-model.d.ts +45 -0
- package/lib/types/skill-model.js +74 -0
- package/lib/types/skill-store.d.ts +22 -1
- package/lib/types/skill-store.js +415 -274
- package/lib/types/unit-members.js +3 -3
- package/lib/types/unit-storage.d.ts +10 -0
- package/lib/types/unit-transaction.d.ts +1 -1
- package/lib/types/unit-undo-plan.js +5 -3
- package/lib/types/unit-undo.d.ts +1 -1
- package/lib/types/unit-undo.js +18 -3
- package/package.json +1 -1
- package/persisted-write-inventory.json +3 -3
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
import { basename, dirname, isAbsolute, join, relative } from 'node:path';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { isCommittedWarning, isProcessAlive } from "./io.js";
|
|
4
|
+
function underRoot(rootDir, target) {
|
|
5
|
+
if (rootDir === undefined || rootDir === '')
|
|
6
|
+
return false;
|
|
7
|
+
const rel = relative(rootDir, target);
|
|
8
|
+
return rel !== '' && !rel.startsWith('..') && !isAbsolute(rel);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Pure preflight for one mutation request. It deliberately does no IO: the
|
|
12
|
+
* caller must resolve roots and read bodies before this point; the unit kernel
|
|
13
|
+
* performs CAS/protection/journal after validation.
|
|
14
|
+
*/
|
|
15
|
+
export function validateMutationRequest(request, limits) {
|
|
16
|
+
const errors = [];
|
|
17
|
+
const roots = [];
|
|
18
|
+
for (const root of request.roots) {
|
|
19
|
+
const name = root.name.trim();
|
|
20
|
+
if (name === '') {
|
|
21
|
+
errors.push({ code: 'root-empty', message: 'mutation root name is empty' });
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (roots.some(entry => entry.name === name)) {
|
|
25
|
+
errors.push({ code: 'root-duplicate', message: 'duplicate mutation root ' + name });
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
roots.push({ name, protection: root.protection });
|
|
29
|
+
}
|
|
30
|
+
const primary = request.name.trim();
|
|
31
|
+
if (request.rootDirs !== undefined) {
|
|
32
|
+
for (const root of roots) {
|
|
33
|
+
if (request.rootDirs[root.name] === undefined) {
|
|
34
|
+
errors.push({ code: 'root-dir-missing', message: 'no directory for mutation root ' + root.name });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (!roots.some(root => root.name === primary)) {
|
|
39
|
+
errors.push({ code: 'root-primary-missing', message: 'primary skill ' + primary + ' is not in roots' });
|
|
40
|
+
}
|
|
41
|
+
if (request.ops.length === 0) {
|
|
42
|
+
errors.push({ code: 'ops-empty', message: 'mutation has no operations' });
|
|
43
|
+
}
|
|
44
|
+
const seen = new Set();
|
|
45
|
+
const ops = [];
|
|
46
|
+
for (const op of request.ops) {
|
|
47
|
+
const target = op.target.trim();
|
|
48
|
+
if (target === '') {
|
|
49
|
+
errors.push({ code: 'target-empty', message: 'operation target is empty' });
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (seen.has(target)) {
|
|
53
|
+
errors.push({ code: 'target-duplicate', message: 'duplicate operation target ' + target, target });
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
seen.add(target);
|
|
57
|
+
const skill = op.skill?.trim();
|
|
58
|
+
if (skill !== undefined && !roots.some(root => root.name === skill)) {
|
|
59
|
+
errors.push({ code: 'op-skill-unknown', message: 'operation skill ' + skill + ' is not in roots', target });
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (request.rootDirs !== undefined) {
|
|
63
|
+
const owner = skill === undefined
|
|
64
|
+
? roots.find(root => underRoot(request.rootDirs?.[root.name], target))
|
|
65
|
+
: underRoot(request.rootDirs[skill], target) ? { name: skill } : undefined;
|
|
66
|
+
if (owner === undefined) {
|
|
67
|
+
errors.push({ code: 'target-outside-root', message: 'operation target is outside its declared root', target });
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (op.kind === 'write') {
|
|
72
|
+
const bytes = Buffer.byteLength(op.content, 'utf8');
|
|
73
|
+
if (bytes > limits.maxSkillFileBytes) {
|
|
74
|
+
errors.push({ code: 'file-too-large', message: 'write exceeds ' + limits.maxSkillFileBytes + ' bytes', target });
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (basename(target) === 'SKILL.md' && op.content.length > limits.maxSkillContentChars) {
|
|
78
|
+
errors.push({ code: 'skill-too-large', message: 'SKILL.md exceeds ' + limits.maxSkillContentChars + ' characters', target });
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
ops.push({ ...op, target });
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
ops.push({ ...op, target });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return { ok: errors.length === 0, roots, ops, errors };
|
|
88
|
+
}
|
|
89
|
+
const noLock = {
|
|
90
|
+
async acquire() {
|
|
91
|
+
return { async release() { } };
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
const identitySerial = async (task) => await task();
|
|
95
|
+
export class MutationUnit {
|
|
96
|
+
io;
|
|
97
|
+
limits;
|
|
98
|
+
serial;
|
|
99
|
+
lock;
|
|
100
|
+
journal;
|
|
101
|
+
snapshots;
|
|
102
|
+
hooks;
|
|
103
|
+
validateWrite;
|
|
104
|
+
transact;
|
|
105
|
+
transactGuard;
|
|
106
|
+
now;
|
|
107
|
+
constructor(options) {
|
|
108
|
+
this.io = options.io;
|
|
109
|
+
this.limits = options.limits;
|
|
110
|
+
this.serial = options.serial ?? identitySerial;
|
|
111
|
+
this.lock = options.lock ?? noLock;
|
|
112
|
+
this.journal = options.journal;
|
|
113
|
+
this.snapshots = options.snapshots;
|
|
114
|
+
this.hooks = options.hooks;
|
|
115
|
+
this.validateWrite = options.validateWrite;
|
|
116
|
+
this.transact = options.transact;
|
|
117
|
+
this.transactGuard = options.transactGuard ?? true;
|
|
118
|
+
this.now = options.now ?? Date.now;
|
|
119
|
+
}
|
|
120
|
+
preview(request) {
|
|
121
|
+
const validation = validateMutationRequest(request, this.limits);
|
|
122
|
+
return {
|
|
123
|
+
ok: validation.ok,
|
|
124
|
+
unitId: request.unitId ?? 'preview-' + String(this.now()),
|
|
125
|
+
roots: validation.roots,
|
|
126
|
+
ops: validation.ops,
|
|
127
|
+
errors: validation.errors,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
async apply(request) {
|
|
131
|
+
const primary = request.name.trim();
|
|
132
|
+
const unitId = request.unitId ?? 'u-' + String(this.now()) + '-' + Math.random().toString(36).slice(2);
|
|
133
|
+
const validation = validateMutationRequest(request, this.limits);
|
|
134
|
+
if (!validation.ok)
|
|
135
|
+
return { ok: false, unitId, dryRun: request.dryRun === true, changed: [], errors: validation.errors };
|
|
136
|
+
if (request.dryRun === true)
|
|
137
|
+
return { ok: true, unitId, dryRun: true, changed: [], errors: [] };
|
|
138
|
+
return await this.serial(async () => {
|
|
139
|
+
let lease;
|
|
140
|
+
try {
|
|
141
|
+
lease = await this.lock.acquire();
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
return { ok: false, unitId, dryRun: false, changed: [], errors: [{ code: 'lock-failed', message: error instanceof Error ? error.message : String(error) }] };
|
|
145
|
+
}
|
|
146
|
+
const changed = [];
|
|
147
|
+
const warnings = [];
|
|
148
|
+
const bestEffort = async (task) => {
|
|
149
|
+
try {
|
|
150
|
+
await task();
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
if (!isCommittedWarning(error))
|
|
154
|
+
throw error;
|
|
155
|
+
warnings.push(error instanceof Error ? error.message : String(error));
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
const snapshots = [];
|
|
159
|
+
try {
|
|
160
|
+
for (const op of validation.ops) {
|
|
161
|
+
const current = await this.io.readText(op.target);
|
|
162
|
+
const baseline = op.expected === undefined ? current : op.expected;
|
|
163
|
+
if (current !== baseline) {
|
|
164
|
+
return { ok: false, unitId, dryRun: false, changed, errors: [{ code: 'cas-drift', message: 'target changed after plan', target: op.target }] };
|
|
165
|
+
}
|
|
166
|
+
snapshots.push({ target: op.target, before: current, baseline });
|
|
167
|
+
}
|
|
168
|
+
for (const op of validation.ops) {
|
|
169
|
+
const refusal = this.validateWrite?.(op) ?? null;
|
|
170
|
+
if (refusal !== null)
|
|
171
|
+
return { ok: false, unitId, dryRun: false, changed: [], errors: [{ code: 'write-validation', message: refusal, target: op.target }] };
|
|
172
|
+
}
|
|
173
|
+
const eventOps = snapshots.map(snapshot => {
|
|
174
|
+
const op = validation.ops.find(entry => entry.target === snapshot.target);
|
|
175
|
+
return {
|
|
176
|
+
target: snapshot.target,
|
|
177
|
+
before: snapshot.before,
|
|
178
|
+
after: op?.kind === 'write' ? op.content : null,
|
|
179
|
+
...(op?.skill === undefined ? {} : { skill: op.skill }),
|
|
180
|
+
};
|
|
181
|
+
});
|
|
182
|
+
const cleanup = request.cleanupPaths === undefined ? {} : { cleanup: request.cleanupPaths };
|
|
183
|
+
if (this.snapshots !== undefined)
|
|
184
|
+
await bestEffort(() => this.snapshots.write({ unitId, state: 'prepared', ops: eventOps, ...cleanup, createdAt: this.now() }));
|
|
185
|
+
await bestEffort(async () => this.journal?.write({ unitId, phase: 'prepared', changed: [], target: validation.roots.find(root => root.name === primary)?.name ?? primary, roots: validation.roots.map(root => root.name) }));
|
|
186
|
+
for (const op of validation.ops) {
|
|
187
|
+
const snapshot = snapshots.find(entry => entry.target === op.target);
|
|
188
|
+
if (snapshot === undefined)
|
|
189
|
+
throw new Error('snapshot missing for ' + op.target);
|
|
190
|
+
try {
|
|
191
|
+
await this.applyOp(op, snapshot.baseline);
|
|
192
|
+
changed.push(op.target);
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
if (error?.noWrite !== true)
|
|
196
|
+
changed.push(op.target);
|
|
197
|
+
if (!isCommittedWarning(error))
|
|
198
|
+
throw error;
|
|
199
|
+
warnings.push(error instanceof Error ? error.message : String(error));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
await bestEffort(async () => this.journal?.write({ unitId, phase: 'unit-written', changed, target: primary, roots: validation.roots.map(root => root.name) }));
|
|
203
|
+
await bestEffort(async () => this.journal?.write({ unitId, phase: 'committed', changed, target: primary, roots: validation.roots.map(root => root.name) }));
|
|
204
|
+
if (this.snapshots !== undefined)
|
|
205
|
+
await bestEffort(() => this.snapshots.write({ unitId, state: 'committed', ops: eventOps, ...cleanup, createdAt: this.now() }));
|
|
206
|
+
try {
|
|
207
|
+
await this.hooks?.committed?.({ unitId, changed, ops: eventOps, roots: validation.roots.map(root => root.name) });
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
// Hooks are audit/history/event side effects; a completed write must not be rolled back by one.
|
|
211
|
+
}
|
|
212
|
+
return { ok: true, unitId, dryRun: false, changed, errors: [], ...(warnings.length === 0 ? {} : { warnings }) };
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
216
|
+
let rollbackFailure = '';
|
|
217
|
+
try {
|
|
218
|
+
await this.rollback(snapshots.filter(snapshot => changed.includes(snapshot.target)));
|
|
219
|
+
}
|
|
220
|
+
catch (rollbackError) {
|
|
221
|
+
rollbackFailure = rollbackError instanceof Error ? rollbackError.message : String(rollbackError);
|
|
222
|
+
}
|
|
223
|
+
const detail = rollbackFailure === '' ? message : message + '; rollback failed: ' + rollbackFailure;
|
|
224
|
+
await this.journal?.write({ unitId, phase: 'failed', changed, error: detail });
|
|
225
|
+
try {
|
|
226
|
+
await this.hooks?.rolledBack?.({
|
|
227
|
+
unitId,
|
|
228
|
+
changed,
|
|
229
|
+
ops: snapshots.map(snapshot => {
|
|
230
|
+
const op = validation.ops.find(entry => entry.target === snapshot.target);
|
|
231
|
+
return {
|
|
232
|
+
target: snapshot.target,
|
|
233
|
+
before: snapshot.before,
|
|
234
|
+
after: op?.kind === 'write' ? op.content : null,
|
|
235
|
+
...(op?.skill === undefined ? {} : { skill: op.skill }),
|
|
236
|
+
};
|
|
237
|
+
}),
|
|
238
|
+
roots: validation.roots.map(root => root.name),
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
// best-effort hook; the structured write error remains authoritative
|
|
243
|
+
}
|
|
244
|
+
return { ok: false, unitId, dryRun: false, changed, errors: [{ code: 'write-failed', message: detail }] };
|
|
245
|
+
}
|
|
246
|
+
finally {
|
|
247
|
+
await lease.release();
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
/** Recover every prepared snapshot known to the port; safe to call at boot. */
|
|
252
|
+
async recoverPrepared() {
|
|
253
|
+
const prepared = await this.snapshots?.listPrepared?.() ?? [];
|
|
254
|
+
const results = [];
|
|
255
|
+
for (const snapshot of prepared)
|
|
256
|
+
results.push(await this.recover(snapshot.unitId));
|
|
257
|
+
return results;
|
|
258
|
+
}
|
|
259
|
+
/** Converge a prepared unit left by a crash. Every path must be at its after-bytes; a drift refuses the whole recovery. */
|
|
260
|
+
async recover(unitId) {
|
|
261
|
+
const snapshot = await this.snapshots?.read(unitId);
|
|
262
|
+
if (snapshot === undefined || snapshot === null) {
|
|
263
|
+
return { ok: false, unitId, dryRun: false, changed: [], errors: [{ code: 'recover-missing-snapshot', message: 'no snapshot for ' + unitId }] };
|
|
264
|
+
}
|
|
265
|
+
if (snapshot.state !== 'prepared') {
|
|
266
|
+
return { ok: false, unitId, dryRun: false, changed: [], errors: [{ code: 'recover-not-needed', message: 'unit is not prepared: ' + unitId }] };
|
|
267
|
+
}
|
|
268
|
+
return await this.serial(async () => {
|
|
269
|
+
let lease;
|
|
270
|
+
try {
|
|
271
|
+
lease = await this.lock.acquire();
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
return { ok: false, unitId, dryRun: false, changed: [], errors: [{ code: 'lock-failed', message: error instanceof Error ? error.message : String(error) }] };
|
|
275
|
+
}
|
|
276
|
+
const reverted = [];
|
|
277
|
+
try {
|
|
278
|
+
for (const op of [...snapshot.ops].reverse()) {
|
|
279
|
+
const current = await this.io.readText(op.target);
|
|
280
|
+
if (current === op.before)
|
|
281
|
+
continue;
|
|
282
|
+
if (current !== op.after) {
|
|
283
|
+
return { ok: false, unitId, dryRun: false, changed: [], errors: [{ code: 'recover-drift', message: 'target is neither before nor after: ' + op.target }] };
|
|
284
|
+
}
|
|
285
|
+
const inverse = op.before === null
|
|
286
|
+
? { kind: 'delete', target: op.target, ...(op.skill === undefined ? {} : { skill: op.skill }) }
|
|
287
|
+
: { kind: 'write', target: op.target, content: op.before, ...(op.skill === undefined ? {} : { skill: op.skill }) };
|
|
288
|
+
await this.applyOp(inverse, op.after);
|
|
289
|
+
reverted.push(op.target);
|
|
290
|
+
}
|
|
291
|
+
for (const path of snapshot.cleanup ?? [])
|
|
292
|
+
await this.io.remove(path).catch(() => { });
|
|
293
|
+
await this.snapshots?.write({ ...snapshot, state: 'undone' });
|
|
294
|
+
await this.journal?.write({ unitId, phase: 'undone', changed: reverted, ...(snapshot.ops[0]?.target === undefined ? {} : { target: snapshot.ops[0].target }), roots: [...new Set(snapshot.ops.map(op => op.skill).filter((skill) => skill !== undefined))] });
|
|
295
|
+
try {
|
|
296
|
+
await this.hooks?.undone?.({ unitId, changed: reverted, ops: snapshot.ops, roots: [...new Set(snapshot.ops.map(op => op.skill).filter((skill) => skill !== undefined))] });
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
// best-effort hook
|
|
300
|
+
}
|
|
301
|
+
return { ok: true, unitId, dryRun: false, changed: reverted, errors: [] };
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
return { ok: false, unitId, dryRun: false, changed: reverted, errors: [{ code: 'recover-failed', message: error instanceof Error ? error.message : String(error) }] };
|
|
305
|
+
}
|
|
306
|
+
finally {
|
|
307
|
+
await lease.release();
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
async undo(unitId) {
|
|
312
|
+
const snapshot = await this.snapshots?.read(unitId);
|
|
313
|
+
if (snapshot === undefined || snapshot === null) {
|
|
314
|
+
return { ok: false, unitId, dryRun: false, changed: [], errors: [{ code: 'undo-missing-snapshot', message: 'no committed snapshot for ' + unitId }] };
|
|
315
|
+
}
|
|
316
|
+
if (snapshot.state === 'prepared') {
|
|
317
|
+
return { ok: false, unitId, dryRun: false, changed: [], errors: [{ code: 'undo-needs-recovery', message: 'unit has a prepared snapshot; run recover first: ' + unitId }] };
|
|
318
|
+
}
|
|
319
|
+
if (snapshot.state === 'undone') {
|
|
320
|
+
return { ok: false, unitId, dryRun: false, changed: [], errors: [{ code: 'undo-already', message: 'unit already undone: ' + unitId }] };
|
|
321
|
+
}
|
|
322
|
+
return await this.serial(async () => {
|
|
323
|
+
let lease;
|
|
324
|
+
try {
|
|
325
|
+
lease = await this.lock.acquire();
|
|
326
|
+
}
|
|
327
|
+
catch (error) {
|
|
328
|
+
return { ok: false, unitId, dryRun: false, changed: [], errors: [{ code: 'lock-failed', message: error instanceof Error ? error.message : String(error) }] };
|
|
329
|
+
}
|
|
330
|
+
const undone = [];
|
|
331
|
+
try {
|
|
332
|
+
for (const op of snapshot.ops) {
|
|
333
|
+
const current = await this.io.readText(op.target);
|
|
334
|
+
if (current !== op.after) {
|
|
335
|
+
return { ok: false, unitId, dryRun: false, changed: [], errors: [{ code: 'undo-preflight', message: 'target changed after the unit: ' + op.target }] };
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
for (const op of [...snapshot.ops].reverse()) {
|
|
339
|
+
const inverse = op.before === null
|
|
340
|
+
? { kind: 'delete', target: op.target, ...(op.skill === undefined ? {} : { skill: op.skill }) }
|
|
341
|
+
: { kind: 'write', target: op.target, content: op.before, ...(op.skill === undefined ? {} : { skill: op.skill }) };
|
|
342
|
+
await this.applyOp(inverse, op.after);
|
|
343
|
+
undone.push(op.target);
|
|
344
|
+
}
|
|
345
|
+
for (const path of snapshot.cleanup ?? [])
|
|
346
|
+
await this.io.remove(path).catch(() => { });
|
|
347
|
+
await this.journal?.write({ unitId, phase: 'undone', changed: undone, ...(snapshot.ops[0]?.target === undefined ? {} : { target: snapshot.ops[0].target }), roots: [...new Set(snapshot.ops.map(op => op.skill).filter((skill) => skill !== undefined))] });
|
|
348
|
+
await this.snapshots?.write({ ...snapshot, state: 'undone' });
|
|
349
|
+
try {
|
|
350
|
+
await this.hooks?.undone?.({ unitId, changed: undone, ops: snapshot.ops, roots: [...new Set(snapshot.ops.map(op => op.skill).filter((skill) => skill !== undefined))] });
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
// best-effort hook; the undo itself already landed
|
|
354
|
+
}
|
|
355
|
+
return { ok: true, unitId, dryRun: false, changed: undone, errors: [] };
|
|
356
|
+
}
|
|
357
|
+
catch (error) {
|
|
358
|
+
for (const target of undone.reverse()) {
|
|
359
|
+
const op = snapshot.ops.find(entry => entry.target === target);
|
|
360
|
+
if (op === undefined)
|
|
361
|
+
continue;
|
|
362
|
+
try {
|
|
363
|
+
if (op.after === null)
|
|
364
|
+
await this.io.remove(target);
|
|
365
|
+
else
|
|
366
|
+
await this.io.writeText(target, op.after);
|
|
367
|
+
}
|
|
368
|
+
catch {
|
|
369
|
+
// best-effort undo compensation; the structured error below remains honest
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
373
|
+
return { ok: false, unitId, dryRun: false, changed: undone, errors: [{ code: 'undo-failed', message }] };
|
|
374
|
+
}
|
|
375
|
+
finally {
|
|
376
|
+
await lease.release();
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
async applyOp(op, baseline) {
|
|
381
|
+
if (this.transact !== undefined && op.kind === 'write') {
|
|
382
|
+
let ran = false;
|
|
383
|
+
await this.transact(this.io, op.target, () => {
|
|
384
|
+
ran = true;
|
|
385
|
+
return op.content;
|
|
386
|
+
});
|
|
387
|
+
if (!ran)
|
|
388
|
+
throw new Error('internal error: the write transaction did not invoke the task; no write was performed');
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
const next = op.kind === 'delete' ? null : op.content;
|
|
392
|
+
if (this.io.transact !== undefined) {
|
|
393
|
+
let conflict = false;
|
|
394
|
+
let wrote = false;
|
|
395
|
+
await this.io.transact(op.target, current => {
|
|
396
|
+
if (current !== baseline) {
|
|
397
|
+
if (this.transactGuard)
|
|
398
|
+
throw new Error('concurrent modification detected: ' + op.target);
|
|
399
|
+
conflict = true;
|
|
400
|
+
return current;
|
|
401
|
+
}
|
|
402
|
+
wrote = true;
|
|
403
|
+
return next;
|
|
404
|
+
});
|
|
405
|
+
if (conflict && !wrote)
|
|
406
|
+
throw Object.assign(new Error('concurrent modification detected: ' + op.target), { noWrite: true });
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
const current = await this.io.readText(op.target);
|
|
410
|
+
if (current !== baseline)
|
|
411
|
+
throw new Error('concurrent modification detected: ' + op.target);
|
|
412
|
+
if (next === null)
|
|
413
|
+
await this.io.remove(op.target);
|
|
414
|
+
else
|
|
415
|
+
await this.io.writeText(op.target, next);
|
|
416
|
+
}
|
|
417
|
+
async rollback(snapshots) {
|
|
418
|
+
for (const snapshot of [...snapshots].reverse()) {
|
|
419
|
+
if (snapshot.before === null)
|
|
420
|
+
await this.io.remove(snapshot.target);
|
|
421
|
+
else
|
|
422
|
+
await this.io.writeText(snapshot.target, snapshot.before);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
function parseFileLock(raw) {
|
|
427
|
+
if (raw === null)
|
|
428
|
+
return undefined;
|
|
429
|
+
try {
|
|
430
|
+
const value = JSON.parse(raw);
|
|
431
|
+
if (typeof value.token !== 'string' || typeof value.pid !== 'number' || typeof value.startedAt !== 'number')
|
|
432
|
+
return undefined;
|
|
433
|
+
return { token: value.token, pid: value.pid, startedAt: value.startedAt };
|
|
434
|
+
}
|
|
435
|
+
catch {
|
|
436
|
+
return undefined;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
const defaultSleep = async (ms) => await new Promise(resolve => setTimeout(resolve, ms));
|
|
440
|
+
/**
|
|
441
|
+
* Cross-process root mutation lock. A live holder is never stolen; a dead pid
|
|
442
|
+
* or corrupt record can be taken over. This is separate from merge's run-level
|
|
443
|
+
* tree lock so an ordinary skill write cannot deadlock a merge run.
|
|
444
|
+
*/
|
|
445
|
+
export function fileMutationLock(io, root, options = {}) {
|
|
446
|
+
const path = join(dirname(root), '.' + basename(root) + '.mutation-lock');
|
|
447
|
+
const waitMs = options.waitMs ?? 5000;
|
|
448
|
+
const pollMs = options.pollMs ?? 25;
|
|
449
|
+
const now = options.now ?? Date.now;
|
|
450
|
+
const sleep = options.sleep ?? defaultSleep;
|
|
451
|
+
const update = async (task) => {
|
|
452
|
+
if (io.transact !== undefined) {
|
|
453
|
+
let ran = false;
|
|
454
|
+
await io.transact(path, current => {
|
|
455
|
+
ran = true;
|
|
456
|
+
return task(current);
|
|
457
|
+
});
|
|
458
|
+
if (!ran)
|
|
459
|
+
throw new Error('mutation-lock-transact-not-run: ' + path);
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
const current = await io.readText(path);
|
|
463
|
+
const next = task(current);
|
|
464
|
+
try {
|
|
465
|
+
if (next === null)
|
|
466
|
+
await io.remove(path);
|
|
467
|
+
else
|
|
468
|
+
await io.writeText(path, next);
|
|
469
|
+
}
|
|
470
|
+
catch (error) {
|
|
471
|
+
if (!isCommittedWarning(error))
|
|
472
|
+
throw error;
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
return {
|
|
476
|
+
async acquire() {
|
|
477
|
+
const deadline = now() + waitMs;
|
|
478
|
+
for (;;) {
|
|
479
|
+
const token = randomUUID();
|
|
480
|
+
let acquired = false;
|
|
481
|
+
await update(current => {
|
|
482
|
+
const existing = parseFileLock(current);
|
|
483
|
+
if (existing !== undefined && isProcessAlive(existing.pid))
|
|
484
|
+
return current;
|
|
485
|
+
acquired = true;
|
|
486
|
+
return JSON.stringify({ token, pid: process.pid, startedAt: now() });
|
|
487
|
+
});
|
|
488
|
+
if (acquired) {
|
|
489
|
+
let released = false;
|
|
490
|
+
return {
|
|
491
|
+
async release() {
|
|
492
|
+
if (released)
|
|
493
|
+
return;
|
|
494
|
+
released = true;
|
|
495
|
+
await update(current => {
|
|
496
|
+
const record = parseFileLock(current);
|
|
497
|
+
return record?.token === token ? null : current;
|
|
498
|
+
});
|
|
499
|
+
},
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
if (now() >= deadline)
|
|
503
|
+
throw new Error('mutation-lock-busy: ' + path);
|
|
504
|
+
await sleep(Math.min(pollMs, Math.max(0, deadline - now())));
|
|
505
|
+
}
|
|
506
|
+
},
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
//# sourceMappingURL=mutation-unit.js.map
|
package/lib/types/prompts.d.ts
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* changes semantically: the bundle digest is the fail-closed signal for
|
|
4
4
|
* review workers, so a stale id across deployments must be distinguishable.
|
|
5
5
|
*/
|
|
6
|
-
export declare const PROMPT_BUNDLE_VERSION =
|
|
7
|
-
export declare const PROMPT_BUNDLE_ID = "dsh-evolution@
|
|
6
|
+
export declare const PROMPT_BUNDLE_VERSION = 23;
|
|
7
|
+
export declare const PROMPT_BUNDLE_ID = "dsh-evolution@23";
|
|
8
8
|
export declare const MEMORY_REVIEW_PROMPT = "[Auto-review \u2014 Memory]\nReview the conversation above and consider saving to memory if appropriate.\n\nFocus on:\n1. Has the user revealed things about themselves \u2014 persona, desires, preferences, or personal details worth remembering?\n2. Has the user expressed expectations about how you should behave, their work style, or ways they want you to operate?\n\nIf something stands out, save it using the memory tool.\nIf nothing is worth saving, just say \"Nothing to save.\" and stop.";
|
|
9
9
|
export declare const SKILL_REVIEW_PROMPT = "[Auto-review \u2014 Skills]\nReview the conversation above and update the skill library. Be ACTIVE \u2014 inspect the session and the existing library thoroughly, and prefer a small, precise update when the session teaches something durable. Activity is not a quota: a precise no-op after checking is better than a vague or forced entry.\n\nTarget shape of the library: CLASS-LEVEL skills, each with a rich SKILL.md and a references/ directory for session-specific detail. Not a long flat list of narrow one-session-one-skill entries. This shapes HOW you update, not WHETHER you update.\n\nA skill body is a reusable decision rule (when / do / check); dates, versions, incident narratives, and one-off evidence belong in references when they would otherwise dominate the body.\n\nSignals to look for (any one of these warrants action):\n \u2022 User corrected your style, tone, format, legibility, or verbosity. Frustration signals like 'stop doing X', 'this is too verbose', 'don't format like this', 'why are you explaining', 'just give me the answer', 'you always do Y and I hate it', or an explicit 'remember this' are FIRST-CLASS skill signals, not just memory signals. Update the relevant skill(s) to embed the preference so the next session starts already knowing.\n \u2022 User corrected your workflow, approach, or sequence of steps. Encode the correction as a pitfall or explicit step in the skill that governs that class of task.\n \u2022 Non-trivial technique, fix, workaround, debugging path, or tool-usage pattern emerged that a future session would benefit from. Capture it.\n \u2022 A skill that got loaded or consulted this session turned out to be wrong, missing a step, or outdated. Patch it NOW.\n\nRead-before-write: update, patch, delete, or write support files ONLY into skills you loaded or read in THIS session. On the plan channel ops on unread skills are rejected; direct writes have no such guard, so treat the rule as binding. CREATE of a brand-new umbrella is the only exception.\n\nPreference order \u2014 prefer the earliest action that fits, but do pick one when a signal above fired:\n 1. UPDATE A CURRENTLY-LOADED SKILL. Look back through the conversation for skills the user loaded or you read. If any of them covers the territory of the new learning, PATCH that one first. It is the skill that was in play, so it's the right one to extend.\n 2. UPDATE AN EXISTING UMBRELLA. If no loaded skill fits but an existing class-level skill does, patch it. Add a subsection, a pitfall, or broaden a trigger.\n 3. ADD A SUPPORT FILE under an existing umbrella. Skills can be packaged with three kinds of support files \u2014 use the right directory per kind:\n \u2022 references/<topic>.md \u2014 session-specific detail (error transcripts, reproduction recipes, provider quirks) AND condensed knowledge banks: quoted research, API docs, external authoritative excerpts, or domain notes you found while working on the problem. Write it concise and for the value of the task, not as a full mirror of upstream docs.\n \u2022 templates/<name>.<ext> \u2014 starter files meant to be copied and modified (boilerplate configs, scaffolding, a known-good example the agent can reproduce with modifications).\n \u2022 scripts/<name>.<ext> \u2014 statically re-runnable actions the skill can invoke directly (verification scripts, fixture generators, deterministic probes, anything the agent should run rather than hand-type each time).\n Add support files via skill_manage action=write_file with file_path starting 'references/', 'templates/', or 'scripts/'. The umbrella's SKILL.md should gain a one-line pointer to any new support file so future agents know it exists.\n 4. RESTRUCTURE a loaded skill whose body grew log-like \u2014 rc/sha/date-dense sections, session-detail spirals, or a fat body with no support files. Use skill_manage action=restructure with restructure: [{\"heading\": \"<the exact ## heading text>\", \"to_file\": \"references/<topic>.md\"}] \u2014 the ENTIRE ## section (from that heading to the next heading) moves into the support file and its position becomes a pointer line. The skill's name and directory never change. Only propose headings that exist verbatim in the body; never invent one, and never restructure a healthy small skill.\n 5. CREATE A NEW CLASS-LEVEL UMBRELLA SKILL only when no existing class-level skill covers the class and no existing umbrella can absorb the learning. The name MUST be at the class level. The name MUST NOT be a specific PR number, error string, feature codename, library-alone name, or 'fix-X / debug-Y / audit-Z-today' session artifact. If the proposed name only makes sense for today's task, it's wrong \u2014 fall back to (1), (2), or (3).\n\nUser-preference embedding (important): when the user expressed a style/format/workflow preference, the update belongs in the SKILL.md body, not just in memory. Memory captures 'who the user is and what the current situation and state of your operations are'; skills capture 'how to do this class of task for this user'. When they complain about how you handled a task, the skill that governs that task needs to carry the lesson.\n\nIf you notice two existing skills that overlap, note it in your reply \u2014 the background curator handles consolidation at scale.\n\nTwo-tier deposition discipline (DSH addition, same spirit as the umbrella rule): before writing, classify the knowledge:\n \u2022 PATTERN (reusable \u2014 symptom \u2192 mechanism \u2192 fix \u2192 verification, still valuable next session) belongs in the SKILL.md body.\n \u2022 LOG (one-off \u2014 commit SHAs, npm/profile states, what this release changed, this session's process narrative) belongs in a references/ file, never the body. Body density IS reuse rate. Keep new entries tight: a pattern fits in 2-8 physical lines; prefer changing the current-state pointer over appending history.\n\nProtected skills (DO NOT edit these):\n \u2022 Bundled skills (shipped with the platform).\n \u2022 Hub-installed skills (installed from a hub).\nPinned skills are read-only to THIS background review pass \u2014 the pinned write guard refuses background changes, so this pass may not update them. They also cannot be archived by any writer (the foreground included): remove the .pinned marker first. Foreground and delegated-subagent update/patch writes to pinned skills remain allowed.\nIf the only skills that need updating are protected, say 'Nothing to save.' and stop.\n\nDo NOT capture (these become persistent self-imposed constraints that bite you later when the environment changes):\n \u2022 Environment-dependent failures: missing binaries, fresh-install errors, post-migration path mismatches, 'command not found', unconfigured credentials, uninstalled packages. The user can fix these \u2014 they are not durable rules.\n \u2022 Negative claims about tools or features ('browser tools do not work', 'X tool is broken', 'cannot use Y'). These harden into refusals the agent cites against itself for months after the actual problem was fixed.\n \u2022 Session-specific transient errors that resolved before the conversation ended. If retrying worked, the lesson is the retry pattern, not the original failure.\n \u2022 One-off task narratives. A user asking 'summarize today's market' or 'analyze this PR' is not a class of work that warrants a skill.\n\nIf a tool failed because of setup state, capture the FIX (install command, config step, env var to set) under an existing setup or troubleshooting skill \u2014 never 'this tool does not work' as a standalone constraint.\n\n'Nothing to save.' is a real option but should NOT be the default. If the session ran smoothly with no corrections and produced no new technique, state what you checked and stop. Otherwise, act.";
|
|
10
10
|
export declare const COMBINED_REVIEW_PROMPT = "[Auto-review]\nReview the conversation above and update two things:\n\n**Memory**: who the user is. Did the user reveal persona, desires, preferences, personal details, or expectations about how you should behave? Save facts about the user and durable preferences with the memory tool.\n\n**Skills**: how to do this class of task. Be ACTIVE \u2014 inspect the session and the existing library thoroughly, and prefer a small, precise update when the session teaches something durable. Activity is not a quota: a precise no-op after checking is better than a vague or forced entry.\n\nTarget shape of the skill library: CLASS-LEVEL skills with a rich SKILL.md and a references/ directory for session-specific detail. Not a long flat list of narrow one-session-one-skill entries.\n\nA skill body is a reusable decision rule (when / do / check); dates, versions, incident narratives, and one-off evidence belong in references when they would otherwise dominate the body.\n\nSignals that warrant a skill update (any one is enough):\n \u2022 User corrected your style, tone, format, legibility, verbosity, or approach. Frustration is a FIRST-CLASS skill signal, not just a memory signal. 'stop doing X', 'don't format like this', 'I hate when you Y' \u2014 embed the lesson in the skill that governs that task so the next session starts fixed.\n \u2022 Non-trivial technique, fix, workaround, or debugging path emerged.\n \u2022 A skill that was loaded or consulted turned out wrong, missing, or outdated \u2014 patch it now.\n\nRead-before-write: update, patch, delete, or write support files ONLY into skills you loaded or read in THIS session. On the plan channel ops on unread skills are rejected; direct writes have no such guard, so treat the rule as binding. CREATE of a brand-new umbrella is the only exception.\n\nPreference order for skills \u2014 pick the earliest that fits:\n 1. UPDATE A CURRENTLY-LOADED SKILL. Check what skills were loaded or read in the conversation. If one of them covers the learning, PATCH it first. It was in play; it's the right place.\n 2. UPDATE AN EXISTING UMBRELLA. Patch it.\n 3. ADD A SUPPORT FILE under an existing umbrella via skill_manage action=write_file. Three kinds: references/<topic>.md for session-specific detail OR condensed knowledge banks (quoted research, API docs excerpts, domain notes) written concise and task-focused; templates/<name>.<ext> for starter files meant to be copied and modified; scripts/<name>.<ext> for statically re-runnable actions (verification, fixture generators, probes). Add a one-line pointer in SKILL.md so future agents find them.\n 4. RESTRUCTURE a loaded skill whose body grew log-like (rc/sha/date-dense sections, session-detail spirals, fat body with no support files) via skill_manage action=restructure with restructure: [{\"heading\": \"<the exact ## heading text>\", \"to_file\": \"references/<topic>.md\"}] \u2014 the ENTIRE ## section moves into the support file and its position becomes a pointer line; the skill's name and directory never change. Only propose headings that exist verbatim in the body.\n 5. CREATE A NEW CLASS-LEVEL UMBRELLA only when no existing class-level skill covers the class and no existing umbrella can absorb the learning. Name at the class level \u2014 NOT a PR number, error string, codename, library-alone name, or 'fix-X / debug-Y' session artifact. If the name only fits today's task, fall back to (1), (2), or (3).\n\nTwo-tier deposition discipline (DSH addition): classify before writing \u2014 PATTERN (symptom \u2192 mechanism \u2192 fix \u2192 verification) goes in the SKILL.md body; LOG (commit SHAs, npm/profile states, this release's change list, this session's narrative) goes in a references/ file. Body density IS reuse rate; a pattern fits in 2-8 physical lines.\n\nUser-preference embedding: when the user complains about how you handled a task, update the skill that governs that task \u2014 memory alone isn't enough. Memory says 'who the user is and what the current situation and state of your operations are'; skills say 'how to do this class of task for this user'. Both should carry user-preference lessons when relevant.\n\nIf you notice overlapping existing skills, mention it \u2014 the background curator handles consolidation.\n\nProtected skills (DO NOT edit these):\n \u2022 Bundled skills (shipped with the platform).\n \u2022 Hub-installed skills (installed from a hub).\nPinned skills are read-only to THIS background review pass \u2014 the pinned write guard refuses background changes, so this pass may not update them. They also cannot be archived by any writer (the foreground included): remove the .pinned marker first. Foreground and delegated-subagent update/patch writes to pinned skills remain allowed.\nIf the only skills that need updating are protected, say 'Nothing to save.' and stop.\n\nDo NOT capture as skills (these become persistent self-imposed constraints that bite you later when the environment changes):\n \u2022 Environment-dependent failures: missing binaries, fresh-install errors, post-migration path mismatches, 'command not found', unconfigured credentials, uninstalled packages. The user can fix these \u2014 they are not durable rules.\n \u2022 Negative claims about tools or features ('browser tools do not work', 'X tool is broken', 'cannot use Y'). These harden into refusals the agent cites against itself for months after the actual problem was fixed.\n \u2022 Session-specific transient errors that resolved before the conversation ended. If retrying worked, the lesson is the retry pattern, not the original failure.\n \u2022 One-off task narratives. A user asking 'summarize today's market' or 'analyze this PR' is not a class of work that warrants a skill.\n\nIf a tool failed because of setup state, capture the FIX (install command, config step, env var to set) under an existing setup or troubleshooting skill \u2014 never 'this tool does not work' as a standalone constraint.\n\nAct on whichever of the two dimensions has real signal. If genuinely nothing stands out on either, say 'Nothing to save.' and stop \u2014 but don't reach for that conclusion as a default.";
|
|
@@ -52,5 +52,5 @@ export interface PromptBundle {
|
|
|
52
52
|
}
|
|
53
53
|
export declare const PROMPT_BUNDLE: PromptBundle;
|
|
54
54
|
export declare function verifyPromptBundle(bundle?: PromptBundle): boolean;
|
|
55
|
-
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. \"##
|
|
55
|
+
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- whenToUse: ONE abstract sentence describing the task situation that should trigger this skill. This is the platform's machine-routing field: do not enumerate a scenario list and do not repeat the description. One line only; quote it if it contains a colon.\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. \"## Prerequisites\" \u2014 exact env vars, install steps, credentials.\n3. \"## How to Run\" \u2014 canonical invocation framed through DSH tools.\n4. \"## Quick Reference\" \u2014 flat command/endpoint list.\n5. \"## Procedure\" \u2014 numbered steps with copy-paste-exact commands.\n6. \"## Pitfalls\" \u2014 known limits and rate limits.\n7. \"## 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.";
|
|
56
56
|
//# sourceMappingURL=prompts.d.ts.map
|
package/lib/types/prompts.js
CHANGED
|
@@ -26,7 +26,7 @@ import { createHash } from 'node:crypto';
|
|
|
26
26
|
* changes semantically: the bundle digest is the fail-closed signal for
|
|
27
27
|
* review workers, so a stale id across deployments must be distinguishable.
|
|
28
28
|
*/
|
|
29
|
-
export const PROMPT_BUNDLE_VERSION =
|
|
29
|
+
export const PROMPT_BUNDLE_VERSION = 23;
|
|
30
30
|
// 0.3.16 (S1.12, T-5): the id is DERIVED from the version — a one-number bump
|
|
31
31
|
// can no longer drift the two apart.
|
|
32
32
|
export const PROMPT_BUNDLE_ID = `dsh-evolution@${PROMPT_BUNDLE_VERSION}`;
|
|
@@ -415,6 +415,7 @@ export const DSH_AUTHORING_STANDARDS = `Follow the Hermes skill-authoring standa
|
|
|
415
415
|
Frontmatter:
|
|
416
416
|
- name: lowercase-hyphenated, <=64 chars, no spaces.
|
|
417
417
|
- 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.
|
|
418
|
+
- whenToUse: ONE abstract sentence describing the task situation that should trigger this skill. This is the platform's machine-routing field: do not enumerate a scenario list and do not repeat the description. One line only; quote it if it contains a colon.
|
|
418
419
|
- version: 0.1.0
|
|
419
420
|
- author: always the literal value "Hermes". NEVER fill it from the environment, git config, or any identity you can probe — 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.
|
|
420
421
|
- 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.
|
|
@@ -423,13 +424,12 @@ Frontmatter:
|
|
|
423
424
|
|
|
424
425
|
Body section order (omit only when empty):
|
|
425
426
|
1. "# <Human Title>" then a 2-3 sentence intro: what it does, what it does NOT do, key dependency stance.
|
|
426
|
-
2. "##
|
|
427
|
-
3. "##
|
|
428
|
-
4. "##
|
|
429
|
-
5. "##
|
|
430
|
-
6. "##
|
|
431
|
-
7. "##
|
|
432
|
-
8. "## Verification" — one check proving the skill worked.
|
|
427
|
+
2. "## Prerequisites" — exact env vars, install steps, credentials.
|
|
428
|
+
3. "## How to Run" — canonical invocation framed through DSH tools.
|
|
429
|
+
4. "## Quick Reference" — flat command/endpoint list.
|
|
430
|
+
5. "## Procedure" — numbered steps with copy-paste-exact commands.
|
|
431
|
+
6. "## Pitfalls" — known limits and rate limits.
|
|
432
|
+
7. "## Verification" — one check proving the skill worked.
|
|
433
433
|
|
|
434
434
|
DSH-tool framing:
|
|
435
435
|
- Reference DSH tools by name in backticks: \`bash\`, \`str_replace_editor\`, \`write\`, \`skill\`, \`skill_manage\`, \`memory\`.
|
|
@@ -18,9 +18,13 @@ export interface ProposalView {
|
|
|
18
18
|
}>;
|
|
19
19
|
owner: OwnerDecision;
|
|
20
20
|
support?: SupportClosure;
|
|
21
|
+
/** The source body left behind after extraction; the panel can show what is KEPT. */
|
|
22
|
+
residual?: string;
|
|
21
23
|
warnings: string[];
|
|
22
24
|
provenance: {
|
|
23
25
|
promptVersion: 'v1';
|
|
26
|
+
promptHash?: string;
|
|
27
|
+
model?: string;
|
|
24
28
|
runId: string;
|
|
25
29
|
at: number;
|
|
26
30
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { EXTRACT_PROMPT_HASH, EXTRACT_PROMPT_VERSION } from "./extract-prompt.js";
|
|
1
2
|
/**
|
|
2
3
|
* Project a pure ExtractPlan into the proposal record every read-only surface consumes.
|
|
3
4
|
*
|
|
@@ -16,8 +17,9 @@ export function proposalFromPlan(plan, provenance) {
|
|
|
16
17
|
parts: plan.parts.map(part => ({ heading: part.heading, anchor: part.anchor, reason: part.sourceText.slice(0, 160) })),
|
|
17
18
|
owner: plan.owner,
|
|
18
19
|
support: plan.support,
|
|
20
|
+
residual: plan.residual,
|
|
19
21
|
warnings: plan.warnings,
|
|
20
|
-
provenance: { promptVersion:
|
|
22
|
+
provenance: { promptVersion: EXTRACT_PROMPT_VERSION, promptHash: EXTRACT_PROMPT_HASH, runId: provenance.runId, at: provenance.at ?? Date.now() },
|
|
21
23
|
status: 'proposed',
|
|
22
24
|
};
|
|
23
25
|
}
|