Closes the 2 grep/Read-verified findings from the S12 cold full-brief re-review (docs/remediation/review.md, WARN 0/1/1/0, 0 dropped) and closes the $-injection CLASS — not the line — across the whole state-updater.mjs mutation surface. See docs/remediation/review.md (S13 ALLOW, 0/0/0/0) for the full closure record: replaceField -> replacement function; the 3 additive-insert sites -> functions (m === $1, behavior-preserving); a scalar assert.match pins last_post_topic; and a behavioral, coverage-complete, self-testing Section 12 guard (check-replace-safety.mjs) that is mutation-proven. Docs three-doc + residuals updated. test-runner.sh 71/0/0, node --test 98/98.
7.9 KiB
LinkedIn Studio — Finish Plan (S13–S17)
Closes every remaining hole: the open S12 review findings + the gaps surfaced in the 2026-05-30 verified assessment. Brief-amendment to
docs/remediation/brief.md(same project). Each session is one STATE.md-driven Voyage session, gated bytest-runner.sh
node --test+/trekreview→ push only on ALLOW (no more WARN-overrides).Operator decisions (2026-05-30): build manual saves entry (S16); triage C13–C46 (S17); fold into this project as a brief-amendment. Dependency: S14 (command set) precedes S15 (router tiering) — the router can't be tiered before the command set is final.
Sequence & dependency
S13 (close S12 WARN + $-class) → ALLOW [finishes the ORIGINAL brief]
│
S14 (command rationalization) → ALLOW [sets the final command set]
│
S15 (UX §6c — router on final set) → ALLOW
│
S16 (saves manual entry) → ALLOW
│
S17 (C13–C46 triage) → ALLOW [process complete]
S13 — Close the open S12 findings + the $-replacement class
Finishes the original brief; brings the existing scope to a clean ALLOW.
- A1 (MINOR):
hooks/scripts/state-updater.mjs:14-18— convertreplaceFieldto a replacement function ((_m) => \${field}: ${value}`) so the untrustedlast_post_topicat:58` is inserted literally. - A2 (MAJOR): add
assert.match(result.content, /^last_post_topic: "\$100 budget — \$& and \$1 rule"$/m)to the existing$-bearing test instate-updater.test.mjs(fails today, passes after A1). - A3 (systemic): audit every
String.replaceinhooks/scripts/*.mjswhose replacement is a string built from a function parameter that can carry user content (grep -nE '\.replace\([^,]+, *\'). ConfirmreplaceFieldwas the last such site. Add a **structural$-safety lint** (test-runner.sh` Section 12) that flags string-replacement sites whose value derives from an untrusted parameter — non-vacuity self-test + e2e mutation-proof, mirroring Sections 8/10/11. - Engine: inline (small, surgical).
- Verify: A2 assert FAILS pre-A1, PASSES post-A1 ·
node --testgreen ·test-runner.shgreen (incl. new Section 12 self-test) · audit-grep returns 0 unsafe sites ·/trekreview→ ALLOW → commit (review.md + S13) → push.
S14 — Command rationalization (re-opens the original command-surface Non-Goal)
Analysis → operator decision → execute. Nothing deleted without explicit per-command yes.
- 14a Analysis (no edits): cold per-command review of all 27 →
docs/remediation/command-rationalization.md. Per command: purpose · overlap with siblings · invocation leverage (algorithmic + likely use) · recommendation keep / develop / merge→X / cut + rationale. Delegate the cold read to an Agent (Opus) for independence. - 14b Operator decision: present the doc; operator decides per command (
AskUserQuestionbatched). No mechanical deletion until approved. - 14c Execute approved: apply merges/cuts; for a merge, fold the source command's unique
surface into the target and delete the source; update
EXPECT_COMMANDSintest-runner.sh, all rosters (CLAUDE.md/README/SKILL.md/router), CHANGELOG, version bump if the surface count changes (breaking → minor/major per SemVer judgment). - Engine: Agent (14a) → inline (14c).
- Verify:
ls commands/*.md | wc -l== every declared count · lint count-guard green · three-doc synced ·grepold count → 0 stale ·/trekreview→ ALLOW → push.
S15 — UX finish (§6c), on the FINAL command set
- B1 Onboarding inline:
commands/onboarding.md— replace the"Run /linkedin:first-post"hand-off with the first-post steps embedded in the wizard, so the flow produces a draft post inline (no dead-end). Verify: a walkthrough yields a draft inside onboarding; 0Run /linkedin:first-postdead-end strings. Scope guard (UI brief §12b): fix the dead-end ONLY — do NOT add extensibility/provider "seams" or progressive- disclosure config to onboarding; those are unresolved UI-brief decisions (keep onboarding lean per the persona "first value without forking"). - B2 Router tiering:
commands/linkedin.md— restructure into Primary (3–4: post/quick/newsletter/firsthour), Secondary (the rest of the final set), Locked ~1K (monetize/outreach/competitive, marked "unlocks later"). Verify: tier sections present; primary ≤4; locked commands flagged, not inline with primaries. - B3 Carousel full-deck clipboard:
commands/carousel.md— assemble the entire deck (every slide's copy + the caption) into the clipboard payload, not just the caption. Verify: clipboard payload contains slide text; grep shows full-deck assembly before theclipboard-helper.mjscall. - Engine: inline.
- Verify: all three grep/observation checks pass ·
test-runner.shgreen ·/trekreview→ ALLOW → push.
S16 — Saves manual-entry surface (operator-requested; lifts the original Non-Goal)
⚠️ CONFLICT — reconcile before building (UI brief §9b/M0). The UI brief makes it binding that all mutable personal data (
assets/analytics/*,queue.json,*.local.md) moves OUT of the plugin tree into a stable per-user data dir, "in the v4.0.0 remediation or immediately after." S16 extends the analytics data model — if built against the current in-treeassets/analytics/it gets reworked by M0. Decision needed: (a) do M0 first (insert as S15.5), then build S16 in the final location; or (b) defer S16 to ride along with M0/the UI build. Do NOT build S16 blind to M0.
- Add a manual-entry path for saves (visible in native LinkedIn post analytics, count-only,
~Sept 2025) to the analytics data model (
scripts/analytics/types + import/report path), additive and backward-compatible. Re-rank the actionable-signal output to include saves where the CSV/manual data now contains it. Dwell stays explicitly unmeasurable (internal-only) — do not fabricate a dwell surface. - Engine: inline (+ analytics CLI knowledge; may need
tsxtypes touch). - Verify: a report run with a manual saves value surfaces it without crashing; existing
CSV-only data still works (backward-compat); honesty wording retained for dwell ·
node --test/ analytics tests green ·/trekreview→ ALLOW → push.
S17 — Triage the uncalibrated audit findings (C13–C46)
- Read the ~34 findings the audit never put through a second hostile pass (
§10); for each: classify still-real / already-fixed / outdated-drop; close every still-real one; record the disposition indocs/remediation/c13-c46-triage.md. Delegate the cold read to an Agent (Opus). - Engine: Agent (triage) → inline (fixes).
- Verify: every C13–C46 finding has a recorded disposition; still-real ones grep-verify closed ·
test-runner.sh+node --testgreen ·/trekreview→ ALLOW → push. Process complete.
Verification (whole plan)
- Per session:
bash scripts/test-runner.shexit 0 ·node --test hooks/scripts/__tests__/*.test.mjsall pass ·/trekreview --project docs/remediation/→ ALLOW (not WARN) before push. - Plan-complete signal: all of S13–S17 pushed on ALLOW;
command-rationalization.md+c13-c46-triage.mdcommitted; no open/trekreviewfinding; STATE.md "Aktiv oppgave" reads "remediering FERDIG — ren ALLOW". - Counts contract stays live: the lint's count-guard (19/27/25/6 today; 27 may change in S14)
is updated in lockstep with any command merge/cut;
grepfor the prior count returns 0 stale hits.
Locked constraints (inherited from brief)
- Opus on everything · no hidden costs (cost-warn
/trekcontinue·/trekreview, standing yes) · three-doc rule · version-sync · bash 3.2 + Node-only hooks · push only to Forgejo · stage own files only · fix-in-next-session for any review finding.