Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,10 @@ the declaration and stable wiring. It does not stage or commit. Resolve local
runtime edits or unknown files first. Interrupted migration is recovered by the
same explicit command; its journal lives outside the checkout.

To migrate and then update skills and project wiring in one invocation, follow
[Legacy project runtime](UPDATE.md#legacy-project-runtime) for wizard consent,
unattended commands, dry-run scope, and recovery after an update failure.

An ignore rule alone cannot migrate tracked runtime files. This runtime lifecycle
covers runtime code and launch wiring; project-local skill copies, retained skill
backups, and mixed user/host settings remain separately owned installation output.
7 changes: 2 additions & 5 deletions QUICK.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,8 @@ node scripts/install.mjs --all --project "$(pwd)"
Installs the [project contracts](DOCS.md#installing)
(+ `.claude/CLAUDE.md` if Claude is selected).
If those managed files already exist, a plain install **refuses and installs
nothing** — see [project runtime versions](DOCS.md#project-runtime-versions)
for legacy migration, then use `--update --project PATH` to refresh skills and
hook wiring while keeping your contracts and selected runtime. Merge contract
changes by hand
([UPDATE.md](UPDATE.md)).
nothing** — use [the update guide](UPDATE.md) for existing installations,
including [legacy runtime migration during upgrades](UPDATE.md#legacy-project-runtime).

**Optional** archive/git guards:

Expand Down
4 changes: 4 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ publication workflow never increments or pushes a package version. A version-onl
change does not invalidate live evidence; package identity, dependencies, and
other package changes do.

For installer-only releases and mixed changes, follow the
[live-check scope](docs/trust-validation/TRUST-VALIDATION-SPEC.md#live-check-scope).
Inspect the plan below before starting any live evaluations.

1. From a clean checkout, inspect which hosts need new evidence:

```bash
Expand Down
26 changes: 26 additions & 0 deletions UPDATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,32 @@ node scripts/install.mjs --update --claude --cursor

`--update` only refreshes hosts that **already have** GSD Path installed.

### Legacy project runtime

The interactive npm installer detects the old `.gsd-path/runtime/` layout when
you choose to update project wiring. Choose **Migrate and continue upgrade** to
migrate first, then update skills and wiring. Cancelling leaves the installation
unchanged. Migration preserves locally modified files by stopping for you to
resolve them; it never stages or commits changes.

For unattended upgrades, migration requires explicit consent:

```bash
npx @opengsd/gsd-path@latest --update --runtime-migrate --project "/absolute/project" --dry-run
npx @opengsd/gsd-path@latest --update --runtime-migrate --project "/absolute/project"
```

The combined dry run previews migration only and writes nothing. The real command
migrates, then validates and applies the update. These are separate operations:
if the update fails, the completed migration remains as an unstaged Git diff.
Review it with `git status --short` and `git diff` in the project.

Migration is needed once per project. For later updates, omit `--runtime-migrate`.
For runtime selection after migration, see
[Project runtime versions](DOCS.md#project-runtime-versions).
Without migration consent, legacy project updates stop before writing files and
print the exact migration command.

### After updating

1. Restart your agent session (hosts reload skills on session start).
Expand Down
15 changes: 12 additions & 3 deletions docs/trust-validation/TRUST-VALIDATION-SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ Every release is trusted only when:
`evidence/releases/<original-package-version>/`. Each host must identify a
distinct run, landing commit, ship commit, and integration commit.
3. `npm run verify:release` validates those receipts and compares each tested
candidate with HEAD. Reuse is allowed only when that host's runtime and
integration inputs are unchanged. Missing, invalid, or stale evidence
candidate with HEAD. Reuse is allowed when that host's workflow inputs are
unchanged under the classification below. Installer tooling is verified by
offline tests instead of repeating agent workflows. Missing, invalid, or stale evidence
requires a fresh run. A current-version receipt takes precedence; a failed
current receipt cannot be hidden by an older pass.

Expand All @@ -44,6 +45,7 @@ validated receipt. No tag alone exempts a host with missing evidence.
| Changed files | Required live checks |
|---|---|
| `platforms/<declared-host>/` | That host if included in release evaluations; multiple host changes combine |
| `scripts/install.mjs`, `scripts/install.py`, `scripts/wizard.mjs`, `scripts/runtime_store.py` | None; offline installer lifecycle, migration, rollback, runtime pinning, and installed-guard tests apply |
| Shared adapters, skills, runtime scripts, `AGENTS.md`, `WORKFLOW.md`, or unclassified paths | Every release evaluation host |
| `docs/`, other root Markdown, `tests/`, `.github/`, `daemon/` | None |
| `scripts/bump_version.mjs`, `scripts/update_release_docs.mjs`, `scripts/prepare_release_evidence.sh`, `scripts/check_trust_evidence.py` | None; automated verification still applies |
Expand All @@ -53,6 +55,13 @@ Added and deleted paths count, including both sides of renames. No old receipt
is relabeled as current evidence. Required receipts retain all existing
candidate, native-child, task, review, archive, integration, and guard checks.

Installer tooling runs during installation, update, and migration; a complete
agent milestone does not replace tests of those operations. This category does
not exempt the files it installs: changes to skills, host adapters, guard
implementations, runtime payloads, the host manifest, or package contents still
use their own live-check classification. Mixed changes combine requirements.
`npm run verify` remains required and includes the installer and guard suites.

Release 1.2.0 includes shared pipeline changes, so all eight evaluation hosts
need evidence that covers those changes. Subsequent documentation, version-only,
and release-policy changes can reuse it. Never rewrite a receipt's candidate
Expand All @@ -78,7 +87,7 @@ partial or unverifiable blocks release. It never becomes an implicit pass.
| Unit and integration tests | Install, state routing, handoff validation, isolation, recovery, guards, archive, integration | `npm run verify` |
| Lightweight live smoke | Real host invocation and a bounded artifact | `.github/workflows/dogfood.yml` |
| Full live milestone | Real child dispatch, build, review, archive, merge, and tag on one host | release receipt |
| Release reconciliation | Every host has validated evidence covering its unchanged runtime inputs | `npm run verify:release` |
| Release reconciliation | Every evaluation host has validated evidence reusable under [Live-check scope](#live-check-scope) | `npm run verify:release` |

Simulated full-cycle tests are strong evidence for the deterministic disk and
Git contract. They do not replace real child-agent execution.
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@opengsd/gsd-path",
"version": "1.2.0",
"version": "1.3.0",
"description": "Disk-backed AI agent pipeline. Docs: DOCS.md (start here), QUICK.md, FULL.md, UPDATE.md",
"license": "MIT",
"repository": {
Expand Down
7 changes: 7 additions & 0 deletions scripts/check_trust_evidence.py
Original file line number Diff line number Diff line change
Expand Up @@ -874,6 +874,12 @@ def changed_host_scope(repo: Path, baseline: str, hosts: Sequence[str]) -> Mappi
"scripts/bump_version.mjs", "scripts/update_release_docs.mjs",
"scripts/prepare_release_evidence.sh", "scripts/check_trust_evidence.py",
}
# Installer execution is covered by offline lifecycle and installed-guard
# tests. Installed skills, adapters and runtime payloads still need live proof.
installer_tools = {
"scripts/install.mjs", "scripts/install.py",
"scripts/wizard.mjs", "scripts/runtime_store.py",
}
for path in filter(None, paths):
if path in {"package.json", "package-lock.json"}:
# Only version fields are exempt; dependency and packaging changes are shared.
Expand All @@ -892,6 +898,7 @@ def changed_host_scope(repo: Path, baseline: str, hosts: Sequence[str]) -> Mappi
continue
elif (path.startswith(("docs/", "tests/", ".github/", "daemon/"))
or path in release_tools
or path in installer_tools
or ("/" not in path and path.endswith(".md")
and path not in {"AGENTS.md", "WORKFLOW.md"})):
continue
Expand Down
26 changes: 24 additions & 2 deletions scripts/install.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1655,7 +1655,7 @@ function usage() {
" --hooks-init add guards to an existing project without changing its contracts\n" +
" --runtime-restore restore the exact declared runtime from --source-root\n" +
" --runtime-upgrade explicitly select the supplied package runtime\n" +
" --runtime-migrate migrate a tracked legacy runtime for review\n" +
" --runtime-migrate migrate a tracked legacy runtime for review; add --update to continue upgrading\n" +
" --hooks-refresh validate the selected runtime; keep its version\n" +
" --hooks-refresh-full refresh native settings/git hooks; target flags create missing configs\n" +
" --dry-run preview without writing\n" +
Expand Down Expand Up @@ -1684,6 +1684,8 @@ export async function main(argv, env = process.env) {
colored: !process.env.NO_COLOR,
version: packageVersion(),
targets: TARGETS,
legacyRuntime: lexists(path.join(process.cwd(), ".gsd-path", "runtime")) &&
!lexists(path.join(process.cwd(), ".gsd-path", "runtime.json")),
installed: (target, local) =>
hasManagedInstall(local ? localRoot(target, process.cwd()) : defaultRoot(target, env)),
});
Expand All @@ -1695,7 +1697,12 @@ export async function main(argv, env = process.env) {
: path.resolve(SCRIPT_DIRECTORY, "..");
const project =
values.project !== undefined ? absolutePath(values.project) : null;
if (["runtime-restore", "runtime-upgrade", "runtime-migrate"].some(name => values[name])) {
const migrateAndUpdate = values["runtime-migrate"] && values.update;
if (migrateAndUpdate && (project === null || values["runtime-restore"] || values["runtime-upgrade"] || values.doctor || values["hooks-init"] || values["hooks-refresh"] || values["hooks-refresh-full"])) {
ui.error("--runtime-migrate --update requires --project and cannot be combined with another runtime, doctor, or hook operation");
return 2;
}
if (!migrateAndUpdate && ["runtime-restore", "runtime-upgrade", "runtime-migrate"].some(name => values[name])) {
const interpreter = requiredPythonRuntime("project runtime");
const result = spawnSync(interpreter, ["-B", path.join(SCRIPT_DIRECTORY, "install.py"),
...argv.filter(arg => arg !== "--no-color")], { stdio: "inherit", env });
Expand Down Expand Up @@ -1800,6 +1807,21 @@ export async function main(argv, env = process.env) {
} else {
plans = selected.map((target) => targetPlan(target, rootFor(target)));
}
if (migrateAndUpdate) {
const interpreter = requiredPythonRuntime("project runtime");
const migration = spawnSync(interpreter, ["-B", path.join(SCRIPT_DIRECTORY, "install.py"),
"--runtime-migrate", "--project", project, "--source-root", sourceRoot,
...(values["dry-run"] ? ["--dry-run"] : [])], { stdio: "inherit", env });
if (migration.error || migration.status !== 0) {
if (migration.error) ui.error(migration.error.message);
return migration.status ?? 1;
}
if (values["dry-run"]) {
ui.result("Migration preview only; nothing was written. Run without --dry-run to migrate, then validate and apply the update.");
return 0;
}
ui.result("Migration completed. Review the unstaged Git diff. Updating skills and wiring next; an update failure will retain the completed migration.");
}
const spin = ui.spinner("Preparing");
try {
const results = await install(sourceRootForInstall, plans, {
Expand Down
21 changes: 10 additions & 11 deletions scripts/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -2641,17 +2641,16 @@ def install(

if project is not None:
_validate_directory_destination(project, "project path")
if dry_run:
_validate_project(
source_root,
project,
selected,
hooks,
[*mutation_roots, *planned_backups],
interpreter,
hooks_dir,
update,
)
_validate_project(
source_root,
project,
selected,
hooks,
[*mutation_roots, *planned_backups],
interpreter,
hooks_dir,
update,
)

results = []
with tempfile.TemporaryDirectory(prefix="gsd-path-install-") as temporary:
Expand Down
5 changes: 4 additions & 1 deletion scripts/runtime_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import os
from pathlib import Path
import shutil
import shlex
import tempfile
from contextlib import contextmanager

Expand Down Expand Up @@ -135,7 +136,9 @@ def prepare(source, project, *, dry_run=False):
status_runtime.validate_runtime(pin)
return pin
if os.path.lexists(project / ".gsd-path/runtime"):
raise ValueError("legacy project runtime requires --runtime-migrate --project PATH; migration produces a reviewable Git diff")
raise ValueError("legacy project runtime requires explicit migration before updating; run: "
f"npx @opengsd/gsd-path@latest --runtime-migrate --project {shlex.quote(str(project.resolve()))}; "
"then retry the update. Migration produces a reviewable Git diff")
return publish(source, dry_run=dry_run)


Expand Down
10 changes: 8 additions & 2 deletions scripts/wizard.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ const confirm = (io, theme, title, yes = "Yes", no = "No") =>

// Pure: runs the question flow and returns install.mjs argv (or null if cancelled).
// `installed(target, local)` reports whether a managed install already exists for that host in that scope.
export async function wizard({ input, output, colored = true, version = "", cwd = process.cwd(), installed = () => false, targets }) {
export async function wizard({ input, output, colored = true, version = "", cwd = process.cwd(), installed = () => false, legacyRuntime = false, targets }) {
const theme = makeTheme(colored);
const keys = keyReader(input);
const io = { keys, output };
Expand All @@ -147,8 +147,13 @@ export async function wizard({ input, output, colored = true, version = "", cwd
hostItems.some((item) => item.checked && hosts.includes(item.value)) &&
(await confirm(io, theme, "Existing installs found. What do you want to do?", "Update in place", "Fresh install"));

const project = await confirm(io, theme, "Write AGENTS.md + WORKFLOW.md contracts into this repo?", `Yes — ${cwd}`, "Not now");
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");
const hooks = project && (await confirm(io, theme, "Install guard hooks (archive immutability, ship-commit purity)?"));
const migrate = project && update && legacyRuntime;
if (migrate) {
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`);
if (!(await confirm(io, theme, "Migrate this project before updating?", "Migrate and continue upgrade", "Cancel"))) return null;
}

const argv = [];
if (hosts.length === targets.length) argv.push("--all");
Expand All @@ -157,6 +162,7 @@ export async function wizard({ input, output, colored = true, version = "", cwd
if (local) argv.push("--local");
if (project) argv.push("--project", cwd);
if (hooks) argv.push("--hooks");
if (migrate) argv.push("--runtime-migrate");

output.write(`\n ${theme.dim("Equivalent command:")}\n ${theme.accent("$")} gsd-path ${argv.join(" ")}\n`);
const go = await select(io, theme, "Ready?", [
Expand Down
Loading
Loading