@orkestrel/scaffold 0.0.55 → 0.0.56
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 +21 -6
- package/dist/bin/main.js +2 -2
- package/dist/bin/main.js.map +1 -1
- package/dist/host/agents/orchestration.md +6 -3
- package/dist/host/agents/skills/orkestrel-publish/references/wave.md +28 -2
- package/dist/host/claude/agents/orkestrel.md +6 -2
- package/dist/host/claude/rules/quality.md +1 -0
- package/dist/host/guides/scaffold.md +187 -68
- package/dist/host/manifest.json +7 -7
- package/dist/host/scripts/codex.sh +0 -0
- package/dist/host/scripts/cursor.sh +0 -0
- package/dist/host/scripts/deps.sh +0 -0
- package/dist/host/scripts/ollama.sh +0 -0
- package/dist/host/tests/policy.test.ts +71 -4
- package/dist/src/core/index.cjs +196 -40
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +113 -12
- package/dist/src/core/index.d.ts +113 -12
- package/dist/src/core/index.js +195 -41
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +140 -12
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +108 -9
- package/dist/src/server/index.d.ts +108 -9
- package/dist/src/server/index.js +141 -15
- package/dist/src/server/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -1211,6 +1211,97 @@ function listDirectories(root) {
|
|
|
1211
1211
|
return directories.sort();
|
|
1212
1212
|
}
|
|
1213
1213
|
/**
|
|
1214
|
+
* Lists the canon paths a target holds, filtered to a plan's groups.
|
|
1215
|
+
*
|
|
1216
|
+
* @param target - The target directory to inspect.
|
|
1217
|
+
* @param groups - The artifact groups the plan covers; a held path whose group
|
|
1218
|
+
* is outside them is not listed.
|
|
1219
|
+
* @returns Every held canon path as a `/`-separated target-relative path, a
|
|
1220
|
+
* directory member expanded to the files beneath it, and `[]` when the target
|
|
1221
|
+
* holds none or cannot be resolved.
|
|
1222
|
+
* @throws `ScaffoldError('TARGET', …)` when a held canon directory cannot be
|
|
1223
|
+
* inventoried; {@link listFiles} states each refusal.
|
|
1224
|
+
*
|
|
1225
|
+
* @remarks
|
|
1226
|
+
* A release stages the canon for reading rather than for a target, so a copy
|
|
1227
|
+
* sitting in one is an artifact of a release that vendored it, and reading it
|
|
1228
|
+
* here is what lets the deletion verb take it.
|
|
1229
|
+
*
|
|
1230
|
+
* A directory member is read by file, which is what pairs the paths a plan
|
|
1231
|
+
* claims inside the canon with their artifacts and leaves only the rest foreign,
|
|
1232
|
+
* with no case for a planned file. The plan's own selection gates the reading,
|
|
1233
|
+
* the same rule that decides a vendored path's group, so a scoped audit reports
|
|
1234
|
+
* nothing outside its groups. A member that cannot resolve inside the target is
|
|
1235
|
+
* not held by it.
|
|
1236
|
+
*
|
|
1237
|
+
* @example
|
|
1238
|
+
* ```ts
|
|
1239
|
+
* import { listCanonPaths } from '@orkestrel/scaffold/server'
|
|
1240
|
+
*
|
|
1241
|
+
* listCanonPaths('vacant', ['orchestration']) // []
|
|
1242
|
+
* ```
|
|
1243
|
+
*/
|
|
1244
|
+
function listCanonPaths(target, groups) {
|
|
1245
|
+
const held = [];
|
|
1246
|
+
for (const member of _src_core.CANON_PATHS) {
|
|
1247
|
+
const full = resolveContainedPath(target, member);
|
|
1248
|
+
if (full === void 0) continue;
|
|
1249
|
+
if (isPhysicalFile(full)) held.push(member);
|
|
1250
|
+
else if (isPhysicalDirectory(full)) for (const name of listFiles(full)) held.push(`${member}/${name}`);
|
|
1251
|
+
}
|
|
1252
|
+
return held.filter((path) => groups.includes((0, _src_core.inferGroup)(path)));
|
|
1253
|
+
}
|
|
1254
|
+
/**
|
|
1255
|
+
* Removes every directory one set of deletions emptied.
|
|
1256
|
+
*
|
|
1257
|
+
* @param target - The directory the deleted paths are relative to.
|
|
1258
|
+
* @param removed - The `/`-separated target-relative paths the deletion took.
|
|
1259
|
+
* @returns Every removed directory as a target-relative path, in the order they
|
|
1260
|
+
* were taken, and `[]` when the target is absent or nothing it holds was
|
|
1261
|
+
* emptied.
|
|
1262
|
+
*
|
|
1263
|
+
* @remarks
|
|
1264
|
+
* A deletion that takes the last file out of a directory leaves the directory
|
|
1265
|
+
* standing, and git records no directory, so a swept target keeps the shape of a
|
|
1266
|
+
* set it no longer holds while every reading of it reports clean. Only an
|
|
1267
|
+
* ancestor of a path this deletion took is a candidate, so nothing the deletion
|
|
1268
|
+
* did not reach is inspected, and the target itself is never a candidate.
|
|
1269
|
+
*
|
|
1270
|
+
* Candidates are taken deepest first, which is what lets a whole chain go: the
|
|
1271
|
+
* directory holding nothing but the emptied directory is empty in turn by the
|
|
1272
|
+
* time it is read. Each one is resolved through the containment law and left
|
|
1273
|
+
* standing when it escapes the target, is not a physical directory, still holds
|
|
1274
|
+
* an entry, or refuses removal, and a directory left standing is absent from the
|
|
1275
|
+
* answer.
|
|
1276
|
+
*
|
|
1277
|
+
* @example
|
|
1278
|
+
* ```ts
|
|
1279
|
+
* import { pruneEmptiedDirectories } from '@orkestrel/scaffold/server'
|
|
1280
|
+
*
|
|
1281
|
+
* pruneEmptiedDirectories('vacant', ['notes/entry.md']) // []
|
|
1282
|
+
* ```
|
|
1283
|
+
*/
|
|
1284
|
+
function pruneEmptiedDirectories(target, removed) {
|
|
1285
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
1286
|
+
for (const path of removed) {
|
|
1287
|
+
let parent = (0, node_path.dirname)(path);
|
|
1288
|
+
while (parent !== "." && parent !== (0, node_path.dirname)(parent) && !candidates.has(parent)) {
|
|
1289
|
+
candidates.add(parent);
|
|
1290
|
+
parent = (0, node_path.dirname)(parent);
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
const ordered = [...candidates].sort((left, right) => (0, _orkestrel_contract.compareValues)(right.split("/").length, left.split("/").length) || (0, _orkestrel_contract.compareValues)(left, right));
|
|
1294
|
+
const pruned = [];
|
|
1295
|
+
for (const candidate of ordered) {
|
|
1296
|
+
const directory = resolveContainedPath(target, candidate);
|
|
1297
|
+
if (directory === void 0 || !isPhysicalDirectory(directory)) continue;
|
|
1298
|
+
const entries = (0, _orkestrel_contract.attempt)(() => (0, node_fs.readdirSync)(directory));
|
|
1299
|
+
if (!entries.success || entries.value.length > 0) continue;
|
|
1300
|
+
if ((0, _orkestrel_contract.attempt)(() => (0, node_fs.rmdirSync)(directory)).success) pruned.push(candidate);
|
|
1301
|
+
}
|
|
1302
|
+
return pruned;
|
|
1303
|
+
}
|
|
1304
|
+
/**
|
|
1214
1305
|
* Read one contained file as its exact bytes in lowercase hexadecimal.
|
|
1215
1306
|
*
|
|
1216
1307
|
* @param root - The containing host directory.
|
|
@@ -1519,8 +1610,11 @@ function readManifestEntry(destination, source) {
|
|
|
1519
1610
|
* went missing, names an undeclared path, or leaves a host-owned path absent
|
|
1520
1611
|
* answers `undefined`. Deferred paths are presence-only and retain the installed
|
|
1521
1612
|
* floor bytes that their catalog or mirror surface owns; repair never writes
|
|
1522
|
-
* those floor bytes.
|
|
1523
|
-
*
|
|
1613
|
+
* those floor bytes. A canon path is read the same way for a different reason:
|
|
1614
|
+
* the canon is staged for reading rather than for a target, so every canon
|
|
1615
|
+
* destination keeps its floor bytes here, claimed or not, and a fill that carries
|
|
1616
|
+
* no row for one is complete rather than spoiled. One `Host` can therefore carry
|
|
1617
|
+
* live host bytes beside floor bytes without mixing baselines within a surface.
|
|
1524
1618
|
*
|
|
1525
1619
|
* The emitted entries keep the release's own order and its storage and
|
|
1526
1620
|
* executable declarations, and carry digests recomputed over the bytes the fill
|
|
@@ -1532,8 +1626,12 @@ function readManifestEntry(destination, source) {
|
|
|
1532
1626
|
* ```ts
|
|
1533
1627
|
* import { filesToHost } from '@orkestrel/scaffold/server'
|
|
1534
1628
|
*
|
|
1535
|
-
*
|
|
1536
|
-
* //
|
|
1629
|
+
* // A floor declaring the host-owned `scripts/codex.sh` path and the canon
|
|
1630
|
+
* // `AGENTS.md` destination. The script's live bytes are taken; the canon
|
|
1631
|
+
* // destination keeps the floor's.
|
|
1632
|
+
* filesToHost([{ path: 'scripts/codex.sh', lookup: 'found', hex: '23212f62696e2f73680a' }], floor)
|
|
1633
|
+
* // { manifest: { entries: [ … ], roots: [ … ], digest: '…' },
|
|
1634
|
+
* // bytes: { 'scripts/codex.sh': '23212f62696e2f73680a', 'AGENTS.md': floor.bytes['AGENTS.md'] } }
|
|
1537
1635
|
* ```
|
|
1538
1636
|
*/
|
|
1539
1637
|
function filesToHost(files, floor) {
|
|
@@ -1541,12 +1639,12 @@ function filesToHost(files, floor) {
|
|
|
1541
1639
|
const held = /* @__PURE__ */ new Map();
|
|
1542
1640
|
for (const file of files) {
|
|
1543
1641
|
if (file.lookup !== "found" || !declared.has(file.path)) return void 0;
|
|
1544
|
-
if (!(0, _src_core.isDeferredPath)(file.path)) held.set(file.path, file.hex);
|
|
1642
|
+
if (!(0, _src_core.isDeferredPath)(file.path) && !(0, _src_core.isCanonPath)(file.path)) held.set(file.path, file.hex);
|
|
1545
1643
|
}
|
|
1546
1644
|
const entries = [];
|
|
1547
1645
|
const bytes = {};
|
|
1548
1646
|
for (const entry of floor.manifest.entries) {
|
|
1549
|
-
const hex = (0, _src_core.isDeferredPath)(entry.destination) ? floor.bytes[entry.destination] : held.get(entry.destination);
|
|
1647
|
+
const hex = (0, _src_core.isDeferredPath)(entry.destination) || (0, _src_core.isCanonPath)(entry.destination) ? floor.bytes[entry.destination] : held.get(entry.destination);
|
|
1550
1648
|
if (hex === void 0) return void 0;
|
|
1551
1649
|
entries.push({
|
|
1552
1650
|
storage: entry.storage,
|
|
@@ -1677,9 +1775,15 @@ function stageBytes(host, root, destinations) {
|
|
|
1677
1775
|
* once. A directory is the same case — declaring an absent directory as an empty
|
|
1678
1776
|
* root would create an empty directory in every generated workspace.
|
|
1679
1777
|
*
|
|
1680
|
-
* The
|
|
1778
|
+
* The walk covers `HOST_PATHS` and `CANON_PATHS` together, because a release
|
|
1779
|
+
* ships both: a target receives the first set, and reads the second one out of
|
|
1780
|
+
* the installed package. Everything downstream — the missing-path refusal, the
|
|
1781
|
+
* storage collision guard, the sort, the digests, and the root inventory — reads
|
|
1782
|
+
* the union, so a canon path is staged under exactly the law a vendored one is.
|
|
1783
|
+
*
|
|
1784
|
+
* The vendoring deny-list applies to what the walk discovers beneath a staged
|
|
1681
1785
|
* directory, where a maintainer's local credential can legitimately sit, and
|
|
1682
|
-
* such a path is skipped. A path
|
|
1786
|
+
* such a path is skipped. A path either list names itself is curated data
|
|
1683
1787
|
* rather than discovery, so it is staged or the stage is refused.
|
|
1684
1788
|
*
|
|
1685
1789
|
* @example
|
|
@@ -1698,7 +1802,7 @@ function stageHost(checkout, host) {
|
|
|
1698
1802
|
const vendored = [];
|
|
1699
1803
|
const roots = [];
|
|
1700
1804
|
const missing = [];
|
|
1701
|
-
for (const path of _src_core.HOST_PATHS) {
|
|
1805
|
+
for (const path of [..._src_core.HOST_PATHS, ..._src_core.CANON_PATHS]) {
|
|
1702
1806
|
const full = resolveContainedPath(source, path);
|
|
1703
1807
|
if (full === void 0) throw new _src_core.ScaffoldError("INVALID", `Vendored path leaves its checkout at ${path}`, {
|
|
1704
1808
|
checkout: source,
|
|
@@ -2652,7 +2756,8 @@ var Materializer = class Materializer {
|
|
|
2652
2756
|
*
|
|
2653
2757
|
* @param plan - The compiled plan to compare.
|
|
2654
2758
|
* @param target - The directory to inspect.
|
|
2655
|
-
* @returns One finding per hydrated planned path, plus foreign files
|
|
2759
|
+
* @returns One finding per hydrated planned path, plus the foreign files
|
|
2760
|
+
* beneath owned host roots and inside the instruction canon.
|
|
2656
2761
|
* @throws {@link ScaffoldError} coded `INVALID` when an argument is not the
|
|
2657
2762
|
* exact shape, `TARGET` when the host or target cannot be read within its
|
|
2658
2763
|
* bounds, and `DESTROYED` after teardown.
|
|
@@ -2660,8 +2765,16 @@ var Materializer = class Materializer {
|
|
|
2660
2765
|
* @remarks
|
|
2661
2766
|
* Host directories expand before the target is read, so this method and
|
|
2662
2767
|
* {@link repair} compare the same paths with the same ownership. Foreign
|
|
2663
|
-
* candidates are files beneath those expanded roots
|
|
2664
|
-
*
|
|
2768
|
+
* candidates are files beneath those expanded roots and files the target holds
|
|
2769
|
+
* at a `CANON_PATHS` member, each in a group the plan selects; a file outside
|
|
2770
|
+
* both populations never becomes a deletion candidate merely because its group
|
|
2771
|
+
* is selected.
|
|
2772
|
+
*
|
|
2773
|
+
* The canon is staged for reading rather than for a target, so a copy of one of
|
|
2774
|
+
* its paths sitting in a target is a superseded artifact and reports foreign. A
|
|
2775
|
+
* path the plan claims inside the canon — the root instruction pointers and the
|
|
2776
|
+
* catalog agent file — pairs with its artifact and is compared like any other
|
|
2777
|
+
* planned path.
|
|
2665
2778
|
*/
|
|
2666
2779
|
audit(plan, target) {
|
|
2667
2780
|
this.#assertAlive();
|
|
@@ -2860,9 +2973,20 @@ var Materializer = class Materializer {
|
|
|
2860
2973
|
* recovery mechanism, so a path it cannot restore is not one this verb takes.
|
|
2861
2974
|
* A tree carrying uncommitted work is refused whole for the same reason.
|
|
2862
2975
|
*
|
|
2976
|
+
* One candidate list carries both foreign populations {@link audit} reports, so
|
|
2977
|
+
* a superseded instruction copy the target holds inside the canon is deleted in
|
|
2978
|
+
* the same transaction as a stray beneath an owned root. Membership decides it,
|
|
2979
|
+
* never byte identity: a copy a release behind no longer matches the bytes the
|
|
2980
|
+
* canon now stages, and matching bytes is exactly how such a copy would be
|
|
2981
|
+
* spared. An untracked leftover is left where it sits and stays a finding, which
|
|
2982
|
+
* is the seam a maintainer keeps a git-ignored file in.
|
|
2983
|
+
*
|
|
2863
2984
|
* The whole call refuses when the preview disagrees with the re-derivation on
|
|
2864
2985
|
* any foreign finding, including one the deletion itself would skip, because a
|
|
2865
2986
|
* preview stale anywhere is stale evidence.
|
|
2987
|
+
*
|
|
2988
|
+
* Every directory the deletions emptied is taken after the commit, so a swept
|
|
2989
|
+
* target does not keep the shape of the set it no longer holds.
|
|
2866
2990
|
*/
|
|
2867
2991
|
remove(plan, audit, worktree, target) {
|
|
2868
2992
|
this.#assertAlive();
|
|
@@ -2980,6 +3104,7 @@ var Materializer = class Materializer {
|
|
|
2980
3104
|
});
|
|
2981
3105
|
for (const name of listFiles(directory)) paths.add(`${root}/${name}`);
|
|
2982
3106
|
}
|
|
3107
|
+
for (const path of listCanonPaths(target, plan.groups)) paths.add(path);
|
|
2983
3108
|
return {
|
|
2984
3109
|
findings: (0, _src_core.planToFindings)(hydrated, readSnapshot(target, [...paths])),
|
|
2985
3110
|
questions: []
|
|
@@ -3285,6 +3410,7 @@ var Materializer = class Materializer {
|
|
|
3285
3410
|
});
|
|
3286
3411
|
const removed = this.#close(transaction, staged, target);
|
|
3287
3412
|
for (const path of removed) this.#emitter.emit("remove", path);
|
|
3413
|
+
pruneEmptiedDirectories(target, removed);
|
|
3288
3414
|
return this.#finish({
|
|
3289
3415
|
target,
|
|
3290
3416
|
written: [],
|
|
@@ -4183,6 +4309,7 @@ exports.isUpstreamHooks = isUpstreamHooks;
|
|
|
4183
4309
|
exports.isUpstreamOptions = isUpstreamOptions;
|
|
4184
4310
|
exports.isVacant = isVacant;
|
|
4185
4311
|
exports.isWorktree = isWorktree;
|
|
4312
|
+
exports.listCanonPaths = listCanonPaths;
|
|
4186
4313
|
exports.listDirectories = listDirectories;
|
|
4187
4314
|
exports.listFiles = listFiles;
|
|
4188
4315
|
exports.matchesAnchor = matchesAnchor;
|
|
@@ -4194,6 +4321,7 @@ exports.matchesPrecondition = matchesPrecondition;
|
|
|
4194
4321
|
exports.matchesProtectedPath = matchesProtectedPath;
|
|
4195
4322
|
exports.matchesSensitivePath = matchesSensitivePath;
|
|
4196
4323
|
exports.pathToStorage = pathToStorage;
|
|
4324
|
+
exports.pruneEmptiedDirectories = pruneEmptiedDirectories;
|
|
4197
4325
|
exports.readAnchor = readAnchor;
|
|
4198
4326
|
exports.readExpectation = readExpectation;
|
|
4199
4327
|
exports.readFileHex = readFileHex;
|