agent-skillboard 0.3.1 → 0.3.2
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/CHANGELOG.md +20 -0
- package/README.md +19 -4
- package/docs/ai-skill-routing-goal.md +5 -0
- package/docs/install.md +15 -4
- package/docs/policy-model.md +7 -0
- package/docs/reference.md +7 -2
- package/docs/user-flow.md +5 -1
- package/docs/value-proof.md +2 -1
- package/docs/versioning.md +9 -4
- package/package.json +1 -1
- package/src/agent-integration-cli.mjs +31 -48
- package/src/agent-integration-command.mjs +38 -0
- package/src/inventory-refresh.mjs +1 -0
- package/src/migration/automatic-v2.mjs +14 -0
- package/src/migration/v2-projection.mjs +2 -1
- package/src/migration/v2-transaction.mjs +4 -0
- package/src/setup-policy-migration.mjs +43 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.3.2 — 2026-07-14
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Made global npm updates and `skillboard setup` automatically upgrade a valid
|
|
10
|
+
version 1 user policy when every migration choice is already understood.
|
|
11
|
+
- Kept the explicit `migrate v2` preview, apply, and rollback commands for
|
|
12
|
+
project policies, audits, and recovery.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- Removed the hidden manual migration step left after a 0.3.1 update while
|
|
17
|
+
preserving explicit terminal denials as disabled and review-only quarantine
|
|
18
|
+
states as enabled, agent-local skills.
|
|
19
|
+
- Kept automatic migration non-mutating for unknown future ambiguity kinds or
|
|
20
|
+
policy skills that are not currently observed, and restored exact version 1
|
|
21
|
+
bytes after any transactional failure.
|
|
22
|
+
- Restored invoking-user ownership for migration backups, manifests, and
|
|
23
|
+
generated inventory during sudo-driven global updates.
|
|
24
|
+
|
|
5
25
|
## 0.3.1 — 2026-07-14
|
|
6
26
|
|
|
7
27
|
### Added
|
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ Valid installed skills default to enabled and agent-local. Sharing is opt-in per
|
|
|
9
9
|
skill. Optional preference ranks enabled skills installed for the current agent
|
|
10
10
|
and never changes availability or copies files.
|
|
11
11
|
|
|
12
|
-
Status: public alpha. Package 0.3.
|
|
12
|
+
Status: public alpha. Package 0.3.2 writes policy schema v2.
|
|
13
13
|
|
|
14
14
|
<p align="center">
|
|
15
15
|
<img src="https://raw.githubusercontent.com/NyXXiR/skillboard/main/skillboard.png" alt="SkillBoard architecture diagram" width="100%">
|
|
@@ -56,6 +56,14 @@ prefix, activate the Node environment that owns each stale copy, and run
|
|
|
56
56
|
uninstall another prefix. Restart or refresh agents after an update because
|
|
57
57
|
some agents cache user skills.
|
|
58
58
|
|
|
59
|
+
When an update finds a valid version 1 user policy, setup automatically migrates
|
|
60
|
+
it only when all reported choices are understood. It creates an adjacent exact
|
|
61
|
+
backup, keeps explicit terminal denials disabled, and maps review-only
|
|
62
|
+
quarantine states to enabled and agent-local. Unknown future ambiguity leaves
|
|
63
|
+
the policy unchanged and prints the explicit preview command instead. The same
|
|
64
|
+
review path is used when a v1 policy skill is not currently observed, avoiding
|
|
65
|
+
an unhealthy generated inventory or silently forgetting a denial.
|
|
66
|
+
|
|
59
67
|
`setup` is safe to rerun. It refreshes managed guidance and inventory, discovers
|
|
60
68
|
late standard agent homes and Hermes profiles, and fills already-selected
|
|
61
69
|
`shared: true` skills into newly active roots. It does not make every skill
|
|
@@ -157,9 +165,16 @@ skillboard migrate v2 --config <path> --yes --json
|
|
|
157
165
|
skillboard migrate v2 --config <path> --rollback <backup> --json
|
|
158
166
|
```
|
|
159
167
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
168
|
+
Starting in 0.3.2, setup and global package updates automatically migrate a
|
|
169
|
+
valid version 1 user policy when the report contains only understood choices.
|
|
170
|
+
The transaction creates an adjacent exact backup. Explicit terminal denials
|
|
171
|
+
remain disabled; review-only quarantine becomes enabled and agent-local. An
|
|
172
|
+
unknown future ambiguity leaves the policy unchanged and prints the preview
|
|
173
|
+
command. A policy skill that is not currently observed also stays on v1 for an
|
|
174
|
+
explicit decision, because forgetting a disabled entry could enable it if it
|
|
175
|
+
later reappears. The explicit commands remain available for project policies,
|
|
176
|
+
audits, and rollback. Other v1 mutations still refuse and point to migration.
|
|
177
|
+
v0.4.0 removes the v1 reader.
|
|
163
178
|
|
|
164
179
|
## Cleanup
|
|
165
180
|
|
|
@@ -45,6 +45,11 @@ inventory, and materializes missing copies only for skills already marked
|
|
|
45
45
|
discovery state. They must not introduce project scope, source trust gates, or
|
|
46
46
|
another authorization layer.
|
|
47
47
|
|
|
48
|
+
When setup encounters version 1 user policy, it may converge that policy to v2
|
|
49
|
+
only if every migration ambiguity is a known review-only quarantine mapping.
|
|
50
|
+
It must preserve terminal denials, create an exact recoverable backup, and leave
|
|
51
|
+
unknown future ambiguity unchanged for an explicit decision.
|
|
52
|
+
|
|
48
53
|
## Development rule
|
|
49
54
|
|
|
50
55
|
Every route, brief, guard, dashboard, generated bridge, and policy mutation must
|
package/docs/install.md
CHANGED
|
@@ -35,6 +35,15 @@ keep. Activate the Node environment that owns each stale prefix, confirm it with
|
|
|
35
35
|
SkillBoard does not automatically uninstall another prefix or request elevated
|
|
36
36
|
permissions for cleanup.
|
|
37
37
|
|
|
38
|
+
From 0.3.2, setup and global postinstall automatically migrate a valid version
|
|
39
|
+
1 user policy when every reported migration choice is understood. Migration
|
|
40
|
+
creates an adjacent exact backup, preserves explicit terminal denials as
|
|
41
|
+
disabled, and makes review-only quarantine states enabled and agent-local. An
|
|
42
|
+
unknown future ambiguity leaves the policy unchanged and prints the explicit
|
|
43
|
+
preview command; no migration files are changed. A v1 policy skill that is not
|
|
44
|
+
currently observed uses the same review path so setup neither invents inventory
|
|
45
|
+
nor silently forgets its policy.
|
|
46
|
+
|
|
38
47
|
Run setup later when lifecycle scripts were skipped, another agent or Hermes
|
|
39
48
|
profile was added, or a managed root needs to be repaired:
|
|
40
49
|
|
|
@@ -104,10 +113,12 @@ skillboard migrate v2 --config <path> --yes --json
|
|
|
104
113
|
skillboard migrate v2 --config <path> --rollback <backup> --json
|
|
105
114
|
```
|
|
106
115
|
|
|
107
|
-
Version 1 is read-only during v0.3.x. v0.4.0 removes the v1 reader.
|
|
108
|
-
|
|
109
|
-
the
|
|
110
|
-
|
|
116
|
+
Version 1 is read-only during v0.3.x. v0.4.0 removes the v1 reader. Setup and
|
|
117
|
+
global postinstall automatically migrate an understood version 1 user policy
|
|
118
|
+
through the same backup and validation transaction. Unknown future ambiguity
|
|
119
|
+
or a policy skill that is not currently observed leaves it unchanged and prints
|
|
120
|
+
the preview command. Explicit preview, apply, and rollback remain available for
|
|
121
|
+
project policies, audits, and recovery.
|
|
111
122
|
|
|
112
123
|
## Uninstall
|
|
113
124
|
|
package/docs/policy-model.md
CHANGED
|
@@ -71,3 +71,10 @@ skillboard migrate v2 --config <path> --rollback <backup> --json
|
|
|
71
71
|
```
|
|
72
72
|
|
|
73
73
|
v0.4.0 removes the v1 reader.
|
|
74
|
+
|
|
75
|
+
Setup and global postinstall automatically upgrade a valid version 1 user
|
|
76
|
+
policy when its migration report contains only understood choices. The
|
|
77
|
+
transaction keeps an exact adjacent backup. An unknown future ambiguity leaves
|
|
78
|
+
the policy unchanged and requires the explicit preview/apply flow. Policy skills
|
|
79
|
+
that are not currently observed also require review rather than being silently
|
|
80
|
+
forgotten.
|
package/docs/reference.md
CHANGED
|
@@ -153,8 +153,13 @@ Preview changes no bytes. Apply creates an adjacent byte-for-byte backup and
|
|
|
153
153
|
writes v2 policy plus generated inventory atomically. Rollback restores the
|
|
154
154
|
selected backup. v0.4.0 removes the v1 reader.
|
|
155
155
|
|
|
156
|
-
Setup and npm postinstall
|
|
157
|
-
|
|
156
|
+
Setup and global npm postinstall automatically migrate a valid version 1 user
|
|
157
|
+
policy when all reported choices are understood. They create an adjacent exact
|
|
158
|
+
backup, preserve terminal denials as disabled, and map review-only quarantine
|
|
159
|
+
to enabled and agent-local. An unknown future ambiguity leaves the policy
|
|
160
|
+
unchanged, changes no migration files, and prints the preview form. The same
|
|
161
|
+
review path names policy skills that are not currently observed. Explicit
|
|
162
|
+
commands remain available for project policies, audit, and recovery.
|
|
158
163
|
|
|
159
164
|
Legacy fields are interpreted only by migration and never become hidden v2
|
|
160
165
|
authorization. Primary examples are `examples/v2-multi-source.config.yaml` and
|
package/docs/user-flow.md
CHANGED
|
@@ -111,4 +111,8 @@ skillboard migrate v2 --config <path> --rollback <backup> --json
|
|
|
111
111
|
```
|
|
112
112
|
|
|
113
113
|
Version 1 remains readable but immutable for the v0.3.x one-release read-only
|
|
114
|
-
window.
|
|
114
|
+
window. Setup and global package updates automatically migrate an understood
|
|
115
|
+
version 1 user policy with an exact adjacent backup. Unknown future ambiguity
|
|
116
|
+
or a policy skill that is not currently observed leaves it unchanged and prints
|
|
117
|
+
the preview command. Project policies and other ordinary commands still require
|
|
118
|
+
explicit migration. v0.4.0 removes the v1 reader.
|
package/docs/value-proof.md
CHANGED
|
@@ -15,7 +15,8 @@ node --test test/v2-agent-sharing.test.mjs test/v2-onboarding-behavior.test.mjs
|
|
|
15
15
|
- Optional preference changes ranking without changing availability.
|
|
16
16
|
- Source and provenance audits do not change guard results.
|
|
17
17
|
- Commands produce the same result from different working directories.
|
|
18
|
-
- Version 1
|
|
18
|
+
- Version 1 project policy remains read-only until explicit migration; setup
|
|
19
|
+
automatically migrates an understood user policy with an exact backup.
|
|
19
20
|
|
|
20
21
|
## Reproduce the user surface
|
|
21
22
|
|
package/docs/versioning.md
CHANGED
|
@@ -22,9 +22,13 @@ Preview is non-mutating, apply creates an adjacent byte-for-byte backup, and
|
|
|
22
22
|
rollback restores the selected backup. Old action ids, hooks, and lock
|
|
23
23
|
projections are invalid after migration.
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
Starting in 0.3.2, setup and global package updates automatically migrate a
|
|
26
|
+
valid version 1 user policy when all migration choices are understood. They use
|
|
27
|
+
the same exact-backup, atomic-write, validation, and recovery transaction as
|
|
28
|
+
the explicit command. Unknown future ambiguity leaves the policy unchanged and
|
|
29
|
+
prints the preview command. A policy skill that is not currently observed also
|
|
30
|
+
requires review, preserving future denial semantics. Doctor and project-local
|
|
31
|
+
commands remain non-mutating.
|
|
28
32
|
|
|
29
33
|
## Versioned v2 contract
|
|
30
34
|
|
|
@@ -39,7 +43,8 @@ schema.
|
|
|
39
43
|
1. Run `npm run check`.
|
|
40
44
|
2. Run `npm pack --dry-run --json` and inspect public contents.
|
|
41
45
|
3. Install the tarball into an isolated prefix and run
|
|
42
|
-
`skillboard doctor --summary`; verify fresh setup, v1
|
|
46
|
+
`skillboard doctor --summary`; verify fresh setup, safe v1 auto-migration,
|
|
47
|
+
exact backup preservation, and a
|
|
43
48
|
late-agent shared-skill reconciliation.
|
|
44
49
|
4. Confirm `CHANGELOG.md` includes the package version.
|
|
45
50
|
5. Push `main` and wait for the complete cross-platform check matrix.
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
2
|
import { mkdir } from "node:fs/promises";
|
|
3
3
|
import { createInterface } from "node:readline";
|
|
4
|
+
import { commandPrefix, shellQuote } from "./agent-integration-command.mjs";
|
|
4
5
|
import { installAgentIntegration, uninstallAgentIntegration } from "./agent-integration-files.mjs";
|
|
5
6
|
import { applyOwnership, applyOwnershipTree, resolveSetupHome, setupOwnership } from "./agent-integration-home.mjs";
|
|
6
7
|
import { setupAgentSkillTargets, supportedAgentNames } from "./agent-skill-roots.mjs";
|
|
@@ -13,6 +14,7 @@ import {
|
|
|
13
14
|
} from "./agent-root-registry.mjs";
|
|
14
15
|
import { refreshAgentInventory } from "./inventory-refresh.mjs";
|
|
15
16
|
import { reconcileSharedSkills } from "./shared-skill-reconcile.mjs";
|
|
17
|
+
import { upgradeLegacyUserPolicy } from "./setup-policy-migration.mjs";
|
|
16
18
|
|
|
17
19
|
export async function runSetupCommand(options, stdout, runtime = defaultRuntime()) {
|
|
18
20
|
assertSetupOptions(options);
|
|
@@ -52,7 +54,21 @@ export async function runSetupCommand(options, stdout, runtime = defaultRuntime(
|
|
|
52
54
|
preserveLegacyPolicy: true
|
|
53
55
|
});
|
|
54
56
|
const configPath = resolve(home, inventory.configPath);
|
|
55
|
-
|
|
57
|
+
let inventoryPath = inventory.inventoryPath === null ? null : resolve(home, inventory.inventoryPath);
|
|
58
|
+
const policyUpgrade = await upgradeLegacyUserPolicy({
|
|
59
|
+
home,
|
|
60
|
+
configPath,
|
|
61
|
+
inventoryPath,
|
|
62
|
+
observedSkillIds: inventory.observedSkillIds,
|
|
63
|
+
failpoint: runtime.migrationFailpoint
|
|
64
|
+
});
|
|
65
|
+
if (policyUpgrade.status === "upgraded") {
|
|
66
|
+
inventoryPath = policyUpgrade.inventoryPath;
|
|
67
|
+
await applyOwnership(configPath, ownership);
|
|
68
|
+
await applyOwnership(inventoryPath, ownership);
|
|
69
|
+
for (const artifact of policyUpgrade.artifacts) await applyOwnership(artifact, ownership);
|
|
70
|
+
inventory = await refreshAgentInventory({ root: home, home, env, registeredRoots });
|
|
71
|
+
}
|
|
56
72
|
const shared = inventoryPath === null
|
|
57
73
|
? { created: [], unchanged: [], preserved: [], blocked: [] }
|
|
58
74
|
: await reconcileSharedSkills({ home, env, targets, configPath, inventoryPath });
|
|
@@ -80,14 +96,24 @@ export async function runSetupCommand(options, stdout, runtime = defaultRuntime(
|
|
|
80
96
|
writeList(stdout, "Blocked shared copies", shared.blocked.map(formatBlockedEntry));
|
|
81
97
|
stdout.write(`User policy: ${inventory.configPath}\n`);
|
|
82
98
|
stdout.write(`Observed skills: ${inventory.scan.scannedSkills}\n`);
|
|
83
|
-
if (
|
|
84
|
-
stdout.write("
|
|
99
|
+
if (policyUpgrade.status === "upgraded") {
|
|
100
|
+
stdout.write("User policy upgraded automatically to version 2.\n");
|
|
101
|
+
stdout.write(`Backup: ${policyUpgrade.backupPath}\n`);
|
|
102
|
+
} else if (policyUpgrade.status === "decision-required") {
|
|
103
|
+
stdout.write("Policy version 1 needs review before migration; no migration files were changed.\n");
|
|
104
|
+
if (policyUpgrade.unobservedSkillIds.length > 0) {
|
|
105
|
+
stdout.write(`Policy skills not currently observed: ${formatList(policyUpgrade.unobservedSkillIds)}\n`);
|
|
106
|
+
}
|
|
85
107
|
stdout.write(`Preview migration: ${commandPrefix(runtime)} migrate v2 --config ${shellQuote(configPath, runtime.platform)} --json\n`);
|
|
86
108
|
}
|
|
87
109
|
stdout.write("Next:\n");
|
|
88
110
|
stdout.write("- Restart or refresh agents that cache user skills.\n");
|
|
89
111
|
stdout.write("- Run skillboard doctor --summary to check policy and executable paths.\n");
|
|
90
|
-
|
|
112
|
+
if (policyUpgrade.status === "decision-required") {
|
|
113
|
+
stdout.write("- User-level policy was preserved and installed skills were rescanned; no project was initialized.\n");
|
|
114
|
+
} else {
|
|
115
|
+
stdout.write("- User-level policy and inventory were refreshed; no project was initialized.\n");
|
|
116
|
+
}
|
|
91
117
|
stdout.write('- Ask the agent in a workspace: "Review this plan and point out weak assumptions."\n');
|
|
92
118
|
stdout.write("- SkillBoard will step in when skills overlap, routing is ambiguous, or you ask for a skill decision.\n");
|
|
93
119
|
return 0;
|
|
@@ -222,46 +248,3 @@ function formatSharedEntry(entry) {
|
|
|
222
248
|
function formatBlockedEntry(entry) {
|
|
223
249
|
return `${entry.agent ?? "unknown"}:${entry.skill}:${entry.reason}`;
|
|
224
250
|
}
|
|
225
|
-
|
|
226
|
-
function commandPrefix(runtime) {
|
|
227
|
-
const entrypoint = runtime.entrypointPath ?? "";
|
|
228
|
-
const normalized = entrypoint.replace(/\\/g, "/");
|
|
229
|
-
if (normalized.includes("/_npx/")) {
|
|
230
|
-
const packageSpec = runtime.packageSpec ?? "agent-skillboard";
|
|
231
|
-
return `npx --yes --package ${shellQuote(packageSpec, runtime.platform)} skillboard`;
|
|
232
|
-
}
|
|
233
|
-
if (isSourceTreeEntrypoint(entrypoint)) {
|
|
234
|
-
return `node ${sourceTreeEntrypoint(entrypoint, runtime.cwd ?? process.cwd(), runtime.platform)}`;
|
|
235
|
-
}
|
|
236
|
-
return "skillboard";
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
function isSourceTreeEntrypoint(entrypoint) {
|
|
240
|
-
if (entrypoint === "") {
|
|
241
|
-
return false;
|
|
242
|
-
}
|
|
243
|
-
const normalized = entrypoint.replace(/\\/g, "/");
|
|
244
|
-
return (normalized === "bin/skillboard.mjs" || normalized.endsWith("/bin/skillboard.mjs"))
|
|
245
|
-
&& !normalized.includes("/node_modules/")
|
|
246
|
-
&& !normalized.includes("/_npx/")
|
|
247
|
-
&& !normalized.includes("/.npm/");
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function sourceTreeEntrypoint(entrypoint, cwd, platform) {
|
|
251
|
-
const absoluteEntrypoint = isAbsolute(entrypoint) ? entrypoint : resolve(cwd, entrypoint);
|
|
252
|
-
const relativeEntrypoint = relative(cwd, absoluteEntrypoint).replace(/\\/g, "/");
|
|
253
|
-
if (!relativeEntrypoint.startsWith("../") && relativeEntrypoint !== ".." && !isAbsolute(relativeEntrypoint)) {
|
|
254
|
-
return shellQuote(relativeEntrypoint, platform);
|
|
255
|
-
}
|
|
256
|
-
return shellQuote(absoluteEntrypoint, platform);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function shellQuote(value, platform = process.platform) {
|
|
260
|
-
if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(value)) {
|
|
261
|
-
return value;
|
|
262
|
-
}
|
|
263
|
-
if (platform === "win32") {
|
|
264
|
-
return `"${value.replace(/"/g, '""')}"`;
|
|
265
|
-
}
|
|
266
|
-
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
267
|
-
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
2
|
+
|
|
3
|
+
export function commandPrefix(runtime) {
|
|
4
|
+
const entrypoint = runtime.entrypointPath ?? "";
|
|
5
|
+
const normalized = entrypoint.replace(/\\/g, "/");
|
|
6
|
+
if (normalized.includes("/_npx/")) {
|
|
7
|
+
const packageSpec = runtime.packageSpec ?? "agent-skillboard";
|
|
8
|
+
return `npx --yes --package ${shellQuote(packageSpec, runtime.platform)} skillboard`;
|
|
9
|
+
}
|
|
10
|
+
if (isSourceTreeEntrypoint(entrypoint)) {
|
|
11
|
+
return `node ${sourceTreeEntrypoint(entrypoint, runtime.cwd ?? process.cwd(), runtime.platform)}`;
|
|
12
|
+
}
|
|
13
|
+
return "skillboard";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isSourceTreeEntrypoint(entrypoint) {
|
|
17
|
+
if (entrypoint === "") return false;
|
|
18
|
+
const normalized = entrypoint.replace(/\\/g, "/");
|
|
19
|
+
return (normalized === "bin/skillboard.mjs" || normalized.endsWith("/bin/skillboard.mjs"))
|
|
20
|
+
&& !normalized.includes("/node_modules/")
|
|
21
|
+
&& !normalized.includes("/_npx/")
|
|
22
|
+
&& !normalized.includes("/.npm/");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function sourceTreeEntrypoint(entrypoint, cwd, platform) {
|
|
26
|
+
const absoluteEntrypoint = isAbsolute(entrypoint) ? entrypoint : resolve(cwd, entrypoint);
|
|
27
|
+
const relativeEntrypoint = relative(cwd, absoluteEntrypoint).replace(/\\/g, "/");
|
|
28
|
+
if (!relativeEntrypoint.startsWith("../") && relativeEntrypoint !== ".." && !isAbsolute(relativeEntrypoint)) {
|
|
29
|
+
return shellQuote(relativeEntrypoint, platform);
|
|
30
|
+
}
|
|
31
|
+
return shellQuote(absoluteEntrypoint, platform);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function shellQuote(value, platform = process.platform) {
|
|
35
|
+
if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(value)) return value;
|
|
36
|
+
if (platform === "win32") return `"${value.replace(/"/g, '""')}"`;
|
|
37
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
38
|
+
}
|
|
@@ -85,6 +85,7 @@ async function refreshLocked(options) {
|
|
|
85
85
|
bootstrappedV2,
|
|
86
86
|
configPath: relativeArtifactPath(root, configPath),
|
|
87
87
|
inventoryPath: generatedInventory === null ? null : relativeArtifactPath(root, inventoryPath),
|
|
88
|
+
observedSkillIds: inventory.skills.map(({ id }) => id).sort((left, right) => left.localeCompare(right)),
|
|
88
89
|
inventoryChanged,
|
|
89
90
|
changed: plan.changed,
|
|
90
91
|
plan,
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const SAFE_AMBIGUITY_KIND = "review_only_quarantine";
|
|
2
|
+
|
|
3
|
+
export function canAutomaticallyMigrateV2(report) {
|
|
4
|
+
if (report?.mode !== "preview" || report.changed !== true || report.target_version !== 2) return false;
|
|
5
|
+
if (!Array.isArray(report.ambiguities)) return false;
|
|
6
|
+
return report.ambiguities.every((ambiguity) => (
|
|
7
|
+
ambiguity?.kind === SAFE_AMBIGUITY_KIND
|
|
8
|
+
&& ambiguity.mapped_enabled === true
|
|
9
|
+
&& ambiguity.requires_grouped_confirmation === true
|
|
10
|
+
&& Array.isArray(ambiguity.skill_ids)
|
|
11
|
+
&& ambiguity.skill_ids.length > 0
|
|
12
|
+
&& ambiguity.skill_ids.every((skillId) => typeof skillId === "string" && skillId.length > 0)
|
|
13
|
+
));
|
|
14
|
+
}
|
|
@@ -46,6 +46,9 @@ export async function migrateV2(options) {
|
|
|
46
46
|
|
|
47
47
|
async function migrateForward(configPath, inventoryPath, options) {
|
|
48
48
|
const inputBytes = await readFile(configPath);
|
|
49
|
+
if (options.expectedInputSha256 !== undefined && sha256(inputBytes) !== options.expectedInputSha256) {
|
|
50
|
+
throw new Error("Config changed after migration preview; no files were changed.");
|
|
51
|
+
}
|
|
49
52
|
const inputText = inputBytes.toString("utf8");
|
|
50
53
|
const { document, parsed } = parseMigrationConfig(inputText);
|
|
51
54
|
const version = parsed.version ?? 1;
|
|
@@ -85,6 +88,7 @@ async function migrateForward(configPath, inventoryPath, options) {
|
|
|
85
88
|
changed: true,
|
|
86
89
|
backup: basename(backup.configBackupPath),
|
|
87
90
|
manifest: basename(backup.manifestPath),
|
|
91
|
+
inventory_backup: backup.inventoryBackupPath === null ? null : basename(backup.inventoryBackupPath),
|
|
88
92
|
...report
|
|
89
93
|
};
|
|
90
94
|
} catch (error) {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { dirname, join, resolve } from "node:path";
|
|
2
|
+
import { canAutomaticallyMigrateV2 } from "./migration/automatic-v2.mjs";
|
|
3
|
+
import { migrateV2 } from "./migration/v2-transaction.mjs";
|
|
4
|
+
|
|
5
|
+
export async function upgradeLegacyUserPolicy(options) {
|
|
6
|
+
if (options.inventoryPath !== null) {
|
|
7
|
+
return { status: "current", inventoryPath: options.inventoryPath, artifacts: [] };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const inventoryPath = resolve(options.home, ".skillboard", "inventory.json");
|
|
11
|
+
const migrationOptions = {
|
|
12
|
+
configPath: options.configPath,
|
|
13
|
+
inventoryPath,
|
|
14
|
+
failpoint: options.failpoint
|
|
15
|
+
};
|
|
16
|
+
const preview = await migrateV2({ ...migrationOptions, apply: false });
|
|
17
|
+
const observed = new Set(options.observedSkillIds);
|
|
18
|
+
const unobservedSkillIds = preview.skills.map(({ id }) => id).filter((id) => !observed.has(id));
|
|
19
|
+
if (!canAutomaticallyMigrateV2(preview) || unobservedSkillIds.length > 0) {
|
|
20
|
+
return {
|
|
21
|
+
status: "decision-required",
|
|
22
|
+
inventoryPath: null,
|
|
23
|
+
artifacts: [],
|
|
24
|
+
preview,
|
|
25
|
+
unobservedSkillIds
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const applied = await migrateV2({
|
|
30
|
+
...migrationOptions,
|
|
31
|
+
apply: true,
|
|
32
|
+
expectedInputSha256: preview.input_sha256
|
|
33
|
+
});
|
|
34
|
+
const directory = dirname(options.configPath);
|
|
35
|
+
const artifactNames = [applied.backup, applied.manifest, applied.inventory_backup].filter(Boolean);
|
|
36
|
+
return {
|
|
37
|
+
status: "upgraded",
|
|
38
|
+
inventoryPath,
|
|
39
|
+
backupPath: join(directory, applied.backup),
|
|
40
|
+
artifacts: artifactNames.map((name) => join(directory, name)),
|
|
41
|
+
report: applied
|
|
42
|
+
};
|
|
43
|
+
}
|