Module: Hecks::Adapters::ClaudeCode
- Defined in:
- lib/hecks/adapters/driven/claude_code.rb
Overview
THE REAL agent FULFILLMENT — shells out to the claude CLI
itself, claude -p --output-format json, one process per call.
Ports::Agent's own scripted double (spec/fixtures/scripted_ agent.{adapter,rb}) is the deterministic sibling every spec binds
instead, the same relationship SecureRandomIdentity already has
to SequentialIdentity.
THIS FILE OWNS TRANSPORT ONLY — spawning the process, unwrapping
the CLI's own JSON envelope ({"result": "..."}) down to the
model's raw text, and parsing THAT text as JSON. It hands back a
plain Hash. Whether that Hash has the keys a caller asked for, and
whether its values are within the closed vocabularies this port
recognizes (a critique's kind, a proposal's verb pattern) is
Ports::Agent::Answers' job, not this file's — see ports/agent.rb's
own header for why that split is load-bearing.
Open3.capture2 with an explicit argv array, never a shell string
— the same shape every other subprocess call in this repo already
uses (bin/rust_conformance), and the reason claude being
shell-aliased to claude --dangerously-skip-permissions in a
human's own terminal never reaches this code at all: an argv spawn
invokes the real binary directly, no shell, no alias.
--allowedTools "" — this adapter asks a question and reads back
text; it never wants the model reaching for a tool mid-answer.
Constant Summary collapse
- TIMEOUT_SECONDS =
120- SYSTEM_PREFIX =
"You are assisting a domain-modeling interview for the hecks " \ "event-sourced framework. Reply with EXACTLY ONE JSON object, no prose " \ "before or after it, no markdown code fence. "
Class Method Summary collapse
-
.ask(state:, asked:) ⇒ Object
THE NEXT BEST QUESTION.
-
.call(system:, payload:) ⇒ Object
── transport ───────────────────────────────────────────────────.
-
.critique(declared:, refusals:, findings:) ⇒ Object
WHAT IS WRONG WITH THIS AS A MODEL — closed to the same kind vocabulary
Ports::Agent::CRITIQUE_KINDSdeclares, spelled out here too since the system prompt is the only place the model itself ever sees that list. -
.interpret(prose:, state:) ⇒ Object
PROSE -> PROPOSED DECLARATIONS.
-
.suggest_name(meaning:, kind:, near:) ⇒ Object
VOCABULARY HELP.
- .unwrap(stdout) ⇒ Object
Class Method Details
.ask(state:, asked:) ⇒ Object
THE NEXT BEST QUESTION. state is whatever
Interview::Session#declaration/#gaps produced — passed
through as JSON, not reformatted, so this adapter never
re-derives what the session already knows.
47 48 49 50 51 52 53 54 |
# File 'lib/hecks/adapters/driven/claude_code.rb', line 47 def ask(state:, asked:) call( system: SYSTEM_PREFIX + "Given the domain model so far, ask the single best next " \ 'discovery question. Reply as {"questions": [{"text": "...", "because": "..."}]} ' \ "with exactly one entry. Never repeat a question already asked.", payload: { state: state, already_asked: asked } ) end |
.call(system:, payload:) ⇒ Object
── transport ───────────────────────────────────────────────────
99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 |
# File 'lib/hecks/adapters/driven/claude_code.rb', line 99 def call(system:, payload:) stdout, status = Timeout.timeout(TIMEOUT_SECONDS) { Open3.capture2( "claude", "-p", "--output-format", "json", "--append-system-prompt", system, "--allowedTools", "", stdin_data: JSON.generate(payload) ) } raise Ports::Agent::Unavailable, "claude exited #{status.exitstatus}: #{stdout}" unless status.success? unwrap(stdout) rescue Timeout::Error raise Ports::Agent::Unavailable, "claude did not answer within #{TIMEOUT_SECONDS}s" rescue Errno::ENOENT => e raise Ports::Agent::Unavailable, "claude is not on PATH: #{e.}" end |
.critique(declared:, refusals:, findings:) ⇒ Object
WHAT IS WRONG WITH THIS AS A MODEL — closed to the same kind
vocabulary Ports::Agent::CRITIQUE_KINDS declares, spelled out
here too since the system prompt is the only place the model
itself ever sees that list.
73 74 75 76 77 78 79 80 81 82 83 |
# File 'lib/hecks/adapters/driven/claude_code.rb', line 73 def critique(declared:, refusals:, findings:) kinds = Ports::Agent::CRITIQUE_KINDS.join(", ") call( system: SYSTEM_PREFIX + "Given a domain declaration, the refusals it already triggered, and " \ "the mechanical findings already reported, judge it on TASTE — do not restate what is " \ "already known. Only use one of these kinds: #{kinds}. Only use severity error or " \ 'warning. Reply as {"findings": [{"kind": "...", "severity": "...", "subject": "...", ' \ '"message": "..."}]}. Reply {"findings": []} if there is nothing worth saying.', payload: { declared: declared, refusals: refusals, findings: findings } ) end |
.interpret(prose:, state:) ⇒ Object
PROSE -> PROPOSED DECLARATIONS.
57 58 59 60 61 62 63 64 65 66 67 |
# File 'lib/hecks/adapters/driven/claude_code.rb', line 57 def interpret(prose:, state:) call( system: SYSTEM_PREFIX + "Given the domain model so far and a sentence the human just said, " \ "propose zero or more declarations that capture what the sentence names as domain " \ "fact. A verb must be fully qualified as Chapter::Aggregate.Command. Reply as " \ '{"proposals": [{"verb": "...", "rationale": "...", "arguments": ' \ '[{"name": "...", "field": "...", "value": "..."}]}]}. If the sentence carries no ' \ 'declaration (a question back, a clarification), reply {"proposals": []}.', payload: { state: state, prose: prose } ) end |
.suggest_name(meaning:, kind:, near:) ⇒ Object
VOCABULARY HELP. Named suggest_name, not name — see
Ports::Agent#suggest_name's own comment for why name is
never a safe module-function name here.
88 89 90 91 92 93 94 95 |
# File 'lib/hecks/adapters/driven/claude_code.rb', line 88 def suggest_name(meaning:, kind:, near:) call( system: SYSTEM_PREFIX + "Suggest a name for a #{kind} meaning \"#{meaning}\", distinct from " \ 'the names already in use nearby. Reply as {"names": [{"name": "...", "because": ' \ '"...", "rejected": ["...", "..."]}]} with exactly one entry.', payload: { meaning: meaning, kind: kind, near: near } ) end |
.unwrap(stdout) ⇒ Object
116 117 118 119 120 121 122 123 124 |
# File 'lib/hecks/adapters/driven/claude_code.rb', line 116 def unwrap(stdout) envelope = JSON.parse(stdout) result = envelope["result"] raise Ports::Agent::ValidationError, "no \"result\" in the claude envelope: #{stdout}" unless result JSON.parse(result) rescue JSON::ParserError => e raise Ports::Agent::ValidationError, "claude's reply was not JSON: #{e.}\n#{stdout}" end |