Skip to content

feat(cli): name failing assets and drift cause on actor_guidance_unverified refusals - #436

Merged
barbatdev merged 4 commits into
mainfrom
fix/384-guidance-drift-cli
Nov 12, 2026
Merged

barbatdev merged 4 commits into
mainfrom
fix/384-guidance-drift-cli

Conversation

@barbatdev

Copy link
Copy Markdown
Contributor

Pull Request

Linked Issue

Fixes #384

PR Type

  • type:feature Feature
  • type:bug Bug fix
  • type:docs Documentation
  • type:refactor Refactoring
  • type:chore Maintenance
  • type:breaking-change Breaking change

Summary

Second of two PRs that replace #383. update refusals for actor_guidance_unverified now print one asset= line per failing asset with its path and drift cause, and the evidence stage reports content drift before mode drift (#384).

Changes

  • internal/cli/update.go: prints asset=<id> path=<path> cause=<cause> ... lines on actor_guidance_unverified refusals.
  • internal/cli/update_guidance_test.go: CLI tests through cortex update --runtime pi, including TestUpdateGuidanceEvidenceRefusalNamesContentBeforeModeDrift.
  • docs/operating.md: documents the refusal and its asset= lines.

Focused Tests

Claim Test command Observed result Limit State Observed by
The CLI reported mode drift for content plus mode drift New CLI test before the precedence fix cause=mode_drift mode_drift=true ... instead of cause=content_drift action=conflict mode_drift=true ... Verified Agent that made the change
The CLI names content drift first Same test after the fix Exact line matches Verified Agent that made the change
Mode-only and content-only refusals unchanged TestUpdateGuidanceRefusalNamesModeDrift, TestUpdateGuidanceRefusalNamesDriftedContent Pass Verified Agent that made the change
Split is faithful git diff of this branch against the resolved #383 tree No difference Verified Parent
Full suites go vet ./...; go test ./... -count=1; python3 -m unittest discover -s tests Clean; 35 packages ok; OK Verified Parent

Failed or not done: none.

Runtime Harness

N/A: CLI output change covered by CLI tests.

Rollback

Revert the single commit.

Chain Dependencies / Out of Scope

Checklist

  • Linked issue is open and status:approved.
  • Exactly one matching type:* label is applied.
  • Focused tests and runtime harness evidence are recorded.
  • Rollback and out-of-scope boundaries are documented.

@barbatdev barbatdev added the type:bug Bug fix label Nov 12, 2026
@barbatdev
barbatdev changed the base branch from fix/384-guidance-drift-core to main November 12, 2026 20:56
@barbatdev

Copy link
Copy Markdown
Contributor Author

Fleet review (glm)

Lens: requirements + adversarial. The diff implements the #384 acceptance criterion end to end: recordedDriftAssets (via the stacked #435 core) now makes content drift the cause when an asset drifted in both content and mode, and the CLI renderer emits the mode facts as an independent flag, so the evidence-stage refusal no longer hides content drift behind mode drift. The exact combined line is pinned by TestUpdateGuidanceEvidenceRefusalNamesContentBeforeModeDrift, matching the correction issue's failure scenario (bytes appended, mode 0640 vs canonical 0600). The stable status line, exit code, and opaque stderr contract are unchanged; paths stay runtime-relative and are asserted non-absolute; refusal writes nothing (tree-compared before/after in every refusal test). Docs updated with the asset= line format and shadow_role cause. No blocking findings.

Suggestion (1)

  • Docs example omits the mode_drift=true field the renderer emits (docs/operating.md:107): The documented mode-drift example line is cause=mode_drift observed_mode=0640 canonical_mode=0600, but actorGuidanceRefusalAssets in internal/cli/update.go emits cause=mode_drift mode_drift=true observed_mode=0640 canonical_mode=0600 for a mode-drift asset. The doc example does not match the actual output for the machine-readable contract it documents.

Verdict: ready to merge

@barbatdev

Copy link
Copy Markdown
Contributor Author

Fleet review (qwen38)

Lens: evidence. Reviewed head 1560ea2 (context diff matches the GitHub files API exactly: docs/operating.md, internal/cli/update.go, internal/cli/update_guidance_test.go). Against correction issue #384 the acceptance criterion holds at the stacked state: the substantive ordering fix lives in the base branch fix/384-guidance-drift-core (core PR #435), where recordedDriftAssets now tests content drift before mode drift, and this head adds the CLI renderer that keeps cause=content_drift plus the mode facts, pinned end-to-end by TestUpdateGuidanceEvidenceRefusalNamesContentBeforeModeDrift through the real cortex update --runtime pi entry point with an exact-line assertion and a no-writes check. The fixture is representative of the issue's failure scenario (bytes appended by an external editor, mode 0640 versus canonical 0600). Evidence gaps are non-blocking: the head diff alone does not contain the fix (it depends on the declared chain), one docs example does not match the emitted line shape, and the renderer's cause dispatch has an implicit default. No blocking findings; merge order (#435 first, then retarget) is the remaining risk to manage.

Suggestion (3)

  • Acceptance criterion Corrección: feat(cli): name failing assets and drift cause on actor_guidance_unverified refusals (#383) #384 is satisfied only at the stacked state; the fix is in base branch fix(installcoord): name drifted guidance assets with content drift first #435, not in this head's diff (internal/installcoord/actor_guidance.go): Issue Corrección: feat(cli): name failing assets and drift cause on actor_guidance_unverified refusals (#383) #384's blocking scenario is caused by recordedDriftAssets' cause precedence, and the content-before-mode fix lives in the base branch fix/384-guidance-drift-core (verified at that ref: the switch tests !owned || ownerHash != slot.SHA256 before case asset.ModeDrift). This head adds only the CLI renderer and tests. That is a legitimate chain (the PR body declares it), but if feat(cli): name failing assets and drift cause on actor_guidance_unverified refusals #436 is ever retargeted to main without fix(installcoord): name drifted guidance assets with content drift first #435 merged first, the criterion is not met by this head alone. TestUpdateGuidanceEvidenceRefusalNamesContentBeforeModeDrift would go RED in that case, so CI catches it — not blocking; just keep the declared merge order explicit at merge time.
  • Documented example line for a mode-only refusal omits the mode_drift=true field the renderer actually emits (docs/operating.md:108): actorGuidanceRefusalAssets renders a mode-drift-only asset as cause=mode_drift mode_drift=true observed_mode=... canonical_mode=... (internal/cli/update.go:577-579), but the docs example added by this PR shows ... cause=mode_drift observed_mode=0640 canonical_mode=0600 without mode_drift=true. The content-drift example matches the implementation. Since the whole point of these lines is machine-readable output for operators, the documented example should show the exact emitted shape so consumers can rely on it; the existing tests pin the implementation shape, so the doc is the side that is off.
  • Renderer defaults any non-mode cause to content_drift instead of dispatching on the explicit cause (internal/cli/update.go:577): The renderer branches on asset.Cause == installcoord.ActorGuidanceModeDrift and emits cause=content_drift for every other value, including the zero value. Today both constructors (recordedDriftAssets and classifiedConflictAssets) always set Cause, so there is no current failure path; but a future third cause, or an unset Cause on a newly constructed asset, would be silently reported as content drift. Matching on ActorGuidanceContentDrift explicitly with a distinct fallback would keep the diagnostic honest as the enum grows.

Verdict: ready to merge

@barbatdev
barbatdev merged commit b9c7926 into main Nov 12, 2026
2 checks passed
@barbatdev
barbatdev deleted the fix/384-guidance-drift-cli branch November 12, 2026 21:03
@barbatdev

Copy link
Copy Markdown
Contributor Author

Post-merge QA (glm)

Lens: requirements + adversarial. The diff implements the #384 acceptance criterion end to end: recordedDriftAssets (via the stacked #435 core) now makes content drift the cause when an asset drifted in both content and mode, and the CLI renderer emits the mode facts as an independent flag, so the evidence-stage refusal no longer hides content drift behind mode drift. The exact combined line is pinned by TestUpdateGuidanceEvidenceRefusalNamesContentBeforeModeDrift, matching the correction issue's failure scenario (bytes appended, mode 0640 vs canonical 0600). The stable status line, exit code, and opaque stderr contract are unchanged; paths stay runtime-relative and are asserted non-absolute; refusal writes nothing (tree-compared before/after in every refusal test). Docs updated with the asset= line format and shadow_role cause. No blocking findings.

Suggestion (1)

  • Docs example omits the mode_drift=true field the renderer emits (docs/operating.md:107): The documented mode-drift example line is cause=mode_drift observed_mode=0640 canonical_mode=0600, but actorGuidanceRefusalAssets in internal/cli/update.go emits cause=mode_drift mode_drift=true observed_mode=0640 canonical_mode=0600 for a mode-drift asset. The doc example does not match the actual output for the machine-readable contract it documents.

Verdict: clean

@barbatdev

Copy link
Copy Markdown
Contributor Author

Post-merge QA (deepseek)

Lens: evidence + test design. This is the CLI half of the #384 correction and it lands on top of the installcoord ordering fix: at the #436 merge commit recordedDriftAssets already takes content drift as the cause first and keeps mode drift as an independent flag (internal/installcoord/actor_guidance.go:213-220), so the acceptance scenario no longer reproduces and TestUpdateGuidanceEvidenceRefusalNamesContentBeforeModeDrift asserts the exact merged line. Evidence quality is good: the new tests drive the public cortex update --runtime pi path, assert the stable reason token, exit code, empty stderr and that a refused run writes nothing (tree comparison), and the diagnostics helper rejects absolute paths. Two gaps remain: the merged doc describes the asset lines as following the status line while the code emits the status line last, and the path-omitted branch of the diagostic renderer is never exercised because the test helper fails on an empty path. No blocking defect found.

Suggestion (1)

  • Documented line order does not match what the merged renderer prints (docs/operating.md:90): The added paragraph says the plan "prints status=conflict reason=actor_guidance_unverified (exit 3) followed by one asset= line per failing asset", but writeActorGuidanceRefusal (internal/cli/update.go:126-135) writes the header, then the asset lines, then the status line last. The example block in the same section shows only asset lines, so the documented order is only inferable from prose that contradicts the code. Fixing the sentence (or moving the status line first) also matters for the claim that the stable status line is unchanged: its content is unchanged, its position is not.

Question (1)

  • The path-omitted diagnostic branch has no evidence (internal/cli/update.go:178): actorGuidanceAsset deliberately omits path= when the candidate carries no file for an identity, but updateGuidanceDiagnostics fails every asset line with an empty path, so that branch can never be observed through the merged tests. If it is reachable (for example a shadow whose role identity is not in the canonical plan), the refusal would print a line the in-tree parser rejects; if it is unreachable, the branch is dead. Either a test that reaches it or a comment stating it cannot be reached would settle which.

Verdict: clean

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

Labels

type:bug Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Corrección: feat(cli): name failing assets and drift cause on actor_guidance_unverified refusals (#383)

1 participant