@try-works/dsh-recursive-mode 0.4.6 → 0.4.8
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 +11 -4
- package/lib/client/contract.d.ts +10 -0
- package/lib/client/doc-viewer.d.ts +50 -0
- package/lib/client/spec-sheet-view.d.ts +237 -0
- package/lib/client/spec-sheet.d.ts +103 -0
- package/lib/client/use-live.d.ts +17 -1
- package/lib/client.js +988 -14
- package/lib/delegation.d.ts +35 -1
- package/lib/errors.d.ts +13 -0
- package/lib/index.js +164 -9
- package/lib/run-spec.d.ts +78 -0
- package/lib/run-start.d.ts +27 -0
- package/package.json +1 -1
- package/src/client/contract.ts +10 -0
- package/src/client/doc-viewer.tsx +131 -12
- package/src/client/slots.ts +12 -0
- package/src/client/spec-sheet-view.ts +325 -0
- package/src/client/spec-sheet.tsx +409 -0
- package/src/client/styles.ts +298 -1
- package/src/client/use-live.ts +23 -2
- package/src/delegation.ts +58 -4
- package/src/errors.ts +13 -0
- package/src/recursive_ask.tool.ts +28 -1
- package/src/run-spec.ts +148 -0
- package/src/run-start.ts +59 -0
- package/src/runtime.ts +78 -24
package/src/run-start.ts
CHANGED
|
@@ -42,6 +42,8 @@
|
|
|
42
42
|
import { readFileSync } from 'node:fs'
|
|
43
43
|
import { join } from 'node:path'
|
|
44
44
|
import { getMdFieldValue } from './status.ts'
|
|
45
|
+
import { classifyArtifact, describeEvidence, unfilledEvidence } from './run-spec.ts'
|
|
46
|
+
import { toolError } from './errors.ts'
|
|
45
47
|
|
|
46
48
|
/** The gate id `recursive_ask` answers for a run start. Deliberately NOT in ASK_GATE_IDS. */
|
|
47
49
|
export const RUN_START_GATE_ID = 'run-start'
|
|
@@ -127,3 +129,60 @@ export function readRunStartApproval(root: string, runId: string): { approved: b
|
|
|
127
129
|
* the same string instead of re-typing it (a re-typed reason is a caller that silently stops matching).
|
|
128
130
|
*/
|
|
129
131
|
export const RUN_START_NOT_APPROVED = 'run not started: phase 0 approval has not been granted'
|
|
132
|
+
|
|
133
|
+
/* ============================ THE ORDERING GUARD ============================ */
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* PHASE 0 — THE GATE CANNOT BE RAISED BEFORE THERE IS A SPEC TO DECIDE ABOUT.
|
|
137
|
+
*
|
|
138
|
+
* THE DEFECT. `recursive_init` scaffolds Phase 0 as a TEMPLATE, and `recursive_ask gate=run-start` raised
|
|
139
|
+
* "start this run or hold?" over it immediately — while every requirement was still `<short title>`, every
|
|
140
|
+
* acceptance criterion was still `[observable condition 1]`, and nothing put the document in front of the
|
|
141
|
+
* person at all. The owner: *"the card ui for accepting the spec appeared, but i was never shown the spec
|
|
142
|
+
* before that so how could i approve if i havent seen it"*. Approving an unfilled template is not a decision
|
|
143
|
+
* about a spec; there is no spec yet, and a card that asks the question anyway teaches a person to answer
|
|
144
|
+
* without reading.
|
|
145
|
+
*
|
|
146
|
+
* ⚠ WHAT THIS DOES *NOT* TOUCH. It does not weaken the gate's own contract, it does not add a second way to
|
|
147
|
+
* start a run, and it does not make the plugin the decider: it only refuses to ASK. A person's own answer
|
|
148
|
+
* still wins (`recordRunStartAnswer` is unchanged), a spec still creates no goal, and cancellation / abort /
|
|
149
|
+
* timeout are still unrelayable. The check runs BEFORE the question is put to anybody, so no card is shown
|
|
150
|
+
* for a document that cannot be approved meaningfully.
|
|
151
|
+
*
|
|
152
|
+
* ⚠ AND IT IS A CHECK ON THE DOCUMENT, NOT ON THE CALLER. A `runId` that does not resolve is not this
|
|
153
|
+
* refusal's business — the ask path already reports that — so the guard says `ok: true` there and lets the
|
|
154
|
+
* existing route handle it.
|
|
155
|
+
*/
|
|
156
|
+
export function runStartSpecGuard(root: string, runId: string): { ok: true } | { ok: false; reason: string } {
|
|
157
|
+
const content = readRunStartArtifact(root, runId)
|
|
158
|
+
if (content === null) {
|
|
159
|
+
// ⚠ A MISSING ARTIFACT IS REFUSED TOO, AND IT IS THE SAME DEFECT. Approving a run that has no Phase 0
|
|
160
|
+
// document is approving something nobody can read — the gate's own question ("approve phase 0 and start
|
|
161
|
+
// this run?") has no referent. The refusal names the path a reader can go and look at.
|
|
162
|
+
return {
|
|
163
|
+
ok: false,
|
|
164
|
+
reason: toolError(
|
|
165
|
+
'RUN_START_SPEC_UNFILLED',
|
|
166
|
+
'there is no Phase 0 document to approve: ' + runStartArtifactPath(root, runId) + ' does not exist yet',
|
|
167
|
+
),
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const verdict = classifyArtifact(content)
|
|
171
|
+
if (verdict.verdict === 'filled') return { ok: true }
|
|
172
|
+
const evidence = unfilledEvidence(verdict)
|
|
173
|
+
const context = verdict.hits.filter((hit) => hit.id !== 'placeholder')
|
|
174
|
+
const contextNote = context.length === 0
|
|
175
|
+
? ''
|
|
176
|
+
: ' (it also carries ' + String(context.length) + ' unfinished marker(s) of its own, starting at line '
|
|
177
|
+
+ String(context[0]?.line ?? 0) + ')'
|
|
178
|
+
// The evidence — line numbers and the placeholder text VERBATIM — travels in the detail: a refusal that
|
|
179
|
+
// asserts "it is a template" without quoting it is a refusal the caller can only take on trust.
|
|
180
|
+
return {
|
|
181
|
+
ok: false,
|
|
182
|
+
reason: toolError(
|
|
183
|
+
'RUN_START_SPEC_UNFILLED',
|
|
184
|
+
RUN_START_ARTIFACT + ' for run ' + JSON.stringify(runId) + ' still carries the template scaffold'
|
|
185
|
+
+ contextNote + ': ' + describeEvidence(evidence),
|
|
186
|
+
),
|
|
187
|
+
}
|
|
188
|
+
}
|
package/src/runtime.ts
CHANGED
|
@@ -1099,7 +1099,16 @@ export class RecursiveRuntime extends Service {
|
|
|
1099
1099
|
result = continuable.rounds[continuable.rounds.length - 1].result ?? null
|
|
1100
1100
|
if (!result) error = 'continuable child produced no final result'
|
|
1101
1101
|
} else {
|
|
1102
|
-
|
|
1102
|
+
// ⚠ A ROUND THAT SETTLED IS A RESULT, EVEN WHEN IT WAS NOT ACCEPTED — and this branch used to
|
|
1103
|
+
// discard it. The condition above requires `continuable.ok`, so a child that REPORTED and was
|
|
1104
|
+
// refused (`success: false`, or a non-completed stop reason) fell through to here: `result` stayed
|
|
1105
|
+
// null, the action record said "NO SETTLEMENT arrived within the wait", and the child's own stop
|
|
1106
|
+
// reason — the one fact that explains the refusal — was dropped on the floor. It is the same defect
|
|
1107
|
+
// as the parked one, one branch over: an absence asserted where the code had evidence. Keeping the
|
|
1108
|
+
// result is what lets the record say "the delegation returned without acceptance; stop reason error"
|
|
1109
|
+
// instead of blaming a wait that ended perfectly well.
|
|
1110
|
+
result = continuable.rounds[continuable.rounds.length - 1]?.result ?? null
|
|
1111
|
+
if (result === null) error = continuable.reason ?? 'continuable delegation failed'
|
|
1103
1112
|
}
|
|
1104
1113
|
} else {
|
|
1105
1114
|
try {
|
|
@@ -1147,7 +1156,14 @@ export class RecursiveRuntime extends Service {
|
|
|
1147
1156
|
id: operation,
|
|
1148
1157
|
act: 'delegate-review',
|
|
1149
1158
|
at: new Date().toISOString().replace(/\.\d{3}Z$/, 'Z'),
|
|
1150
|
-
|
|
1159
|
+
// ⚠ A PARK IS NOT A REFUSAL, and `unaccepted` said it was. This line used to be
|
|
1160
|
+
// `accepted ? 'accepted' : 'unaccepted'`, so a round that had merely not settled yet was indexed
|
|
1161
|
+
// exactly like a delegation that was evaluated and refused — while the round it really was (still in
|
|
1162
|
+
// flight, resume the same child) was nowhere in the run's own operation log. The defect the action
|
|
1163
|
+
// record had, the log had too. The new value is honest on both readings that matter: it is not
|
|
1164
|
+
// `accepted`, so every retry gate still treats the operation as unfinished and retryable — which is
|
|
1165
|
+
// what a parked round is — and it no longer claims the delegation was judged and rejected.
|
|
1166
|
+
outcome: parked ? 'parked' : (evaluation.accepted ? 'accepted' : 'unaccepted'),
|
|
1151
1167
|
phase: input.phase,
|
|
1152
1168
|
})
|
|
1153
1169
|
}
|
|
@@ -1158,8 +1174,8 @@ export class RecursiveRuntime extends Service {
|
|
|
1158
1174
|
runId: input.runId,
|
|
1159
1175
|
subagentId: input.childId,
|
|
1160
1176
|
phase: input.phase,
|
|
1161
|
-
// ⚠ FU-17 — the kind is stated in the record. `Status` says accepted or
|
|
1162
|
-
// child PRODUCED the phase's work or JUDGED it, and a reader of a run could not tell the two apart.
|
|
1177
|
+
// ⚠ FU-17 — the kind is stated in the record. `Status` says accepted, failed or parked; nothing said whether
|
|
1178
|
+
// the child PRODUCED the phase's work or JUDGED it, and a reader of a run could not tell the two apart.
|
|
1163
1179
|
purpose: input.role + (input.kind === 'work' ? ' (work)' : '') + ' for run ' + input.runId,
|
|
1164
1180
|
executionMode: decision.tier + (input.mode !== 'one-shot' ? ' (continuable)' : ''),
|
|
1165
1181
|
artifactPath: input.artifactPath,
|
|
@@ -1171,32 +1187,70 @@ export class RecursiveRuntime extends Service {
|
|
|
1171
1187
|
findings: evaluation.accepted && result?.structured ? [(result.structured as { verdict?: string })?.verdict ?? 'accepted'] : undefined,
|
|
1172
1188
|
success: evaluation.accepted,
|
|
1173
1189
|
stopReason: result?.stopReason,
|
|
1190
|
+
// ⚠ A PARKED ROUND IS RECORDED AS PARKED — the whole defect in one field. `success: evaluation.accepted`
|
|
1191
|
+
// is false for a park (correct: nothing was accepted), and `writeActionRecord` reads this flag to state
|
|
1192
|
+
// the third state instead of collapsing it into `failed`.
|
|
1193
|
+
...(parked ? { parked: true } : {}),
|
|
1174
1194
|
// ⚠ FU-9 — AND SAY WHICH KIND OF FAILURE, because the record previously could not. `result == null` means
|
|
1175
1195
|
// the provider never produced anything at all (never started, or returned nothing) — which is what the
|
|
1176
1196
|
// live record's `Stop Reason: n/a` was quietly telling me — while a present result that failed to be
|
|
1177
1197
|
// accepted means a child DID run and its work was refused. Different problems, identical artifacts.
|
|
1198
|
+
//
|
|
1199
|
+
// ⚠ AND `parked` IS BRANCHED FIRST. A parked round produced NO result at all — that IS what parking
|
|
1200
|
+
// means — so without this branch first it fell into the `result == null` text below: "the continuable
|
|
1201
|
+
// start was made and NO SETTLEMENT arrived within the wait (the child never reported, or never ran)".
|
|
1202
|
+
// That is the exact false conclusion this fix exists for, and it was reached whatever the record's
|
|
1203
|
+
// Status said. Order is therefore load-bearing here.
|
|
1178
1204
|
failure: evaluation.accepted
|
|
1179
1205
|
? undefined
|
|
1180
|
-
:
|
|
1181
|
-
// ⚠
|
|
1182
|
-
//
|
|
1183
|
-
//
|
|
1184
|
-
//
|
|
1185
|
-
//
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1206
|
+
: parked
|
|
1207
|
+
// ⚠ WHAT IS KNOWN, AND ONLY WHAT IS KNOWN, WITH THE ID THE READER NEEDS TO ACT.
|
|
1208
|
+
//
|
|
1209
|
+
// The text this replaces said "the child never reported, or never ran" — a CONCLUSION drawn from an
|
|
1210
|
+
// ABSENCE, and it was false: a live child went on to complete three review rounds and reply eighteen
|
|
1211
|
+
// minutes later, while the main agent read `Status: failed`, concluded the child was dead, and
|
|
1212
|
+
// obtained its review by other means. So the parked message asserts nothing about the child's state
|
|
1213
|
+
// beyond "no settlement had landed when the wait ended", keeps "may still be working" as the
|
|
1214
|
+
// possibility it is, and NAMES the childId plus the exact next step, because advice to resume is
|
|
1215
|
+
// unactionable without the id. The identity diagnostics stay, because they are what makes a
|
|
1216
|
+
// misconfigured provider readable — but they are diagnostics, not the reason.
|
|
1217
|
+
? 'no settlement had landed when the wait ended, so this round is PARKED, not failed: nothing was'
|
|
1218
|
+
+ ' accepted and nothing was refused, and the child may still be working. The next step is to RESUME'
|
|
1219
|
+
+ ' this round, not to re-dispatch it or replace the child: call `recursive_review` again on a later'
|
|
1220
|
+
+ ' turn with childId ' + String(continuable?.childId ?? input.childId) + ' (the child the round was'
|
|
1221
|
+
+ ' started for, which stays resumable). Diagnostics: tier ' + decision.tier
|
|
1222
|
+
+ ', provider ' + (decision.provider ?? 'none chosen')
|
|
1223
|
+
+ ', names on offer [' + (this.lastProviderNames.join(', ') || 'none') + ']'
|
|
1224
|
+
// ⚠ FU-9 — THE PARENT IDENTITY, because the host refuses a prompt when it cannot resolve the
|
|
1225
|
+
// parent session as a live Agent (`subagent/parent-unavailable`, index.ts L429-436), and the tool
|
|
1226
|
+
// builds this handle with a CAST (`exec.agent as unknown as SubagentParentHandle`). A cast is not
|
|
1227
|
+
// a contract: if the id here is not the one the host looks up, the refusal is real and the
|
|
1228
|
+
// classifier's crash has been hiding it. Printing it here costs nothing and settles the question.
|
|
1229
|
+
+ '; parent id ' + ((input.parent as { id?: string } | undefined)?.id ?? 'none')
|
|
1230
|
+
+ ', parent session keys [' + (input.parent === undefined ? 'no parent' : Object.keys(input.parent as object).join(', ')) + ']'
|
|
1231
|
+
: result == null
|
|
1232
|
+
// ⚠ THE TWO STATES ARE NOT THE SAME AND THE MESSAGE USED TO CONFLATE THEM. The one-shot path cannot
|
|
1233
|
+
// resolve to nothing — the host's `start` returns a run or throws (assertCapabilities, expectProvider)
|
|
1234
|
+
// — so a null result on the CONTINUABLE path means the opposite of what I first wrote: the start WAS
|
|
1235
|
+
// made and NO SETTLEMENT ARRIVED within the wait. That distinction cost me two rounds of looking at
|
|
1236
|
+
// provider names, so the record now states which path a run took and what it was waiting for.
|
|
1237
|
+
// (A park is handled above and never reaches this branch; this one is a continuable round that
|
|
1238
|
+
// produced neither a result nor the park signal, which IS a failure to report.)
|
|
1239
|
+
? (input.mode !== 'one-shot'
|
|
1240
|
+
? 'the continuable start was made and NO SETTLEMENT arrived within the wait (the child never reported,'
|
|
1241
|
+
+ ' or never ran); tier ' + decision.tier + ', provider ' + (decision.provider ?? 'none chosen')
|
|
1242
|
+
+ ', names on offer [' + (this.lastProviderNames.join(', ') || 'none') + ']'
|
|
1243
|
+
// ⚠ FU-9 — THE PARENT IDENTITY, because the host refuses a prompt when it cannot resolve the
|
|
1244
|
+
// parent session as a live Agent (`subagent/parent-unavailable`, index.ts L429-436), and the tool
|
|
1245
|
+
// builds this handle with a CAST (`exec.agent as unknown as SubagentParentHandle`). A cast is not
|
|
1246
|
+
// a contract: if the id here is not the one the host looks up, the refusal is real and the
|
|
1247
|
+
// classifier's crash has been hiding it. Printing it here costs nothing and settles the question.
|
|
1248
|
+
+ '; parent id ' + ((input.parent as { id?: string } | undefined)?.id ?? 'none')
|
|
1249
|
+
+ ', parent session keys [' + (input.parent === undefined ? 'no parent' : Object.keys(input.parent as object).join(', ')) + ']'
|
|
1250
|
+
: 'no delegate result was produced by the one-shot path; tier ' + decision.tier
|
|
1251
|
+
+ ', provider ' + (decision.provider ?? 'none chosen'))
|
|
1252
|
+
: 'the delegation returned without acceptance; stop reason ' + (result.stopReason ?? 'none reported')
|
|
1253
|
+
+ (result.success === false ? ' (the child itself reported success:false)' : ''),
|
|
1200
1254
|
})
|
|
1201
1255
|
|
|
1202
1256
|
// T35: report the mode that ACTUALLY ran, not the one that was asked for. A
|