Indeed, those redirection routines aren't called anywhere. But they're redefined extensively around the Caché Class API.
As you said it's cryptic and powerful enough to dictate the buffer workflow even though you don't use it explicitly.

From my experiments, what I notice is:

wstr handles every write

wnl handles every !

wchr handles every single char

wtab handles tab usage
wff  handles every form feed

As used here.
 

%Status always returned $$$OK and putting -3 instead -1 provided me the same feedback. The last line is still empty.

Maybe it's something regarding the redirection routines. I can't really see that:

wnl() do output($char(13,10)_$$$FormatText("Port (%1): ", hookName)) quit

As correct, since wnl doesn't even take a parameter  where I could decide what to do.

Updated to use %Stream.GlobalCharacter instead, thank you.
Using eol also didn't resolved I guess. Here's the feedback:

 

Port: Executing hook extension OnAfterSave from the class Port.HookTest ...
Port (OnAfterSave):
Port (OnAfterSave): First lineSecond line
Port (OnAfterSave): Third line
Port (OnAfterSave):
Port: OnAfterSave returned without errors.

ClassMethod OnAfterSave(
sourceControl As %Studio.Extension.Base = "",
InternalName,
Location,
Object) As %Status
{
  // ! skips the first line leaving it empty, actual message is pushed down to the second line.
  // should not even print it.
  write !, "First line"
  
  // First line is here, printed along with the second.
  write "Second line", !
  
  // Third is still third line. But ! writes another empty line.
  write "Third line", !
  return $$$OK
}


As you suggested, I changed it to use eol:

 if $isobject(content) {
    do content.OutputToDevice()
    do content.MoveTo(content.Size - 1)
    do content.ReadLine(, sc, .isEOL)
  }
     
  if 'isEOL write !

Since no one answered that yet, maybe there isn't a known way of doing it.
If that's really the case, I suggest you to create your procedure.

This could work as the following:
 

1 - Routine B watches a global generated from Routine A using a scheduled task.

2 - Process A triggers an exception.

3 - Exception on routine A executes subroutine or method to that uses $stack to capture execution data.

4 - Routine A stores data into a global and quits abnormally.

5 - Routine B watches for new entries in the global, marks it as used/processed.

I agree with @Amir Samary. Even though by using inheritance you're allowed to use method generators, it's still against against the SIngle Responsibility Principle, since the code is binding directly to the persistence (which it's SRP should be to work as a storage representation).

Binding another responsibility to it could mean  that sometime in the future the persistence would need to be rewritten to match the newer requirements, thus risking to break current applications and elevating technical debt.

So my golden rule is: unless you're 120% sure that the result generated by the binded class won't change, or you're 120% sure that it still submits to the SRP, only then you can "adapt" the persistent class.

But before that, I would recommend (priority order):

1 - Use an utility method that takes the binding class name and returns the JSON or vice-versa. Can take an optional configuration object or flags to overwrite and determine the serialization behavior for the current case.

 

2 - Create a mirror class that extends exclusively from the Adapter.

 

This keeps the persistent class clean and still allows it to be serialized.

Also, there's a method for doing what I suggest on 1, I think it's been discussed here already.

I think that the default convention for REST usage has been exploited too much already, to the point that most would prefer to have an extra feature than follow it strictly. Even Google and Facebook do it: though they use REST most of their services, they also allow to pass query parameters.

I think it's due to their performance or readability policies.

EDIT:

By allowing query parameters, you can also prevent another rule break: 

Let's say I need to pass a lot of parameters to FETCH data from a resource. As REST demand, you need to use GET verb to fetch data from the resource if you won't change it.

However if you can't use query parameters, you need to use POST or PUT to provide the payload (that modifies data) and this would break the CRUD rule as well.

This is where I would balance the rule breaks, that is, I can't really disregard breaking a rule that actually prevents me from breaking another that's even more explicit.