@lmzhen/dsh-evolution-core 0.26.15 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/README.md +16 -1
  2. package/lib/composition/index.js +3 -0
  3. package/lib/composition-D8Y3-ROd.js +604 -0
  4. package/lib/ensure-MEYMj9jn.js +222 -0
  5. package/lib/errors-Br2-uUSK.js +57 -0
  6. package/lib/http-routes-BP-S3KV-.js +149 -0
  7. package/lib/http-routes.js +2 -0
  8. package/lib/index.js +777 -840
  9. package/lib/restrictor.js +37 -0
  10. package/lib/subagent/index.js +186 -0
  11. package/lib/types/citations.js +192 -0
  12. package/lib/types/{preset-composition.d.ts → composition/composer.d.ts} +2 -2
  13. package/lib/types/composition/composer.js +681 -0
  14. package/lib/types/composition/ensure.d.ts +10 -0
  15. package/lib/types/composition/ensure.js +95 -0
  16. package/lib/types/composition/index.d.ts +5 -0
  17. package/lib/types/composition/index.js +7 -0
  18. package/lib/types/composition/preset-ids.d.ts +17 -0
  19. package/lib/types/composition/preset-ids.js +21 -0
  20. package/lib/types/composition/restricted-preset.d.ts +36 -0
  21. package/lib/types/composition/restricted-preset.js +42 -0
  22. package/lib/types/constants.js +223 -0
  23. package/lib/types/cost.js +67 -0
  24. package/lib/types/curator.d.ts +2 -0
  25. package/lib/types/curator.js +346 -0
  26. package/lib/types/diagnostics.d.ts +26 -0
  27. package/lib/types/diagnostics.js +23 -0
  28. package/lib/types/document-body.js +91 -0
  29. package/lib/types/drift-signals.js +319 -0
  30. package/lib/types/env.js +25 -0
  31. package/lib/types/errors.d.ts +4 -1
  32. package/lib/types/errors.js +59 -0
  33. package/lib/types/events.js +16 -0
  34. package/lib/types/evidence.js +65 -0
  35. package/lib/types/evolution-events.js +479 -0
  36. package/lib/types/fold-untrusted.js +44 -0
  37. package/lib/types/frontmatter.js +623 -0
  38. package/lib/types/fuzzy-match.js +135 -0
  39. package/lib/types/gates.js +51 -0
  40. package/lib/types/guards.js +27 -0
  41. package/lib/types/health-baseline.js +63 -0
  42. package/lib/types/http-routes.d.ts +67 -35
  43. package/lib/types/http-routes.js +135 -0
  44. package/lib/types/index.d.ts +13 -2
  45. package/lib/types/index.js +101 -0
  46. package/lib/types/instance-scope.js +69 -0
  47. package/lib/types/io.js +675 -0
  48. package/lib/types/learn-prompt.js +42 -0
  49. package/lib/types/limits.js +63 -0
  50. package/lib/types/live.js +107 -0
  51. package/lib/types/long-runs.js +96 -0
  52. package/lib/types/memory-store.d.ts +0 -16
  53. package/lib/types/memory-store.js +606 -0
  54. package/lib/types/mutations.js +95 -0
  55. package/lib/types/namespace-migration.js +156 -0
  56. package/lib/types/numeric.js +47 -0
  57. package/lib/types/opt-in.js +101 -0
  58. package/lib/types/package-identity.d.ts +14 -0
  59. package/lib/types/package-identity.js +47 -0
  60. package/lib/types/params.js +328 -0
  61. package/lib/types/payload.js +36 -0
  62. package/lib/types/platform-line.generated.d.ts +10 -0
  63. package/lib/types/platform-line.generated.js +12 -0
  64. package/lib/types/platform-services.d.ts +2 -2
  65. package/lib/types/platform-services.js +52 -0
  66. package/lib/types/probe.d.ts +9 -3
  67. package/lib/types/probe.js +122 -0
  68. package/lib/types/prompts.js +409 -0
  69. package/lib/types/quality.js +308 -0
  70. package/lib/types/reachability.d.ts +46 -0
  71. package/lib/types/reachability.js +122 -0
  72. package/lib/types/redact.js +257 -0
  73. package/lib/types/reference-rewrite.js +144 -0
  74. package/lib/types/reports.js +110 -0
  75. package/lib/types/restrictor.d.ts +11 -0
  76. package/lib/types/restrictor.js +48 -0
  77. package/lib/types/review-channel.js +59 -0
  78. package/lib/types/run-registry.js +345 -0
  79. package/lib/types/scope.js +7 -0
  80. package/lib/types/serial.js +17 -0
  81. package/lib/types/session-fold.d.ts +15 -0
  82. package/lib/types/session-fold.js +28 -0
  83. package/lib/types/session-projection.js +295 -0
  84. package/lib/types/signals.js +209 -0
  85. package/lib/types/skill-discovery.js +66 -0
  86. package/lib/types/skill-health.js +134 -0
  87. package/lib/types/skill-history.js +745 -0
  88. package/lib/types/skill-lint.js +112 -0
  89. package/lib/types/skill-reads.js +67 -0
  90. package/lib/types/skill-row-cells.js +123 -0
  91. package/lib/types/skill-store.d.ts +34 -3
  92. package/lib/types/skill-store.js +3528 -0
  93. package/lib/types/state-store.js +60 -0
  94. package/lib/types/store-ledger.d.ts +44 -0
  95. package/lib/types/store-ledger.js +117 -0
  96. package/lib/types/subagent/channel.d.ts +4 -0
  97. package/lib/types/subagent/channel.js +131 -0
  98. package/lib/types/subagent/index.d.ts +4 -0
  99. package/lib/types/subagent/index.js +5 -0
  100. package/lib/types/subagent/readiness.d.ts +6 -0
  101. package/lib/types/subagent/readiness.js +35 -0
  102. package/lib/types/subagent/types.d.ts +58 -0
  103. package/lib/types/subagent/types.js +2 -0
  104. package/lib/types/threats.js +338 -0
  105. package/lib/types/tool-dispatch.d.ts +5 -0
  106. package/lib/types/tool-dispatch.js +535 -0
  107. package/lib/types/tree-write.d.ts +53 -0
  108. package/lib/types/tree-write.js +40 -0
  109. package/lib/types/unit-members.js +121 -0
  110. package/lib/types/usage.js +474 -0
  111. package/lib/types/write-authority.d.ts +87 -0
  112. package/lib/types/write-authority.js +23 -0
  113. package/lib/types/write-gates.d.ts +111 -0
  114. package/lib/types/write-gates.js +344 -0
  115. package/lib/types/write-inventory.js +158 -0
  116. package/package.json +39 -11
@@ -0,0 +1,87 @@
1
+ /**
2
+ * G1/1.1: the ONE authority behind a write.
3
+ *
4
+ * Before this type existed the inputs of the can-I-write decision came from two places with two
5
+ * lifecycles: `origin` (plus the policy snapshot and the confirm seam) travelled in the tool
6
+ * package gate context, while `threatExemptLabels` rode the SkillLibrary constructor. Callers that
7
+ * reached the bytes through another path built neither, and the mutators defaulted `origin` to
8
+ * foreground - so an omitted authority silently read as the operator acting, and no mechanism said
9
+ * otherwise (measured: two entry points in the same curator file, one passing background_review and
10
+ * one defaulting).
11
+ *
12
+ * The shape below is where those inputs converge. The follow-up batch makes it a REQUIRED argument
13
+ * of every mutator (content and lifecycle alike) and deletes the defaults, so an omission is a
14
+ * compile error instead of a silent claim about who wrote the bytes.
15
+ */
16
+ import type { WriteOrigin } from './skill-store.ts';
17
+ /**
18
+ * The confirmation contract, HOISTED here from tool-skill-manage (G1/1.1 follow-up): the authority
19
+ * carries the confirm seam, so the seam's vocabulary lives with it. tool-skill-manage re-exports
20
+ * these names from write-gates.ts unchanged, so nothing importing them from there moves.
21
+ */
22
+ export type WriteConfirmMode = 'auto' | 'ask' | 'timeout';
23
+ /** The mode a deployment that configures nothing gets. */
24
+ export declare const DEFAULT_WRITE_CONFIRM_MODE: WriteConfirmMode;
25
+ /** Seconds a timeout-mode prompt waits before the write is cancelled. */
26
+ export declare const DEFAULT_WRITE_CONFIRM_TIMEOUT_SECONDS = 120;
27
+ /** What the operator is told about the target before answering. */
28
+ export interface ConfirmContext {
29
+ readonly description?: string | undefined;
30
+ readonly versions?: number | undefined;
31
+ }
32
+ /** One confirm question, as the gate writes it and the seam asks it. */
33
+ export interface WriteConfirmRequest {
34
+ readonly action: string;
35
+ readonly name: string;
36
+ readonly question: {
37
+ readonly id: string;
38
+ readonly header: string;
39
+ readonly question: string;
40
+ readonly options: readonly {
41
+ readonly label: string;
42
+ }[];
43
+ };
44
+ readonly confirmLabel: string;
45
+ readonly signal?: AbortSignal;
46
+ }
47
+ /** Ask the human to confirm one irreversible skill write. MUST be total: every unavailability
48
+ * resolves to true, so no failure of the question service blocks the operator's own write. */
49
+ export type WriteConfirm = (request: WriteConfirmRequest) => Promise<boolean>;
50
+ /**
51
+ * The batch-one migration aid: every call site becomes ONE of these instead of hand-building the
52
+ * object - so the shape cannot drift between callers, and each site still has to say its own
53
+ * origin out loud (origin is required here on purpose: the whole point is that no caller inherits
54
+ * a default identity).
55
+ */
56
+ export declare function authorityFrom(input: {
57
+ origin: WriteOrigin;
58
+ threatExemptLabels?: readonly string[];
59
+ protectedNames?: readonly string[] | undefined;
60
+ readNames?: ReadonlySet<string> | undefined;
61
+ confirm?: WriteConfirm;
62
+ confirmMode?: WriteConfirmMode;
63
+ confirmTimeoutSeconds?: number;
64
+ /** Optional: a call site that has nothing to report may leave it out, and the gates then stay
65
+ * silent instead of forcing every one of the twelve construction points to invent a logger. */
66
+ warn?: (message: string) => void;
67
+ }): WriteAuthority;
68
+ export interface WriteAuthority {
69
+ /** Who is asking: the operator own session, a background review, the curator, the graph face. */
70
+ readonly origin: WriteOrigin;
71
+ /** Labels the threat scan must not refuse (P2-18). Migrated here from the library constructor:
72
+ * one decision must not read its policy from two objects. */
73
+ readonly threatExemptLabels: readonly string[];
74
+ /** The policy snapshot protected list; undefined when no policy row is mounted. */
75
+ readonly protectedNames: readonly string[] | undefined;
76
+ /** Names this session read successfully; undefined when the session log is not readable. */
77
+ readonly readNames: ReadonlySet<string> | undefined;
78
+ /** The human confirm seam, read only where a question is actually asked. */
79
+ readonly confirm?: WriteConfirm;
80
+ /** skillWriteConfirm: auto (the default) skips the question entirely. */
81
+ readonly confirmMode?: WriteConfirmMode;
82
+ /** skillWriteConfirmTimeoutSeconds, read only in timeout mode. */
83
+ readonly confirmTimeoutSeconds?: number;
84
+ /** Report a degraded gate; must not throw. */
85
+ readonly warn: (message: string) => void;
86
+ }
87
+ //# sourceMappingURL=write-authority.d.ts.map
@@ -0,0 +1,23 @@
1
+ /** The mode a deployment that configures nothing gets. */
2
+ export const DEFAULT_WRITE_CONFIRM_MODE = 'auto';
3
+ /** Seconds a timeout-mode prompt waits before the write is cancelled. */
4
+ export const DEFAULT_WRITE_CONFIRM_TIMEOUT_SECONDS = 120;
5
+ /**
6
+ * The batch-one migration aid: every call site becomes ONE of these instead of hand-building the
7
+ * object - so the shape cannot drift between callers, and each site still has to say its own
8
+ * origin out loud (origin is required here on purpose: the whole point is that no caller inherits
9
+ * a default identity).
10
+ */
11
+ export function authorityFrom(input) {
12
+ return {
13
+ origin: input.origin,
14
+ threatExemptLabels: input.threatExemptLabels ?? [],
15
+ protectedNames: input.protectedNames,
16
+ readNames: input.readNames,
17
+ ...input.confirm === undefined ? {} : { confirm: input.confirm },
18
+ ...input.confirmMode === undefined ? {} : { confirmMode: input.confirmMode },
19
+ ...input.confirmTimeoutSeconds === undefined ? {} : { confirmTimeoutSeconds: input.confirmTimeoutSeconds },
20
+ warn: input.warn ?? (() => { }),
21
+ };
22
+ }
23
+ //# sourceMappingURL=write-authority.js.map
@@ -0,0 +1,111 @@
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 { type WriteConfirm, type WriteConfirmMode } from './write-authority.ts';
26
+ import type { ConfirmContext } from './write-authority.ts';
27
+ import { type WriteOrigin } from './index.ts';
28
+ /** The point whose verdict is being taken. */
29
+ export type WriteGatePoint = 'admission' | 'execution';
30
+ /** What every gate reads. */
31
+ /**
32
+ * G1/1.2: the policy snapshot's protected list, read through an `unknown` boundary.
33
+ *
34
+ * This used to be package-private to tool-skill-manage, which is why the graph commands could not ask
35
+ * the same question and ended up enforcing nothing. It lives here so every write entry point reads the
36
+ * SAME list from the SAME place; the `unknown` boundary stays because the Context augmentation types the
37
+ * getter non-optionally while at runtime the policy row may be absent.
38
+ * @param policy - `ctx.get('evolutionPolicy')`, or anything else when the row is not mounted.
39
+ * @returns the protected names, or undefined when no policy snapshot is readable.
40
+ */
41
+ /**
42
+ * G1/1.2: what a write entry point must supply for the shared admission sequence.
43
+ *
44
+ * The gates need policy, confirm, the session's read set and a warn sink; every caller can obtain those
45
+ * from its own `ctx`, but assembling them into a valid `WriteGateContext` is the seam's business, not each
46
+ * caller's. Without this, "all five entry points call the same entry" means five hand-built contexts and
47
+ * five chances to leave `protectedNames` out - which is exactly how the graph commands ended up enforcing
48
+ * nothing.
49
+ */
50
+ export interface SkillWriteGateServices {
51
+ /** `ctx.get('evolutionPolicy')`, or anything else when the row is not mounted. */
52
+ readonly policy: unknown;
53
+ /** The human confirm seam; `undefined` refuses to ask (the gates then refuse the write). */
54
+ readonly confirm: WriteConfirm | undefined;
55
+ /** Names this session read successfully; `undefined` when the session log is not readable. */
56
+ readonly readNames: ReadonlySet<string> | undefined;
57
+ /** Report a degraded gate; must not throw. */
58
+ readonly warn: (message: string) => void;
59
+ /** The registry's `skillWriteConfirm`; `auto` (the default) skips the question. */
60
+ readonly confirmMode?: WriteConfirmMode;
61
+ /** The registry's `skillWriteConfirmTimeoutSeconds`, read only in `timeout` mode. */
62
+ readonly confirmTimeoutSeconds?: number;
63
+ /** Lazily read the target's own words for the confirmation card. */
64
+ readonly describeTarget?: (() => Promise<ConfirmContext | undefined>) | undefined;
65
+ }
66
+ /**
67
+ * Assemble a gate context from a caller's services. The protected list is read HERE, so no caller can
68
+ * forget it or read it from a second place.
69
+ * @param services - the four required services plus the optional registry knobs.
70
+ * @param request - the point asking, the arguments under test, and the write origin.
71
+ * @returns a context the shared sequence accepts.
72
+ */
73
+ export declare function skillWriteGateContext(services: SkillWriteGateServices, request: {
74
+ readonly point: WriteGatePoint;
75
+ readonly args: unknown;
76
+ readonly origin: WriteOrigin;
77
+ }): WriteGateContext;
78
+ export declare function protectedSkillNamesOf(policy: unknown): readonly string[] | undefined;
79
+ export interface WriteGateContext {
80
+ /** The point asking: the tool's admission of a call, or its execution at the bytes. */
81
+ readonly point: WriteGatePoint;
82
+ /** The parsed (admission) or stored (replay) arguments — unvalidated, hence the shape gate. */
83
+ readonly args: unknown;
84
+ /** The library write origin: `foreground` is the operator's own session. */
85
+ readonly origin: WriteOrigin;
86
+ /** The policy snapshot's protected list; `undefined` when no policy row is mounted. */
87
+ readonly protectedNames: readonly string[] | undefined;
88
+ /** Names this session read successfully; `undefined` when the session log is not readable. */
89
+ readonly readNames: ReadonlySet<string> | undefined;
90
+ /** The human confirm seam — read only by the gates that apply to `'admission'`. */
91
+ readonly confirm: WriteConfirm | undefined;
92
+ /** The registry's `skillWriteConfirm`: `auto` (the default) skips the question entirely. */
93
+ readonly confirmMode?: WriteConfirmMode;
94
+ /** The registry's `skillWriteConfirmTimeoutSeconds`, read only in `timeout` mode. */
95
+ readonly confirmTimeoutSeconds?: number;
96
+ /** Read the target's own words for the confirmation card. LAZY on purpose: a write that is not
97
+ * asked about must not pay a read for a question nobody sees. Absent means "say nothing extra". */
98
+ readonly describeTarget?: (() => Promise<ConfirmContext | undefined>) | undefined;
99
+ /** Report a degraded gate; must not throw. The implementation decides how often it speaks — the
100
+ * shipped seam latches once per PROCESS, because the conditions it reports (no question service,
101
+ * an unreadable session log) belong to the deployment, not to one write. */
102
+ readonly warn: (message: string) => void;
103
+ }
104
+ /**
105
+ * Run the sequence for one write attempt.
106
+ * @param context - the asking point, the write, and the seams the human-facing gate needs.
107
+ * @returns the FIRST refusal, or null when every applicable gate passed. Both points return the
108
+ * message as the tool result unchanged, so the two entries refuse with identical wording.
109
+ */
110
+ export declare function runWriteGates(context: WriteGateContext): Promise<string | null>;
111
+ //# sourceMappingURL=write-gates.d.ts.map
@@ -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