@mjasnikovs/pi-task 0.38.15 → 0.38.17
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 +26 -0
- package/dist/config/config.js +68 -17
- package/dist/shared/child-process.js +9 -16
- package/dist/task/accept-debt.d.ts +7 -5
- package/dist/task/accept-debt.js +18 -14
- package/dist/task/artifact-closure.js +18 -63
- package/dist/task/auto-orchestrator.js +211 -218
- package/dist/task/autofix-ledger.d.ts +113 -0
- package/dist/task/autofix-ledger.js +152 -0
- package/dist/task/boot-probe.d.ts +109 -1
- package/dist/task/boot-probe.js +139 -23
- package/dist/task/child-runner.d.ts +50 -6
- package/dist/task/child-runner.js +48 -69
- package/dist/task/command-run.d.ts +49 -6
- package/dist/task/command-run.js +154 -18
- package/dist/task/coverage-loop.d.ts +11 -0
- package/dist/task/coverage-loop.js +16 -0
- package/dist/task/external-context.d.ts +9 -12
- package/dist/task/external-context.js +5 -5
- package/dist/task/failure-classifier.d.ts +9 -1
- package/dist/task/failure-classifier.js +9 -0
- package/dist/task/final-gate-fix.d.ts +22 -26
- package/dist/task/final-gate-fix.js +16 -31
- package/dist/task/final-gate.d.ts +10 -2
- package/dist/task/final-gate.js +55 -89
- package/dist/task/fix-child.d.ts +64 -0
- package/dist/task/fix-child.js +66 -0
- package/dist/task/gate-deps.js +20 -13
- package/dist/task/lint-fix.d.ts +7 -0
- package/dist/task/lint-fix.js +45 -9
- package/dist/task/orchestrator.d.ts +33 -24
- package/dist/task/orchestrator.js +75 -46
- package/dist/task/phases.d.ts +120 -34
- package/dist/task/phases.js +221 -134
- package/dist/task/plan-orchestrator.js +2 -2
- 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/repo-health-check.d.ts +21 -21
- package/dist/task/repo-health-check.js +43 -112
- package/dist/task/run-end.d.ts +77 -0
- package/dist/task/run-end.js +37 -0
- package/dist/task/run-final-gate.js +71 -79
- 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 +9 -1
- package/dist/task/task-gates.js +27 -6
- package/dist/task/terminal-outcome.d.ts +1 -1
- package/dist/task/terminal-outcome.js +12 -0
- package/dist/task/verify-work.d.ts +46 -0
- package/dist/task/verify-work.js +51 -3
- package/dist/workers/brave-search.d.ts +7 -0
- package/dist/workers/brave-search.js +36 -55
- package/dist/workers/ddg-search.d.ts +1 -1
- package/dist/workers/ddg-search.js +27 -47
- package/dist/workers/docs-core.d.ts +71 -1
- package/dist/workers/docs-core.js +131 -71
- package/dist/workers/exa-search.d.ts +2 -2
- package/dist/workers/exa-search.js +53 -68
- package/dist/workers/html-clean.js +67 -88
- package/dist/workers/http-request.d.ts +74 -0
- package/dist/workers/http-request.js +103 -0
- package/dist/workers/npm-version.js +37 -42
- package/dist/workers/pi-worker-core.d.ts +13 -2
- package/dist/workers/pi-worker-core.js +35 -25
- package/dist/workers/pi-worker-docs.d.ts +1 -1
- package/dist/workers/pi-worker-docs.js +49 -68
- package/dist/workers/pi-worker-fetch.d.ts +1 -1
- package/dist/workers/pi-worker-fetch.js +20 -21
- package/dist/workers/pi-worker-search.js +6 -4
- package/dist/workers/pi-worker.js +5 -4
- package/dist/workers/search-core.d.ts +1 -1
- package/dist/workers/search-core.js +36 -42
- package/dist/workers/search-types.d.ts +13 -0
- package/dist/workers/search-types.js +27 -0
- package/dist/workers/shared.d.ts +51 -11
- package/dist/workers/shared.js +0 -0
- package/dist/workers/worker-channels.d.ts +60 -0
- package/dist/workers/worker-channels.js +98 -0
- package/package.json +1 -1
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the PLAN-SHAPING loops record, and the decisions that record makes.
|
|
3
|
+
*
|
|
4
|
+
* `GateTally`'s and `AutofixLedger`'s twin, one phase earlier. CONTEXT.md records
|
|
5
|
+
* that shape twice already: a long loop threading mutable locals by closure, with
|
|
6
|
+
* pure helpers extracted for testability while the ORDERING and CARRY-FORWARD
|
|
7
|
+
* decisions stayed in the caller. `coverPlan` was the third instance — five locals
|
|
8
|
+
* (`planTitles`, `best`, `round`, `roundCap`, `bonusRoundUsed`) plus a
|
|
9
|
+
* snapshot-before-overwrite pair (`priorCovered`, `priorMissing`) that existed
|
|
10
|
+
* ONLY because the bonus-round decision was made downstream from the evidence it
|
|
11
|
+
* needed, so the loop had to save a copy of `best` before replacing it.
|
|
12
|
+
*
|
|
13
|
+
* The last real bug here says the shape out loud, in the loop's own comment:
|
|
14
|
+
* *"This used to be two assignments, and the second one kept the OLD plan's
|
|
15
|
+
* accounting whenever the new plan's coverage-map child faulted
|
|
16
|
+
* (`cand.accounting ?? accounting`) — binding requirements to titles they were
|
|
17
|
+
* never mapped against."* `AutofixLedger`'s indictment, verbatim: the decision was
|
|
18
|
+
* made downstream from the evidence.
|
|
19
|
+
*
|
|
20
|
+
* `consider` closes it by construction rather than by comment. It compares, it
|
|
21
|
+
* replaces the plan WHOLE (titles and accounting together, because they are one
|
|
22
|
+
* value), and it grants the bonus round IN THE SAME CALL that adopts — the way
|
|
23
|
+
* `judge(outcome, edited)` enters the demoted signature in the call that demotes.
|
|
24
|
+
* There is no window in which the snapshot and the replacement can disagree.
|
|
25
|
+
*
|
|
26
|
+
* NO I/O. No `logPlanDebug`, no notify, no child — for the same reason `GateTally`
|
|
27
|
+
* performs none: a record that performs effects cannot be driven by a test that
|
|
28
|
+
* only wants the verdict. The caller trails what the returned decision says.
|
|
29
|
+
*/
|
|
30
|
+
import { decideAdoption, normMissingArea } from './coverage-loop.js';
|
|
31
|
+
/**
|
|
32
|
+
* The coverage loop's record: the best plan seen, the rounds spent, and the
|
|
33
|
+
* one-shot bonus round.
|
|
34
|
+
*
|
|
35
|
+
* Methods are named for what they MEAN, not for the field they touch.
|
|
36
|
+
*/
|
|
37
|
+
export class CoverageLedger {
|
|
38
|
+
_best;
|
|
39
|
+
_opts;
|
|
40
|
+
_round = 0;
|
|
41
|
+
_cap;
|
|
42
|
+
_bonusUsed = false;
|
|
43
|
+
constructor(_best, _opts) {
|
|
44
|
+
this._best = _best;
|
|
45
|
+
this._opts = _opts;
|
|
46
|
+
this._cap = _opts.cap;
|
|
47
|
+
}
|
|
48
|
+
/** The best-covered plan seen so far — the one that reprompts, and the one that ships. */
|
|
49
|
+
best() {
|
|
50
|
+
return this._best;
|
|
51
|
+
}
|
|
52
|
+
/** Rounds spent so far. */
|
|
53
|
+
round() {
|
|
54
|
+
return this._round;
|
|
55
|
+
}
|
|
56
|
+
/** May another reprompt round run? */
|
|
57
|
+
mayRetry() {
|
|
58
|
+
return this._round < this._cap;
|
|
59
|
+
}
|
|
60
|
+
/** Spend a round. Call once per reprompt, before the child runs. */
|
|
61
|
+
startRound() {
|
|
62
|
+
return ++this._round;
|
|
63
|
+
}
|
|
64
|
+
/** What is still uncovered in the shipping plan, or null when nothing is. */
|
|
65
|
+
unresolved() {
|
|
66
|
+
return this._best.plan.missing.length > 0 ? this._best.plan.missing : null;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Judge one candidate against the best plan and, if it wins, adopt it.
|
|
70
|
+
*
|
|
71
|
+
* The bonus-round grant is decided HERE, against the pre-adoption plan this
|
|
72
|
+
* method still holds — not by a caller reading a snapshot it took beforehand.
|
|
73
|
+
* Two guards keep it off generic judge churn: the grounded covered-set must
|
|
74
|
+
* strictly GROW (a flaky judge relabelling the same-shaped plan's gap does not
|
|
75
|
+
* qualify), and the candidate must expose a NEW area (a gap already present is
|
|
76
|
+
* one we have reprompted against or will). Bounded to one, so a judge that
|
|
77
|
+
* flags forever still cannot loop the plan phase.
|
|
78
|
+
*/
|
|
79
|
+
consider(cand) {
|
|
80
|
+
const decision = decideAdoption(this._best.plan, cand.plan, this._opts.hasRequirements);
|
|
81
|
+
if (!decision.adopt)
|
|
82
|
+
return { adopted: false, decision, grantedBonusRound: false };
|
|
83
|
+
const priorCovered = this._best.plan.covered.size;
|
|
84
|
+
const priorMissing = new Set(this._best.plan.missing.map(normMissingArea));
|
|
85
|
+
// WHOLE, titles and accounting together. They are one value; splitting them
|
|
86
|
+
// is a bug this codebase has already had.
|
|
87
|
+
this._best = cand;
|
|
88
|
+
const grant = !this._bonusUsed
|
|
89
|
+
&& this._round >= this._cap
|
|
90
|
+
&& this._opts.hasRequirements
|
|
91
|
+
&& cand.plan.covered.size > priorCovered
|
|
92
|
+
&& cand.plan.missing.some((m) => !priorMissing.has(normMissingArea(m)));
|
|
93
|
+
if (grant) {
|
|
94
|
+
this._bonusUsed = true;
|
|
95
|
+
this._cap++;
|
|
96
|
+
}
|
|
97
|
+
return { adopted: true, decision, grantedBonusRound: grant };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// DECOMPOSE's two retry budgets (`emptyAttempts`, `smallRetryUsed`) are NOT here.
|
|
101
|
+
// They look like this shape and are not: that loop keys on `isSuspectPlan`, a
|
|
102
|
+
// predicate over the SPEC LENGTH rather than a title-count floor, and its two
|
|
103
|
+
// counters already sit inside a nine-line comment explaining why a single counter
|
|
104
|
+
// was wrong. Wrapping them in a class that does not model `isSuspectPlan` would
|
|
105
|
+
// move the code without concentrating the decision.
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
* is unit-testable without a TUI or a model.
|
|
31
31
|
*/
|
|
32
32
|
import type { AskSpec } from '../remote/bridge.js';
|
|
33
|
+
import { type QuestionRule } from './question-source.js';
|
|
33
34
|
import { type PlanEntry } from './plan-io.js';
|
|
34
35
|
import { type PendingQuestion } from './question-dialog.js';
|
|
35
36
|
export { resolveAnswer } from './question-dialog.js';
|
|
@@ -65,27 +66,7 @@ export declare const MAX_PLAN_QUESTIONS = 8;
|
|
|
65
66
|
* recommend.
|
|
66
67
|
*/
|
|
67
68
|
export declare const PLAN_FORMAT_HINT: string;
|
|
68
|
-
|
|
69
|
-
* opposed to output the parser simply could not read. */
|
|
70
|
-
export declare function isNoneReply(raw: string): boolean;
|
|
71
|
-
/**
|
|
72
|
-
* Which of the parsed entries is the actual question.
|
|
73
|
-
*
|
|
74
|
-
* parseClarifyList turns EVERY numbered line into an entry, and the local model
|
|
75
|
-
* sometimes writes a numbered analysis note or two before the question it was
|
|
76
|
-
* asked for (measured live: the first numbered line was a note like
|
|
77
|
-
* "1. gateDebugWriter in orchestrator.ts — wraps a raw append function"). Taking
|
|
78
|
-
* entry 0 blindly then shows the note as the question and loses the SUGGESTED
|
|
79
|
-
* line that was attached further down.
|
|
80
|
-
*
|
|
81
|
-
* The SUGGESTED line is the reliable marker of the real question — the prompt
|
|
82
|
-
* requires exactly one, and parseClarifyList attaches it to the entry it follows.
|
|
83
|
-
* So: prefer the first entry that has one; fall back to the first entry when none
|
|
84
|
-
* does, which is the case the format re-prompt then covers.
|
|
85
|
-
*/
|
|
86
|
-
export declare function pickQuestion<T extends {
|
|
87
|
-
suggested?: string;
|
|
88
|
-
}>(parsed: T[]): T | undefined;
|
|
69
|
+
export { isNoneReply, pickQuestion } from './question-source.js';
|
|
89
70
|
/**
|
|
90
71
|
* Does the question offer the user a choice between two named alternatives?
|
|
91
72
|
* Deliberately shallow — an "X or Y?" in the question's own clause.
|
|
@@ -132,6 +113,35 @@ export declare function planDecisiveHint(question: string, suggested: string): s
|
|
|
132
113
|
* subject. It costs one extra child call on the questions where it fires.
|
|
133
114
|
*/
|
|
134
115
|
export declare function planForkHint(question: string): string;
|
|
116
|
+
/**
|
|
117
|
+
* PLAN's quality rules, in order.
|
|
118
|
+
*
|
|
119
|
+
* Each is worth exactly one corrective re-prompt (the child is stateless, so each
|
|
120
|
+
* hint quotes the question back), and each DEGRADES rather than discards when the
|
|
121
|
+
* defect survives — a question with a weak default still beats no question.
|
|
122
|
+
*
|
|
123
|
+
* Only {@link CLARIFY_QUALITY_RULES} is shared with `/task-auto`, and only the
|
|
124
|
+
* deferral rule is in it. The other two were MEASURED here (10/15 fork-shaped
|
|
125
|
+
* questions shipped one option; the SUGGESTED requirement is in both prompts) but
|
|
126
|
+
* each costs one extra child call every time it fires, and clarify is the most
|
|
127
|
+
* A/B'd path in the codebase — moving them there is its own experiment, not a
|
|
128
|
+
* side effect of sharing a state machine. Recorded rather than done.
|
|
129
|
+
*/
|
|
130
|
+
export declare const PLAN_QUALITY_RULES: ReadonlyArray<QuestionRule>;
|
|
131
|
+
/**
|
|
132
|
+
* The deferral rule alone — the one clarify shares.
|
|
133
|
+
*
|
|
134
|
+
* It exists because an accepted "clarify with the user before proceeding" rode
|
|
135
|
+
* into `/task`'s handoff AS AN AUTHORITATIVE DECISION and produced a task whose
|
|
136
|
+
* ACCEPTANCE was "a planning document with placeholder sections" and whose VERIFY
|
|
137
|
+
* asserted that no source file had changed. Clarify's answers ride into the
|
|
138
|
+
* decompose prompt and the AUTO file with exactly the same authority and had no
|
|
139
|
+
* guard at all — the same bug, one command over, waiting.
|
|
140
|
+
*
|
|
141
|
+
* It is also the only one of the three that costs nothing on the happy path: a
|
|
142
|
+
* decisive default never triggers it.
|
|
143
|
+
*/
|
|
144
|
+
export declare const CLARIFY_QUALITY_RULES: ReadonlyArray<QuestionRule>;
|
|
135
145
|
/** The ask spec the session hands to the UI: an {@link AskSpec} plus the picker
|
|
136
146
|
* entries. Kept structurally identical to what phaseGrill/planAuto build so the
|
|
137
147
|
* same SessionUI.ask serves all three. */
|
|
@@ -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
|
+
}
|