"Remote update awareness - /update and install.sh --check compare installed vs latest published release, not the local payload"
TASK-IMP-070: Remote update awareness
§1 - Description
install.sh --check compares a target repo's .cyberos/VERSION against the LOCAL payload's VERSION, and the /update command trusts that answer. Once TASK-IMP-069 publishes every release, the honest comparison is against the latest published version. This task adds that third data point and rewrites the verdict logic around it.
Normative clauses:
- A script
tools/install/check-latest.shMUST resolve the latest published version and print exactly one linelatest=<X.Y.Z|unknown> source=<url|offline>. Resolution order:$CYBEROS_RELEASE_ENDPOINTwhen set (an https URL or a local file path returning either a bareX.Y.Zor GitHub's/releases/latestJSON, from whichtag_nameminus thevis taken), else the repo's publicreleases/latestpage redirect (Location header names the tag; immune to API rate limits), with thereleases/latestAPI URL as fallback (amended post-ship 2026-07-12 after a live 403 rate-limit). Total network budget MUST be <= 3 seconds (curl--max-time 3); any failure MUST yieldlatest=unknown source=offlinewith exit 0 - the script never breaks a caller. install.sh --check <repo>MUST report three values -installed(<repo>/.cyberos/VERSION),payload(the local payload's VERSION),latest(via check-latest.sh, skipped whenCYBEROS_OFFLINE=1) - followed by exactly one verdict line:verdict=up_to_date(installed == latest, or latest unknown and installed == payload),verdict=repo_stale(installed < payload or installed < latest), orverdict=payload_stale(payload < latest).installed >= latestMUST count as up to date - the check never advises a downgrade. Each non-clean verdict MUST print the exact next command (bash <payload>/install.sh <repo>for repo_stale; the TASK-IMP-069 download one-liner orbuild.shfor payload_stale).- Version comparison MUST be numeric semver (major, minor, patch), not string comparison.
plugin/commands/update.mdMUST direct the agent to run the extended--checkand act on the verdict: onpayload_stale, fetch the latest release payload (or rebuild from a current checkout) BEFORE re-running init, and never report "up to date" from the local-payload comparison alone. Onlatest=unknownit MUST say the remote check was skipped and the answer is only as fresh as the local payload.plugin/commands/changelog.mdMUST, whenlatestis newer thaninstalled, link the GitHub Releases page as the span to read (installed+1 .. latest), in addition to the local.cyberos/manifest.yamldetails.- Offline behavior MUST be first-class:
CYBEROS_OFFLINE=1(or network failure) degrades--checkto today's local comparison plus an explicitlatest=unknown source=offlinenote. Exit code semantics of--check(0 = ran) MUST NOT change. - Nothing in this task mutates a target repo; the check remains read-only and auto-update stays out of scope.
§2 - Why this design
The three-value report makes the two failure directions distinguishable: a stale TARGET repo (fix: rerun init) versus a stale LOCAL payload (fix: download/rebuild), which today collapse into one misleading "up to date". A separate check-latest.sh keeps network code out of install.sh's critical path, gives the desktop Ops tab (TASK-APP-001) and /update one shared resolver, and makes the endpoint overridable so tests run on file://-style fixtures with zero network.
§3 - Contract
check-latest.sh
env: CYBEROS_RELEASE_ENDPOINT (optional), CYBEROS_OFFLINE=1 -> immediate unknown
stdout: latest=1.8.0 source=https://api.github.com/repos/cyberskill-official/cyberos/releases/latest
or: latest=unknown source=offline
exit: always 0
install.sh --check <repo> (extended output, order fixed)
installed=1.2.0
payload=1.7.0
latest=1.8.0 source=<url>
verdict=repo_stale
next: bash <payload>/install.sh <repo>
§4 - Acceptance criteria
- Endpoint override, bare version (§1 #1) - with
CYBEROS_RELEASE_ENDPOINTpointing at a fixture file containing1.8.0, the script printslatest=1.8.0with that source. - Endpoint override, GitHub JSON (§1 #1) - with a fixture containing
{"tag_name": "v1.8.0", ...}, the script printslatest=1.8.0. - Failure degrades, never breaks (§1 #1, #6) - with an unreachable endpoint, output is
latest=unknown source=offline, exit 0, and--checkstill completes with a verdict. - Three values + verdict, all four states (§1 #2) - fixtures produce each verdict: installed==payload==latest ->
up_to_date; installed<payload ->repo_stalewith the init command; payload<latest ->payload_stalewith the fetch instruction; latest unknown + installed==payload ->up_to_datewith the offline note. - Numeric semver compare (§1 #3) - 1.10.0 ranks above 1.9.0 (string compare would invert it).
- CYBEROS_OFFLINE honored (§1 #6) - with
CYBEROS_OFFLINE=1, no network attempt is made (endpoint fixture untouched) and the offline note appears. - /update doc drives the verdicts (§1 #4) - update.md names the three values, the two stale verdicts with their distinct actions, and forbids the local-only "up to date" claim.
- /changelog links the release span (§1 #5) - changelog.md instructs linking the Releases page when latest > installed.
§5 - Verification
# tools/install/tests/test_check_latest.sh
# Fixtures under $TMP: bare-version file, tag_name JSON file, unreachable path,
# plus a scratch repo + scratch payload for the --check verdict matrix.
t01_bare_version_endpoint() # AC 1
t02_github_json_endpoint() # AC 2
t03_unreachable_degrades() # AC 3
t04_verdict_matrix() # AC 4 (4 sub-cases, exact verdict + next line asserted)
t05_numeric_semver() # AC 5
t06_offline_env() # AC 6
t07_update_doc_contract() # AC 7 (greps update.md for the three keys + both verdicts)
t08_changelog_doc_contract() # AC 8
§6 - Implementation skeleton
check-latest.sh: ~40 lines - endpoint resolution, curl -sf --max-time 3 (or cat for a local path), parse bare/JSON, print. install.sh --check: keep existing reads, add the resolver call + a ver_ge() numeric comparator + verdict table from §3.
§7 - Dependencies
Depends on TASK-IMP-069 (there must BE a published latest). TASK-APP-001's Check button inherits the richer output for free since it shells out to install.sh --check.
§8 - Example payloads
$ CYBEROS_OFFLINE=1 bash dist/cyberos/install.sh --check ~/Projects/clientA
installed=1.6.0
payload=1.7.0
latest=unknown source=offline
verdict=repo_stale
next: bash dist/cyberos/install.sh /Users/stephen/Projects/clientA
§9 - Open questions
None blocking. A cached last-known-latest under ~/.cache/cyberos/ was considered and dropped: a 3-second budget per explicit check does not need a cache, and a cache adds a staleness surface of its own.
§10 - Failure modes inventory
- GitHub API rate limit (60/h unauthenticated) - resolver returns unknown; verdict falls back to local compare with the offline note. Never a hard failure.
- Endpoint returns HTML (proxy portal) - parse yields no semver; treated as unknown, not as a garbage version (regex-gated).
- Pre-release tags (v1.8.0-rc1) - resolver takes
tag_nameas-is; nonX.Y.Zstrings are rejected by the same regex and reported unknown (pre-releases are not "latest" for updaters). - Clock-skew style confusion (installed NEWER than latest, e.g. developing ahead of the last tag) -
installed > latestmaps toup_to_date(never advise a downgrade). - Two payloads on one machine (repo checkout + downloaded release) - the report prints which payload path it compared, so the operator sees which source answered.
§11 - Implementation notes
Keep the output keys machine-parseable (key=value, one per line) - TASK-APP-001 parses them, and future telemetry can too. update.md and changelog.md are agent-facing prose; keep the verdict names verbatim so agents can branch on them.
End of TASK-IMP-070.
Audit
TASK-IMP-070 audit
§1 - Verdict summary
Audited for verdict-table totality, degradation honesty, and doc-contract precision (two command docs are normative deliverables here). The draft's verdict table had an undefined cell (installed > latest) and an untested resolver failure path; both closed. Traceability closes over t01-t08 in tools/install/tests/test_check_latest.sh.
§2 - Findings (all resolved)
ISS-001 undefined verdict cell could advise a downgrade
installed > latest (developing ahead of the last tag) fell through the table. Resolved: §1 #2 ">= counts as up to date - never advise a downgrade", echoed in §10 #4.
ISS-002 resolver failure could break callers
A curl failure propagating non-zero would turn an advisory check into a blocker. Resolved: §1 #1 exit-0-always contract with latest=unknown source=offline; AC 3 asserts the caller still completes.
ISS-003 pre-release tags could become "latest"
v1.8.0-rc1 parsed naively poisons the comparison. Resolved: X.Y.Z regex gate; non-matching tags report unknown (§10 #3).
ISS-004 no hermetic test path
The resolver needed real network in the first cut (TRACE-002 risk). Resolved: CYBEROS_RELEASE_ENDPOINT accepts a local file (bare version or GitHub JSON), t01/t02.
ISS-005 string comparison bug class
1.10.0 vs 1.9.0 ordering had no clause. Resolved: §1 #3 numeric semver compare + AC 5 regression case.
ISS-006 machine-readability unpinned
TASK-APP-001 parses this output; free-form prose would break it. Resolved: §3 fixed key=value line contract + §11 stability note.
§3 - Resolution
All six findings addressed as cited. Offline behavior, verdict totality, and both command-doc contracts are now falsifiable. Score = 10/10.
End of TASK-IMP-070 audit.
§10 - Post-implementation gates (2026-07-12, ship run)
- §10.4 coverage gate: PASS - t01-t08 green on fresh rerun; all three prior suites green (33 cases total). Report: .workflow/TASK-IMP-070/artefacts-bundle.md.
- TRACE-004 closure: PASS. awh/caf: N/A (declared); floor = bash -n + suites.
- HITL gate 1: APPROVED by Stephen Cheng 2026-07-12. HITL gate 2: ACCEPTED same date via explicit operator pre-authorization at the review gate; gates stayed green.
- Deviations recorded: build.sh vendors check-latest.sh (frontmatter updated); legacy one-line --check format replaced by the machine-parseable three-value contract.
TASK-IMP-070 shipped 2026-07-12. Wave A (version coupling) complete: TASK-IMP-068 + 069 + 070 all done.
§11 - Post-ship amendment (2026-07-12, first live release)
Field finding: with v1.8.1 published, the resolver still returned unknown - the unauthenticated GitHub API was 403 rate-limited for the operator's IP (the §10 #1 failure mode, observed live, degraded exactly as designed). Hardening: the resolver now tries the releases/latest page REDIRECT first (Location header names the tag; not subject to API rate limits) with the API as fallback. §1 #1 amended; t01-t08 unaffected (endpoint-override paths unchanged), suite green.
- 2026-07-12 (post-ship, during TASK-SKILL-118 regression): t04B assumed the live repo VERSION exceeds its 1.0.0 fixture; the PR #44 semver rollback to 0.1.0 falsified that. Fixed by pinning the payload copy's VERSION to 2.0.0 (the t05 pattern) - the suite is now independent of the repo's current version. Verdict unchanged: PASS, Score = 10/10.