Skip to content

feat(lifecycle): update and uninstall directory targets via recorded ownership - #354

Merged
barbatdev merged 7 commits into
mainfrom
feat/issue-342-lifecycle
Oct 21, 2026
Merged

barbatdev merged 7 commits into
mainfrom
feat/issue-342-lifecycle

Conversation

@barbatdev

Copy link
Copy Markdown
Contributor

Stacked on the directory install target. cortex update --target-dir <dir> --catalog <dir> [--apply] refreshes a previously installed directory target; a target with no prior state is refused with reason=not_installed instead of becoming a first install. cortex uninstall --target-dir <dir> removes exactly the owned files (state file, skills) and never touches foreign files; the uninstall backup stays under the target's .cortex as rollback evidence.

Full go test ./internal/... green; go vet clean.

Fixes #342

@barbatdev barbatdev added the type:feature New feature label Oct 21, 2026
@barbatdev
barbatdev changed the base branch from feat/issue-341-install-dir to main October 21, 2026 10:58
# Conflicts:
#	internal/cli/doctor.go
#	internal/skillroot/resolve.go
#	tests/test_community_policy.py
@barbatdev

Copy link
Copy Markdown
Contributor Author

Fleet review (glm)

Lens: requirements + adversarial. The update flow fails closed correctly on a target with no prior state, and the parser correctly rejects unknown and mutually exclusive arguments. One blocking finding: uninstall of a nonexistent target directory reports success (exitOK), contradicting issue #342's acceptance criterion that update/uninstall against a directory with no prior Cortex ownership fails closed — and it is internally inconsistent with the same function, which returns exitConflict for an existing unowned directory. Questions cover two acceptance criteria whose tests do not verify what they promise ("reports what was left", "only affected owned files with manifest refresh"), plus an unexplained weakening of the foundation-documents test.

Blocking (1)

  • Uninstall of a nonexistent target directory exits OK instead of failing closed (internal/cli/doctor.go:458): runDirectoryUninstall returns exitOK with status=not_installed when deps.rootExists is false, while the same function returns exitConflict for an existing directory with zero ownership records. Issue feat: include directory targets in update and uninstall via recorded ownership #342 acceptance criterion: "Running update/uninstall against a directory with no prior Cortex ownership fails closed with an explicit diagnostic." A nonexistent path trivially has no prior Cortex ownership, yet uninstall reports success, unlike update which refuses with not_applied in the equivalent case (covered by TestUpdateDirectoryTargetFailsClosedOnUnknownState). There is also no test for uninstall against a never-installed target.
    • Failure scenario: Operator runs cortex uninstall --target-dir /home/user/typo-dir where the path does not exist. The command exits 0 reporting not_installed, so an automation script treats the target as cleanly uninstalled; the same command against an existing empty directory exits with conflict. The acceptance criterion requires fail-closed with an explicit diagnostic in both cases.

Suggestion (1)

  • Duplicated assertEqual inventory-count lines are merge artifacts (tests/test_community_policy.py:461): The inventory count is asserted four times (two identical pairs) where one pair suffices; the same pattern grows in the stacked PRs. Harmless but noisy; collapse to a single pair when the stack settles.

Question (3)

  • "Reports what was left" is not verified by the uninstall test (internal/cli/directory_lifecycle_test.go:118): Acceptance criterion: "Uninstall removes only owned files; a directory containing foreign files keeps them and reports what was left." The test asserts the foreign sentinel file survives, but never asserts that the uninstall output names the foreign files it left behind.
  • "Rewrites only affected owned files, with readback and manifest refresh" is only half-verified (internal/cli/directory_lifecycle_test.go:71): Acceptance criterion: "Update after upstream content changes rewrites only affected owned files, with readback and manifest refresh." The test asserts the patched skill changed, but never asserts that unaffected owned files stayed byte-identical, nor that the recorded install-state hashes were refreshed to match the new content.
  • Unexplained removal of docs/README.md from the foundation-documents test (tests/test_community_policy.py:559): test_foundation_documents_state_clean_target_status drops "docs/README.md" from its checked-files list, but this PR neither deletes docs/README.md nor mentions the change in the body. Nothing in the directory-lifecycle scope explains weakening a foundation-document check. If an earlier commit in the stack deleted the file, the PR diff should show it; it does not.

Verdict: not ready

@barbatdev

Copy link
Copy Markdown
Contributor Author

Fleet review (qwen38)

Lens: evidence. This is the lifecycle slice (issue #342): update/uninstall dispatch, directory update/uninstall paths, and three end-to-end tests driving the real install → update → uninstall flow through runWithDependencies. The fails-closed update test and the foreign-file isolation test are strong, representative evidence. Weaker spots are all evidence gaps, not demonstrated defects: 'rewrites only affected owned files, with readback and manifest refresh' is asserted by only one of its three clauses; the uninstall 'no prior ownership' branch is untested; and the acceptance phrase 'reports what was left' is not visibly implemented. The Python policy-test edit again shows mechanical conflict residue (three duplicated assertEqual(404) pairs) and the same unexplained de-pinning of 'docs/README.md'. No blocking findings.

Suggestion (2)

  • The uninstall fails-closed paths are untested (internal/cli/directory_lifecycle_test.go:106): Criterion 3 names update AND uninstall against a directory with no prior Cortex ownership. Only the update branch is tested (TestUpdateDirectoryTargetFailsClosedOnUnknownState). In runDirectoryUninstall, a fresh existing directory takes the 'observation.Records() == 0 → not_installed' path (doctor.go, runDirectoryUninstall) and a missing path takes rootExists → not_installed; neither has a test, and the empty-records branch returns exitConflict — a behavior worth pinning because it differs from the missing-path branch's exitOK.
  • Conflict residue: three identical assertEqual(404) pairs (tests/test_community_policy.py:458): The replaced 403 assertion became the same pair repeated three times, and two EXPECTED_FILES lines use tab indentation against the file's space style. Mechanical merge cleanup; one pair suffices. (Stacked siblings feat(doctor): report directory target presence, writability, and ownership #355/feat(qa): project QA actors as skills for directory targets and document the mode #356 carry the same defect at 405/406.)

Question (3)

  • Criterion 'rewrites only affected owned files, with readback and manifest refresh' is only partly evidenced (internal/cli/directory_lifecycle_test.go:101): Acceptance criterion 1 of feat: include directory targets in update and uninstall via recorded ownership #342 has three clauses. TestUpdateDirectoryTargetPlansAndApplies proves the affected file changed (skill() != before) but never asserts that unaffected owned files stayed byte-identical, and never reads install-state.json to prove the manifest refreshed (digest/record update) after --apply. If apply rewrote every projected skill or left the state stale, the suite would still pass.
  • Does directory uninstall 'report what was left', as the criterion says? (internal/cli/doctor.go:289): Criterion 2: 'Uninstall removes only owned files; a directory containing foreign files keeps them and reports what was left.' The test proves foreign files are kept (sentinel + names map), but I see no output line enumerating what remained — writeUninstallResult reports status/counters. Is 'reports what was left' considered satisfied by the status output, or is a leftover report missing from this work unit?
  • Why was 'docs/README.md' removed from the pinned README strings? (tests/test_community_policy.py:559): This is the oldest head of the stack, so asking here: the diff drops the assertion that README.md references 'docs/README.md'. The string still exists in README.md at these heads, so nothing is broken today, but a policy pin was silently loosened during the stacked-branch merges. Intentional?

Verdict: ready to merge

@barbatdev

Copy link
Copy Markdown
Contributor Author

Post-merge QA (glm)

Update and uninstall for directory targets (issue #342). All three acceptance criteria have real coverage: plan is read-only and apply refreshes the projected skill; update refuses a target with no prior state (not_installed) instead of turning an update into a first install; uninstall removes owned files and the state file while keeping a foreign sentinel and reporting the remaining tree. parseUpdateArgs enforces runtime/target-dir mutual exclusion and requires --catalog. Two minor gaps: the 'rewrites only affected owned files' criterion is not directly asserted (no check that unchanged owned files stay untouched), and the uninstall test does not assert that the output reports which foreign files were left.

Question (2)

  • 'Rewrites only affected owned files' is not directly asserted (internal/cli/directory_lifecycle_test.go:58): Issue feat: include directory targets in update and uninstall via recorded ownership #342 requires that update 'rewrites only affected owned files'. The test asserts the patched skill changed but never asserts that other owned files kept their inode/mtime/content, so an update that rewrites every owned file would pass. If the transaction already guarantees replace-only-on-diff, a one-line assertion would pin it.
  • Uninstall output reporting of foreign leftovers is unasserted (internal/cli/directory_lifecycle_test.go:93): Issue feat: include directory targets in update and uninstall via recorded ownership #342 says uninstall 'reports what was left' when foreign files exist. The test checks the filesystem afterwards but not stdout/stderr content, so the reporting half of the criterion is unverified.

Verdict: clean

@barbatdev

Copy link
Copy Markdown
Contributor Author

Post-merge QA (deepseek)

Lens: evidence + test design.

The merge covers #342 with real end-to-end evidence: directoryLifecycleInstall drives the production dispatch to a genuinely installed state, and the three tests then check the plan writes nothing, apply actually changes the projected skill, an uninstalled target refuses instead of becoming a first install, and uninstall keeps a foreign file while removing the state file and every owned skill. The ownership wiring (NewUninstallRoot, the RuntimeDirectory arm in validUninstallRoot, the runtimeRank entry) is small and consistent with the existing tables, and the --target-dir/--runtime mutual exclusion in parseUpdateArgs holds.

Two defects and two evidence gaps. The main one: runDirectoryUpdate maps every candidate-build failure to invalid_catalog, and the candidate build also fails when the target path is not absolute, so the documented relative-path invocation of update reports a catalog error that has nothing to do with the catalog. Separately, this PR drops a still-true assertion (docs/README.md listed in the root README) from test_foundation_documents_state_clean_target_status — unrelated to directory lifecycle and unexplained in the PR body. On the evidence side, the update test never checks the manifest refresh the acceptance criteria name, and its ../../catalog fixture resolves only because Go runs the package test with internal/cli as the working directory.

Blocking (1)

  • A relative or malformed --target-dir is reported as invalid_catalog (internal/cli/update.go:245): buildDirectoryUpdateCandidate fails for two unrelated reasons: the catalog cannot be built, or skillroot.ResolveIntoDirectory rejects the target (its validPath requires filepath.IsAbs). Both collapse into writeError(stderr, "invalid_catalog") with exit 1. runUninstallCommand gets this right — NewUninstallRoot returns invalid_target for the same class of path — so update now contradicts its sibling command and the operating guide, which documents ./agent-skills style paths as usable.
    • Failure scenario: An installed target at ./agent-skills with a valid ./catalog. cortex update --target-dir ./agent-skills --catalog ./catalog passes os.Stat, then ResolveIntoDirectory refuses the non-absolute root, and the command prints error=invalid_catalog and exits 1. The operator hunts a catalog problem that does not exist, and no diagnostic mentions the path.

Suggestion (2)

  • Acceptance criterion "manifest refresh" is asserted only through skill content (internal/cli/directory_lifecycle_test.go:62): The issue requires update to rewrite affected owned files "with readback and manifest refresh". The test proves the skill body changed and that a plan writes nothing, but never reads .cortex/install-state.json before and after apply to show the manifest (snapshot fingerprint and artifact digests) was refreshed. A regression that rewrote files while leaving a stale manifest — the state uninstall and doctor depend on — would pass.
  • ../../catalog fixture couples the test to the package working directory (internal/cli/directory_lifecycle_test.go:113): TestUpdateDirectoryTargetFailsClosedOnUnknownState passes --catalog ../../catalog, which only resolves because go test runs with internal/cli as the working directory and a top-level catalog/ exists. The refusal it targets (not_installed) is reached only after a successful catalog build, so a future layout change would turn this into a silent invalid_catalog failure with a confusing message. An absolute fixture path built from the repo root would make the premise explicit.

Question (1)

  • Unrelated removal of a docs/README.md assertion from the foundation-documents test (tests/test_community_policy.py:559): The diff drops "docs/README.md" from the tuple of strings the root README.md must contain in test_foundation_documents_state_clean_target_status. That reference still exists on the merged tree (README.md:89, "Documentation map"), and nothing in feat: include directory targets in update and uninstall via recorded ownership #342 touches the root README, so the change only removes a live assertion. Why does a directory-lifecycle PR narrow an unrelated community-policy check?

Verdict: blocking

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type:feature New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: include directory targets in update and uninstall via recorded ownership

1 participant