Skip to content
Open
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
Binary file modified bin/native/darwin-arm64/loaf
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -363,11 +363,12 @@ Every schema migration is backup-first, idempotent, and tested from the current
- **U8 verification and review:** `LOAF_VALIDATE_TYPESCRIPT=1 npm run build` passed all targets and synchronized generated Go artifacts. Fresh retained smokes passed for Claude startup, Codex startup, and OpenCode request delivery; the OpenCode receipt additionally proves plugin load, root-session lookup, no supplied auth, and disposable-state cleanup. The strict capability suite, 19 Node smoke-parser tests, `npm run test`, `npm run typecheck`, executable Change check, render-drift, ephemeral-provenance, check-secrets, validate-commit, security-audit, and `git diff --check` passed. The parity regression test was falsified by the former unconditional Amp-hook expectation and now requires the unsupported capture hook to remain absent while preserving every enforcement hook. Initial final reviews found the omitted Amp missing/disabled-plugin bypass and substitution-damaged generated guidance; after correcting both and sweeping the U8-touched orchestration sources, final Sol/high and Claude Opus verification returned `APPROVE` with no blocking findings. Claude separately noted pre-existing plural-substitution defects in unrelated housekeeping, wrap, and foundations guidance; U9's documentation reconciliation retains that boundary.
- **U9 content-ownership primitive:** managed fences use version-independent body fingerprints with legacy migration, tamper refusal, atomic user-prose preservation, and strict fence parsing; the shared skill manifest v2 records deterministic per-tree digests with v1 migration, stale/missing convergence, foreign/tamper/race refusal, and rollback, covered by mode, symlink, strict-manifest, and fence-parser tests. Rebuilding changed the candidate native binary hash, so the strict capability suite failed closed on stale receipts; fresh isolated Claude startup, Codex startup, and OpenCode request smokes passed, refreshed retained hashes, and integrated tests passed afterward.
- **U9 target-adapter manifest slice:** every native build target now emits one deterministic strict manifest whose adapter identities come from the validated version-3 capability evidence and whose managed-instruction digest is version-independent. Concrete hook/plugin artifacts bind content plus permission mode, while merged user hook projections retain their existing mode; malformed metadata, source or installed content/mode drift, unsafe paths, symlinks, duplicate keys, and trailing JSON fail closed. OpenCode, Cursor, Codex, and Amp installs persist strict ownership state, converge same-version changes, migrate recognized legacy surfaces, retire unchanged stale artifacts, preserve foreign content, reject unowned or modified destinations, revalidate each destination immediately before mutation, and roll back only actual mutations in reverse order with restoration failures surfaced alongside the primary error and current recovery state. Claude Code receives the build manifest but remains intentionally outside CLI install ownership because the CLI has no Claude installer. Focused manifest/install tests, the full internal CLI suite, `go vet ./...`, formatting, and diff checks passed; this slice does not complete V12 or U9.
- **U9 installed-ownership doctor slice:** `loaf doctor` now runs one read-only, non-fixable aggregate ownership check for recorded or detected OpenCode, Cursor, Codex, and Amp installs. It reads each recorded configuration root as authoritative, requires the strict installed manifest, skips the managed-instruction pseudo-artifact, and validates every declared hook, plugin, or merged projection through safe destination resolution plus exact projection/content digest and concrete permission-mode ownership; malformed or symlinked manifests, unsafe or symlinked artifact paths, missing artifacts, foreign Loaf-projection drift, content drift, and mode drift fail closed. Missing records for detected legacy installs, missing manifests, malformed records or manifests, wrong-target manifests, and record/manifest/trusted executable-distribution version disagreement are reported deterministically alongside other target and artifact defects. The check has no fix function, and `doctor --fix` leaves target bytes and permission modes unchanged. If the executable distribution root cannot be proven, doctor retains legacy fence behavior but makes no current-version ownership claim from an unrelated project package. Focused doctor tests, formatting, and diff checks passed; this slice does not implement source-to-build comparison, capability or smoke diagnostics, repair, or V12 completion.
- **Search-parity incident evidence:** a real global divergence belonging entirely to another project reproduced Claude Code's nonblocking SessionStart failure because schema validation incorrectly treated the derived FTS index as a whole-database gate. A two-project regression falsified the fix under the old gate, then proved current-project context, recent, and log remain available while global parity stays non-ready and search still returns the typed divergence refusal. The rebuilt Claude plugin's exact SessionStart command exits zero with no stderr against that unchanged database; no global repair or unrelated-project mutation was performed.
- **Direct evidence passed during implementation:** independent two-process `journal defer` convergence passed at count 10; schema-upgrade rollback and stale-source refusal tests passed; state, CLI, and command-surface suites passed in the integrated workspace. Atomic staged storage copy SIGKILL and concurrent-publisher coverage passed at count 10 and under `-race` count 3. Final Sol review was clean.
- **Project-move race evidence:** a deterministic independent-store claimant race passed at focused count 10 and under `-race` count 3; disabling transactional rejection falsified the guard. The protected path retained one owner/current mapping and both projects' journals remained preserved.
- **Final verification gates:** `LOAF_VALIDATE_TYPESCRIPT=1 npm run build` passed all targets with Go artifact synchronization; `npm run test` and typecheck passed. Executable Change check passed with only the expected unmaterialized `release-after` warning. Render-drift, ephemeral-provenance, changed-text secret, validate-commit, and security checks passed. Isolated public smokes proved manual origin capture, atomic defer/search, current schema, backup plus verify recovery/provenance readiness, and contract-v2 synthesis/lineage/blocker/Change projection without touching the global database.
- **Remaining boundary:** U7 normalized envelopes, outcome-aware trusted seams, namespaced origins, lifecycle suppression, safe-hook preflight, and replay evidence are implemented. U8 now has strict installed model-visible proof for Claude startup, Codex startup, and OpenCode request delivery; OpenCode completion is disabled, Cursor remains candidate, and Amp remains an explicitly unsupported automatic-context fallback with a foreground-turn candidate trigger. The U9 content-ownership primitive and target-adapter manifest slice are implemented: deterministic strict build manifests cover Claude Code, OpenCode, Cursor, Codex, and Amp, while strict CLI install ownership covers OpenCode, Cursor, Codex, and Amp because Claude Code intentionally has no CLI installer. U9 still owns doctor diagnostics, broader documentation reconciliation, legacy `~/.amp` retirement, isolated builds/upgrades, and approved real installed cross-harness dogfood; V12 and the root Change remain open.
- **Remaining boundary:** U7 normalized envelopes, outcome-aware trusted seams, namespaced origins, lifecycle suppression, safe-hook preflight, and replay evidence are implemented. U8 now has strict installed model-visible proof for Claude startup, Codex startup, and OpenCode request delivery; OpenCode completion is disabled, Cursor remains candidate, and Amp remains an explicitly unsupported automatic-context fallback with a foreground-turn candidate trigger. The U9 content-ownership primitive, target-adapter manifest slice, and read-only installed-ownership doctor slice are implemented: deterministic strict build manifests cover Claude Code, OpenCode, Cursor, Codex, and Amp, while strict CLI install ownership and recorded-install doctor checks cover OpenCode, Cursor, Codex, and Amp because Claude Code intentionally has no CLI installer. U9 still owns source-to-build comparison, capability mismatch and missing-adapter diagnostics, smoke freshness and absence diagnostics, broader documentation reconciliation, legacy `~/.amp` cleanup, isolated builds/upgrades, and approved real installed cross-harness dogfood; V12, U9, and the root Change remain open.

## Verification Contract

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"evidence_version": 2,
"timestamp": "2026-07-14T00:53:40.132Z",
"timestamp": "2026-07-14T01:36:06.452Z",
"target": "claude-code",
"surface": "cli",
"version": "2.1.207",
Expand Down Expand Up @@ -41,7 +41,7 @@
"stderr_empty": true,
"model_visible_marker_observed": true,
"assistant_marker_match": true,
"marker": "LOAF_U8_SMOKE_DCBBB6ABD741",
"marker": "LOAF_U8_SMOKE_5CE1278E9700",
"hook_observation": {
"event_name": "SessionStart:startup",
"native_json": true,
Expand All @@ -52,6 +52,6 @@
"hooks_path": "plugins/loaf/hooks/hooks.json",
"hooks_sha256": "5c7343a34af37d6c6e930981f71e405b8023975de4146822058f98c15251b7c2",
"native_binary_path": "plugins/loaf/bin/native/darwin-arm64/loaf",
"native_binary_sha256": "93e4ce1f4961b186429fc423627780eb384661b71490dc03d73bab07f86006b8"
"native_binary_sha256": "902e41eea856eacaf172fc9708aa873bb2bc67c360ab00c7ac36070b106855f4"
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"evidence_version": 2,
"timestamp": "2026-07-14T00:53:48.175Z",
"timestamp": "2026-07-14T01:36:15.758Z",
"target": "codex",
"surface": "cli",
"version": "0.144.1",
Expand Down Expand Up @@ -39,7 +39,7 @@
"stderr": "Reading additional input from stdin...",
"model_visible_marker_observed": true,
"assistant_marker_match": true,
"marker": "LOAF_CODEX_U8_88ABE98B1675",
"marker": "LOAF_CODEX_U8_FFFA85032CEE",
"hook_observation": {
"event_name": "SessionStart:startup",
"native_json": true,
Expand All @@ -50,6 +50,6 @@
"hooks_path": "dist/codex/.codex/hooks.json",
"hooks_sha256": "0f9e3feb7204c3309a7db6a02224ad881f6801a8cdd126ec6222e6d9a804c33d",
"native_binary_path": "bin/native/darwin-arm64/loaf",
"native_binary_sha256": "93e4ce1f4961b186429fc423627780eb384661b71490dc03d73bab07f86006b8"
"native_binary_sha256": "902e41eea856eacaf172fc9708aa873bb2bc67c360ab00c7ac36070b106855f4"
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"evidence_version": 2,
"timestamp": "2026-07-14T00:54:04.170Z",
"timestamp": "2026-07-14T01:36:41.695Z",
"target": "opencode",
"surface": "cli",
"version": "1.17.18",
Expand Down Expand Up @@ -39,11 +39,11 @@
"root_session_lookup_proven": true,
"no_auth_supplied": true,
"cleanup_succeeded": true,
"marker": "LOAF_OPENCODE_U8_A65933F45C20",
"marker": "LOAF_OPENCODE_U8_5817D3B6970E",
"candidate_artifacts": {
"hooks_path": "dist/opencode/plugins/hooks.ts",
"hooks_sha256": "7715cf7701ce54a9652c0fdaa67025493d0b3407153b89fdfb3be23f26438bef",
"native_binary_path": "bin/native/darwin-arm64/loaf",
"native_binary_sha256": "93e4ce1f4961b186429fc423627780eb384661b71490dc03d73bab07f86006b8"
"native_binary_sha256": "902e41eea856eacaf172fc9708aa873bb2bc67c360ab00c7ac36070b106855f4"
}
}
133 changes: 128 additions & 5 deletions internal/cli/doctor.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cli

import (
"encoding/json"
"fmt"
"io"
"os"
Expand Down Expand Up @@ -65,13 +66,30 @@ func (r Runner) runDoctor(args []string, out io.Writer, runtimeRoot string) erro
writeDoctorHelp(out)
return nil
}
report := runDoctorChecks(out, doctorContext{projectRoot: runtimeRoot}, options, packageVersion(runtimeRoot))
loafRoot, resolveErr := resolveLoafPackageRoot(r.WorkingDir, runtimeRoot)
fenceVersion := packageVersion(runtimeRoot)
if resolveErr == nil {
fenceVersion = packageVersion(loafRoot)
}
targetAdapterVersion := ""
if executable, err := os.Executable(); err == nil {
targetAdapterVersion = targetAdapterVersionFromExecutable(executable)
}
report := runDoctorChecks(out, doctorContext{projectRoot: runtimeRoot}, options, fenceVersion, targetAdapterVersion)
if report.Failures > 0 {
return ExitError{Code: 1}
}
return nil
}

func targetAdapterVersionFromExecutable(executable string) string {
root, ok := findLoafPackageRoot(filepath.Dir(executable), map[string]bool{})
if !ok {
return ""
}
return packageVersion(root)
}

func parseLoafDoctorArgs(args []string) (doctorOptions, error) {
var options doctorOptions
for _, arg := range args {
Expand All @@ -98,11 +116,11 @@ func writeDoctorHelp(out io.Writer) {
fmt.Fprint(out, " -h, --help Show help\n")
}

func runDoctorChecks(out io.Writer, ctx doctorContext, options doctorOptions, cliVersion string) doctorReport {
func runDoctorChecks(out io.Writer, ctx doctorContext, options doctorOptions, fenceVersion string, targetAdapterVersion string) doctorReport {
report := doctorReport{}
fmt.Fprintf(out, "\n%s\n\n", ansiBold("loaf doctor"))

for _, check := range doctorChecks(cliVersion) {
for _, check := range doctorChecks(fenceVersion, targetAdapterVersion) {
result := safeRunDoctorCheck(check, ctx)
printDoctorCheckLine(out, check, result, options)

Expand All @@ -125,15 +143,120 @@ func runDoctorChecks(out io.Writer, ctx doctorContext, options doctorOptions, cl
return report
}

func doctorChecks(cliVersion string) []doctorCheck {
func doctorChecks(fenceVersion string, targetAdapterVersion string) []doctorCheck {
return []doctorCheck{
checkCanonicalAgentsFile(),
checkAgentsSymlink(),
checkClaudeSymlink(),
checkStaleCursorMdc(),
checkFencedVersion(cliVersion),
checkFencedVersion(fenceVersion),
checkDuplicateFencedSections(),
checkTargetAdapterOwnership(targetAdapterVersion),
}
}

func checkTargetAdapterOwnership(cliVersion string) doctorCheck {
return doctorCheck{
Name: "target-adapter-ownership",
Description: "Recorded target adapter artifacts match their managed ownership manifests",
Run: func(_ doctorContext) doctorResult {
targets := []string{"amp", "codex", "cursor", "opencode"}
checked := 0
failures := 0
details := []string{}
fail := func(detail string) {
failures++
details = append(details, detail)
}
defaults := defaultInstallConfigDirs()
for _, target := range targets {
record, state, recordErr := readDoctorInstallRecord(target)
if state == "absent" {
if isLoafInstalledForTargetInstall(target, defaults[target]) {
fail(fmt.Sprintf("%s: Loaf install detected without an install record", target))
}
continue
}
if recordErr != nil {
fail(fmt.Sprintf("%s: install record is invalid: %v", target, recordErr))
continue
}
checked++
if record.Target != target || record.ConfigDir == "" {
fail(fmt.Sprintf("%s: install record has an invalid target or config directory", target))
continue
}
manifest, err := readTargetAdapterManifest(filepath.Join(record.ConfigDir, targetInstallManifestFile))
if err != nil {
fail(fmt.Sprintf("%s: ownership manifest is unavailable: %v", target, err))
continue
}
if manifest.Target != target {
fail(fmt.Sprintf("%s: ownership manifest declares %s", target, manifest.Target))
continue
}
if record.Version != manifest.PackageVersion || (cliVersion != "" && record.Version != cliVersion) {
versions := fmt.Sprintf("record=%s manifest=%s", record.Version, manifest.PackageVersion)
if cliVersion != "" {
versions += " current=" + cliVersion
}
fail(fmt.Sprintf("%s: version mismatch %s", target, versions))
}
options := targetInstallOptions{Target: target, ConfigDir: record.ConfigDir, CodexHome: record.ConfigDir, AmpPluginsDir: filepath.Join(record.ConfigDir, "plugins")}
for _, artifact := range manifest.Artifacts {
if artifact.Kind == "instruction" {
continue
}
path, err := targetAdapterDestination(options, artifact)
if err != nil {
fail(fmt.Sprintf("%s %s: unsafe destination: %v", target, artifact.ID, err))
continue
}
snapshot, err := readTargetAdapterSnapshot(path)
if err != nil {
fail(fmt.Sprintf("%s %s: cannot inspect: %v", target, artifact.ID, err))
continue
}
if !snapshot.exists {
fail(fmt.Sprintf("%s %s: missing %s", target, artifact.ID, path))
continue
}
matches, err := targetAdapterSnapshotMatchesArtifact(target, artifact, snapshot)
if err != nil {
fail(fmt.Sprintf("%s %s: cannot validate: %v", target, artifact.ID, err))
continue
}
if !matches {
fail(fmt.Sprintf("%s %s: drifted %s", target, artifact.ID, path))
continue
}
details = append(details, fmt.Sprintf("%s %s: matches", target, artifact.ID))
}
}
if failures > 0 {
return doctorResult{Status: doctorFail, Message: "Target adapter ownership drift detected", Detail: strings.Join(details, "\n")}
}
if checked == 0 {
return doctorResult{Status: doctorSkip, Message: "No recorded target adapter installs"}
}
return doctorResult{Status: doctorPass, Message: fmt.Sprintf("%d recorded target adapter install(s) match ownership manifests", checked), Detail: strings.Join(details, "\n")}
},
}
}

func readDoctorInstallRecord(target string) (installTargetRecord, string, error) {
body, err := os.ReadFile(installRecordPath(installHome(), target))
if os.IsNotExist(err) {
return installTargetRecord{}, "absent", nil
}
if err != nil {
return installTargetRecord{}, "invalid", err
}
var record installTargetRecord
if err := json.Unmarshal(body, &record); err != nil {
return installTargetRecord{}, "invalid", err
}
return record, "present", nil
}

func checkAgentsSymlink() doctorCheck {
Expand Down
Loading
Loading