@lmzhen/dsh-evolution-core 0.26.26 → 0.27.1
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 +16 -1
- package/lib/composition/index.js +3 -0
- package/lib/composition-D8Y3-ROd.js +604 -0
- package/lib/ensure-MEYMj9jn.js +222 -0
- package/lib/errors-Br2-uUSK.js +57 -0
- package/lib/http-routes-BP-S3KV-.js +149 -0
- package/lib/http-routes.js +2 -0
- package/lib/index.js +66 -850
- package/lib/restrictor.js +37 -0
- package/lib/subagent/index.js +186 -0
- package/lib/types/citations.js +192 -0
- package/lib/types/{preset-composition.d.ts → composition/composer.d.ts} +1 -1
- package/lib/types/composition/composer.js +681 -0
- package/lib/types/composition/ensure.d.ts +10 -0
- package/lib/types/composition/ensure.js +95 -0
- package/lib/types/composition/index.d.ts +5 -0
- package/lib/types/composition/index.js +7 -0
- package/lib/types/composition/preset-ids.d.ts +17 -0
- package/lib/types/composition/preset-ids.js +21 -0
- package/lib/types/composition/restricted-preset.d.ts +36 -0
- package/lib/types/composition/restricted-preset.js +42 -0
- package/lib/types/constants.js +223 -0
- package/lib/types/cost.js +67 -0
- package/lib/types/curator.js +346 -0
- package/lib/types/diagnostics.d.ts +26 -0
- package/lib/types/diagnostics.js +23 -0
- package/lib/types/document-body.js +91 -0
- package/lib/types/drift-signals.js +319 -0
- package/lib/types/env.js +25 -0
- package/lib/types/errors.d.ts +4 -1
- package/lib/types/errors.js +59 -0
- package/lib/types/events.js +16 -0
- package/lib/types/evidence.js +65 -0
- package/lib/types/evolution-events.js +479 -0
- package/lib/types/fold-untrusted.js +44 -0
- package/lib/types/frontmatter.js +623 -0
- package/lib/types/fuzzy-match.js +135 -0
- package/lib/types/gates.js +51 -0
- package/lib/types/guards.js +27 -0
- package/lib/types/health-baseline.js +63 -0
- package/lib/types/http-routes.d.ts +67 -35
- package/lib/types/http-routes.js +135 -0
- package/lib/types/index.d.ts +7 -2
- package/lib/types/index.js +101 -0
- package/lib/types/instance-scope.js +69 -0
- package/lib/types/io.js +675 -0
- package/lib/types/learn-prompt.js +42 -0
- package/lib/types/limits.js +63 -0
- package/lib/types/live.js +107 -0
- package/lib/types/long-runs.js +96 -0
- package/lib/types/memory-store.d.ts +0 -16
- package/lib/types/memory-store.js +606 -0
- package/lib/types/mutations.js +95 -0
- package/lib/types/namespace-migration.js +156 -0
- package/lib/types/numeric.js +47 -0
- package/lib/types/opt-in.js +101 -0
- package/lib/types/package-identity.js +47 -0
- package/lib/types/params.js +328 -0
- package/lib/types/payload.js +36 -0
- package/lib/types/platform-line.generated.d.ts +10 -0
- package/lib/types/platform-line.generated.js +12 -0
- package/lib/types/platform-services.d.ts +2 -2
- package/lib/types/platform-services.js +52 -0
- package/lib/types/probe.js +122 -0
- package/lib/types/prompts.js +409 -0
- package/lib/types/quality.js +308 -0
- package/lib/types/reachability.js +122 -0
- package/lib/types/redact.js +257 -0
- package/lib/types/reference-rewrite.js +144 -0
- package/lib/types/reports.js +110 -0
- package/lib/types/restrictor.d.ts +11 -0
- package/lib/types/restrictor.js +48 -0
- package/lib/types/review-channel.js +59 -0
- package/lib/types/run-registry.js +345 -0
- package/lib/types/scope.js +7 -0
- package/lib/types/serial.js +17 -0
- package/lib/types/session-fold.d.ts +15 -0
- package/lib/types/session-fold.js +28 -0
- package/lib/types/session-projection.js +295 -0
- package/lib/types/signals.js +209 -0
- package/lib/types/skill-discovery.js +66 -0
- package/lib/types/skill-health.js +134 -0
- package/lib/types/skill-history.js +745 -0
- package/lib/types/skill-lint.js +112 -0
- package/lib/types/skill-reads.js +67 -0
- package/lib/types/skill-row-cells.js +123 -0
- package/lib/types/skill-store.js +3528 -0
- package/lib/types/state-store.js +60 -0
- package/lib/types/store-ledger.js +117 -0
- package/lib/types/subagent/channel.d.ts +4 -0
- package/lib/types/subagent/channel.js +131 -0
- package/lib/types/subagent/index.d.ts +4 -0
- package/lib/types/subagent/index.js +5 -0
- package/lib/types/subagent/readiness.d.ts +6 -0
- package/lib/types/subagent/readiness.js +35 -0
- package/lib/types/subagent/types.d.ts +58 -0
- package/lib/types/subagent/types.js +2 -0
- package/lib/types/threats.js +338 -0
- package/lib/types/tool-dispatch.d.ts +5 -0
- package/lib/types/tool-dispatch.js +535 -0
- package/lib/types/tree-write.js +40 -0
- package/lib/types/unit-members.js +121 -0
- package/lib/types/usage.js +474 -0
- package/lib/types/write-authority.js +23 -0
- package/lib/types/write-gates.js +344 -0
- package/lib/types/write-inventory.js +158 -0
- package/package.json +39 -11
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The write-admission sequence: ONE ordered table of the reasons a skill write is refused.
|
|
3
|
+
*
|
|
4
|
+
* Two points read this table (design `dsh-evolution-write-gate-design.md` §3). The ADMISSION
|
|
5
|
+
* point (`execute`, BEFORE the approval seam) runs every gate that applies to `'admission'`, so a
|
|
6
|
+
* write that is refused is never staged and never spends the operator's attention twice. The
|
|
7
|
+
* EXECUTION point (the `executeCore` funnel, reached by the direct path and by the approval
|
|
8
|
+
* replay) runs the gates that apply to `'execution'`: the stored plan of a replayed write passed
|
|
9
|
+
* no schema, and the protected list can change while a record sits pending, so those verdicts are
|
|
10
|
+
* re-decided at the bytes.
|
|
11
|
+
*
|
|
12
|
+
* Order is part of the contract: a gate that can only refuse comes before one that needs a round
|
|
13
|
+
* trip with the operator, so a write that is refused anyway never asks anything.
|
|
14
|
+
*
|
|
15
|
+
* A gate decides whether a write may be ATTEMPTED. The library decides whether it can be
|
|
16
|
+
* PERFORMED — its own structured refusals (a restructure element that is not an object, a patch
|
|
17
|
+
* anchor that no longer matches, an `absorbed_into` that does not exist) stay where the bytes are
|
|
18
|
+
* read.
|
|
19
|
+
*
|
|
20
|
+
* Rule bodies live in `evolution-core` (the read-before-write rule in `skill-reads`, the
|
|
21
|
+
* required-argument table in `SKILL_ACTION_REQUIRED_FIELDS`); this module owns the ORDER and the
|
|
22
|
+
* applicability of each gate, which is an execution-point fact of this tool.
|
|
23
|
+
* @module @lmzhen/dsh-evolution-core/write-gates
|
|
24
|
+
*/
|
|
25
|
+
import { DEFAULT_WRITE_CONFIRM_MODE, DEFAULT_WRITE_CONFIRM_TIMEOUT_SECONDS } from "./write-authority.js";
|
|
26
|
+
import { SKILL_ACTION_REQUIRED_FIELDS, errorText, isUnreadWrite } from "./index.js";
|
|
27
|
+
/** The confirm question's stable id — one question per write, so an answer to an earlier prompt
|
|
28
|
+
* can never be read as the answer to a later one. */
|
|
29
|
+
const CONFIRM_QUESTION_ID = 'evolution-skill-write';
|
|
30
|
+
/** The cancel label every confirm question offers. */
|
|
31
|
+
const CANCEL_LABEL = '取消';
|
|
32
|
+
/** What a `timeout`-mode deadline settles with: comparable by identity against the seam's
|
|
33
|
+
* boolean, so the two outcomes stay distinguishable without a mutable flag (which a linter
|
|
34
|
+
* cannot follow through a timer callback, and a reader cannot either). */
|
|
35
|
+
const CONFIRM_TIMED_OUT = 'confirm-timed-out';
|
|
36
|
+
/**
|
|
37
|
+
* How the ONE confirmation behaves (registry `skillWriteConfirm`).
|
|
38
|
+
*
|
|
39
|
+
* `auto` writes without asking — the default, because an unattended run (a background pass, a
|
|
40
|
+
* scheduled review, a headless session) must not park a tool call on a question nobody will answer.
|
|
41
|
+
* `ask` waits for as long as it takes, which is the deployment that wants the gate in the loop.
|
|
42
|
+
* `timeout` asks and cancels the write on its own deadline.
|
|
43
|
+
*/
|
|
44
|
+
// G1/1.1: the confirm vocabulary lives in write-authority.ts with the authority that carries it.
|
|
45
|
+
// D-09 follow-up: this file used to RE-EXPORT it for compatibility, which made index.ts export the same
|
|
46
|
+
// name from two modules (TS2308: WriteConfirmMode/WriteConfirmRequest/DEFAULT_WRITE_CONFIRM_MODE).
|
|
47
|
+
// The names stay reachable through the package root via write-authority - one owner, no second export.
|
|
48
|
+
/** A usable deadline: the configured value when it is a positive finite number, else the default. */
|
|
49
|
+
function timeoutSecondsOf(configured) {
|
|
50
|
+
return configured !== undefined && Number.isFinite(configured) && configured >= 1
|
|
51
|
+
? Math.floor(configured)
|
|
52
|
+
: DEFAULT_WRITE_CONFIRM_TIMEOUT_SECONDS;
|
|
53
|
+
}
|
|
54
|
+
/** The scalar fields the tool schema types as strings; the replay channel has no schema.
|
|
55
|
+
* `action` is one of them: a non-string action used to fall through to the library as "Unknown
|
|
56
|
+
* action" while the read gate re-defaulted it to `patch` and refused with E-318 (review
|
|
57
|
+
* 2026-09-26, P2), so the shape gate refuses it by name instead. */
|
|
58
|
+
const SCALAR_ARG_FIELDS = ['action', 'name', 'content', 'old_string', 'new_string', 'file_path', 'file_content', 'absorbed_into'];
|
|
59
|
+
/** The scalar fields the tool schema types as BOOLEANS; the replay channel has no schema either.
|
|
60
|
+
* T2-V3/A28: `args.replace_all === true` answered "false" for anything else, so a staged
|
|
61
|
+
* `replace_all: 'yes'` silently patched only the FIRST occurrence — a wrong count with no signal.
|
|
62
|
+
* The plan channel already refuses a non-boolean `replace_all` by name (S1-C1); this is the same
|
|
63
|
+
* rule on the replay channel, so the two channels cannot disagree about one flag. */
|
|
64
|
+
const FLAG_ARG_FIELDS = ['replace_all'];
|
|
65
|
+
/**
|
|
66
|
+
* Assemble a gate context from a caller's services. The protected list is read HERE, so no caller can
|
|
67
|
+
* forget it or read it from a second place.
|
|
68
|
+
* @param services - the four required services plus the optional registry knobs.
|
|
69
|
+
* @param request - the point asking, the arguments under test, and the write origin.
|
|
70
|
+
* @returns a context the shared sequence accepts.
|
|
71
|
+
*/
|
|
72
|
+
export function skillWriteGateContext(services, request) {
|
|
73
|
+
return {
|
|
74
|
+
point: request.point,
|
|
75
|
+
args: request.args,
|
|
76
|
+
origin: request.origin,
|
|
77
|
+
protectedNames: protectedSkillNamesOf(services.policy),
|
|
78
|
+
readNames: services.readNames,
|
|
79
|
+
confirm: services.confirm,
|
|
80
|
+
...(services.confirmMode === undefined ? {} : { confirmMode: services.confirmMode }),
|
|
81
|
+
...(services.confirmTimeoutSeconds === undefined ? {} : { confirmTimeoutSeconds: services.confirmTimeoutSeconds }),
|
|
82
|
+
...(services.describeTarget === undefined ? {} : { describeTarget: services.describeTarget }),
|
|
83
|
+
warn: services.warn,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
export function protectedSkillNamesOf(policy) {
|
|
87
|
+
const snapshot = policy?.get?.();
|
|
88
|
+
return snapshot?.protectedSkillNames;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Narrow the arguments once for every gate.
|
|
92
|
+
*
|
|
93
|
+
* The admission point reaches the gates AFTER the schema, the replay channel BEFORE any
|
|
94
|
+
* validation, so a gate reads only what it can prove: anything else counts as absent, exactly as
|
|
95
|
+
* the missing-argument check already treated a non-string action.
|
|
96
|
+
* @param args - the arguments as the caller sent them.
|
|
97
|
+
* @returns the narrowed view.
|
|
98
|
+
*/
|
|
99
|
+
function gateArgsOf(args) {
|
|
100
|
+
const scalars = (typeof args === 'object' && args !== null ? args : {});
|
|
101
|
+
const action = scalars.action;
|
|
102
|
+
const name = scalars.name;
|
|
103
|
+
const absorbedInto = scalars.absorbed_into;
|
|
104
|
+
return {
|
|
105
|
+
action: typeof action === 'string' ? action : undefined,
|
|
106
|
+
name: typeof name === 'string' ? name : '',
|
|
107
|
+
// A BLANK absorbed_into is absent: the plan validator reads it the same way
|
|
108
|
+
// (`!(op.absorbed_into ?? '').trim()`), and the archive call itself is truthiness-based
|
|
109
|
+
// (`args.absorbed_into ? { absorbedInto } : {}`) — so `''` performs a BARE archive and must not
|
|
110
|
+
// borrow the merge exemption (review 2026-09-26, P1).
|
|
111
|
+
absorbedInto: typeof absorbedInto === 'string' && absorbedInto.trim() !== '' ? absorbedInto : undefined,
|
|
112
|
+
scalars,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The scalar arguments must be strings before any other gate reads them as names.
|
|
117
|
+
*
|
|
118
|
+
* v20 (D-1, V8-09 sibling): the schema does not strictly guarantee scalar shapes (the F-07 class
|
|
119
|
+
* of garbage that slipped past it), and a non-string scalar used to escape as a bare TypeError
|
|
120
|
+
* from SkillLibrary (`name.trim()` / `md.includes(...)`). Family posture: a STRUCTURED refusal.
|
|
121
|
+
* Absent is legal — the branches that need a value have their own remedies.
|
|
122
|
+
*/
|
|
123
|
+
const ARGUMENT_SHAPE = {
|
|
124
|
+
id: 'argument-shape',
|
|
125
|
+
appliesTo: ['admission', 'execution'],
|
|
126
|
+
run: ({ view }) => {
|
|
127
|
+
for (const field of SCALAR_ARG_FIELDS) {
|
|
128
|
+
const value = view.scalars[field];
|
|
129
|
+
if (value === undefined || value === null || typeof value === 'string')
|
|
130
|
+
continue;
|
|
131
|
+
return `skill_manage: "${field}" must be a string (got ${typeof value}); refusing the write.`;
|
|
132
|
+
}
|
|
133
|
+
// T2-V3/A28: the FLAGS are shapes too. A non-boolean `replace_all` used to mean "false" at the
|
|
134
|
+
// patch site, so the write landed with a different count than the caller asked for.
|
|
135
|
+
for (const field of FLAG_ARG_FIELDS) {
|
|
136
|
+
const value = view.scalars[field];
|
|
137
|
+
if (value === undefined || value === null || typeof value === 'boolean')
|
|
138
|
+
continue;
|
|
139
|
+
return `skill_manage: "${field}" must be a boolean (got ${typeof value}); refusing the write.`;
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* V27 G5.1 (v27 T-2) / OPT-05 (2026-09): name the missing per-action arguments before anything is
|
|
146
|
+
* read or written. The table comes from core (the plan validator reads the same rows), and the
|
|
147
|
+
* OWN-property narrowing is required because a plain-object index resolves inherited members
|
|
148
|
+
* (`constructor`, `toString`, …) to truthy functions. An EMPTY string is deliberately NOT
|
|
149
|
+
* missing: it reaches the library, whose branch messages carry the more specific remedy.
|
|
150
|
+
*/
|
|
151
|
+
const MISSING_ARGS = {
|
|
152
|
+
id: 'missing-args',
|
|
153
|
+
appliesTo: ['admission', 'execution'],
|
|
154
|
+
run: ({ view }) => {
|
|
155
|
+
const action = view.action;
|
|
156
|
+
const required = action === undefined || !Object.hasOwn(SKILL_ACTION_REQUIRED_FIELDS, action)
|
|
157
|
+
? []
|
|
158
|
+
: SKILL_ACTION_REQUIRED_FIELDS[action] ?? [];
|
|
159
|
+
const missing = required.filter(field => view.scalars[field] === undefined || view.scalars[field] === null);
|
|
160
|
+
if (missing.length === 0)
|
|
161
|
+
return null;
|
|
162
|
+
return `skill_manage ${action} requires ${missing.join(', ')}; the tool description lists the arguments per action.`;
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
/**
|
|
166
|
+
* v30 REV-02: the immutable policy's protected list is enforced at plan validation, but the
|
|
167
|
+
* replay channel executes STORED plans — one accepted under an older policy must not land on a
|
|
168
|
+
* skill the CURRENT policy protects. Foreground (operator) writes are deliberately not gated by
|
|
169
|
+
* this list, and a deployment without the policy row keeps the previous behavior.
|
|
170
|
+
*/
|
|
171
|
+
const POLICY_PROTECTION = {
|
|
172
|
+
id: 'policy-protection',
|
|
173
|
+
appliesTo: ['admission', 'execution'],
|
|
174
|
+
run: ({ view, origin, protectedNames }) => {
|
|
175
|
+
if (origin === 'foreground')
|
|
176
|
+
return null;
|
|
177
|
+
if (view.name === '' || !protectedNames?.includes(view.name))
|
|
178
|
+
return null;
|
|
179
|
+
return `skill_manage: "${view.name}" is protected by the current policy (protectedSkillNames); replayed/autonomous writes are refused.`;
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
/**
|
|
183
|
+
* Read-before-write (design §4.2): a non-foreground write must have read its target in the
|
|
184
|
+
* session that authored it, so a subagent or the review's own model cannot blind-overwrite a body
|
|
185
|
+
* it never saw. The read set comes from `collectReadSkillNames`, which counts only a read that did
|
|
186
|
+
* not fail — a failed or timed-out read does not pass this gate.
|
|
187
|
+
*
|
|
188
|
+
* Admission only. The reads belong to the WRITING session, and the replayed record has none: it
|
|
189
|
+
* passed this gate at its own staging point, or the review's plan filter on the plan path.
|
|
190
|
+
*
|
|
191
|
+
* An unreadable session log is NOT an empty read set: the write proceeds with one warning, the
|
|
192
|
+
* same soft-probe posture as the cross-source check, because a deployment whose sessions expose no
|
|
193
|
+
* log must not refuse every autonomous write.
|
|
194
|
+
*/
|
|
195
|
+
const READ_BEFORE_WRITE = {
|
|
196
|
+
id: 'read-before-write',
|
|
197
|
+
appliesTo: ['admission'],
|
|
198
|
+
run: ({ view, origin, readNames, warn }) => {
|
|
199
|
+
if (origin === 'foreground')
|
|
200
|
+
return null;
|
|
201
|
+
if (readNames === undefined) {
|
|
202
|
+
warn('skill_manage: the read-before-write check could not run for this call — the session log is not readable here, so the write is allowed.');
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
// A missing action is NOT this gate's business: the tool schema requires one, so an absent
|
|
206
|
+
// action is a malformed call whose refusal the LIBRARY owns ("Unknown action") — answering
|
|
207
|
+
// E-318 would send the model to read a skill for a call that cannot work (review 2026-09-26,
|
|
208
|
+
// P2). The rule's default-to-`patch` (V6-26) stays where it belongs, on the plan path.
|
|
209
|
+
if (view.action === undefined)
|
|
210
|
+
return null;
|
|
211
|
+
if (!isUnreadWrite({ action: view.action, name: view.name }, readNames))
|
|
212
|
+
return null;
|
|
213
|
+
return errorText('e-318-skill-write-without-a-read', { a1: view.name });
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
/**
|
|
217
|
+
* The confirm gate (design §4.1): a foreground create, or a bare foreground delete, is
|
|
218
|
+
* irreversible and gets ONE question. Admission only — a replayed record already carries the human
|
|
219
|
+
* release that staged it, so asking there would be the second interruption for one write.
|
|
220
|
+
*
|
|
221
|
+
* `delete` with `absorbed_into` is exempt: that is the merge protocol (an umbrella absorbs the source),
|
|
222
|
+
* and asking once per absorbed skill would turn one merge into N questions. A BARE delete is the
|
|
223
|
+
* destructive primitive the plan layer reserves for a human — a review may only delete into an
|
|
224
|
+
* umbrella (core's `SKILL_ACTION_REQUIRED_FIELDS` docblock).
|
|
225
|
+
*
|
|
226
|
+
* The gate is UX, not a security door (those are policy protection and read-before-write): the seam
|
|
227
|
+
* is required to be total, so an unmounted question service or a caller that is not the live root
|
|
228
|
+
* agent PROCEEDS — the operator's own session is the authority that asked for the write.
|
|
229
|
+
*
|
|
230
|
+
* Whether the question is put at all is CONFIGURED (`skillWriteConfirm`): `auto` writes without
|
|
231
|
+
* asking, `ask` waits for an answer indefinitely (what this gate did before the knob existed), and
|
|
232
|
+
* `timeout` asks but cancels the write on its own deadline. `auto` is the default because an
|
|
233
|
+
* unattended run — a background pass, a scheduled review, a headless session — must not park a tool
|
|
234
|
+
* call on a question nobody will answer.
|
|
235
|
+
*/
|
|
236
|
+
const HUMAN_CONFIRM = {
|
|
237
|
+
id: 'human-confirm',
|
|
238
|
+
appliesTo: ['admission'],
|
|
239
|
+
run: async ({ view, origin, confirm, warn, confirmMode, confirmTimeoutSeconds, describeTarget }) => {
|
|
240
|
+
const action = view.action;
|
|
241
|
+
if (action === undefined || origin !== 'foreground' || view.name === '')
|
|
242
|
+
return null;
|
|
243
|
+
const destructive = action === 'create' || (action === 'delete' && view.absorbedInto === undefined);
|
|
244
|
+
if (!destructive)
|
|
245
|
+
return null;
|
|
246
|
+
const mode = confirmMode ?? DEFAULT_WRITE_CONFIRM_MODE;
|
|
247
|
+
if (mode === 'auto')
|
|
248
|
+
return null;
|
|
249
|
+
if (confirm === undefined) {
|
|
250
|
+
warn('skill_manage: no confirmation channel is mounted, so the write proceeds unconfirmed.');
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
const confirmLabel = action === 'create' ? '创建' : '删除';
|
|
254
|
+
const seconds = timeoutSecondsOf(confirmTimeoutSeconds);
|
|
255
|
+
const cancel = new AbortController();
|
|
256
|
+
// The deadline is enforced HERE, not only carried by the signal: the signal lets the client
|
|
257
|
+
// take the card down, while the race guarantees the call returns even if nothing is listening
|
|
258
|
+
// to it — a prompt that outlives its deadline is the parked tool call this mode exists to
|
|
259
|
+
// prevent.
|
|
260
|
+
let timer;
|
|
261
|
+
const deadline = mode === 'timeout'
|
|
262
|
+
? new Promise((resolve) => {
|
|
263
|
+
timer = setTimeout(() => {
|
|
264
|
+
cancel.abort();
|
|
265
|
+
resolve(CONFIRM_TIMED_OUT);
|
|
266
|
+
}, seconds * 1000);
|
|
267
|
+
// Never hold the process open: a one-shot CLI run must not linger for the rest of the
|
|
268
|
+
// window after the write already returned. A server keeps the loop alive anyway, which is
|
|
269
|
+
// where the deadline does its work.
|
|
270
|
+
timer.unref();
|
|
271
|
+
})
|
|
272
|
+
: undefined;
|
|
273
|
+
// The question says what the OPERATOR needs: the consequence, then the target in its own words.
|
|
274
|
+
// Asking for a decision by name alone is asking someone to authorize an unknown artifact.
|
|
275
|
+
const context = await describeTarget?.();
|
|
276
|
+
const details = [
|
|
277
|
+
context?.description === undefined || context.description === '' ? null : '功能:' + context.description,
|
|
278
|
+
context?.versions === undefined ? null : '已记录的内容版本数:' + String(context.versions),
|
|
279
|
+
].filter((line) => line !== null);
|
|
280
|
+
const consequence = action === 'create'
|
|
281
|
+
? `创建技能 "${view.name}"?会在家族目录树里新建一个技能目录。`
|
|
282
|
+
: `删除技能 "${view.name}"?它会被归档到 .archive 并离开目录。`;
|
|
283
|
+
const asked = confirm({
|
|
284
|
+
action,
|
|
285
|
+
name: view.name,
|
|
286
|
+
confirmLabel,
|
|
287
|
+
question: {
|
|
288
|
+
id: CONFIRM_QUESTION_ID,
|
|
289
|
+
header: '确认',
|
|
290
|
+
// ONE line: the platform's question composer does not preserve newlines, so a multi-line
|
|
291
|
+
// question arrives as a run-on sentence. The facts are separated instead.
|
|
292
|
+
question: details.length === 0 ? consequence : consequence + ' ' + details.join(' · '),
|
|
293
|
+
options: [{ label: confirmLabel }, { label: CANCEL_LABEL }],
|
|
294
|
+
},
|
|
295
|
+
...mode === 'timeout' ? { signal: cancel.signal } : {},
|
|
296
|
+
});
|
|
297
|
+
let outcome;
|
|
298
|
+
try {
|
|
299
|
+
outcome = deadline === undefined ? await asked : await Promise.race([asked, deadline]);
|
|
300
|
+
}
|
|
301
|
+
finally {
|
|
302
|
+
if (timer !== undefined)
|
|
303
|
+
clearTimeout(timer);
|
|
304
|
+
}
|
|
305
|
+
if (outcome === true)
|
|
306
|
+
return null;
|
|
307
|
+
// Running out of time is not the same answer as being declined: the write was cancelled
|
|
308
|
+
// because nobody was there to decide, and the refusal names the knob that changes that. Which
|
|
309
|
+
// promise the race returned first is a microtask-order detail — a platform that answers the
|
|
310
|
+
// abort with its own dismissal can settle `asked` first — so the case is decided by the
|
|
311
|
+
// deadline's signal, which only this gate aborts.
|
|
312
|
+
if (outcome === CONFIRM_TIMED_OUT || cancel.signal.aborted) {
|
|
313
|
+
return errorText('e-319-skill-write-confirm-timed-out', {
|
|
314
|
+
a1: view.name,
|
|
315
|
+
a2: action === 'create' ? '创建' : '删除',
|
|
316
|
+
a3: String(seconds),
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
return errorText('e-317-skill-write-not-confirmed', {
|
|
320
|
+
a1: view.name,
|
|
321
|
+
a2: action === 'create' ? '创建' : '删除',
|
|
322
|
+
});
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
/** The sequence, in order. A gate whose `appliesTo` omits the asking point is skipped. */
|
|
326
|
+
const GATES = [ARGUMENT_SHAPE, MISSING_ARGS, POLICY_PROTECTION, READ_BEFORE_WRITE, HUMAN_CONFIRM];
|
|
327
|
+
/**
|
|
328
|
+
* Run the sequence for one write attempt.
|
|
329
|
+
* @param context - the asking point, the write, and the seams the human-facing gate needs.
|
|
330
|
+
* @returns the FIRST refusal, or null when every applicable gate passed. Both points return the
|
|
331
|
+
* message as the tool result unchanged, so the two entries refuse with identical wording.
|
|
332
|
+
*/
|
|
333
|
+
export async function runWriteGates(context) {
|
|
334
|
+
const input = { ...context, view: gateArgsOf(context.args) };
|
|
335
|
+
for (const gate of GATES) {
|
|
336
|
+
if (!gate.appliesTo.includes(context.point))
|
|
337
|
+
continue;
|
|
338
|
+
const refusal = await gate.run(input);
|
|
339
|
+
if (refusal !== null)
|
|
340
|
+
return refusal;
|
|
341
|
+
}
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
//# sourceMappingURL=write-gates.js.map
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* B3 / G4 (0.3.78): the persisted-write inventory — WHICH file the family
|
|
3
|
+
* writes, WHO writes it, and WHAT keeps two writers apart.
|
|
4
|
+
*
|
|
5
|
+
* The table lives in `persisted-write-inventory.json` at the package root so
|
|
6
|
+
* the TypeScript side, the architecture gate (`verify-arch-guards` rule N20)
|
|
7
|
+
* and the regression specs read ONE file (the row-overrides.json pattern).
|
|
8
|
+
*
|
|
9
|
+
* "What keeps two writers apart" has exactly three answers in this family:
|
|
10
|
+
* - `transact` — the IO backend's cross-process write lock (transactIo);
|
|
11
|
+
* - `write-lock` — the per-target `<path>.lock` protocol of the skill tree;
|
|
12
|
+
* - `instance-claim` — the per-home single-instance claim (instance-scope.ts),
|
|
13
|
+
* for a writer whose sweeps cannot be expressed as one locked file.
|
|
14
|
+
* A site that answers with NONE of them is the "two instances, one file" class:
|
|
15
|
+
* a per-instance serial queue looks like serialization and is not.
|
|
16
|
+
* @module @lmzhen/dsh-evolution-core/src/write-inventory
|
|
17
|
+
*/
|
|
18
|
+
import { readFileSync } from 'node:fs';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
const SITES_URL = new URL('../persisted-write-inventory.json', import.meta.url);
|
|
21
|
+
const SERIALIZATIONS = ['transact', 'write-lock', 'instance-claim'];
|
|
22
|
+
/** Parse + validate the table. A malformed table throws at import: a table the
|
|
23
|
+
* gate cannot read must never degrade into "no declared write sites". */
|
|
24
|
+
function parseTable(raw) {
|
|
25
|
+
// The input is UNKNOWN JSON, so each narrowing below is a real runtime check (and each later check
|
|
26
|
+
// is written so the previous one already excluded its negation — the linter rejects tautologies).
|
|
27
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
28
|
+
throw new Error('evolution-core: persisted-write-inventory.json must be an object with { symbols, sites }');
|
|
29
|
+
}
|
|
30
|
+
const table = raw;
|
|
31
|
+
// typeof null === 'object', so the null test stays — and it comes FIRST: after `typeof !== 'object'`
|
|
32
|
+
// TypeScript has already dropped null, and the linter is right to call that test a tautology.
|
|
33
|
+
if (table.symbols === null || typeof table.symbols !== 'object' || Array.isArray(table.symbols)) {
|
|
34
|
+
throw new Error('evolution-core: persisted-write-inventory.json needs a symbols legend (the roots every path is written against)');
|
|
35
|
+
}
|
|
36
|
+
for (const [symbol, entry] of Object.entries(table.symbols)) {
|
|
37
|
+
const problems = [];
|
|
38
|
+
if (!symbol.startsWith('<') || !symbol.endsWith('>'))
|
|
39
|
+
problems.push('symbol name (must be <name>)');
|
|
40
|
+
if (entry === null || typeof entry !== 'object')
|
|
41
|
+
problems.push('entry');
|
|
42
|
+
else {
|
|
43
|
+
const symbolEntry = entry;
|
|
44
|
+
if (typeof symbolEntry.resolver !== 'string' || symbolEntry.resolver === '')
|
|
45
|
+
problems.push('resolver');
|
|
46
|
+
if (symbolEntry.segments !== undefined
|
|
47
|
+
&& (!Array.isArray(symbolEntry.segments) || symbolEntry.segments.some(segment => typeof segment !== 'string')))
|
|
48
|
+
problems.push('segments');
|
|
49
|
+
}
|
|
50
|
+
if (problems.length > 0) {
|
|
51
|
+
throw new Error(`evolution-core: persisted-write-inventory.json symbol ${symbol} is malformed (missing/invalid: ${problems.join(', ')})`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (!Array.isArray(table.sites))
|
|
55
|
+
throw new Error('evolution-core: persisted-write-inventory.json needs a sites array');
|
|
56
|
+
const sites = [];
|
|
57
|
+
for (const [index, entry] of table.sites.entries()) {
|
|
58
|
+
const site = entry;
|
|
59
|
+
const problems = [];
|
|
60
|
+
if (typeof site.id !== 'string' || site.id === '')
|
|
61
|
+
problems.push('id');
|
|
62
|
+
if (typeof site.path !== 'string' || site.path === '')
|
|
63
|
+
problems.push('path');
|
|
64
|
+
if (typeof site.writer !== 'string' || site.writer === '')
|
|
65
|
+
problems.push('writer');
|
|
66
|
+
if (typeof site.marker !== 'string' || site.marker === '')
|
|
67
|
+
problems.push('marker');
|
|
68
|
+
if (typeof site.serializedBy !== 'string' || !SERIALIZATIONS.includes(site.serializedBy))
|
|
69
|
+
problems.push('serializedBy');
|
|
70
|
+
if (site.serializedBy === 'instance-claim' && typeof site.instance !== 'string')
|
|
71
|
+
problems.push('instance');
|
|
72
|
+
if (!Array.isArray(site.state))
|
|
73
|
+
problems.push('state');
|
|
74
|
+
if (problems.length > 0) {
|
|
75
|
+
throw new Error(`evolution-core: persisted-write-inventory.json entry ${index} is malformed (missing/invalid: ${problems.join(', ')})`);
|
|
76
|
+
}
|
|
77
|
+
sites.push({
|
|
78
|
+
id: site.id,
|
|
79
|
+
path: site.path,
|
|
80
|
+
writer: site.writer,
|
|
81
|
+
serializedBy: site.serializedBy,
|
|
82
|
+
marker: site.marker,
|
|
83
|
+
...(typeof site.instance === 'string' ? { instance: site.instance } : {}),
|
|
84
|
+
state: site.state,
|
|
85
|
+
note: typeof site.note === 'string' ? site.note : '',
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
const ids = new Set(sites.map(site => site.id));
|
|
89
|
+
if (ids.size !== sites.length)
|
|
90
|
+
throw new Error('evolution-core: persisted-write-inventory.json declares duplicate site ids');
|
|
91
|
+
// Every entry was validated above, so the unknown JSON is now the table's own shape.
|
|
92
|
+
return { symbols: table.symbols, sites };
|
|
93
|
+
}
|
|
94
|
+
/** Instance keys held by writer services — the single source shared by the
|
|
95
|
+
* claim site and the inventory row that declares it (rule N20 checks the row's
|
|
96
|
+
* `instance` names one of these). */
|
|
97
|
+
export const INSTANCE_KEYS = {
|
|
98
|
+
/** The per-home curator: report writing + the retention sweep. */
|
|
99
|
+
curator: 'evolution-curator',
|
|
100
|
+
};
|
|
101
|
+
// Both caches are READONLY on purpose: the table's own shape is readonly, and copying it into a
|
|
102
|
+
// mutable array would be a second source of truth for the same rows.
|
|
103
|
+
let cachedSites;
|
|
104
|
+
let cachedSymbols;
|
|
105
|
+
/**
|
|
106
|
+
* The declared persisted write sites, in file order.
|
|
107
|
+
*
|
|
108
|
+
* v43 audit (S2-3 / P1-10): this used to be a module-scope readFileSync plus
|
|
109
|
+
* parse, so an unshipped asset threw AT IMPORT — one absent file took the whole
|
|
110
|
+
* family's load down (0.3.79 shipped a tarball without this asset and every
|
|
111
|
+
* package failed to load). The read is lazy now: importing the package never
|
|
112
|
+
* fails on this asset, while the first caller still gets a loud, descriptive
|
|
113
|
+
* failure instead of an empty table ("no declared write sites" would silently
|
|
114
|
+
* disable rule N20).
|
|
115
|
+
* @internal S2.4 (PLAN 2026-09-16): no production caller — the arch guard
|
|
116
|
+
* reads the JSON asset directly; the tests are the only in-tree consumers.
|
|
117
|
+
* @returns the sites, in file order.
|
|
118
|
+
*/
|
|
119
|
+
export function persistedWriteSites() {
|
|
120
|
+
if (cachedSites !== undefined)
|
|
121
|
+
return cachedSites;
|
|
122
|
+
let raw;
|
|
123
|
+
try {
|
|
124
|
+
raw = readFileSync(fileURLToPath(SITES_URL), 'utf8');
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
const cause = error instanceof Error ? error.message : String(error);
|
|
128
|
+
throw new Error(`evolution-core: persisted-write-inventory.json is unreadable (${cause}) — the package asset is missing or was not shipped; the write-inventory rules cannot be evaluated without it`);
|
|
129
|
+
}
|
|
130
|
+
const table = parseTable(JSON.parse(raw));
|
|
131
|
+
cachedSites = table.sites;
|
|
132
|
+
cachedSymbols = table.symbols;
|
|
133
|
+
return table.sites;
|
|
134
|
+
}
|
|
135
|
+
/** The inventory's symbol legend — the roots every declared path is written against, with the
|
|
136
|
+
* segments the gate checks against the resolver's source.
|
|
137
|
+
* @internal no production caller — the gates read the JSON asset directly. */
|
|
138
|
+
export function writePathSymbols() {
|
|
139
|
+
persistedWriteSites();
|
|
140
|
+
return cachedSymbols ?? {};
|
|
141
|
+
}
|
|
142
|
+
/** Sites serialized by the per-home instance claim, with their instance keys.
|
|
143
|
+
* @internal S2.4 (PLAN 2026-09-16): no production caller — the arch guard
|
|
144
|
+
* reads the JSON asset directly; the tests are the only in-tree consumers. */
|
|
145
|
+
export function instanceClaimedWriteSites() {
|
|
146
|
+
return persistedWriteSites().filter((site) => site.serializedBy === 'instance-claim');
|
|
147
|
+
}
|
|
148
|
+
/** One declared site by id. An undeclared id throws — a stale caller must fail
|
|
149
|
+
* loud rather than read "nothing is declared".
|
|
150
|
+
* @internal S2.4 (PLAN 2026-09-16): no production caller — the arch guard
|
|
151
|
+
* reads the JSON asset directly; the tests are the only in-tree consumers. */
|
|
152
|
+
export function persistedWriteSite(id) {
|
|
153
|
+
const site = persistedWriteSites().find(candidate => candidate.id === id);
|
|
154
|
+
if (site === undefined)
|
|
155
|
+
throw new Error(`evolution-core: no persisted write site "${id}" in persisted-write-inventory.json`);
|
|
156
|
+
return site;
|
|
157
|
+
}
|
|
158
|
+
//# sourceMappingURL=write-inventory.js.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.
|
|
4
|
+
"version": "0.27.1",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -18,12 +18,28 @@
|
|
|
18
18
|
"types": "./lib/types/index.d.ts",
|
|
19
19
|
"default": "./lib/index.js"
|
|
20
20
|
},
|
|
21
|
+
"./composition": {
|
|
22
|
+
"types": "./lib/types/composition/index.d.ts",
|
|
23
|
+
"default": "./lib/composition/index.js"
|
|
24
|
+
},
|
|
25
|
+
"./subagent": {
|
|
26
|
+
"types": "./lib/types/subagent/index.d.ts",
|
|
27
|
+
"default": "./lib/subagent/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./http-routes": {
|
|
30
|
+
"types": "./lib/types/http-routes.d.ts",
|
|
31
|
+
"default": "./lib/http-routes.js"
|
|
32
|
+
},
|
|
33
|
+
"./restrictor": {
|
|
34
|
+
"types": "./lib/types/restrictor.d.ts",
|
|
35
|
+
"default": "./lib/restrictor.js"
|
|
36
|
+
},
|
|
21
37
|
"./package.json": "./package.json",
|
|
22
38
|
"./row-overrides.json": "./row-overrides.json",
|
|
23
39
|
"./persisted-write-inventory.json": "./persisted-write-inventory.json"
|
|
24
40
|
},
|
|
25
41
|
"files": [
|
|
26
|
-
"lib
|
|
42
|
+
"lib/**/*.js",
|
|
27
43
|
"lib/types/**/*.d.ts",
|
|
28
44
|
"row-overrides.json",
|
|
29
45
|
"persisted-write-inventory.json"
|
|
@@ -31,18 +47,30 @@
|
|
|
31
47
|
"license": "MIT",
|
|
32
48
|
"dependencies": {
|
|
33
49
|
"yaml": "^2.4.2",
|
|
34
|
-
"zod": "^4.4.3"
|
|
50
|
+
"zod": "^4.4.3",
|
|
51
|
+
"js-yaml": "^4.1.0"
|
|
35
52
|
},
|
|
36
53
|
"peerDependencies": {
|
|
37
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
38
|
-
"@deepseek-ai/dsh-scope": "^0.2.
|
|
39
|
-
"@deepseek-ai/dsh-session": "^0.2.
|
|
40
|
-
"@deepseek-ai/dsh-session-projection": "^0.2.
|
|
54
|
+
"@deepseek-ai/cordis": "^4.0.5",
|
|
55
|
+
"@deepseek-ai/dsh-scope": "^0.2.1-rc.1",
|
|
56
|
+
"@deepseek-ai/dsh-session": "^0.2.1-rc.1",
|
|
57
|
+
"@deepseek-ai/dsh-session-projection": "^0.2.1-rc.1",
|
|
58
|
+
"@deepseek-ai/dsh-agent": "^0.2.1-rc.1",
|
|
59
|
+
"@deepseek-ai/dsh-subagent": "^0.2.1-rc.1",
|
|
60
|
+
"@deepseek-ai/dsh-tools": "^0.2.1-rc.1"
|
|
41
61
|
},
|
|
42
62
|
"devDependencies": {
|
|
43
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
44
|
-
"@deepseek-ai/dsh-scope": "^0.2.
|
|
45
|
-
"@deepseek-ai/dsh-session": "^0.2.
|
|
46
|
-
"@deepseek-ai/dsh-session-projection": "^0.2.
|
|
63
|
+
"@deepseek-ai/cordis": "^4.0.5",
|
|
64
|
+
"@deepseek-ai/dsh-scope": "^0.2.1-rc.1",
|
|
65
|
+
"@deepseek-ai/dsh-session": "^0.2.1-rc.1",
|
|
66
|
+
"@deepseek-ai/dsh-session-projection": "^0.2.1-rc.1",
|
|
67
|
+
"@deepseek-ai/dsh-agent": "^0.2.1-rc.1",
|
|
68
|
+
"@deepseek-ai/dsh-subagent": "^0.2.1-rc.1",
|
|
69
|
+
"@deepseek-ai/dsh-tools": "^0.2.1-rc.1",
|
|
70
|
+
"@types/js-yaml": "^4.0.9",
|
|
71
|
+
"js-yaml": "^4.1.0"
|
|
72
|
+
},
|
|
73
|
+
"engines": {
|
|
74
|
+
"dsh": ">=0.2.1-rc.1 <0.2.2-0"
|
|
47
75
|
}
|
|
48
76
|
}
|