@try-works/dsh-recursive-mode 0.4.4 → 0.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -179,7 +179,7 @@ control plane on disk is the only state it trusts across restarts.
179
179
  | `recursive_audit_team` | Fan a phase out across roles (audit) |
180
180
  | `recursive_review` | **Independent review** of the phase artifact, with a repair path |
181
181
  | `recursive_delegate` | **Delegate the work of a phase** to a durable child; it produces, you judge |
182
- | `recursive_ask` | Ask the workspace a question, with the control plane as context |
182
+ | `recursive_ask` | Ask a **human** gate as a structured decision: `tdd-mode`, `qa-signoff`, `gate-block`, or `run-start` — the phase-0 approval that decides whether the run goal exists |
183
183
  | `recursive_preview` | Preview what a tool would do, without doing it |
184
184
 
185
185
  ### 4.2 The command surface
@@ -405,9 +405,20 @@ prove. Three facts shape the picture:
405
405
  the audit contract: `01-as-is`, `01.5-root-cause`, `02-to-be-plan`, `03-implementation-summary`,
406
406
  `03.5-code-review`, `04-test-summary`, `06-decisions-update`, `07-state-update`, `08-memory-impact`.
407
407
  `00-requirements`, `00-worktree` and `05-manual-qa` are not in it.
408
- 3. **`recursive_ask` is not a subagent tool.** It carries the workflow's three **human** gates —
409
- `ASK_GATE_IDS = ['tdd-mode', 'qa-signoff', 'gate-block']` — as structured decisions rather than prose, so the
410
- answer is validated and citeable.
408
+ 3. **`recursive_ask` is not a subagent tool.** It carries the workflow's **four human gates** —
409
+ `tdd-mode`, `qa-signoff` and `gate-block` (the list in `ASK_GATE_IDS`), plus **`run-start`**, the
410
+ phase-0 approval that decides whether a run's goal exists at all — as structured decisions rather
411
+ than prose, so the answer is validated and citeable.
412
+
413
+ `run-start` is unlike the other three: it is the only gate that asks the harness's **blocking
414
+ human channel**, and the only one that **arms a goal**. Creating a spec therefore creates no goal;
415
+ nothing runs unattended until a person approves it. When the mounted channel cannot deliver the
416
+ question, the refusal **names the channel's own cause** (`RM5503`) instead of asserting one — and a
417
+ composition that cannot render a card at all can still record the decision explicitly by passing
418
+ `answer` with `relay: true`, which is reported as relayed rather than as a person's selection. A
419
+ person's own answer always wins, and a question that was cancelled, aborted or timed out is **never**
420
+ relayable. A channel that *did* reach a person whose answer was not one of the offered labels is
421
+ `RM5504`, which is a different event from `RM5503`.
411
422
 
412
423
  ```mermaid
413
424
  flowchart TB
package/lib/errors.d.ts CHANGED
@@ -61,12 +61,30 @@ export declare const TOOL_ERRORS: {
61
61
  readonly problem: "this gate has no default artifact, so one must be named";
62
62
  readonly next: "pass artifact: <file> so the answer has somewhere durable to land";
63
63
  };
64
+ readonly RELAY_ONLY_FOR_RUN_START: {
65
+ readonly code: "RM1150";
66
+ readonly klass: "input";
67
+ readonly problem: "relay applies only to the run-start gate, which is the only gate that asks the user-questions channel";
68
+ readonly next: "drop relay for this gate and answer it with one of its own labels, or call recursive_ask with gate: run-start when the decision is whether to start the run";
69
+ };
64
70
  readonly MISSING_RUN_ID: {
65
71
  readonly code: "RM1101";
66
72
  readonly klass: "input";
67
73
  readonly problem: "runId is required";
68
74
  readonly next: "call recursive_status with no runId to see the latest run id in this workspace";
69
75
  };
76
+ /**
77
+ * A run id is the NAME of the run directory and is joined onto the run layer
78
+ * as one path segment, so a path-shaped id is refused before any directory is
79
+ * created. See `run-id.ts` for the rule and for why it is not the runtime that
80
+ * learns to accept a path.
81
+ */
82
+ readonly BAD_RUN_ID: {
83
+ readonly code: "RM1107";
84
+ readonly klass: "input";
85
+ readonly problem: "runId is not a single directory name";
86
+ readonly next: "pass the run directory name such as 03-something, then call recursive_init again";
87
+ };
70
88
  readonly MISSING_ARTIFACT: {
71
89
  readonly code: "RM1102";
72
90
  readonly klass: "input";
@@ -133,6 +151,33 @@ export declare const TOOL_ERRORS: {
133
151
  readonly problem: "the run has unresolved delegated work, so this phase cannot lock yet";
134
152
  readonly next: "call recursive_status to see the pending delegation, have the child write its reply.md, then lock again";
135
153
  };
154
+ readonly RUN_START_NO_CHANNEL: {
155
+ readonly code: "RM5502";
156
+ readonly klass: "runtime";
157
+ readonly problem: "the run-start gate needs an answer, and this composition mounts no user-questions channel to ask one directly";
158
+ readonly next: string;
159
+ };
160
+ /**
161
+ * ⚠ RM5503 USED TO LIE. Its text asserted "so no person was asked" for EVERY cause, because the caller
162
+ * that produced it had already thrown the cause away — and a live session showed the cost: the gate
163
+ * failed 22.9 s into the call with a reason nobody could see, and its `Next:` clause prescribed the very
164
+ * call that had just failed, so no route to start a run remained. The problem statement now claims only
165
+ * what the gate knows (no decision came back), and the cause travels in the `detail` the caller
166
+ * supplies. `RUN_START_ANSWER_UNUSABLE` carries the one case this entry must NOT cover: a person was
167
+ * reached and their answer was not an approval.
168
+ */
169
+ readonly RUN_START_UNANSWERED: {
170
+ readonly code: "RM5503";
171
+ readonly klass: "runtime";
172
+ readonly problem: "the run-start question reached no decision: the mounted user-questions channel failed before a person answered it";
173
+ readonly next: string;
174
+ };
175
+ readonly RUN_START_ANSWER_UNUSABLE: {
176
+ readonly code: "RM5504";
177
+ readonly klass: "runtime";
178
+ readonly problem: "a person was asked to start this run and their answer was not one of the labels the run-start gate offered";
179
+ readonly next: "call recursive_ask again and have the person choose exactly \"Start run\" or \"Hold\"; a skipped or custom answer is not an approval, and no relayed answer can replace a decision the person made";
180
+ };
136
181
  readonly RUNTIME_REFUSED: {
137
182
  readonly code: "RM5501";
138
183
  readonly klass: "runtime";
@@ -81,12 +81,42 @@ export declare function isRunGoal(goal: GoalViewLike | undefined, runId: string)
81
81
  * Sync a run's durable goal to the requested phase. Safe: never touches a goal
82
82
  * whose objective is not this run's marker, and never re-creates over a
83
83
  * non-complete foreign goal.
84
+ *
85
+ * ⚠ `approved` IS THE PHASE-0 GATE, and it defaults to the SAFE direction. A goal is not a label:
86
+ * `create` returns an ARMED view and the harness starts driving autonomous goal rounds for the
87
+ * session, so creating one is starting the run. The owner's rule is that phase 0 requires explicit
88
+ * approval, which means the projection must be unable to arm anything on its own — hence a default of
89
+ * `false` and an explicit refusal in EVERY branch that would call `create`, including the two
90
+ * replace-a-completed-goal branches (an unapproved run cannot have reached `complete`, but "cannot
91
+ * happen" is what the single unguarded branch relied on too).
92
+ *
93
+ * ⚠ AND IT IS REACHED ON ORDINARY WORK, so the unapproved path is QUIET AND IDEMPOTENT: no goal is
94
+ * created, nothing is written, no error is thrown, and the run's artifacts are untouched. The caller
95
+ * reads {@link RUN_START_NOT_APPROVED} to tell "this run has not been started yet" apart from a real
96
+ * failure, so a normal phase step never surfaces a warning.
97
+ *
98
+ * `approved` is passed IN rather than read here because this module is pure: it takes the goal service
99
+ * seam and nothing else, and the plugin's own filesystem reads live in the runtime (see
100
+ * `RecursiveRuntime.readRunStartApproval`).
101
+ */
102
+ export declare function syncRunGoal(service: GoalServiceLike | undefined | null, agent: AgentHandle, runId: string, runState: RunState, approved?: boolean): SyncResult;
103
+ /**
104
+ * Block the current run goal (used on a gate-block). Never touches a foreign goal.
105
+ *
106
+ * ⚠ A RUN THAT WAS NEVER STARTED HAS NO GOAL TO BLOCK, so this reports the unapproved state in the
107
+ * same words as {@link syncRunGoal} rather than "no current goal to block": the caller's question is
108
+ * "why is there no goal", and the answer must not depend on which entry point happened to ask.
84
109
  */
85
- export declare function syncRunGoal(service: GoalServiceLike | undefined | null, agent: AgentHandle, runId: string, runState: RunState): SyncResult;
86
- /** Block the current run goal (used on a gate-block). Never touches a foreign goal. */
87
110
  export declare function blockRunGoal(service: GoalServiceLike | undefined | null, agent: AgentHandle, runId: string, reason: {
88
111
  code: string;
89
112
  message: string;
90
113
  }): SyncResult;
91
- /** Bridge a run's blocked goal back to active (used on a reopen). */
92
- export declare function resumeRunGoal(service: GoalServiceLike | undefined | null, agent: AgentHandle, runId: string): SyncResult;
114
+ /**
115
+ * Bridge a run's blocked goal back to active (used on a reopen).
116
+ *
117
+ * ⚠ REOPEN IS NOT A BACK DOOR TO STARTING A RUN. It routes through {@link syncRunGoal}, so a reopen of
118
+ * an unapproved run cannot create the goal that init deliberately withheld. An APPROVED run is
119
+ * unaffected: its approval outlives the reopen, because the approval is a durable line in the run's
120
+ * own Phase 0 artifact rather than a value held in memory (verified in `tests/run-start-approval.spec.ts`).
121
+ */
122
+ export declare function resumeRunGoal(service: GoalServiceLike | undefined | null, agent: AgentHandle, runId: string, approved?: boolean): SyncResult;