-
Notifications
You must be signed in to change notification settings - Fork 3
plat 033
| Coordination | Value |
|---|---|
| Assigned agent | Claude Code |
| Ticket state |
implemented (the two reproduced call sites; shared mechanism, not every caller) |
| Last synchronized | 2026-08-05 |
Claim this ticket in this file before implementation. During active work, update this fragment rather than the shared index; synchronize the index once at handoff, review, or completion.
- Priority: P1
- Owner: managed mutation and changelog evidence writer
-
Source finding:
HARNESS-CHANGELOG-REF-PLACEHOLDER -
Source workflow:
Workflow/social-media -
Source fingerprint:
ae0b8a1b78ba5a0c -
Problem: managed planning tools can record
before_ref == after_ref == sha256("[]")withchanges: []even when an artifact changed. Some entries also name a target the tool did not write, and a confirmed mutation was omitted from the changelog. - Impact: the changelog proves only that a tool ran. Artifact Review cannot establish what changed, reconstruct an outage-triage window, or verify the claimed mutation boundary.
-
Current evidence:
- six retained entries use
sha256:4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945, the SHA-256 of the literal string[], for both refs; - three entries have
changes: []whileplanning/step_config.jsonhas the matching modification time; - one
write_workflow_manifestentry targetsplanning/plan.jsonwhile its reason describes a directworkflow.jsonwrite; - the latest Fixer reproduced the behavior across 11
update_step_configcalls, including requested fields that were silently dropped.
- six retained entries use
- Required fix: snapshot the actual target artifact before mutation and after the persisted mutation; hash those artifact states; record the fields actually accepted and changed; use the artifact actually written as the target; and fail closed if a requested field is unsupported instead of recording a misleading successful edit.
- Acceptance: a real mutation produces correct, different content refs and a non-empty change description; a no-op is explicitly recorded as a no-op; an unsupported field fails without a success entry; the target matches the written artifact; and every sanctioned mutation emits exactly one entry.
-
Required tests:
update_step_configmutation, no-op, unsupported-field, target-integrity, and crash/rollback cases using the real managed writer.
PLAT-012 asks whether every material managed mutation is covered by the changelog. This ticket asks whether an emitted entry is truthful. The current Social Media evidence was produced after the PLAT-012 implementation and is therefore a separate, presently reproduced integrity defect rather than proof that PLAT-012's coverage work never existed.
Root cause confirmed bit-for-bit against the evidence.
completePlanChangelogEntry (planning_agent.go) computed before_ref/
after_ref by hashing entry.Changes[].OldValue/NewValue — never the
actual artifact. sha256("[]") is literally the SHA-256 of the JSON encoding
of an empty Changes list; hashing the string "[]" by hand reproduces
4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945 exactly,
matching the ticket's cited hash. Two call sites reproduce the evidence
directly:
-
update_step_config(interactive_workshop_manager.go) never setChangesat all — every call collapsed both refs to the same placeholder, regardless of what fields actually changed. This matches "11update_step_configcalls, including requested fields that were silently dropped" exactly — there was no mechanism that could have recorded them. -
write_workflow_manifest(workflow_manifest.go, viaLogCanonicalArtifactChange) passedchanges: nildespite already having read the previous file content (previous) and having the new content (data) sitting right there — real before/after bytes were available and simply not passed through. ItsTargetalso fell throughcompletePlanChangelogEntry's tool-name-substring default (onlyworkflow_config/step_config/learning/evaluationare special-cased;"write_workflow_manifest"doesn't contain"workflow_config") toplanning/plan.json, reproducing the "targetsplanning/plan.jsonwhile its reason describes a directworkflow.jsonwrite" evidence exactly.
Fix, at the shared choke point plus the two reproduced callers:
-
PlanChangelogEntrygainsBeforeSnapshot/AfterSnapshot(json:"-", never persisted themselves) andNoOp(json:"no_op,omitempty").completePlanChangelogEntrynow hashes the snapshot when present instead of deriving fromChanges, and setsNoOponly when both snapshots are real and hash identically — an entry with no snapshot stays "unknown," it is never misreported as a confirmed no-op. Callers that only ever setChanges(the other ~13logPlanChange/PlanChangelogEntry{...}sites inplanning_agent.go) are unaffected — same ref computation as before, verified byTestCompletePlanChangelogEntryUnchangedForCallersWithoutSnapshots. -
LogCanonicalArtifactChangegainedtarget string, beforeSnapshot, afterSnapshot interface{}parameters (both existing callers updated:controller_execution.go's learnings-update call passes empty/nil — it already builds honestChangesfrom real content hashes — andworkflow_manifest.gonow passes"workflow.json",previous, andstring(data)). -
update_step_confignow: snapshotstargetConfigvia a JSON round trip immediately after loading it and before any field mutation runs (decoupling the snapshot from the in-place mutations that follow); afterWriteStepConfigsToSubdirsucceeds, re-reads the persisted file (not the in-memory struct) and snapshots the matching step's entry — so the after-snapshot reflects what was actually written, including any silent normalization/drop the writer applies, which is exactly the failure mode the evidence describes; passes the correctconfigSubdir-derived target path instead of relying on the heuristic; and records one realPlanFieldChange(Field: "agent_configs", old/new = the two snapshots) sochangesis never empty again.
What this does NOT cover (scoped out, not silently skipped):
-
Fail-closed on unsupported fields (required fix item 4) — not
implemented.
update_step_configstill accepts and silently ignores a field name it doesn't model; this fix makes the recorded refs honest about whatever WAS accepted, it does not reject anything. - The other ~13
PlanChangelogEntry{...}call sites (add/update/delete step, routing steps, human-input steps, evaluation-plan updates, etc.) were not audited individually for the same defect — onlyupdate_step_configandwrite_workflow_manifestwere reproduced in the evidence and fixed. A comment inplan_change_backlog.go(toUnreviewedPlanChange) suggestsupdate_step_configwas uniquely bad among these ("update_step_config records none today"), implying the others likely already populateChangescorrectly, but this was not independently re-verified per call site. -
No end-to-end test of the
update_step_configtool handler itself — the shared mechanism (completePlanChangelogEntry,LogCanonicalArtifactChange) is unit-tested directly; the wiring insideinteractive_workshop_manager.gowas verified by code reading and a full build, not by driving the tool through a constructedInteractiveWorkshopManager+ fake controller. - No-op, unsupported-field, target-integrity, and crash/rollback test cases named in "Required tests" — only the no-op and target-integrity cases are covered, and only at the shared-mechanism level, not through the real tool handler.
Tests: plat033_changelog_ref_test.go — snapshot-preferred refs, no-op
only-when-real-evidence, backward-compatible refs for Changes-only callers,
and LogCanonicalArtifactChange's target/snapshot wiring end-to-end through
writePlanChangelogEntry. Confirmed to fail to compile against the pre-fix
code (stashed and re-ran).
Remaining/runtime reverify: confirm on a real Social Media run that a
fresh update_step_config call produces a before_ref/after_ref pair that
differ when a field actually changed, that changes is non-empty, and that
write_workflow_manifest entries target workflow.json.
Source finding: PUL-988DF96A in Workflow/hetznerssh, reported by
Artifact Review on 2026-08-02.
This is a valid remaining part of PLAT-033, not a workflow-only process
problem. The current WriteWorkflowManifest call passes the fixed reason
"workflow.json was written directly; recorded so artifact drift review can see the change." and changes: nil. The earlier repair makes its target and
before/after hashes truthful, but a human reading the changelog still cannot
tell which manifest fields changed without reconstructing the JSON or using
Git.
The reviewer is right about that outcome, but its suggested remedy (ask each future caller to write a better sentence) is not sufficient: this shared writer already has both JSON documents and should compute the evidence deterministically.
Required follow-up: parse the previous and persisted workflow.json,
produce a stable, secret-safe list of changed JSON paths, and pass it as
PlanFieldChange evidence. Values for secret-bearing fields must be redacted
or omitted; field paths plus added/removed/changed status are enough for
the changelog. Keep the current content hashes as the authoritative exact
boundary. A no-op must still produce no entry.
Acceptance: a real manifest write records target workflow.json, distinct
content refs, and deterministic changed-path evidence; changing one nested
field does not claim unrelated paths; secret values do not appear in the
changelog; and an unchanged write creates no entry.
WriteWorkflowManifest now derives its changes[] evidence from the JSON it
already read and the JSON it just persisted. It emits sorted leaf paths such as
workflow.json.capabilities.selected_skills, with only lifecycle markers
(absent → added, present → changed, or removed → absent). It does not
copy old or new JSON values into the changelog, including for secret-bearing
fields. Arrays are represented as one changed path so an insertion or reorder
does not manufacture a long, unstable index diff.
The fixed writer records the short reason “Recorded workflow.json field changes for artifact drift review.” and leaves the existing before/after content hashes as the authoritative exact mutation boundary. An unchanged manifest still emits no entry.
Tests: TestWorkflowManifestChangelogChangesIsStableAndValueFree covers
nested scalar and array changes, deterministic order, and no value leakage;
TestWorkflowManifestChangelogChangesCreationAndCorruptPriorState covers a
new manifest and an unparseable historic manifest. Runtime reverify on the next
manifest mutation should confirm a non-empty changes[] list alongside the
already-fixed workflow.json target and distinct refs.
Tectonicus Pulse found that 159 of 231 managed changelog entries have none
of changes[], before_ref, or after_ref. The exact historic callers cannot
be truthfully backfilled; doing so would fabricate mutation provenance. The
finding does prove that fixing two reproduced callers was not enough to satisfy
the ticket's "every sanctioned mutation" acceptance condition.
Remaining implementation boundary: audit every managed mutation writer, make each either emit an actual target plus before/after snapshot and changed fields or fail closed, and add a table-driven coverage test that registers every sanctioned mutation tool/caller. New entries must be complete; old incomplete entries remain explicitly historical/unknown. This is platform-owned work, not a Tectonicus workflow repair.
Auto-synced from docs/ on main. Edit there, not here.