@mjasnikovs/pi-task 0.38.16 → 0.38.18
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/dist/config/config.d.ts +37 -0
- package/dist/config/config.js +81 -18
- package/dist/task/accept-debt.js +2 -1
- package/dist/task/artifact-closure.js +18 -63
- package/dist/task/auto-orchestrator.js +205 -214
- package/dist/task/boot-probe.d.ts +46 -0
- package/dist/task/boot-probe.js +41 -21
- package/dist/task/coverage-loop.d.ts +11 -0
- package/dist/task/coverage-loop.js +16 -0
- package/dist/task/final-gate-fix.js +14 -24
- package/dist/task/final-gate.js +6 -1
- package/dist/task/fix-child.d.ts +64 -0
- package/dist/task/fix-child.js +66 -0
- package/dist/task/lint-fix.d.ts +7 -0
- package/dist/task/lint-fix.js +45 -9
- package/dist/task/orchestrator.js +9 -2
- package/dist/task/phases.d.ts +66 -4
- package/dist/task/phases.js +94 -34
- package/dist/task/plan-rounds.d.ts +86 -0
- package/dist/task/plan-rounds.js +105 -0
- package/dist/task/plan-session.d.ts +31 -21
- package/dist/task/plan-session.js +97 -120
- package/dist/task/qa-transcript.d.ts +100 -0
- package/dist/task/qa-transcript.js +99 -0
- package/dist/task/question-source.d.ts +117 -0
- package/dist/task/question-source.js +174 -0
- package/dist/task/serve-entry.js +6 -57
- package/dist/task/shipped-source.d.ts +67 -0
- package/dist/task/shipped-source.js +144 -0
- package/dist/task/task-gates.d.ts +1 -1
- package/dist/task/task-gates.js +4 -2
- package/dist/task/verify-work.d.ts +46 -0
- package/dist/task/verify-work.js +51 -3
- package/dist/task/widget.js +41 -9
- package/dist/workers/docs-core.d.ts +71 -1
- package/dist/workers/docs-core.js +131 -71
- package/dist/workers/pi-worker-core.js +23 -8
- package/package.json +1 -1
|
@@ -29,9 +29,8 @@
|
|
|
29
29
|
* persistence) arrives through {@link PlanSessionDeps}, so the whole interaction
|
|
30
30
|
* is unit-testable without a TUI or a model.
|
|
31
31
|
*/
|
|
32
|
-
import {
|
|
32
|
+
import { makeQuestionSource } from './question-source.js';
|
|
33
33
|
import { stripInlineMarkdown } from './inline-markdown.js';
|
|
34
|
-
import { isDuplicateQuestion, DUP_REPROMPT_HINT, MAX_DUP_STRIKES } from './question-dedup.js';
|
|
35
34
|
import { yoloPickAnswer } from './yolo.js';
|
|
36
35
|
import { formatPlanTranscript } from './plan-io.js';
|
|
37
36
|
import { buildOptionCards, resolveAnswer } from './question-dialog.js';
|
|
@@ -73,29 +72,13 @@ export const PLAN_FORMAT_HINT = '[SYSTEM NOTE: Your previous reply did NOT follo
|
|
|
73
72
|
+ 'REQUIRED and must never be blank. Add an "ALT: " line only for a binary A-or-B fork. '
|
|
74
73
|
+ 'If nothing is left to ask, output the single token NONE and nothing else. No preamble, '
|
|
75
74
|
+ 'no analysis, no other text.]';
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
*
|
|
84
|
-
* parseClarifyList turns EVERY numbered line into an entry, and the local model
|
|
85
|
-
* sometimes writes a numbered analysis note or two before the question it was
|
|
86
|
-
* asked for (measured live: the first numbered line was a note like
|
|
87
|
-
* "1. gateDebugWriter in orchestrator.ts — wraps a raw append function"). Taking
|
|
88
|
-
* entry 0 blindly then shows the note as the question and loses the SUGGESTED
|
|
89
|
-
* line that was attached further down.
|
|
90
|
-
*
|
|
91
|
-
* The SUGGESTED line is the reliable marker of the real question — the prompt
|
|
92
|
-
* requires exactly one, and parseClarifyList attaches it to the entry it follows.
|
|
93
|
-
* So: prefer the first entry that has one; fall back to the first entry when none
|
|
94
|
-
* does, which is the case the format re-prompt then covers.
|
|
95
|
-
*/
|
|
96
|
-
export function pickQuestion(parsed) {
|
|
97
|
-
return parsed.find(q => q.suggested !== undefined && q.suggested.length > 0) ?? parsed[0];
|
|
98
|
-
}
|
|
75
|
+
// Re-exported, NOT re-implemented. After the state machine moved to
|
|
76
|
+
// question-source.ts these were byte-identical copies: production read THAT
|
|
77
|
+
// module's, while plan-session.test.ts and four `scripts/live-*.ts` A/B harnesses
|
|
78
|
+
// read these — so a fix to `pickQuestion`'s heuristic would land in one copy while
|
|
79
|
+
// the harnesses kept measuring the other, and the measurement would silently stop
|
|
80
|
+
// describing shipped behaviour. That is the drift class this pass removes.
|
|
81
|
+
export { isNoneReply, pickQuestion } from './question-source.js';
|
|
99
82
|
/**
|
|
100
83
|
* Does the question offer the user a choice between two named alternatives?
|
|
101
84
|
* Deliberately shallow — an "X or Y?" in the question's own clause.
|
|
@@ -171,6 +154,75 @@ export function planForkHint(question) {
|
|
|
171
154
|
+ 'time emit BOTH lines:\nSUGGESTED: <the option you recommend>\nALT: <the other option>\n'
|
|
172
155
|
+ 'Nothing else.]');
|
|
173
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* A default that DEFERS decides nothing, and an accepted deferral reaches the
|
|
159
|
+
* consumer dressed as an authoritative decision.
|
|
160
|
+
*
|
|
161
|
+
* Declared as its own constant because it is the one rule BOTH dialogs use, and
|
|
162
|
+
* `CLARIFY_QUALITY_RULES` referencing it by `id` string would be the retyped
|
|
163
|
+
* literal with no compile link that this pass exists to remove — a rename would
|
|
164
|
+
* silently yield `[undefined]` and throw on the first clarify question of every
|
|
165
|
+
* run.
|
|
166
|
+
*/
|
|
167
|
+
const DEFERRAL_RULE = {
|
|
168
|
+
id: 'SUGGESTED deferred the decision',
|
|
169
|
+
detect: (q, plain) => q.suggested !== undefined && isDeferralSuggestion(q.suggested) ?
|
|
170
|
+
planDecisiveHint(plain, q.suggested)
|
|
171
|
+
: null,
|
|
172
|
+
// When only the recommendation defers, the ALT is still a real commitment:
|
|
173
|
+
// promote it so the question keeps a usable default. With no ALT the option is
|
|
174
|
+
// DROPPED rather than shown, so an empty submit records an unanswered question
|
|
175
|
+
// instead of a decision the user never made.
|
|
176
|
+
repair: q => {
|
|
177
|
+
const { alt: _alt, suggested: _suggested, ...rest } = q;
|
|
178
|
+
return q.alt !== undefined ? { ...rest, suggested: q.alt } : { ...rest };
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
/**
|
|
182
|
+
* PLAN's quality rules, in order.
|
|
183
|
+
*
|
|
184
|
+
* Each is worth exactly one corrective re-prompt (the child is stateless, so each
|
|
185
|
+
* hint quotes the question back), and each DEGRADES rather than discards when the
|
|
186
|
+
* defect survives — a question with a weak default still beats no question.
|
|
187
|
+
*
|
|
188
|
+
* Only {@link CLARIFY_QUALITY_RULES} is shared with `/task-auto`, and only the
|
|
189
|
+
* deferral rule is in it. The other two were MEASURED here (10/15 fork-shaped
|
|
190
|
+
* questions shipped one option; the SUGGESTED requirement is in both prompts) but
|
|
191
|
+
* each costs one extra child call every time it fires, and clarify is the most
|
|
192
|
+
* A/B'd path in the codebase — moving them there is its own experiment, not a
|
|
193
|
+
* side effect of sharing a state machine. Recorded rather than done.
|
|
194
|
+
*/
|
|
195
|
+
export const PLAN_QUALITY_RULES = [
|
|
196
|
+
{
|
|
197
|
+
// A question with no SUGGESTED leaves the picker with nothing to
|
|
198
|
+
// recommend, which is the one thing the prompt says must never happen.
|
|
199
|
+
id: 'no SUGGESTED',
|
|
200
|
+
detect: q => (q.suggested === undefined ? PLAN_FORMAT_HINT : null)
|
|
201
|
+
},
|
|
202
|
+
DEFERRAL_RULE,
|
|
203
|
+
{
|
|
204
|
+
// A fork-shaped question that ships one option leaves the user typing out
|
|
205
|
+
// the alternative the model itself just named (10/15 measured live).
|
|
206
|
+
id: 'fork-shaped question with no ALT',
|
|
207
|
+
detect: (q, plain) => q.alt === undefined && q.suggested !== undefined && looksLikeFork(plain) ?
|
|
208
|
+
planForkHint(plain)
|
|
209
|
+
: null
|
|
210
|
+
}
|
|
211
|
+
];
|
|
212
|
+
/**
|
|
213
|
+
* The deferral rule alone — the one clarify shares.
|
|
214
|
+
*
|
|
215
|
+
* It exists because an accepted "clarify with the user before proceeding" rode
|
|
216
|
+
* into `/task`'s handoff AS AN AUTHORITATIVE DECISION and produced a task whose
|
|
217
|
+
* ACCEPTANCE was "a planning document with placeholder sections" and whose VERIFY
|
|
218
|
+
* asserted that no source file had changed. Clarify's answers ride into the
|
|
219
|
+
* decompose prompt and the AUTO file with exactly the same authority and had no
|
|
220
|
+
* guard at all — the same bug, one command over, waiting.
|
|
221
|
+
*
|
|
222
|
+
* It is also the only one of the three that costs nothing on the happy path: a
|
|
223
|
+
* decisive default never triggers it.
|
|
224
|
+
*/
|
|
225
|
+
export const CLARIFY_QUALITY_RULES = [DEFERRAL_RULE];
|
|
174
226
|
// ─── Pending question ────────────────────────────────────────────────────────
|
|
175
227
|
/**
|
|
176
228
|
* Build the picker for a pending model question: the recommendation first (index
|
|
@@ -237,12 +289,7 @@ export const ASK_TITLE = 'Ask the model';
|
|
|
237
289
|
export const ASK_QUESTION = 'What do you want to ask about this task? The answer is recorded as a note; it does not decide anything by itself.';
|
|
238
290
|
export async function runPlanSession(deps) {
|
|
239
291
|
const entries = [];
|
|
240
|
-
const asked = [];
|
|
241
292
|
const render = (s) => deps.renderMarkdown?.(s) ?? s;
|
|
242
|
-
let dupStrikes = 0;
|
|
243
|
-
let dupHint = null;
|
|
244
|
-
/** Set for exactly one corrective re-prompt after a malformed reply. */
|
|
245
|
-
let formatHint = null;
|
|
246
293
|
/** The model has nothing (more) to ask: NONE, the cap, or the dup backstop. */
|
|
247
294
|
let exhausted = false;
|
|
248
295
|
let pending = null;
|
|
@@ -250,103 +297,33 @@ export async function runPlanSession(deps) {
|
|
|
250
297
|
entries.push(entry);
|
|
251
298
|
await deps.onEntries?.(entries);
|
|
252
299
|
};
|
|
300
|
+
const questions = makeQuestionSource({
|
|
301
|
+
generate: hint => deps.generateQuestion(formatPlanTranscript(entries), hint),
|
|
302
|
+
formatHint: PLAN_FORMAT_HINT,
|
|
303
|
+
rules: PLAN_QUALITY_RULES,
|
|
304
|
+
cap: MAX_PLAN_QUESTIONS,
|
|
305
|
+
log: msg => deps.logDebug?.(`plan: ${msg}`)
|
|
306
|
+
});
|
|
253
307
|
for (;;) {
|
|
254
308
|
if (pending === null && !exhausted) {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
exhausted = true;
|
|
258
|
-
continue;
|
|
259
|
-
}
|
|
260
|
-
deps.setStatus?.(`thinking of question ${asked.length + 1}…`);
|
|
261
|
-
const raw = await deps.generateQuestion(formatPlanTranscript(entries), formatHint ?? dupHint);
|
|
309
|
+
deps.setStatus?.(`thinking of question ${questions.asked().length + 1}…`);
|
|
310
|
+
const drawn = await questions.next();
|
|
262
311
|
deps.setStatus?.(undefined);
|
|
263
|
-
|
|
264
|
-
// parseClarifyList returns [] both for a deliberate NONE and for
|
|
265
|
-
// output it could not parse at all — treating the second as "no
|
|
266
|
-
// questions left" would end the planning session on a formatting
|
|
267
|
-
// slip. Tell them apart, and give a malformed reply exactly one
|
|
268
|
-
// corrective re-prompt (the same one-shot recovery the grill
|
|
269
|
-
// auto-answer does with GRILL_AUTO_FORMAT_HINT).
|
|
270
|
-
if (parsed.length === 0) {
|
|
271
|
-
if (!isNoneReply(raw) && formatHint === null) {
|
|
272
|
-
deps.logDebug?.('plan: unparseable question reply — one format re-prompt');
|
|
273
|
-
formatHint = PLAN_FORMAT_HINT;
|
|
274
|
-
continue;
|
|
275
|
-
}
|
|
276
|
-
deps.logDebug?.('plan: model has no further questions (NONE)');
|
|
277
|
-
formatHint = null;
|
|
312
|
+
if (drawn.kind === 'exhausted') {
|
|
278
313
|
exhausted = true;
|
|
279
314
|
continue;
|
|
280
315
|
}
|
|
281
|
-
const
|
|
282
|
-
const { question, suggested, alt } = picked;
|
|
283
|
-
const plain = stripInlineMarkdown(question);
|
|
284
|
-
// The duplicate backstop runs BEFORE either quality re-prompt: a
|
|
285
|
-
// question that is about to be discarded as a re-ask must not first
|
|
286
|
-
// buy itself an extra child call to be polished.
|
|
287
|
-
if (isDuplicateQuestion(asked, plain)) {
|
|
288
|
-
dupStrikes++;
|
|
289
|
-
deps.logDebug?.(`plan: duplicate question, strike ${dupStrikes}/${MAX_DUP_STRIKES}`);
|
|
290
|
-
formatHint = null;
|
|
291
|
-
if (dupStrikes >= MAX_DUP_STRIKES) {
|
|
292
|
-
exhausted = true;
|
|
293
|
-
continue;
|
|
294
|
-
}
|
|
295
|
-
dupHint = DUP_REPROMPT_HINT;
|
|
296
|
-
continue;
|
|
297
|
-
}
|
|
298
|
-
// A question with no SUGGESTED line leaves the picker with nothing to
|
|
299
|
-
// recommend, which is the one thing the prompt says must never happen.
|
|
300
|
-
// One-shot recovery; if it still comes back bare we show the question
|
|
301
|
-
// anyway — a question with no default beats no question.
|
|
302
|
-
if (suggested === undefined && formatHint === null) {
|
|
303
|
-
deps.logDebug?.('plan: question had no SUGGESTED — one format re-prompt');
|
|
304
|
-
formatHint = PLAN_FORMAT_HINT;
|
|
305
|
-
continue;
|
|
306
|
-
}
|
|
307
|
-
// A default that defers decides nothing, and an accepted deferral
|
|
308
|
-
// reaches /task dressed as an authoritative decision. One re-prompt to
|
|
309
|
-
// make it decisive; if it comes back deferring anyway the option is
|
|
310
|
-
// DROPPED rather than shown, so an empty submit records "(skipped)" —
|
|
311
|
-
// an unanswered question — instead of a decision the user never made.
|
|
312
|
-
const defers = suggested !== undefined && isDeferralSuggestion(suggested);
|
|
313
|
-
if (defers && formatHint === null) {
|
|
314
|
-
deps.logDebug?.('plan: SUGGESTED deferred the decision — one re-prompt');
|
|
315
|
-
formatHint = planDecisiveHint(plain, suggested);
|
|
316
|
-
continue;
|
|
317
|
-
}
|
|
318
|
-
// When only the recommendation defers, the ALT is still a real
|
|
319
|
-
// commitment: promote it so the question keeps a usable default.
|
|
320
|
-
const usableSuggested = defers ? alt : suggested;
|
|
321
|
-
const usableAlt = defers ? undefined : alt;
|
|
322
|
-
if (defers) {
|
|
323
|
-
deps.logDebug?.(usableSuggested === undefined ?
|
|
324
|
-
'plan: SUGGESTED still deferred — question shown with no recommendation'
|
|
325
|
-
: 'plan: SUGGESTED still deferred — promoted the ALT to the recommendation');
|
|
326
|
-
}
|
|
327
|
-
// A question that offers a choice but ships one option leaves the
|
|
328
|
-
// user typing out the alternative the model just named. Same one-shot
|
|
329
|
-
// budget, quoting the question back so the (stateless) child re-asks
|
|
330
|
-
// this one instead of a new one.
|
|
331
|
-
if (usableAlt === undefined && formatHint === null && !defers && looksLikeFork(plain)) {
|
|
332
|
-
deps.logDebug?.('plan: fork-shaped question with no ALT — one re-prompt');
|
|
333
|
-
formatHint = planForkHint(plain);
|
|
334
|
-
continue;
|
|
335
|
-
}
|
|
336
|
-
formatHint = null;
|
|
337
|
-
dupStrikes = 0;
|
|
338
|
-
dupHint = null;
|
|
339
|
-
asked.push(plain);
|
|
316
|
+
const { question, suggested, alt } = drawn.q;
|
|
340
317
|
pending = {
|
|
341
|
-
plain,
|
|
318
|
+
plain: drawn.plain,
|
|
342
319
|
shown: render(question),
|
|
343
|
-
...(
|
|
344
|
-
suggested: stripInlineMarkdown(
|
|
345
|
-
shownSuggested: render(
|
|
320
|
+
...(suggested !== undefined && {
|
|
321
|
+
suggested: stripInlineMarkdown(suggested),
|
|
322
|
+
shownSuggested: render(suggested)
|
|
346
323
|
}),
|
|
347
|
-
...(
|
|
348
|
-
alt: stripInlineMarkdown(
|
|
349
|
-
shownAlt: render(
|
|
324
|
+
...(alt !== undefined && {
|
|
325
|
+
alt: stripInlineMarkdown(alt),
|
|
326
|
+
shownAlt: render(alt)
|
|
350
327
|
})
|
|
351
328
|
};
|
|
352
329
|
}
|
|
@@ -394,7 +371,7 @@ export async function runPlanSession(deps) {
|
|
|
394
371
|
// had run out of questions may now have one. Re-open the generator.
|
|
395
372
|
if (exhausted) {
|
|
396
373
|
exhausted = false;
|
|
397
|
-
|
|
374
|
+
questions.reopen();
|
|
398
375
|
}
|
|
399
376
|
continue; // the pending question, if any, is still unanswered
|
|
400
377
|
}
|
|
@@ -408,7 +385,7 @@ export async function runPlanSession(deps) {
|
|
|
408
385
|
await commit({ kind: 'stated', text });
|
|
409
386
|
if (exhausted) {
|
|
410
387
|
exhausted = false;
|
|
411
|
-
|
|
388
|
+
questions.reopen();
|
|
412
389
|
}
|
|
413
390
|
continue;
|
|
414
391
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Q&A TRANSCRIPT of one adaptive dialog — grill (`/task`) or clarify
|
|
3
|
+
* (`/task-auto`) — and the one place its numbering, its formatting and its
|
|
4
|
+
* provenance policy live.
|
|
5
|
+
*
|
|
6
|
+
* `question-dialog.ts` unified the ANSWER side of these loops: the picker cards
|
|
7
|
+
* and the reply mapping. It did not unify what the loop RECORDS, and that was
|
|
8
|
+
* eight retyped push sites across two files, each choosing a suffix by hand
|
|
9
|
+
* according to which branch of an `if/else` it stood in.
|
|
10
|
+
*
|
|
11
|
+
* The cost is written into the code. `phases.ts` states the invariant in a
|
|
12
|
+
* comment — *"No provenance stamp here, unlike clarify's transcript: this string
|
|
13
|
+
* is fed back VERBATIM into the next grill-gen prompt, so a `(accepted
|
|
14
|
+
* recommendation)` suffix would become model input"* — and TWELVE LINES ABOVE it,
|
|
15
|
+
* the YOLO branch pushes `${answer} ${YOLO_STAMP}` into that very array. The rule
|
|
16
|
+
* was violated inside the loop body that declares it, because the rule lived in
|
|
17
|
+
* prose and the decision lived at each push.
|
|
18
|
+
*
|
|
19
|
+
* Here it is a property of a value: an entry states its KIND, and the policy says
|
|
20
|
+
* where that kind's provenance is allowed to appear.
|
|
21
|
+
*
|
|
22
|
+
* NOT unified here: `plan-session.ts`'s `PlanEntry` transcript. It is a different
|
|
23
|
+
* shape — decisions vs advisory notes, persisted to its own task file, with no
|
|
24
|
+
* `Qn:`/`An:` numbering — and folding it in would be a rename, not a deepening.
|
|
25
|
+
*/
|
|
26
|
+
/** How one answer was arrived at. */
|
|
27
|
+
export type QaKind =
|
|
28
|
+
/** grill: the research-backed auto-answer resolved it without asking. */
|
|
29
|
+
'auto'
|
|
30
|
+
/** clarify: answer-side triage found the spec already settles it. */
|
|
31
|
+
| 'auto-resolved'
|
|
32
|
+
/** clarify: the host answers this fork deterministically (plan shape). */
|
|
33
|
+
| 'host-set'
|
|
34
|
+
/** unattended: took the recommended option. */
|
|
35
|
+
| 'yolo'
|
|
36
|
+
/** unattended: nothing safe to take, so the fork is left open. */
|
|
37
|
+
| 'yolo-skip'
|
|
38
|
+
/** the human took the recommendation — green card, or an empty submit. */
|
|
39
|
+
| 'accepted'
|
|
40
|
+
/** the human wrote their own answer. */
|
|
41
|
+
| 'typed';
|
|
42
|
+
/**
|
|
43
|
+
* The provenance suffix each kind carries. A new kind is a compile error until it
|
|
44
|
+
* declares one; `typed` declares the empty string, which is a statement, not a
|
|
45
|
+
* gap — a human's own words are the baseline everything else is marked against.
|
|
46
|
+
*/
|
|
47
|
+
export declare const QA_PROVENANCE: Record<QaKind, string>;
|
|
48
|
+
export interface QaPolicy {
|
|
49
|
+
/** Kinds whose provenance appears in the RECORD — persisted, and handed on. */
|
|
50
|
+
record: ReadonlySet<QaKind>;
|
|
51
|
+
/**
|
|
52
|
+
* Does the text fed back to the QUESTION GENERATOR carry provenance?
|
|
53
|
+
*
|
|
54
|
+
* The two dialogs genuinely disagree, and both say so in their own comments.
|
|
55
|
+
* Grill's is fed VERBATIM into the next grill-gen prompt, so a suffix there
|
|
56
|
+
* becomes model input describing how the answer was obtained rather than what
|
|
57
|
+
* it was. Clarify deliberately shows its generator the provenance, so a
|
|
58
|
+
* question the triage already settled reads as settled and is not re-asked.
|
|
59
|
+
* An option, not a unification — it is observable either way.
|
|
60
|
+
*/
|
|
61
|
+
generatorSeesProvenance: boolean;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* GRILL: stamps `auto` and both YOLO kinds in the record, and shows the generator
|
|
65
|
+
* nothing.
|
|
66
|
+
*
|
|
67
|
+
* `accepted` is deliberately NOT in the record set. Grill's record reaches
|
|
68
|
+
* `COMPOSE_PROMPT` and `CRITIQUE_PROMPT`, where it is named GROUND TRUTH; clarify's
|
|
69
|
+
* reaches a decompose prompt. Whether those two should agree is a prompt question
|
|
70
|
+
* with its own A/B, so today's answer is preserved rather than harmonised here.
|
|
71
|
+
*/
|
|
72
|
+
export declare const GRILL_QA_POLICY: QaPolicy;
|
|
73
|
+
/** CLARIFY: stamps every non-typed kind, in the record and to the generator alike. */
|
|
74
|
+
export declare const CLARIFY_QA_POLICY: QaPolicy;
|
|
75
|
+
export interface QaEntry {
|
|
76
|
+
kind: QaKind;
|
|
77
|
+
question: string;
|
|
78
|
+
answer: string;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* One dialog's transcript. Owns the numbering and both renderings; performs no
|
|
82
|
+
* I/O and knows nothing about children, pickers or sessions — which is what makes
|
|
83
|
+
* it drivable by a test that only wants to know what a model would have been
|
|
84
|
+
* shown.
|
|
85
|
+
*/
|
|
86
|
+
export declare class QaTranscript {
|
|
87
|
+
private readonly _policy;
|
|
88
|
+
private readonly _entries;
|
|
89
|
+
constructor(_policy: QaPolicy);
|
|
90
|
+
/** How many questions have been answered. Also the next question's number − 1. */
|
|
91
|
+
get length(): number;
|
|
92
|
+
get entries(): ReadonlyArray<QaEntry>;
|
|
93
|
+
/** Record one answered question. The number is assigned here, not by the caller. */
|
|
94
|
+
add(kind: QaKind, question: string, answer: string): void;
|
|
95
|
+
/** The persisted / handed-on transcript, with provenance per the policy. */
|
|
96
|
+
forRecord(): string;
|
|
97
|
+
/** The text fed back into the next question-generation prompt. */
|
|
98
|
+
forGenerator(): string;
|
|
99
|
+
private _render;
|
|
100
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Q&A TRANSCRIPT of one adaptive dialog — grill (`/task`) or clarify
|
|
3
|
+
* (`/task-auto`) — and the one place its numbering, its formatting and its
|
|
4
|
+
* provenance policy live.
|
|
5
|
+
*
|
|
6
|
+
* `question-dialog.ts` unified the ANSWER side of these loops: the picker cards
|
|
7
|
+
* and the reply mapping. It did not unify what the loop RECORDS, and that was
|
|
8
|
+
* eight retyped push sites across two files, each choosing a suffix by hand
|
|
9
|
+
* according to which branch of an `if/else` it stood in.
|
|
10
|
+
*
|
|
11
|
+
* The cost is written into the code. `phases.ts` states the invariant in a
|
|
12
|
+
* comment — *"No provenance stamp here, unlike clarify's transcript: this string
|
|
13
|
+
* is fed back VERBATIM into the next grill-gen prompt, so a `(accepted
|
|
14
|
+
* recommendation)` suffix would become model input"* — and TWELVE LINES ABOVE it,
|
|
15
|
+
* the YOLO branch pushes `${answer} ${YOLO_STAMP}` into that very array. The rule
|
|
16
|
+
* was violated inside the loop body that declares it, because the rule lived in
|
|
17
|
+
* prose and the decision lived at each push.
|
|
18
|
+
*
|
|
19
|
+
* Here it is a property of a value: an entry states its KIND, and the policy says
|
|
20
|
+
* where that kind's provenance is allowed to appear.
|
|
21
|
+
*
|
|
22
|
+
* NOT unified here: `plan-session.ts`'s `PlanEntry` transcript. It is a different
|
|
23
|
+
* shape — decisions vs advisory notes, persisted to its own task file, with no
|
|
24
|
+
* `Qn:`/`An:` numbering — and folding it in would be a rename, not a deepening.
|
|
25
|
+
*/
|
|
26
|
+
import { YOLO_STAMP } from './yolo.js';
|
|
27
|
+
/**
|
|
28
|
+
* The provenance suffix each kind carries. A new kind is a compile error until it
|
|
29
|
+
* declares one; `typed` declares the empty string, which is a statement, not a
|
|
30
|
+
* gap — a human's own words are the baseline everything else is marked against.
|
|
31
|
+
*/
|
|
32
|
+
export const QA_PROVENANCE = {
|
|
33
|
+
auto: '(auto)',
|
|
34
|
+
'auto-resolved': '(auto-resolved — already settled by the spec)',
|
|
35
|
+
'host-set': '(host-set — plan granularity is not left to chance)',
|
|
36
|
+
yolo: YOLO_STAMP,
|
|
37
|
+
'yolo-skip': YOLO_STAMP,
|
|
38
|
+
accepted: '(accepted recommendation)',
|
|
39
|
+
typed: ''
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* GRILL: stamps `auto` and both YOLO kinds in the record, and shows the generator
|
|
43
|
+
* nothing.
|
|
44
|
+
*
|
|
45
|
+
* `accepted` is deliberately NOT in the record set. Grill's record reaches
|
|
46
|
+
* `COMPOSE_PROMPT` and `CRITIQUE_PROMPT`, where it is named GROUND TRUTH; clarify's
|
|
47
|
+
* reaches a decompose prompt. Whether those two should agree is a prompt question
|
|
48
|
+
* with its own A/B, so today's answer is preserved rather than harmonised here.
|
|
49
|
+
*/
|
|
50
|
+
export const GRILL_QA_POLICY = {
|
|
51
|
+
record: new Set(['auto', 'yolo', 'yolo-skip']),
|
|
52
|
+
generatorSeesProvenance: false
|
|
53
|
+
};
|
|
54
|
+
/** CLARIFY: stamps every non-typed kind, in the record and to the generator alike. */
|
|
55
|
+
export const CLARIFY_QA_POLICY = {
|
|
56
|
+
record: new Set(['auto-resolved', 'host-set', 'yolo', 'yolo-skip', 'accepted']),
|
|
57
|
+
generatorSeesProvenance: true
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* One dialog's transcript. Owns the numbering and both renderings; performs no
|
|
61
|
+
* I/O and knows nothing about children, pickers or sessions — which is what makes
|
|
62
|
+
* it drivable by a test that only wants to know what a model would have been
|
|
63
|
+
* shown.
|
|
64
|
+
*/
|
|
65
|
+
export class QaTranscript {
|
|
66
|
+
_policy;
|
|
67
|
+
_entries = [];
|
|
68
|
+
constructor(_policy) {
|
|
69
|
+
this._policy = _policy;
|
|
70
|
+
}
|
|
71
|
+
/** How many questions have been answered. Also the next question's number − 1. */
|
|
72
|
+
get length() {
|
|
73
|
+
return this._entries.length;
|
|
74
|
+
}
|
|
75
|
+
get entries() {
|
|
76
|
+
return this._entries;
|
|
77
|
+
}
|
|
78
|
+
/** Record one answered question. The number is assigned here, not by the caller. */
|
|
79
|
+
add(kind, question, answer) {
|
|
80
|
+
this._entries.push({ kind, question, answer });
|
|
81
|
+
}
|
|
82
|
+
/** The persisted / handed-on transcript, with provenance per the policy. */
|
|
83
|
+
forRecord() {
|
|
84
|
+
return this._render(e => this._policy.record.has(e.kind));
|
|
85
|
+
}
|
|
86
|
+
/** The text fed back into the next question-generation prompt. */
|
|
87
|
+
forGenerator() {
|
|
88
|
+
return this._render(e => this._policy.generatorSeesProvenance && this._policy.record.has(e.kind));
|
|
89
|
+
}
|
|
90
|
+
_render(stamped) {
|
|
91
|
+
return this._entries
|
|
92
|
+
.map((e, i) => {
|
|
93
|
+
const suffix = stamped(e) ? QA_PROVENANCE[e.kind] : '';
|
|
94
|
+
const answer = suffix ? `${e.answer} ${suffix}` : e.answer;
|
|
95
|
+
return `Q${i + 1}: ${e.question}\nA${i + 1}: ${answer}`;
|
|
96
|
+
})
|
|
97
|
+
.join('\n');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the NEXT question comes from — the other half of `question-dialog.ts`.
|
|
3
|
+
*
|
|
4
|
+
* `question-dialog.ts` unified the ANSWER side of the adaptive dialogs (the picker
|
|
5
|
+
* cards, the reply mapping) and its own docstring makes the argument for doing so:
|
|
6
|
+
* *"It was written three times… the two mirrors were never converted, and they had
|
|
7
|
+
* already drifted apart in three ways… The next edit to any of them is where the
|
|
8
|
+
* bug lands."* The QUESTION side — generate → parse → pick the real question →
|
|
9
|
+
* dedupe → spend one corrective re-prompt → yield or exhaust — was left behind,
|
|
10
|
+
* and it had drifted between the two loops that use the SAME parser on the SAME
|
|
11
|
+
* prompt format (`plan-prompts.ts` says `parseClarifyList` parses it "UNCHANGED";
|
|
12
|
+
* `auto-prompts.ts` specifies the identical shape).
|
|
13
|
+
*
|
|
14
|
+
* The five drifts, all in clarify's favour of being wrong:
|
|
15
|
+
*
|
|
16
|
+
* 1. **Which entry is the question.** `parseClarifyList` pushes an entry for
|
|
17
|
+
* EVERY numbered line, and the local model writes numbered analysis notes
|
|
18
|
+
* before the question it was asked for (measured live). `pickQuestion` prefers
|
|
19
|
+
* the first entry carrying a `SUGGESTED:` line; clarify took `parsed[0]`
|
|
20
|
+
* blindly, showing the note as the question and losing the recommendation
|
|
21
|
+
* attached further down.
|
|
22
|
+
* 2. **NONE vs unparseable.** The parser returns `[]` for both. Clarify's
|
|
23
|
+
* `if (parsed.length === 0) break` ended the whole clarify — and decomposed the
|
|
24
|
+
* feature with ZERO clarifications — on a formatting slip.
|
|
25
|
+
* 3. **A re-typed sentinel.** `isNoneReply`'s regex was a byte-identical second
|
|
26
|
+
* copy of the parser's own.
|
|
27
|
+
* 4. **Missing SUGGESTED** bought one corrective re-prompt in plan and none in
|
|
28
|
+
* clarify, so clarify showed a card-less question.
|
|
29
|
+
* 5. **The deferral guard.** It exists because an accepted "clarify with the user
|
|
30
|
+
* before proceeding" rode into `/task`'s handoff AS AN AUTHORITATIVE DECISION
|
|
31
|
+
* and produced a task whose VERIFY asserted no source file had changed.
|
|
32
|
+
* Clarify's answers ride into the decompose prompt and the AUTO file with
|
|
33
|
+
* exactly the same authority, and had no guard.
|
|
34
|
+
*
|
|
35
|
+
* NOT unified here: grill's generation loop. It uses a different parser
|
|
36
|
+
* (`parseGrillQuestions`, which yields bare strings) and has no `SUGGESTED` at
|
|
37
|
+
* generation time at all — grill's recommendation comes from `phaseAutoAnswer`
|
|
38
|
+
* one step later, so every quality rule below is inapplicable to it. Folding it in
|
|
39
|
+
* would mean a generic over the parsed shape with one consumer opting out of the
|
|
40
|
+
* entire rule table: a wider interface for less behaviour.
|
|
41
|
+
*/
|
|
42
|
+
import { type ClarifyQuestion } from './parsers.js';
|
|
43
|
+
/**
|
|
44
|
+
* The cap on distinct questions ONE adaptive dialog may ask.
|
|
45
|
+
*
|
|
46
|
+
* Clarify and plan each declared their own `8`, linked only by a comment saying
|
|
47
|
+
* "matches /task-auto's MAX_CLARIFY_QUESTIONS, for the same reason". They bound
|
|
48
|
+
* the same thing for the same reason; this is that reason, once.
|
|
49
|
+
*/
|
|
50
|
+
export declare const MAX_DIALOG_QUESTIONS = 8;
|
|
51
|
+
/** True when the reply is the deliberate "nothing left to ask" sentinel, as
|
|
52
|
+
* opposed to output the parser simply could not read. */
|
|
53
|
+
export declare function isNoneReply(raw: string): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Which of the parsed entries is the actual question.
|
|
56
|
+
*
|
|
57
|
+
* `parseClarifyList` turns EVERY numbered line into an entry, and the local model
|
|
58
|
+
* sometimes writes a numbered analysis note or two before the question it was
|
|
59
|
+
* asked for (measured live: the first numbered line was a note like
|
|
60
|
+
* "1. gateDebugWriter in orchestrator.ts — wraps a raw append function"). Taking
|
|
61
|
+
* entry 0 blindly then shows the note as the question and loses the SUGGESTED line
|
|
62
|
+
* attached further down. The prompt requires exactly one SUGGESTED, and the parser
|
|
63
|
+
* attaches it to the entry it follows.
|
|
64
|
+
*/
|
|
65
|
+
export declare function pickQuestion<T extends {
|
|
66
|
+
suggested?: string;
|
|
67
|
+
}>(parsed: T[]): T | undefined;
|
|
68
|
+
/**
|
|
69
|
+
* One QUALITY rule: a defect in an otherwise-usable question that is worth exactly
|
|
70
|
+
* one corrective re-prompt.
|
|
71
|
+
*
|
|
72
|
+
* `detect` returns the hint to re-prompt with, or null to pass. `repair` is what
|
|
73
|
+
* to do when the SAME defect survives the re-prompt — a question with a bad
|
|
74
|
+
* default still beats no question, so a rule degrades rather than discards.
|
|
75
|
+
*/
|
|
76
|
+
export interface QuestionRule {
|
|
77
|
+
id: string;
|
|
78
|
+
detect: (q: ClarifyQuestion, plain: string) => string | null;
|
|
79
|
+
/** Applied only when the defect survived its one re-prompt. */
|
|
80
|
+
repair?: (q: ClarifyQuestion) => ClarifyQuestion;
|
|
81
|
+
}
|
|
82
|
+
export interface QuestionSourceDeps {
|
|
83
|
+
/**
|
|
84
|
+
* Ask the model for the next question. `hint` is the corrective re-prompt to
|
|
85
|
+
* prepend, or null. The caller closes over its own transcript and prompt — the
|
|
86
|
+
* source never builds one.
|
|
87
|
+
*/
|
|
88
|
+
generate: (hint: string | null) => Promise<string>;
|
|
89
|
+
/** The corrective re-prompt for a reply the parser could not read. */
|
|
90
|
+
formatHint: string;
|
|
91
|
+
/** Ordered quality rules; each may fire at most once per question. */
|
|
92
|
+
rules?: ReadonlyArray<QuestionRule>;
|
|
93
|
+
cap?: number;
|
|
94
|
+
log?: (msg: string) => void;
|
|
95
|
+
}
|
|
96
|
+
export type NextQuestion = {
|
|
97
|
+
kind: 'question';
|
|
98
|
+
q: ClarifyQuestion;
|
|
99
|
+
plain: string;
|
|
100
|
+
index: number;
|
|
101
|
+
} | {
|
|
102
|
+
kind: 'exhausted';
|
|
103
|
+
why: 'none' | 'cap' | 'dups' | 'unparseable';
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* A deep module over a state machine that was five mutable locals per site.
|
|
107
|
+
*
|
|
108
|
+
* The interface is one method. Behind it: the cap, the duplicate backstop and its
|
|
109
|
+
* strike budget, the NONE-vs-unparseable distinction, `pickQuestion`, the one-shot
|
|
110
|
+
* budget shared by every quality rule, and the hint precedence between a format
|
|
111
|
+
* re-prompt and a duplicate re-prompt.
|
|
112
|
+
*/
|
|
113
|
+
export declare function makeQuestionSource(deps: QuestionSourceDeps): {
|
|
114
|
+
next: () => Promise<NextQuestion>;
|
|
115
|
+
asked: () => ReadonlyArray<string>;
|
|
116
|
+
reopen: () => void;
|
|
117
|
+
};
|