If you run
variable c clob
exec dbms_utility.expand_sql_text('select * from pi_step2_v',:c)
you can see where the issue is. Your final query becomes
SELECT "A1"."IDP" "IDP","A1"."IDI" "IDI","A1"."IDP_S1" "IDP_S1","A1"."IDI_S1" "IDI_S1"
FROM
(SELECT /*+ QB_NAME ("STEP2") */ "A3"."IDP" "IDP","A3"."IDI" "IDI","A2"."IDP" "IDP_S1","A2"."IDI" "IDI_S1"
FROM
(SELECT /*+ QB_NAME ("MAIN") */ "A5"."IDP" "IDP","A4"."IDI" "IDI"
FROM "SCOTT"."TP" "A5","SCOTT"."TI" "A4"
WHERE "A5"."IDP"="A4"."IDP"(+)) "A3",
(SELECT /*+ QB_NAME ("STEP1") */ "A6"."IDP" "IDP","A6"."IDI" "IDI"
FROM
(SELECT /*+ QB_NAME ("MAIN") */ "A8"."IDP" "IDP","A7"."IDI" "IDI"
FROM "SCOTT"."TP" "A8","SCOTT"."TI" "A7"
WHERE "A8"."IDP"="A7"."IDP"(+)) "A6"
WHERE "A6"."IDP"<>0) "A2"
WHERE "A3"."IDI"="A2"."IDI") "A1"
hence the duplicate block names.
There is a fairly extensive discussion on Jonathan Lewis's post on this
https://jonathanlewis.wordpress.com/2022/07/26/hinting-5/ which talks about the challenges of hinting when the optimizer is doing transformations.
In general, you would generate the outline (with the system generated block names) and then use them to force a hint deeper into the plan.