@@ -56,7 +56,9 @@ Return JSON:
5656- `declared`: the workflow's inputs — anything the goal supplies as data (a
5757 repository, a topic, an id), so the plan works for the NEXT goal of its
5858 kind with different values. `inputs` supplies this run's value for every
59- required one. Declared values are attached to ask steps automatically.
59+ required one. Declared values are attached to ask steps automatically —
60+ NEVER also paste a value into an ask: a pasted value makes the plan
61+ single-use, so it cannot be kept for future goals, and it is refused.
6062- The LAST step's output is the run's answer: make it the step that produces
6163 the deliverable.
6264
@@ -96,6 +98,17 @@ pub fn lower(answer: &Value) -> Result<(WorkflowGraph, Map<String, Value>, Strin
9698 let declared = parse_declared ( answer) ;
9799 let inputs = answer[ "inputs" ] . as_object ( ) . cloned ( ) . unwrap_or_default ( ) ;
98100
101+ // A declared value pasted into an ask defeats the declaration: the
102+ // lowering attaches the value anyway, so the paste is redundant now and
103+ // poisonous later — selected for a different value, the prompt would
104+ // carry BOTH, and the keep gate would rightly refuse to file the plan.
105+ // Refused here, where the feedback round can fix it, rather than
106+ // discovered as an unkeepable graph after a satisfied run.
107+ let pasted = pasted_values ( & steps, & declared, & inputs) ;
108+ if !pasted. is_empty ( ) {
109+ return Err ( IntakeError :: Invalid ( pasted. join ( "; " ) ) ) ;
110+ }
111+
99112 let mut nodes = vec ! [ Node {
100113 id: "start" . into( ) ,
101114 kind: NodeKind :: Trigger ,
@@ -166,6 +179,38 @@ pub fn lower(answer: &Value) -> Result<(WorkflowGraph, Map<String, Value>, Strin
166179 Ok ( ( graph, inputs, why) )
167180}
168181
182+ /// Ask steps that restate a declared value instead of relying on the
183+ /// attachment. Only distinctive values count — refusing a plan because an
184+ /// ask contains the word "on" would block perfectly reusable recipes — and
185+ /// only DECLARED inputs: undeclared entries are trimmed by the author gate
186+ /// and never attached, so their values in an ask are just prose.
187+ fn pasted_values (
188+ steps : & [ Step ] ,
189+ declared : & [ ( String , String , bool ) ] ,
190+ inputs : & Map < String , Value > ,
191+ ) -> Vec < String > {
192+ let mut problems = Vec :: new ( ) ;
193+ for step in steps {
194+ let Action :: Ask { prompt, .. } = & step. action else {
195+ continue ;
196+ } ;
197+ for ( name, _, _) in declared {
198+ let Some ( value) = inputs. get ( name) . and_then ( Value :: as_str) . map ( str:: trim) else {
199+ continue ;
200+ } ;
201+ if crate :: reuse:: distinctive ( value) && prompt. contains ( value) {
202+ problems. push ( format ! (
203+ "step `{}` pastes the value of input `{name}` into its ask — remove \
204+ it; declared values are attached automatically, and a pasted value \
205+ makes the plan single-use",
206+ step. id
207+ ) ) ;
208+ }
209+ }
210+ }
211+ problems
212+ }
213+
169214/// The generated prompt expression for an ask step.
170215///
171216/// A jq program the model never sees: the instruction as a quoted literal,
0 commit comments