@opengsd/gsd-path 1.2.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -66,3 +66,35 @@ are documented here. Format follows [Keep a Changelog](https://keepachangelog.co
66
66
  - Restore same-cycle recovery after partial review completion
67
67
  - Isolate assignment rejections and align native panel exclusions
68
68
  - Fix dispatch isolation, legacy pinning, and independent panel selection
69
+
70
+ ## [1.3.0] - 2026-09-21
71
+
72
+ ### Added
73
+ - migrate legacy runtimes during upgrades
74
+
75
+ ### Changed
76
+ - Clarify receipt reuse and consolidate release policy guidance
77
+ - Clarify legacy upgrade guidance and verify syntax
78
+ - update release notes for v1.2.0
79
+
80
+ ### Fixed
81
+ - validate installer changes without live host reruns
82
+ - preserve closed review cycles in host recovery
83
+ - clarify owner gates and accept serial task release evidence
84
+
85
+ ### Other
86
+ - Clarify reviewer ownership; live verification remains pending
87
+ - Reject unretired verification worktrees and branches in release evidence
88
+
89
+ ## [1.3.1] - 2026-09-22
90
+
91
+ ### Changed
92
+ - Clarify legacy runtime backup guidance
93
+ - Consolidate legacy runtime migration guidance
94
+ - prepare 1.3.1 patch candidate
95
+ - Clarify legacy runtime migration documentation
96
+ - update release notes for v1.3.0
97
+
98
+ ### Fixed
99
+ - describe untracked legacy migration in help
100
+ - preserve older untracked runtimes during migration
package/DOCS.md CHANGED
@@ -398,12 +398,19 @@ files. The supplied package must match both the version and digest; custom sourc
398
398
  builds may share a release version while having different digests. No command
399
399
  silently substitutes a newer runtime, and status/guards never install or download.
400
400
 
401
- For an old tracked `.gsd-path/runtime/`, run `gsd-path --runtime-migrate --project
401
+ For an old `.gsd-path/runtime/`, run `gsd-path --runtime-migrate --project
402
402
  /absolute/project --dry-run`, then the same command without `--dry-run` and review
403
403
  the Git diff. Migration removes the generated files from the checkout and writes
404
404
  the declaration and stable wiring. It does not stage or commit. Resolve local
405
- runtime edits or unknown files first. Interrupted migration is recovered by the
406
- same explicit command; its journal lives outside the checkout.
405
+ edits to tracked runtime files or unknown files first. When untracked managed
406
+ files differ from the supplied package, migration saves those files' original
407
+ bytes under `~/.gsd-path/runtime-migrations/backups/` and prints the backup
408
+ directory for review. Interrupted migration is recovered by the same explicit
409
+ command; its journal lives outside the checkout.
410
+
411
+ To migrate and then update skills and project wiring in one invocation, follow
412
+ [Legacy project runtime](UPDATE.md#legacy-project-runtime) for wizard consent,
413
+ unattended commands, dry-run scope, and recovery after an update failure.
407
414
 
408
415
  An ignore rule alone cannot migrate tracked runtime files. This runtime lifecycle
409
416
  covers runtime code and launch wiring; project-local skill copies, retained skill
package/QUICK.md CHANGED
@@ -67,11 +67,8 @@ node scripts/install.mjs --all --project "$(pwd)"
67
67
  Installs the [project contracts](DOCS.md#installing)
68
68
  (+ `.claude/CLAUDE.md` if Claude is selected).
69
69
  If those managed files already exist, a plain install **refuses and installs
70
- nothing** — see [project runtime versions](DOCS.md#project-runtime-versions)
71
- for legacy migration, then use `--update --project PATH` to refresh skills and
72
- hook wiring while keeping your contracts and selected runtime. Merge contract
73
- changes by hand
74
- ([UPDATE.md](UPDATE.md)).
70
+ nothing** — use [the update guide](UPDATE.md) for existing installations,
71
+ including [legacy runtime migration during upgrades](UPDATE.md#legacy-project-runtime).
75
72
 
76
73
  **Optional** archive/git guards:
77
74
 
package/README.md CHANGED
@@ -16,14 +16,14 @@ Support means the installer and dispatch contract exist. See the
16
16
  proof and each host's guard tier.
17
17
 
18
18
  <!-- release-docs -->
19
- **Latest npm release:** [@opengsd/gsd-path@1.2.0](https://www.npmjs.com/package/@opengsd/gsd-path/v/1.2.0) — [release notes](CHANGELOG.md#120---2026-09-20)
19
+ **Latest npm release:** [@opengsd/gsd-path@1.3.1](https://www.npmjs.com/package/@opengsd/gsd-path/v/1.3.1) — [release notes](CHANGELOG.md#131---2026-09-22)
20
20
 
21
21
  **Recent highlights**
22
- - add Path settings and project history dashboard
23
- - add optional Jev evidence screening
24
- - store worktrees and pinned runtimes outside project checkouts
25
- - centralize sub-agent model selection policy
26
- - reuse unchanged host receipts for releases
22
+ - describe untracked legacy migration in help
23
+ - preserve older untracked runtimes during migration
24
+ - Clarify legacy runtime backup guidance
25
+ - Consolidate legacy runtime migration guidance
26
+ - prepare 1.3.1 patch candidate
27
27
 
28
28
  <!-- /release-docs -->
29
29
 
package/UPDATE.md CHANGED
@@ -98,6 +98,32 @@ node scripts/install.mjs --update --claude --cursor
98
98
 
99
99
  `--update` only refreshes hosts that **already have** GSD Path installed.
100
100
 
101
+ ### Legacy project runtime
102
+
103
+ The interactive npm installer detects the old `.gsd-path/runtime/` layout when
104
+ you choose to update project wiring. Choose **Migrate and continue upgrade** to
105
+ migrate first, then update skills and wiring. Cancelling leaves the installation
106
+ unchanged. See [Project runtime versions](DOCS.md#project-runtime-versions)
107
+ for migration safety checks and backups. Migration never stages or commits changes.
108
+
109
+ For unattended upgrades, migration requires explicit consent:
110
+
111
+ ```bash
112
+ npx @opengsd/gsd-path@latest --update --runtime-migrate --project "/absolute/project" --dry-run
113
+ npx @opengsd/gsd-path@latest --update --runtime-migrate --project "/absolute/project"
114
+ ```
115
+
116
+ The combined dry run previews migration only and writes nothing. The real command
117
+ migrates, then validates and applies the update. These are separate operations:
118
+ if the update fails, the completed migration remains as an unstaged Git diff.
119
+ Review it with `git status --short` and `git diff` in the project.
120
+
121
+ Migration is needed once per project. For later updates, omit `--runtime-migrate`.
122
+ For runtime selection after migration, see
123
+ [Project runtime versions](DOCS.md#project-runtime-versions).
124
+ Without migration consent, legacy project updates stop before writing files and
125
+ print the exact migration command.
126
+
101
127
  ### After updating
102
128
 
103
129
  1. Restart your agent session (hosts reload skills on session start).
@@ -134,8 +160,8 @@ local edits. Clean source checkouts still refresh before a project update.
134
160
  Older unstamped installs show **Unknown — version metadata unavailable**.
135
161
  This does not establish that their runtime is outdated or broken. **Update**
136
162
  uses the guarded migration for legacy runtime directories and an upgrade for
137
- version-pinned runtimes. Migration leaves a reviewable Git diff and refuses
138
- modified or unknown runtime files; the dashboard shows the failure reason.
163
+ version-pinned runtimes. Migration leaves a reviewable Git diff; the dashboard
164
+ shows the failure reason when it stops.
139
165
  See [Project runtime versions](DOCS.md#project-runtime-versions) for manual
140
166
  migration and preview commands. Use `--doctor --project PATH` to check runtime files.
141
167
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengsd/gsd-path",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
4
4
  "description": "Disk-backed AI agent pipeline. Docs: DOCS.md (start here), QUICK.md, FULL.md, UPDATE.md",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1655,7 +1655,7 @@ function usage() {
1655
1655
  " --hooks-init add guards to an existing project without changing its contracts\n" +
1656
1656
  " --runtime-restore restore the exact declared runtime from --source-root\n" +
1657
1657
  " --runtime-upgrade explicitly select the supplied package runtime\n" +
1658
- " --runtime-migrate migrate a tracked legacy runtime for review\n" +
1658
+ " --runtime-migrate migrate a legacy runtime for review; add --update to continue upgrading\n" +
1659
1659
  " --hooks-refresh validate the selected runtime; keep its version\n" +
1660
1660
  " --hooks-refresh-full refresh native settings/git hooks; target flags create missing configs\n" +
1661
1661
  " --dry-run preview without writing\n" +
@@ -1684,6 +1684,8 @@ export async function main(argv, env = process.env) {
1684
1684
  colored: !process.env.NO_COLOR,
1685
1685
  version: packageVersion(),
1686
1686
  targets: TARGETS,
1687
+ legacyRuntime: lexists(path.join(process.cwd(), ".gsd-path", "runtime")) &&
1688
+ !lexists(path.join(process.cwd(), ".gsd-path", "runtime.json")),
1687
1689
  installed: (target, local) =>
1688
1690
  hasManagedInstall(local ? localRoot(target, process.cwd()) : defaultRoot(target, env)),
1689
1691
  });
@@ -1695,7 +1697,12 @@ export async function main(argv, env = process.env) {
1695
1697
  : path.resolve(SCRIPT_DIRECTORY, "..");
1696
1698
  const project =
1697
1699
  values.project !== undefined ? absolutePath(values.project) : null;
1698
- if (["runtime-restore", "runtime-upgrade", "runtime-migrate"].some(name => values[name])) {
1700
+ const migrateAndUpdate = values["runtime-migrate"] && values.update;
1701
+ if (migrateAndUpdate && (project === null || values["runtime-restore"] || values["runtime-upgrade"] || values.doctor || values["hooks-init"] || values["hooks-refresh"] || values["hooks-refresh-full"])) {
1702
+ ui.error("--runtime-migrate --update requires --project and cannot be combined with another runtime, doctor, or hook operation");
1703
+ return 2;
1704
+ }
1705
+ if (!migrateAndUpdate && ["runtime-restore", "runtime-upgrade", "runtime-migrate"].some(name => values[name])) {
1699
1706
  const interpreter = requiredPythonRuntime("project runtime");
1700
1707
  const result = spawnSync(interpreter, ["-B", path.join(SCRIPT_DIRECTORY, "install.py"),
1701
1708
  ...argv.filter(arg => arg !== "--no-color")], { stdio: "inherit", env });
@@ -1800,6 +1807,21 @@ export async function main(argv, env = process.env) {
1800
1807
  } else {
1801
1808
  plans = selected.map((target) => targetPlan(target, rootFor(target)));
1802
1809
  }
1810
+ if (migrateAndUpdate) {
1811
+ const interpreter = requiredPythonRuntime("project runtime");
1812
+ const migration = spawnSync(interpreter, ["-B", path.join(SCRIPT_DIRECTORY, "install.py"),
1813
+ "--runtime-migrate", "--project", project, "--source-root", sourceRoot,
1814
+ ...(values["dry-run"] ? ["--dry-run"] : [])], { stdio: "inherit", env });
1815
+ if (migration.error || migration.status !== 0) {
1816
+ if (migration.error) ui.error(migration.error.message);
1817
+ return migration.status ?? 1;
1818
+ }
1819
+ if (values["dry-run"]) {
1820
+ ui.result("Migration preview only; nothing was written. Run without --dry-run to migrate, then validate and apply the update.");
1821
+ return 0;
1822
+ }
1823
+ ui.result("Migration completed. Review the unstaged Git diff. Updating skills and wiring next; an update failure will retain the completed migration.");
1824
+ }
1803
1825
  const spin = ui.spinner("Preparing");
1804
1826
  try {
1805
1827
  const results = await install(sourceRootForInstall, plans, {
@@ -2641,17 +2641,16 @@ def install(
2641
2641
 
2642
2642
  if project is not None:
2643
2643
  _validate_directory_destination(project, "project path")
2644
- if dry_run:
2645
- _validate_project(
2646
- source_root,
2647
- project,
2648
- selected,
2649
- hooks,
2650
- [*mutation_roots, *planned_backups],
2651
- interpreter,
2652
- hooks_dir,
2653
- update,
2654
- )
2644
+ _validate_project(
2645
+ source_root,
2646
+ project,
2647
+ selected,
2648
+ hooks,
2649
+ [*mutation_roots, *planned_backups],
2650
+ interpreter,
2651
+ hooks_dir,
2652
+ update,
2653
+ )
2655
2654
 
2656
2655
  results = []
2657
2656
  with tempfile.TemporaryDirectory(prefix="gsd-path-install-") as temporary:
@@ -2840,6 +2839,8 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
2840
2839
  _release_install_locks(locks, created)
2841
2840
  print(f"runtime: {runtime_action} {pin['version']} ({pin['digest']})"
2842
2841
  + (" (dry run)" if arguments.dry_run else ""))
2842
+ if pin.get("backup"):
2843
+ print(f"legacy runtime backup: {pin['backup']}")
2843
2844
  return 0
2844
2845
  except (InstallerError, OSError, ValueError) as error:
2845
2846
  print(f"error: {error}", file=sys.stderr)
@@ -6,6 +6,7 @@ import json
6
6
  import os
7
7
  from pathlib import Path
8
8
  import shutil
9
+ import shlex
9
10
  import tempfile
10
11
  from contextlib import contextmanager
11
12
 
@@ -135,7 +136,9 @@ def prepare(source, project, *, dry_run=False):
135
136
  status_runtime.validate_runtime(pin)
136
137
  return pin
137
138
  if os.path.lexists(project / ".gsd-path/runtime"):
138
- raise ValueError("legacy project runtime requires --runtime-migrate --project PATH; migration produces a reviewable Git diff")
139
+ raise ValueError("legacy project runtime requires explicit migration before updating; run: "
140
+ f"npx @opengsd/gsd-path@latest --runtime-migrate --project {shlex.quote(str(project.resolve()))}; "
141
+ "then retry the update. Migration produces a reviewable Git diff")
139
142
  return publish(source, dry_run=dry_run)
140
143
 
141
144
 
@@ -232,6 +235,7 @@ def operate(source, project, action, *, dry_run=False):
232
235
  raise ValueError("legacy runtime contains unknown files; preserve and resolve them before migration")
233
236
  managed = entries + [project / ".gsd-path/status_runtime.py"]
234
237
  managed += [project / ".gsd-path" / n for n in GUARDS if (project / ".gsd-path" / n).exists()]
238
+ divergent = []
235
239
  for path in managed:
236
240
  if path.is_symlink() or not path.is_file():
237
241
  raise ValueError(f"unsafe migration file: {path}")
@@ -246,7 +250,7 @@ def operate(source, project, action, *, dry_run=False):
246
250
  if clean.stdout:
247
251
  raise ValueError(f"locally modified migration file: {path}")
248
252
  elif path.read_bytes() != (source / "scripts" / path.name).read_bytes():
249
- raise ValueError(f"untracked migration file differs from source: {path}")
253
+ divergent.append(path)
250
254
  pin = publish(source, dry_run=dry_run)
251
255
  if dry_run:
252
256
  return pin
@@ -254,6 +258,17 @@ def operate(source, project, action, *, dry_run=False):
254
258
  declaration = project / ".gsd-path/runtime.json"
255
259
  if os.path.lexists(declaration):
256
260
  raise ValueError("runtime declaration already exists; refusing legacy migration")
261
+ backup = None
262
+ if divergent:
263
+ backup_root = migration_journal(project).parent / "backups"
264
+ if backup_root.is_symlink():
265
+ raise ValueError(f"unsafe runtime migration backup directory: {backup_root}")
266
+ backup_root.mkdir(parents=True, exist_ok=True)
267
+ backup = Path(tempfile.mkdtemp(prefix="legacy-", dir=backup_root))
268
+ for path in divergent:
269
+ saved = backup / path.relative_to(project / ".gsd-path")
270
+ saved.parent.mkdir(parents=True, exist_ok=True)
271
+ saved.write_bytes(originals[path])
257
272
  updates = {project / ".gsd-path/status_runtime.py": (source / "scripts/status_runtime.py").read_bytes(),
258
273
  declaration: pin_text(pin).encode()}
259
274
  updates.update({p: None for p in entries})
@@ -275,4 +290,4 @@ def operate(source, project, action, *, dry_run=False):
275
290
  recover_migration(project)
276
291
  raise
277
292
  journal.unlink()
278
- return pin
293
+ return {**pin, "backup": str(backup)} if backup else pin
@@ -125,7 +125,7 @@ const confirm = (io, theme, title, yes = "Yes", no = "No") =>
125
125
 
126
126
  // Pure: runs the question flow and returns install.mjs argv (or null if cancelled).
127
127
  // `installed(target, local)` reports whether a managed install already exists for that host in that scope.
128
- export async function wizard({ input, output, colored = true, version = "", cwd = process.cwd(), installed = () => false, targets }) {
128
+ export async function wizard({ input, output, colored = true, version = "", cwd = process.cwd(), installed = () => false, legacyRuntime = false, targets }) {
129
129
  const theme = makeTheme(colored);
130
130
  const keys = keyReader(input);
131
131
  const io = { keys, output };
@@ -147,8 +147,13 @@ export async function wizard({ input, output, colored = true, version = "", cwd
147
147
  hostItems.some((item) => item.checked && hosts.includes(item.value)) &&
148
148
  (await confirm(io, theme, "Existing installs found. What do you want to do?", "Update in place", "Fresh install"));
149
149
 
150
- const project = await confirm(io, theme, "Write AGENTS.md + WORKFLOW.md contracts into this repo?", `Yes — ${cwd}`, "Not now");
150
+ const project = await confirm(io, theme, update ? "Refresh this project's wiring (keep its contracts and selected runtime)?" : "Write AGENTS.md + WORKFLOW.md contracts into this repo?", `Yes — ${cwd}`, "Not now");
151
151
  const hooks = project && (await confirm(io, theme, "Install guard hooks (archive immutability, ship-commit purity)?"));
152
+ const migrate = project && update && legacyRuntime;
153
+ if (migrate) {
154
+ output.write(`\n This project uses the old runtime layout. Migration moves managed runtime files\n outside the repo and leaves an unstaged Git diff for review. Local edits are preserved.\n`);
155
+ if (!(await confirm(io, theme, "Migrate this project before updating?", "Migrate and continue upgrade", "Cancel"))) return null;
156
+ }
152
157
 
153
158
  const argv = [];
154
159
  if (hosts.length === targets.length) argv.push("--all");
@@ -157,6 +162,7 @@ export async function wizard({ input, output, colored = true, version = "", cwd
157
162
  if (local) argv.push("--local");
158
163
  if (project) argv.push("--project", cwd);
159
164
  if (hooks) argv.push("--hooks");
165
+ if (migrate) argv.push("--runtime-migrate");
160
166
 
161
167
  output.write(`\n ${theme.dim("Equivalent command:")}\n ${theme.accent("$")} gsd-path ${argv.join(" ")}\n`);
162
168
  const go = await select(io, theme, "Ready?", [