wicked-crew 0.7.24 → 0.7.26
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/dist/api/endpoint-manifest-live.d.ts.map +1 -1
- package/dist/api/endpoint-manifest-live.js +5 -0
- package/dist/api/endpoint-manifest-live.js.map +1 -1
- package/dist/api/eval-compare.d.ts +306 -0
- package/dist/api/eval-compare.d.ts.map +1 -0
- package/dist/api/eval-compare.js +610 -0
- package/dist/api/eval-compare.js.map +1 -0
- package/dist/api/eval-sample.d.ts +158 -0
- package/dist/api/eval-sample.d.ts.map +1 -0
- package/dist/api/eval-sample.js +88 -0
- package/dist/api/eval-sample.js.map +1 -0
- package/dist/api/routes.d.ts +5 -0
- package/dist/api/routes.d.ts.map +1 -1
- package/dist/api/routes.js +31 -0
- package/dist/api/routes.js.map +1 -1
- package/dist/api/run-files.d.ts +12 -2
- package/dist/api/run-files.d.ts.map +1 -1
- package/dist/api/run-files.js +17 -5
- package/dist/api/run-files.js.map +1 -1
- package/dist/api/server.d.ts +20 -0
- package/dist/api/server.d.ts.map +1 -1
- package/dist/api/server.js +62 -9
- package/dist/api/server.js.map +1 -1
- package/dist/api/skills.d.ts +88 -0
- package/dist/api/skills.d.ts.map +1 -0
- package/dist/api/skills.js +264 -0
- package/dist/api/skills.js.map +1 -0
- package/dist/api/testing.d.ts +2 -75
- package/dist/api/testing.d.ts.map +1 -1
- package/dist/api/testing.js +14 -28
- package/dist/api/testing.js.map +1 -1
- package/dist/core/adapter.d.ts +42 -0
- package/dist/core/adapter.d.ts.map +1 -1
- package/dist/core/adapter.js +83 -12
- package/dist/core/adapter.js.map +1 -1
- package/dist/interactive/bridge-root.d.ts +117 -7
- package/dist/interactive/bridge-root.d.ts.map +1 -1
- package/dist/interactive/bridge-root.js +229 -9
- package/dist/interactive/bridge-root.js.map +1 -1
- package/dist/interactive/doc-delete-routes.d.ts +2 -0
- package/dist/interactive/doc-delete-routes.d.ts.map +1 -1
- package/dist/interactive/doc-delete-routes.js +3 -19
- package/dist/interactive/doc-delete-routes.js.map +1 -1
- package/dist/interactive/doc-list-routes.d.ts +42 -0
- package/dist/interactive/doc-list-routes.d.ts.map +1 -0
- package/dist/interactive/doc-list-routes.js +165 -0
- package/dist/interactive/doc-list-routes.js.map +1 -0
- package/dist/interactive/project-root.d.ts +36 -0
- package/dist/interactive/project-root.d.ts.map +1 -0
- package/dist/interactive/project-root.js +45 -0
- package/dist/interactive/project-root.js.map +1 -0
- package/dist/interactive/proxy-routes.d.ts +4 -1
- package/dist/interactive/proxy-routes.d.ts.map +1 -1
- package/dist/interactive/proxy-routes.js +4 -22
- package/dist/interactive/proxy-routes.js.map +1 -1
- package/dist/projects/default-project.d.ts +10 -0
- package/dist/projects/default-project.d.ts.map +1 -0
- package/dist/projects/default-project.js +10 -0
- package/dist/projects/default-project.js.map +1 -0
- package/dist/projects/routes.d.ts +0 -2
- package/dist/projects/routes.d.ts.map +1 -1
- package/dist/projects/routes.js +1 -2
- package/dist/projects/routes.js.map +1 -1
- package/dist/skills/bundle.d.ts +94 -0
- package/dist/skills/bundle.d.ts.map +1 -0
- package/dist/skills/bundle.js +187 -0
- package/dist/skills/bundle.js.map +1 -0
- package/dist/skills/contain.d.ts +57 -0
- package/dist/skills/contain.d.ts.map +1 -0
- package/dist/skills/contain.js +90 -0
- package/dist/skills/contain.js.map +1 -0
- package/dist/skills/core-closure.d.ts +83 -0
- package/dist/skills/core-closure.d.ts.map +1 -0
- package/dist/skills/core-closure.js +130 -0
- package/dist/skills/core-closure.js.map +1 -0
- package/dist/skills/engine-env.d.ts +48 -0
- package/dist/skills/engine-env.d.ts.map +1 -0
- package/dist/skills/engine-env.js +69 -0
- package/dist/skills/engine-env.js.map +1 -0
- package/dist/skills/frontmatter.d.ts +77 -0
- package/dist/skills/frontmatter.d.ts.map +1 -0
- package/dist/skills/frontmatter.js +190 -0
- package/dist/skills/frontmatter.js.map +1 -0
- package/dist/skills/guards.d.ts +72 -0
- package/dist/skills/guards.d.ts.map +1 -0
- package/dist/skills/guards.js +147 -0
- package/dist/skills/guards.js.map +1 -0
- package/dist/skills/live-generations.d.ts +80 -0
- package/dist/skills/live-generations.d.ts.map +1 -0
- package/dist/skills/live-generations.js +163 -0
- package/dist/skills/live-generations.js.map +1 -0
- package/dist/skills/plugin-source.d.ts +109 -0
- package/dist/skills/plugin-source.d.ts.map +1 -0
- package/dist/skills/plugin-source.js +272 -0
- package/dist/skills/plugin-source.js.map +1 -0
- package/dist/skills/refs.d.ts +58 -0
- package/dist/skills/refs.d.ts.map +1 -0
- package/dist/skills/refs.js +133 -0
- package/dist/skills/refs.js.map +1 -0
- package/dist/skills/root-fence.d.ts +53 -0
- package/dist/skills/root-fence.d.ts.map +1 -0
- package/dist/skills/root-fence.js +109 -0
- package/dist/skills/root-fence.js.map +1 -0
- package/dist/skills/root-names.d.ts +42 -0
- package/dist/skills/root-names.d.ts.map +1 -0
- package/dist/skills/root-names.js +44 -0
- package/dist/skills/root-names.js.map +1 -0
- package/dist/skills/runtime.d.ts +130 -0
- package/dist/skills/runtime.d.ts.map +1 -0
- package/dist/skills/runtime.js +246 -0
- package/dist/skills/runtime.js.map +1 -0
- package/dist/skills/store.d.ts +895 -0
- package/dist/skills/store.d.ts.map +1 -0
- package/dist/skills/store.js +3575 -0
- package/dist/skills/store.js.map +1 -0
- package/dist/skills/tree.d.ts +292 -0
- package/dist/skills/tree.d.ts.map +1 -0
- package/dist/skills/tree.js +645 -0
- package/dist/skills/tree.js.map +1 -0
- package/dist/skills/venv.d.ts +52 -0
- package/dist/skills/venv.d.ts.map +1 -0
- package/dist/skills/venv.js +80 -0
- package/dist/skills/venv.js.map +1 -0
- package/dist/studio/assets/index-CK1uJWs2.css +32 -0
- package/dist/studio/assets/index-FcZg3WDR.js +539 -0
- package/dist/studio/index.html +2 -2
- package/dist/studio/testid-inventory.json +611 -7
- package/endpoint-manifest.json +183 -1
- package/package.json +4 -3
- package/dist/studio/assets/index-BmXt63Ij.js +0 -537
- package/dist/studio/assets/index-eso7X_3n.css +0 -32
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bundle = the DEPENDENCY CLOSURE of the plugin root (design v3 §4), not a directory heuristic:
|
|
3
|
+
*
|
|
4
|
+
* .claude-plugin/{plugin,archetypes,components,specialist,stack-registry}.json
|
|
5
|
+
* the manifest + the runtime catalogs
|
|
6
|
+
* garden's scripts read, BY NAME
|
|
7
|
+
* (`PLUGIN_MANIFEST_FILES`) — plugin.json,
|
|
8
|
+
* archetypes.json and components.json
|
|
9
|
+
* REQUIRED at publish
|
|
10
|
+
* (`REQUIRED_PLUGIN_CATALOGS`); anything
|
|
11
|
+
* else in that directory (marketplace.json,
|
|
12
|
+
* a publish-time listing) is outside
|
|
13
|
+
* skills/** nested layout VERBATIM — never renamed
|
|
14
|
+
* scripts/** minus ci/, wg/ and the wg-… dev tools what `${CLAUDE_PLUGIN_ROOT}/scripts/…` resolves
|
|
15
|
+
* schemas/** `../schemas/evidence.json` links
|
|
16
|
+
* docs/examples/** `qe/refs/campaign.md` → the copyable workflows
|
|
17
|
+
* pyproject.toml, uv.lock `uv run` from the plugin root needs them
|
|
18
|
+
*
|
|
19
|
+
* Hooks, the site, tests, `.venv`, the operator's `_meta/`, and everything else a plugin checkout
|
|
20
|
+
* carries never come along. The content hash of THIS set is the baseline's identity.
|
|
21
|
+
*
|
|
22
|
+
* # ONE allowlist (codex round 6 on #480; the exact spelling — codex round 7)
|
|
23
|
+
*
|
|
24
|
+
* `inBundleClosure` is the closure as a predicate over a plugin-relative path — the single source
|
|
25
|
+
* three consumers share: the seed/refresh copy (`pluginBundleFiles` keeps only members), the support
|
|
26
|
+
* file API (`store.resolveSupportFile` refuses a path outside it with an `outside-closure` blocked
|
|
27
|
+
* envelope) and publish/analyze (a file found under `effective/` outside it — a direct filesystem
|
|
28
|
+
* edit — is a BLOCKING `outside-closure` finding naming the path). What the seed would not copy,
|
|
29
|
+
* the API cannot add and a snapshot never ships. The `.claude-plugin` half is an allowlist BY NAME —
|
|
30
|
+
* design amendment v3.5 §1, adjudicated against the live 12.32.0 layout (six files there; five are
|
|
31
|
+
* runtime catalogs garden's scripts read — `specialist.json` by `scripts/crew/specialist_discovery.py`,
|
|
32
|
+
* `stack-registry.json` by the pack/capability registries; `marketplace.json` is a publish-time
|
|
33
|
+
* listing), superseding the v3 "three files" spelling that predated the catalog audit; `scripts/wg/`
|
|
34
|
+
* IS a directory in the live plugin, so it is excluded as a directory alongside `ci/`, and the
|
|
35
|
+
* `wg-…` prefix exclusion stays.
|
|
36
|
+
*
|
|
37
|
+
* # No-follow BELOW the source root (codex round 6)
|
|
38
|
+
*
|
|
39
|
+
* The plugin root is resolved ONCE (`realPluginRoot` — an operator's symlinked config dir reaches
|
|
40
|
+
* it legitimately); every designated entry below it — each named manifest file, each root file,
|
|
41
|
+
* each bundle directory and everything under it — is lstat-walked, and a symlink among them refuses
|
|
42
|
+
* the whole ingestion by name (`PluginSourceSymlinkError`): nothing is copied. A link inside a
|
|
43
|
+
* PRUNED subtree (`scripts/ci/`, `node_modules/`, `.venv/`) or at a non-designated name
|
|
44
|
+
* (`.claude-plugin/marketplace.json`) is never reached — it is not part of the closure, so it is
|
|
45
|
+
* neither copied nor judged.
|
|
46
|
+
*/
|
|
47
|
+
import { lstatSync } from 'node:fs';
|
|
48
|
+
import { noFollowEntry, PLUGIN_MANIFEST_REL, PluginSourceSymlinkError, realPluginRoot } from './plugin-source.js';
|
|
49
|
+
import { toPosix, walkTree } from './tree.js';
|
|
50
|
+
/** The plugin subdirectory skills live in (and the prefix of every manifest `dir`). */
|
|
51
|
+
export const SKILLS_SUBDIR = 'skills';
|
|
52
|
+
/** The manifest directory. */
|
|
53
|
+
export const PLUGIN_MANIFEST_DIRNAME = '.claude-plugin';
|
|
54
|
+
/**
|
|
55
|
+
* The files under `.claude-plugin/` that ride the bundle — BY NAME (codex round 7, adjudicated on the
|
|
56
|
+
* live 12.32.0 layout): the plugin manifest and the four runtime catalogs garden's scripts read.
|
|
57
|
+
* `marketplace.json` (the marketplace's publish-time listing) and anything else are outside.
|
|
58
|
+
*/
|
|
59
|
+
export const PLUGIN_MANIFEST_FILES = [
|
|
60
|
+
'plugin.json',
|
|
61
|
+
'archetypes.json',
|
|
62
|
+
'components.json',
|
|
63
|
+
'specialist.json',
|
|
64
|
+
'stack-registry.json',
|
|
65
|
+
];
|
|
66
|
+
/**
|
|
67
|
+
* The runtime catalogs every snapshot MUST carry beside `plugin.json` (design v3 §4): garden's
|
|
68
|
+
* runtime reads `archetypes.json` (`scripts/crew/archetypes_v11.py:60-74` raises when it is absent)
|
|
69
|
+
* and `components.json`. Their absence in `effective/` is a BLOCKING publish finding
|
|
70
|
+
* (`missing-plugin-manifest`) — a seed copies what the source has (a source missing them is a
|
|
71
|
+
* defective install the publish names, not a seed failure).
|
|
72
|
+
*/
|
|
73
|
+
export const REQUIRED_PLUGIN_CATALOGS = [
|
|
74
|
+
`${PLUGIN_MANIFEST_DIRNAME}/archetypes.json`,
|
|
75
|
+
`${PLUGIN_MANIFEST_DIRNAME}/components.json`,
|
|
76
|
+
];
|
|
77
|
+
/** Root-level files copied when present. */
|
|
78
|
+
const BUNDLE_ROOT_FILES = ['pyproject.toml', 'uv.lock'];
|
|
79
|
+
/** Directories copied whole (POSIX-relative to the plugin root). */
|
|
80
|
+
const BUNDLE_DIRS = [SKILLS_SUBDIR, 'scripts', 'schemas', 'docs/examples'];
|
|
81
|
+
/** Top-level `scripts/` entries that are dev tooling, never runtime: the `ci/` and `wg/` directories and any `wg-…` maintainer tool. */
|
|
82
|
+
function isScriptsDevDir(rel) {
|
|
83
|
+
return !rel.includes('/') && (rel === 'ci' || rel === 'wg' || rel.startsWith('wg-'));
|
|
84
|
+
}
|
|
85
|
+
/** The human spelling of the closure — for findings and refusals. */
|
|
86
|
+
export const BUNDLE_CLOSURE_SPELLING = `${PLUGIN_MANIFEST_DIRNAME}/{${PLUGIN_MANIFEST_FILES.map((f) => f.replace(/\.json$/, '')).join(',')}}.json, ${SKILLS_SUBDIR}/**, scripts/** (minus ci/, wg/ and wg-*), schemas/**, docs/examples/**, ${BUNDLE_ROOT_FILES.join(', ')}`;
|
|
87
|
+
/**
|
|
88
|
+
* Whether a plugin-relative POSIX path is a member of the bundle closure (module header: the ONE
|
|
89
|
+
* allowlist). Lexical — the caller has already validated the path's shape.
|
|
90
|
+
*/
|
|
91
|
+
export function inBundleClosure(rel) {
|
|
92
|
+
const segments = rel.split('/');
|
|
93
|
+
if (segments.length === 2 && segments[0] === PLUGIN_MANIFEST_DIRNAME)
|
|
94
|
+
return PLUGIN_MANIFEST_FILES.includes(segments[1] ?? '');
|
|
95
|
+
if (segments.length === 1)
|
|
96
|
+
return BUNDLE_ROOT_FILES.includes(rel);
|
|
97
|
+
for (const dir of BUNDLE_DIRS) {
|
|
98
|
+
const prefix = `${dir}/`;
|
|
99
|
+
if (!rel.startsWith(prefix))
|
|
100
|
+
continue;
|
|
101
|
+
if (dir === 'scripts' && isScriptsDevDir(rel.slice(prefix.length).split('/')[0] ?? ''))
|
|
102
|
+
return false;
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
/** The plugin root has no `.claude-plugin/plugin.json`. */
|
|
108
|
+
export class NotAPluginRootError extends Error {
|
|
109
|
+
dir;
|
|
110
|
+
constructor(dir) {
|
|
111
|
+
super(`${dir} has no ${toPosix(PLUGIN_MANIFEST_REL)} — not a plugin root`);
|
|
112
|
+
this.dir = dir;
|
|
113
|
+
this.name = 'NotAPluginRootError';
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* The bundle file set of `pluginRoot`, sorted by `rel` — every member of the closure the source
|
|
118
|
+
* carries, reached without following a link below the once-resolved root (module header). Throws
|
|
119
|
+
* `NotAPluginRootError` (no manifest) or `PluginSourceSymlinkError` (a link among the designated
|
|
120
|
+
* entries — nothing is copied by the caller either).
|
|
121
|
+
*/
|
|
122
|
+
export function pluginBundleFiles(pluginRoot) {
|
|
123
|
+
const root = realPluginRoot(pluginRoot);
|
|
124
|
+
if (root === null)
|
|
125
|
+
throw new NotAPluginRootError(pluginRoot);
|
|
126
|
+
const manifestAbs = noFollowEntry(root, [PLUGIN_MANIFEST_DIRNAME, 'plugin.json'], pluginRoot);
|
|
127
|
+
if (manifestAbs === null || !lstatSync(manifestAbs).isFile())
|
|
128
|
+
throw new NotAPluginRootError(pluginRoot);
|
|
129
|
+
const out = [];
|
|
130
|
+
// The manifest files BY NAME (the dir itself was walked no-follow on the way to plugin.json): a
|
|
131
|
+
// designated name that is a link is refused; a name outside the allowlist is never touched.
|
|
132
|
+
for (const name of PLUGIN_MANIFEST_FILES) {
|
|
133
|
+
const abs = noFollowEntry(root, [PLUGIN_MANIFEST_DIRNAME, name], pluginRoot);
|
|
134
|
+
if (abs !== null && lstatSync(abs).isFile())
|
|
135
|
+
out.push({ rel: `${PLUGIN_MANIFEST_DIRNAME}/${name}`, abs });
|
|
136
|
+
}
|
|
137
|
+
for (const rel of BUNDLE_ROOT_FILES) {
|
|
138
|
+
const abs = noFollowEntry(root, [rel], pluginRoot);
|
|
139
|
+
if (abs !== null && lstatSync(abs).isFile())
|
|
140
|
+
out.push({ rel, abs });
|
|
141
|
+
}
|
|
142
|
+
for (const dir of BUNDLE_DIRS) {
|
|
143
|
+
const abs = noFollowEntry(root, dir.split('/'), pluginRoot);
|
|
144
|
+
if (abs === null || !lstatSync(abs).isDirectory())
|
|
145
|
+
continue;
|
|
146
|
+
// Links are ENUMERATED, never skipped (`walkTree`): a linked `skills/<x>` dir or a linked file
|
|
147
|
+
// inside a skill is refused by name rather than silently left out of the bundle.
|
|
148
|
+
const tree = walkTree(abs, dir === 'scripts' ? isScriptsDevDir : undefined);
|
|
149
|
+
const link = tree.links[0];
|
|
150
|
+
if (link !== undefined)
|
|
151
|
+
throw new PluginSourceSymlinkError(pluginRoot, `${dir}/${link.rel}`, link.target);
|
|
152
|
+
for (const f of tree.files) {
|
|
153
|
+
const rel = `${dir}/${f.rel}`;
|
|
154
|
+
if (inBundleClosure(rel))
|
|
155
|
+
out.push({ rel, abs: f.abs }); // the predicate is the source of truth; the prune above is its fast path
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return out.sort((a, b) => (a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0));
|
|
159
|
+
}
|
|
160
|
+
/** Every `skills/<rel>` dir in a file set that holds a `SKILL.md` (`skills/SKILL.md` itself is not a skill). */
|
|
161
|
+
export function skillDirsOf(files) {
|
|
162
|
+
const dirs = new Set();
|
|
163
|
+
const prefix = `${SKILLS_SUBDIR}/`;
|
|
164
|
+
for (const f of files) {
|
|
165
|
+
if (!f.rel.startsWith(prefix) || !f.rel.endsWith('/SKILL.md'))
|
|
166
|
+
continue;
|
|
167
|
+
const dir = f.rel.slice(0, -'/SKILL.md'.length);
|
|
168
|
+
if (dir !== SKILLS_SUBDIR)
|
|
169
|
+
dirs.add(dir);
|
|
170
|
+
}
|
|
171
|
+
return dirs;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* The skill dir that OWNS a plugin-relative path: the deepest `skillDirs` entry the path sits
|
|
175
|
+
* under, or `null` for a support file. Nested-skill ownership (v3 §6) is exactly this rule.
|
|
176
|
+
*/
|
|
177
|
+
export function owningSkillDir(rel, skillDirs) {
|
|
178
|
+
let best = null;
|
|
179
|
+
for (const dir of skillDirs) {
|
|
180
|
+
if (rel === dir || rel.startsWith(`${dir}/`)) {
|
|
181
|
+
if (best === null || dir.length > best.length)
|
|
182
|
+
best = dir;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return best;
|
|
186
|
+
}
|
|
187
|
+
//# sourceMappingURL=bundle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bundle.js","sourceRoot":"","sources":["../../src/skills/bundle.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill-scoped containment for the `/skills` file manager (design v3 §API).
|
|
3
|
+
*
|
|
4
|
+
* NOT `api/open-path.ts` `isInsideRoot`: that helper FOLLOWS symlinks (a `<root>/link-to-outside`
|
|
5
|
+
* resolves to its real target, which is the right question for "may I open this file the run
|
|
6
|
+
* produced" and the wrong one for "may I write here") and accepts the root itself. Writes into the
|
|
7
|
+
* effective plugin root need the stricter rule:
|
|
8
|
+
*
|
|
9
|
+
* 1. the URL wildcard arrives DECODED EXACTLY ONCE — by Fastify (find-my-way decodes every `%xx`,
|
|
10
|
+
* `%2F` included, before routing; a malformed escape is Fastify's own 400) — and the store
|
|
11
|
+
* never decodes again (codex round 5 on #480: a second decode turned `100%25.txt` into a
|
|
12
|
+
* refused `100%.txt` and the literal filename `a%252Fb.txt` into the path `a/b.txt`);
|
|
13
|
+
* 2. normalize into segments — refuse empty, `.`, `..`, backslashes, NUL, absolute / drive-prefixed;
|
|
14
|
+
* 3. lstat-walk EVERY existing component and refuse a symlink anywhere on the way (the plugin
|
|
15
|
+
* tree carries none; one appearing is either an attack or an accident, and either way the
|
|
16
|
+
* write must not follow it);
|
|
17
|
+
* 4. the root itself is not a file — an empty remainder is refused.
|
|
18
|
+
*
|
|
19
|
+
* Atomic tmp+rename writes are the store's job (`tree.ts` `writeFileAtomic`); this module only
|
|
20
|
+
* decides WHERE. And the walk is necessary, not sufficient (design v3.5 §3; codex round 8): the
|
|
21
|
+
* path it answers is then OPENED with `O_NOFOLLOW` where the platform has it, and the lstat-then-open
|
|
22
|
+
* gap is closed everywhere with a post-open identity check (`fstat` dev/ino against the lstat result
|
|
23
|
+
* — `tree.ts` `openRegularNoFollow`); copies read their source through such an open, never by
|
|
24
|
+
* path; a staged tree is re-walked and re-hashed after the copy and before the rename into place.
|
|
25
|
+
* Residual, documented: a directory component swapped between the walk and the rename by another
|
|
26
|
+
* writer with access to the crew-owned state home — mitigated by the single-writer daemon and the
|
|
27
|
+
* post-operation verification, not eliminated.
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* `reserved`: a name the store itself owns at that level (`snapshot.json`, `manifest.json`, `current`,
|
|
31
|
+
* `views/`, `.venv`). `outside-closure`: a support path outside the bundle closure — the ONE allowlist
|
|
32
|
+
* of what the seed copies and a snapshot may carry (bundle.ts `inBundleClosure`; codex round 6).
|
|
33
|
+
*/
|
|
34
|
+
export type SkillPathReason = 'invalid' | 'symlink' | 'nested-skill' | 'root' | 'reserved' | 'outside-closure';
|
|
35
|
+
/** A skill-relative path the store refuses. `reason` names why; the route answers 400 with `message`. */
|
|
36
|
+
export declare class SkillPathError extends Error {
|
|
37
|
+
readonly reason: SkillPathReason;
|
|
38
|
+
constructor(reason: SkillPathReason, message: string);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Validate a POSIX-relative path into its segments. Rejects: empty, NUL, backslashes (paths are
|
|
42
|
+
* POSIX; a backslash is never a separator here and never a filename), absolute / drive-prefixed,
|
|
43
|
+
* and `.` / `..` / empty segments.
|
|
44
|
+
*/
|
|
45
|
+
export declare function validateRelSegments(rel: string): string[];
|
|
46
|
+
/**
|
|
47
|
+
* `join(root, ...segments)` after an lstat walk refusing any symlink component — the walk itself
|
|
48
|
+
* is `tree.ts` `assertNoSymlinkComponents` (the one no-follow rule every store path goes
|
|
49
|
+
* through); this wrapper only speaks the file manager's error type. `root` is the SKILLS root:
|
|
50
|
+
* the store passes `effective/<skill dir>/<path>` (or `baseline/<hash>/…`) as segments, so the
|
|
51
|
+
* skill directory itself is a walked component — replacing it with a link is refused, not
|
|
52
|
+
* followed. A component that does not exist yet ends the walk (a not-yet-written leaf, or its new
|
|
53
|
+
* parent dirs) — the write creates them. Any filesystem error other than ENOENT propagates: an
|
|
54
|
+
* unreadable component is not judged lexically.
|
|
55
|
+
*/
|
|
56
|
+
export declare function containedPath(root: string, segments: ReadonlyArray<string>): string;
|
|
57
|
+
//# sourceMappingURL=contain.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contain.d.ts","sourceRoot":"","sources":["../../src/skills/contain.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAIH;;;;GAIG;AACH,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG,SAAS,GAAG,cAAc,GAAG,MAAM,GAAG,UAAU,GAAG,iBAAiB,CAAC;AAE/G,yGAAyG;AACzG,qBAAa,cAAe,SAAQ,KAAK;IAC3B,QAAQ,CAAC,MAAM,EAAE,eAAe;gBAAvB,MAAM,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM;CAI9D;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAQzD;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,MAAM,CAoBnF"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill-scoped containment for the `/skills` file manager (design v3 §API).
|
|
3
|
+
*
|
|
4
|
+
* NOT `api/open-path.ts` `isInsideRoot`: that helper FOLLOWS symlinks (a `<root>/link-to-outside`
|
|
5
|
+
* resolves to its real target, which is the right question for "may I open this file the run
|
|
6
|
+
* produced" and the wrong one for "may I write here") and accepts the root itself. Writes into the
|
|
7
|
+
* effective plugin root need the stricter rule:
|
|
8
|
+
*
|
|
9
|
+
* 1. the URL wildcard arrives DECODED EXACTLY ONCE — by Fastify (find-my-way decodes every `%xx`,
|
|
10
|
+
* `%2F` included, before routing; a malformed escape is Fastify's own 400) — and the store
|
|
11
|
+
* never decodes again (codex round 5 on #480: a second decode turned `100%25.txt` into a
|
|
12
|
+
* refused `100%.txt` and the literal filename `a%252Fb.txt` into the path `a/b.txt`);
|
|
13
|
+
* 2. normalize into segments — refuse empty, `.`, `..`, backslashes, NUL, absolute / drive-prefixed;
|
|
14
|
+
* 3. lstat-walk EVERY existing component and refuse a symlink anywhere on the way (the plugin
|
|
15
|
+
* tree carries none; one appearing is either an attack or an accident, and either way the
|
|
16
|
+
* write must not follow it);
|
|
17
|
+
* 4. the root itself is not a file — an empty remainder is refused.
|
|
18
|
+
*
|
|
19
|
+
* Atomic tmp+rename writes are the store's job (`tree.ts` `writeFileAtomic`); this module only
|
|
20
|
+
* decides WHERE. And the walk is necessary, not sufficient (design v3.5 §3; codex round 8): the
|
|
21
|
+
* path it answers is then OPENED with `O_NOFOLLOW` where the platform has it, and the lstat-then-open
|
|
22
|
+
* gap is closed everywhere with a post-open identity check (`fstat` dev/ino against the lstat result
|
|
23
|
+
* — `tree.ts` `openRegularNoFollow`); copies read their source through such an open, never by
|
|
24
|
+
* path; a staged tree is re-walked and re-hashed after the copy and before the rename into place.
|
|
25
|
+
* Residual, documented: a directory component swapped between the walk and the rename by another
|
|
26
|
+
* writer with access to the crew-owned state home — mitigated by the single-writer daemon and the
|
|
27
|
+
* post-operation verification, not eliminated.
|
|
28
|
+
*/
|
|
29
|
+
import { assertNoSymlinkComponents, assertSafeRelSegments, SymlinkComponentError, unsafeSegmentReason, UnsafePathSegmentError } from './tree.js';
|
|
30
|
+
/** A skill-relative path the store refuses. `reason` names why; the route answers 400 with `message`. */
|
|
31
|
+
export class SkillPathError extends Error {
|
|
32
|
+
reason;
|
|
33
|
+
constructor(reason, message) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.reason = reason;
|
|
36
|
+
this.name = 'SkillPathError';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Validate a POSIX-relative path into its segments. Rejects: empty, NUL, backslashes (paths are
|
|
41
|
+
* POSIX; a backslash is never a separator here and never a filename), absolute / drive-prefixed,
|
|
42
|
+
* and `.` / `..` / empty segments.
|
|
43
|
+
*/
|
|
44
|
+
export function validateRelSegments(rel) {
|
|
45
|
+
// The ONE segment rule (tree.ts `assertSafeRelSegments`), spoken in the file manager's error type.
|
|
46
|
+
try {
|
|
47
|
+
return assertSafeRelSegments(rel);
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
if (err instanceof UnsafePathSegmentError)
|
|
51
|
+
throw new SkillPathError('invalid', `invalid path ${JSON.stringify(rel)}: ${err.reason}`);
|
|
52
|
+
throw err;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* `join(root, ...segments)` after an lstat walk refusing any symlink component — the walk itself
|
|
57
|
+
* is `tree.ts` `assertNoSymlinkComponents` (the one no-follow rule every store path goes
|
|
58
|
+
* through); this wrapper only speaks the file manager's error type. `root` is the SKILLS root:
|
|
59
|
+
* the store passes `effective/<skill dir>/<path>` (or `baseline/<hash>/…`) as segments, so the
|
|
60
|
+
* skill directory itself is a walked component — replacing it with a link is refused, not
|
|
61
|
+
* followed. A component that does not exist yet ends the walk (a not-yet-written leaf, or its new
|
|
62
|
+
* parent dirs) — the write creates them. Any filesystem error other than ENOENT propagates: an
|
|
63
|
+
* unreadable component is not judged lexically.
|
|
64
|
+
*/
|
|
65
|
+
export function containedPath(root, segments) {
|
|
66
|
+
if (segments.length === 0)
|
|
67
|
+
throw new SkillPathError('root', 'the root itself is not a file');
|
|
68
|
+
// Re-check EVERY component lexically, independent of where it came from (codex round 3): a
|
|
69
|
+
// manifest-sourced skill `dir` or file record is not trusted to be a safe segment — a `..`,
|
|
70
|
+
// an absolute/drive-prefixed piece, a separator or a NUL would join OUT of the root before the
|
|
71
|
+
// symlink walk ever ran. The request's own path is validated by `validateRelSegments`; this is
|
|
72
|
+
// the same rule applied to the FINAL joined path so the trusted-`dir` half cannot escape either.
|
|
73
|
+
for (const seg of segments) {
|
|
74
|
+
const why = unsafeSegmentReason(seg);
|
|
75
|
+
if (why !== null) {
|
|
76
|
+
throw new SkillPathError('invalid', `unsafe path segment ${JSON.stringify(seg)} (${why}) — the joined path must stay inside the skills root`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
return assertNoSymlinkComponents(root, segments);
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
if (err instanceof SymlinkComponentError)
|
|
84
|
+
throw new SkillPathError('symlink', err.message);
|
|
85
|
+
if (err instanceof UnsafePathSegmentError)
|
|
86
|
+
throw new SkillPathError('invalid', err.message);
|
|
87
|
+
throw err;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=contain.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contain.js","sourceRoot":"","sources":["../../src/skills/contain.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,EAAE,yBAAyB,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAC;AASjJ,yGAAyG;AACzG,MAAM,OAAO,cAAe,SAAQ,KAAK;IAClB;IAArB,YAAqB,MAAuB,EAAE,OAAe;QAC3D,KAAK,CAAC,OAAO,CAAC,CAAC;QADI,WAAM,GAAN,MAAM,CAAiB;QAE1C,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;IAC/B,CAAC;CACF;AAED;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAC,GAAW;IAC7C,mGAAmG;IACnG,IAAI,CAAC;QACH,OAAO,qBAAqB,CAAC,GAAG,CAAC,CAAC;IACpC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,GAAG,YAAY,sBAAsB;YAAE,MAAM,IAAI,cAAc,CAAC,SAAS,EAAE,gBAAgB,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;QACrI,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,aAAa,CAAC,IAAY,EAAE,QAA+B;IACzE,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,MAAM,IAAI,cAAc,CAAC,MAAM,EAAE,+BAA+B,CAAC,CAAC;IAC7F,2FAA2F;IAC3F,4FAA4F;IAC5F,+FAA+F;IAC/F,+FAA+F;IAC/F,iGAAiG;IACjG,KAAK,MAAM,GAAG,IAAI,QAAQ,EAAE,CAAC;QAC3B,MAAM,GAAG,GAAG,mBAAmB,CAAC,GAAG,CAAC,CAAC;QACrC,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;YACjB,MAAM,IAAI,cAAc,CAAC,SAAS,EAAE,uBAAuB,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,GAAG,sDAAsD,CAAC,CAAC;QAChJ,CAAC;IACH,CAAC;IACD,IAAI,CAAC;QACH,OAAO,yBAAyB,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;IACnD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,GAAG,YAAY,qBAAqB;YAAE,MAAM,IAAI,cAAc,CAAC,SAAS,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;QAC3F,IAAI,GAAG,YAAY,sBAAsB;YAAE,MAAM,IAAI,cAAc,CAAC,SAAS,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;QAC5F,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core-by-reference = the REGISTERED-REFERENCE CLOSURE (design v3 §5), recomputed at every publish:
|
|
3
|
+
*
|
|
4
|
+
* seeds every `skill_ref` of every workflow the daemon knows — core's drop-ins as mirrored in
|
|
5
|
+
* `BUILTIN_WORKFLOWS` (asserted field-for-field against wicked-core's `workflows/*.json`
|
|
6
|
+
* by tests/builtin-overlay-shadow.test.ts), crew's TS-generated workflows
|
|
7
|
+
* (capture-learnings → repo-learn, domain-extraction → domain/extractor/coverage) and
|
|
8
|
+
* any user-registered def — `adapter.listWorkflows()` is that one list;
|
|
9
|
+
* closure plus each referenced skill's mandates, transitively. A skill mandates another in two
|
|
10
|
+
* ways, and BOTH are read (a mandate that is missed is a phase the engine refuses):
|
|
11
|
+
*
|
|
12
|
+
* declared the frontmatter `mandates:` list — consumed from the PARSED YAML
|
|
13
|
+
* (`parseFrontmatter().mandates`, frontmatter.ts): each entry's decoded
|
|
14
|
+
* scalar names a catalog skill, so `"wicked-garden-gamma"` mandates
|
|
15
|
+
* `wicked-garden-gamma` exactly as the parser reads it (codex round 4: the
|
|
16
|
+
* closure used to find mandates by a regex over the RAW text, which an
|
|
17
|
+
* escaped scalar slipped past). The frontmatter block is never regex-scanned.
|
|
18
|
+
* prose the BODY's qualified-name mentions (`repo-learn/SKILL.md:79` "Use the
|
|
19
|
+
* **wicked-garden-search** skill", `:113` "`wicked-garden-mem` `recall`") —
|
|
20
|
+
* the live catalog declares no `mandates:` today (0/142), so its real
|
|
21
|
+
* dependencies live in prose. Scanned over the body only
|
|
22
|
+
* (`bodyWithFrontmatterBlanked`): a frontmatter scalar is a parser's
|
|
23
|
+
* business, not a prose mention.
|
|
24
|
+
*
|
|
25
|
+
* Two kinds of ABSENCE are reported, and both block a publish (codex review of #480 — the closure
|
|
26
|
+
* used to fail open, warning on a missing registered ref and dropping an absent mandate silently):
|
|
27
|
+
*
|
|
28
|
+
* missing a registered ref that names no catalog skill — a run dispatching that phase
|
|
29
|
+
* would find no skill in the snapshot;
|
|
30
|
+
* absentMandates a name a CORE skill mandates (declared or in prose) that no catalog skill
|
|
31
|
+
* answers to — the mandated method is missing, with file:line.
|
|
32
|
+
*
|
|
33
|
+
* What counts as a qualified-name MENTION in prose is deliberately narrow so prose does not
|
|
34
|
+
* masquerade as a mandate: `wicked-garden-<x>` / `wicked-garden:<x>` not glued to a preceding name
|
|
35
|
+
* character; a token ending in `-`/`_` is a glob or prefix (`wicked-garden-qe-acceptance-test-*`),
|
|
36
|
+
* not a name; a token continued by `:` is a Claude subagent type (`wicked-garden:crew:implementer`),
|
|
37
|
+
* not a skill. A DECLARED mandate is taken verbatim (normalized from the Claude colon form) — the
|
|
38
|
+
* operator wrote it as a requirement, so a name the catalog lacks is absent, never ignored.
|
|
39
|
+
*/
|
|
40
|
+
import type { WorkflowDef } from '../core/types.js';
|
|
41
|
+
/** One qualified name a SKILL.md requires: the catalog name it spells and the 1-based line. */
|
|
42
|
+
export interface NameMention {
|
|
43
|
+
name: string;
|
|
44
|
+
line: number;
|
|
45
|
+
}
|
|
46
|
+
/** The non-null `skill_ref`s across a set of workflow defs. */
|
|
47
|
+
export declare function registeredSkillRefs(workflows: ReadonlyArray<WorkflowDef>): Set<string>;
|
|
48
|
+
/**
|
|
49
|
+
* Every well-formed qualified-name token in `text` with its line — catalog-agnostic, PROSE only.
|
|
50
|
+
* Glob/prefix tokens (trailing `-`/`_`) and `:`-continued subagent types are not names and are not
|
|
51
|
+
* returned. Callers hand this the body (`bodyWithFrontmatterBlanked`), never the frontmatter.
|
|
52
|
+
*/
|
|
53
|
+
export declare function mentionedTokens(text: string): NameMention[];
|
|
54
|
+
/**
|
|
55
|
+
* Everything a SKILL.md mandates, in file order: the DECLARED `mandates:` entries as the YAML parser
|
|
56
|
+
* decoded them (positioned by node range), then the body's prose mentions (positioned by line). A
|
|
57
|
+
* frontmatter that does not parse declares nothing here — the frontmatter guard blocks it on its own.
|
|
58
|
+
*/
|
|
59
|
+
export declare function mandateMentions(skillMd: string): NameMention[];
|
|
60
|
+
/** Catalog names `skillMd` mandates (declared + prose), excluding `self`. */
|
|
61
|
+
export declare function mentionedSkillNames(skillMd: string, catalogNames: ReadonlySet<string>, self: string): Set<string>;
|
|
62
|
+
export interface AbsentMandate {
|
|
63
|
+
/** The core skill whose SKILL.md names the absent skill. */
|
|
64
|
+
from: string;
|
|
65
|
+
/** The qualified name no catalog skill answers to. */
|
|
66
|
+
name: string;
|
|
67
|
+
/** 1-based line in `from`'s SKILL.md. */
|
|
68
|
+
line: number;
|
|
69
|
+
}
|
|
70
|
+
export interface CoreClosure {
|
|
71
|
+
core: Set<string>;
|
|
72
|
+
/** Registered refs that name no catalog skill. */
|
|
73
|
+
missing: string[];
|
|
74
|
+
/** Names core skills mandate (declared or in prose) that no catalog skill answers to, with file:line. */
|
|
75
|
+
absentMandates: AbsentMandate[];
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* BFS from `refs` over "SKILL.md mandates" (`mandateMentions`). `catalog` maps every catalog name
|
|
79
|
+
* (disabled skills included — disabling does not remove a skill from the catalog) to its SKILL.md
|
|
80
|
+
* text.
|
|
81
|
+
*/
|
|
82
|
+
export declare function coreClosure(refs: Iterable<string>, catalog: ReadonlyMap<string, string>): CoreClosure;
|
|
83
|
+
//# sourceMappingURL=core-closure.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"core-closure.d.ts","sourceRoot":"","sources":["../../src/skills/core-closure.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAMpD,+FAA+F;AAC/F,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,+DAA+D;AAC/D,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,aAAa,CAAC,WAAW,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,CAQtF;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,EAAE,CAW3D;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,WAAW,EAAE,CAI9D;AAED,6EAA6E;AAC7E,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAMjH;AAED,MAAM,WAAW,aAAa;IAC5B,4DAA4D;IAC5D,IAAI,EAAE,MAAM,CAAC;IACb,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;IACb,yCAAyC;IACzC,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAClB,kDAAkD;IAClD,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,yGAAyG;IACzG,cAAc,EAAE,aAAa,EAAE,CAAC;CACjC;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,WAAW,CAgCrG"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core-by-reference = the REGISTERED-REFERENCE CLOSURE (design v3 §5), recomputed at every publish:
|
|
3
|
+
*
|
|
4
|
+
* seeds every `skill_ref` of every workflow the daemon knows — core's drop-ins as mirrored in
|
|
5
|
+
* `BUILTIN_WORKFLOWS` (asserted field-for-field against wicked-core's `workflows/*.json`
|
|
6
|
+
* by tests/builtin-overlay-shadow.test.ts), crew's TS-generated workflows
|
|
7
|
+
* (capture-learnings → repo-learn, domain-extraction → domain/extractor/coverage) and
|
|
8
|
+
* any user-registered def — `adapter.listWorkflows()` is that one list;
|
|
9
|
+
* closure plus each referenced skill's mandates, transitively. A skill mandates another in two
|
|
10
|
+
* ways, and BOTH are read (a mandate that is missed is a phase the engine refuses):
|
|
11
|
+
*
|
|
12
|
+
* declared the frontmatter `mandates:` list — consumed from the PARSED YAML
|
|
13
|
+
* (`parseFrontmatter().mandates`, frontmatter.ts): each entry's decoded
|
|
14
|
+
* scalar names a catalog skill, so `"wicked-garden-gamma"` mandates
|
|
15
|
+
* `wicked-garden-gamma` exactly as the parser reads it (codex round 4: the
|
|
16
|
+
* closure used to find mandates by a regex over the RAW text, which an
|
|
17
|
+
* escaped scalar slipped past). The frontmatter block is never regex-scanned.
|
|
18
|
+
* prose the BODY's qualified-name mentions (`repo-learn/SKILL.md:79` "Use the
|
|
19
|
+
* **wicked-garden-search** skill", `:113` "`wicked-garden-mem` `recall`") —
|
|
20
|
+
* the live catalog declares no `mandates:` today (0/142), so its real
|
|
21
|
+
* dependencies live in prose. Scanned over the body only
|
|
22
|
+
* (`bodyWithFrontmatterBlanked`): a frontmatter scalar is a parser's
|
|
23
|
+
* business, not a prose mention.
|
|
24
|
+
*
|
|
25
|
+
* Two kinds of ABSENCE are reported, and both block a publish (codex review of #480 — the closure
|
|
26
|
+
* used to fail open, warning on a missing registered ref and dropping an absent mandate silently):
|
|
27
|
+
*
|
|
28
|
+
* missing a registered ref that names no catalog skill — a run dispatching that phase
|
|
29
|
+
* would find no skill in the snapshot;
|
|
30
|
+
* absentMandates a name a CORE skill mandates (declared or in prose) that no catalog skill
|
|
31
|
+
* answers to — the mandated method is missing, with file:line.
|
|
32
|
+
*
|
|
33
|
+
* What counts as a qualified-name MENTION in prose is deliberately narrow so prose does not
|
|
34
|
+
* masquerade as a mandate: `wicked-garden-<x>` / `wicked-garden:<x>` not glued to a preceding name
|
|
35
|
+
* character; a token ending in `-`/`_` is a glob or prefix (`wicked-garden-qe-acceptance-test-*`),
|
|
36
|
+
* not a name; a token continued by `:` is a Claude subagent type (`wicked-garden:crew:implementer`),
|
|
37
|
+
* not a skill. A DECLARED mandate is taken verbatim (normalized from the Claude colon form) — the
|
|
38
|
+
* operator wrote it as a requirement, so a name the catalog lacks is absent, never ignored.
|
|
39
|
+
*/
|
|
40
|
+
import { bodyWithFrontmatterBlanked, parseFrontmatter, PLUGIN_NAME, SKILL_NAME_PREFIX } from './frontmatter.js';
|
|
41
|
+
/** `wicked-garden-<x>` or the Claude plugin form `wicked-garden:<x>`, not glued to a preceding name char. */
|
|
42
|
+
const NAME_TOKEN_RE = new RegExp(`(?<![A-Za-z0-9_-])${PLUGIN_NAME}[:-]([A-Za-z0-9_-]+)(:?)`, 'g');
|
|
43
|
+
/** The non-null `skill_ref`s across a set of workflow defs. */
|
|
44
|
+
export function registeredSkillRefs(workflows) {
|
|
45
|
+
const out = new Set();
|
|
46
|
+
for (const w of workflows) {
|
|
47
|
+
for (const p of w.phases) {
|
|
48
|
+
if (typeof p.skill_ref === 'string' && p.skill_ref !== '')
|
|
49
|
+
out.add(p.skill_ref);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Every well-formed qualified-name token in `text` with its line — catalog-agnostic, PROSE only.
|
|
56
|
+
* Glob/prefix tokens (trailing `-`/`_`) and `:`-continued subagent types are not names and are not
|
|
57
|
+
* returned. Callers hand this the body (`bodyWithFrontmatterBlanked`), never the frontmatter.
|
|
58
|
+
*/
|
|
59
|
+
export function mentionedTokens(text) {
|
|
60
|
+
const out = [];
|
|
61
|
+
const lines = text.split('\n');
|
|
62
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
63
|
+
for (const m of (lines[i] ?? '').matchAll(NAME_TOKEN_RE)) {
|
|
64
|
+
const raw = m[1] ?? '';
|
|
65
|
+
if (m[2] === ':' || raw.endsWith('-') || raw.endsWith('_'))
|
|
66
|
+
continue;
|
|
67
|
+
out.push({ name: SKILL_NAME_PREFIX + raw, line: i + 1 });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Everything a SKILL.md mandates, in file order: the DECLARED `mandates:` entries as the YAML parser
|
|
74
|
+
* decoded them (positioned by node range), then the body's prose mentions (positioned by line). A
|
|
75
|
+
* frontmatter that does not parse declares nothing here — the frontmatter guard blocks it on its own.
|
|
76
|
+
*/
|
|
77
|
+
export function mandateMentions(skillMd) {
|
|
78
|
+
const parsed = parseFrontmatter(skillMd);
|
|
79
|
+
const declared = parsed.ok ? parsed.mandates.map((m) => ({ name: m.name, line: m.line })) : [];
|
|
80
|
+
return [...declared, ...mentionedTokens(bodyWithFrontmatterBlanked(skillMd))];
|
|
81
|
+
}
|
|
82
|
+
/** Catalog names `skillMd` mandates (declared + prose), excluding `self`. */
|
|
83
|
+
export function mentionedSkillNames(skillMd, catalogNames, self) {
|
|
84
|
+
const out = new Set();
|
|
85
|
+
for (const { name } of mandateMentions(skillMd)) {
|
|
86
|
+
if (name !== self && catalogNames.has(name))
|
|
87
|
+
out.add(name);
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* BFS from `refs` over "SKILL.md mandates" (`mandateMentions`). `catalog` maps every catalog name
|
|
93
|
+
* (disabled skills included — disabling does not remove a skill from the catalog) to its SKILL.md
|
|
94
|
+
* text.
|
|
95
|
+
*/
|
|
96
|
+
export function coreClosure(refs, catalog) {
|
|
97
|
+
const names = new Set(catalog.keys());
|
|
98
|
+
const core = new Set();
|
|
99
|
+
const missing = [];
|
|
100
|
+
const absentMandates = [];
|
|
101
|
+
const queue = [];
|
|
102
|
+
for (const ref of refs) {
|
|
103
|
+
if (!names.has(ref)) {
|
|
104
|
+
missing.push(ref);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (!core.has(ref)) {
|
|
108
|
+
core.add(ref);
|
|
109
|
+
queue.push(ref);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
while (queue.length > 0) {
|
|
113
|
+
const name = queue.shift();
|
|
114
|
+
for (const mention of mandateMentions(catalog.get(name) ?? '')) {
|
|
115
|
+
if (mention.name === name)
|
|
116
|
+
continue;
|
|
117
|
+
if (!names.has(mention.name)) {
|
|
118
|
+
absentMandates.push({ from: name, name: mention.name, line: mention.line });
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (!core.has(mention.name)) {
|
|
122
|
+
core.add(mention.name);
|
|
123
|
+
queue.push(mention.name);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
absentMandates.sort((a, b) => (a.from < b.from ? -1 : a.from > b.from ? 1 : a.line - b.line));
|
|
128
|
+
return { core, missing: [...new Set(missing)].sort(), absentMandates };
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=core-closure.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"core-closure.js","sourceRoot":"","sources":["../../src/skills/core-closure.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The engine handoff: `WICKED_SKILLS_SNAPSHOT` — exactly ONE variable (design v3 DECISION 2,
|
|
3
|
+
* v3.1 §2, v3.4 §2).
|
|
4
|
+
*
|
|
5
|
+
* wicked-core consumes exactly one skills input — the absolute REAL path of a published snapshot
|
|
6
|
+
* (`<state home>/skills/snapshots/<gen>`, every component — the LAST included — a real directory;
|
|
7
|
+
* never the `current` link itself and never a parent dir it would have to infer a manifest from;
|
|
8
|
+
* `WICKED_SKILLS_CURRENT` is withdrawn — v3.1 §2). Crew resolves `current` and exports the path
|
|
9
|
+
* here; the engine reads `process.env` per worker spawn (the `WICKED_WORKER_HOME` discipline in
|
|
10
|
+
* api/seat-signin.ts), so applying it at boot and after every publish is the whole mechanism — the
|
|
11
|
+
* next spawn sees the new snapshot, no daemon or engine restart.
|
|
12
|
+
*
|
|
13
|
+
* `WICKED_CREW_STATE_HOME` is RETIRED as an engine input (v3.4 §2 — it reverses the round-4
|
|
14
|
+
* "passed alongside" decision; core#399 pass 6 retires it on the engine side too). Core derives the
|
|
15
|
+
* state home it fences from the snapshot path's fixed layout — parent named `snapshots`,
|
|
16
|
+
* grandparent named `skills`, else a config error naming the path — so crew exports NOTHING beside
|
|
17
|
+
* the snapshot. The canonical state home is still REPORTED to humans (`GET /diagnostics` →
|
|
18
|
+
* `skills.stateHome`, `canonicalCrewStateHome()`); it is never an engine input.
|
|
19
|
+
*
|
|
20
|
+
* The degradation ladder (v3 §3) is the engine's: with the snapshot variable unset it falls back to
|
|
21
|
+
* the live installed garden cache and logs `skills.fallback`; with it set to a path that does not
|
|
22
|
+
* resolve, every launch FAILS loudly (config error). Crew uses both rungs deliberately
|
|
23
|
+
* (runtime.ts): a VERIFIED snapshot path when it has one; the boot value / unset ONLY when the
|
|
24
|
+
* configuration is absent (no garden installed — an operator-exported value, or the hermetic test
|
|
25
|
+
* arming, survives); and a non-existent refusal path (`<root>/refused/skills.{blocked,config}`)
|
|
26
|
+
* when the configuration is present but defective — a blocked first publish or a corrupt root
|
|
27
|
+
* must never be "restored" into a live-cache fallback that bypasses recorded disablement.
|
|
28
|
+
*/
|
|
29
|
+
export declare const SKILLS_SNAPSHOT_ENGINE_ENV = "WICKED_SKILLS_SNAPSHOT";
|
|
30
|
+
/** The value this process BOOTED with (the `BOOT_WORKER_HOME` discipline): what "nothing to offer" restores. */
|
|
31
|
+
export declare const BOOT_SKILLS_SNAPSHOT: string | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* `crewStateHome()` with every link resolved — the spelling `GET /diagnostics` reports as
|
|
34
|
+
* `skills.stateHome` for humans (NOT an engine input, v3.4 §2). A state home that does not exist
|
|
35
|
+
* yet (a fresh `--db` parent before the first store lands) is spelled absolute, unresolved.
|
|
36
|
+
*/
|
|
37
|
+
export declare function canonicalCrewStateHome(): string;
|
|
38
|
+
/**
|
|
39
|
+
* Point the engine at `snapshotPath`, or — when the daemon has no published snapshot — restore
|
|
40
|
+
* the booted value EXACTLY: deleted when the process booted without one, and PRESERVED when it
|
|
41
|
+
* booted with an explicitly empty one (design v3.5 §4, v3.1 restated: set-but-empty is a
|
|
42
|
+
* configuration error core refuses loudly; only an ABSENT variable reaches the live-cache fallback
|
|
43
|
+
* rung — converting `""` to "unset" would have opened that rung by accident). `fallback` is the
|
|
44
|
+
* boot value; tests pass it explicitly. Nothing else is exported: the snapshot path is the engine's
|
|
45
|
+
* ONE skills input (v3.4 §2).
|
|
46
|
+
*/
|
|
47
|
+
export declare function applySkillsSnapshotEnv(snapshotPath: string | null | undefined, fallback?: string | undefined): void;
|
|
48
|
+
//# sourceMappingURL=engine-env.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine-env.d.ts","sourceRoot":"","sources":["../../src/skills/engine-env.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAOH,eAAO,MAAM,0BAA0B,2BAA2B,CAAC;AAEnE,gHAAgH;AAChH,eAAO,MAAM,oBAAoB,EAAE,MAAM,GAAG,SAAmD,CAAC;AAEhG;;;;GAIG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,CAO/C;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CACpC,YAAY,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACvC,QAAQ,GAAE,MAAM,GAAG,SAAgC,GAClD,IAAI,CAQN"}
|