@mjasnikovs/pi-task 0.18.15 → 0.18.16

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.
Files changed (55) hide show
  1. package/README.md +2 -2
  2. package/dist/task/accept-debt.d.ts +28 -1
  3. package/dist/task/accept-debt.js +61 -3
  4. package/dist/task/auto-io.d.ts +4 -2
  5. package/dist/task/auto-io.js +6 -3
  6. package/dist/task/auto-orchestrator.d.ts +1 -0
  7. package/dist/task/auto-orchestrator.js +135 -19
  8. package/dist/task/auto-prompts.d.ts +5 -5
  9. package/dist/task/auto-prompts.js +9 -2
  10. package/dist/task/contracts.d.ts +8 -0
  11. package/dist/task/contracts.js +4 -2
  12. package/dist/task/decompose-fidelity.d.ts +47 -0
  13. package/dist/task/decompose-fidelity.js +132 -0
  14. package/dist/task/final-gate-fix.d.ts +22 -3
  15. package/dist/task/final-gate-fix.js +72 -7
  16. package/dist/task/final-gate.d.ts +48 -1
  17. package/dist/task/final-gate.js +182 -34
  18. package/dist/task/gate-deps.d.ts +7 -0
  19. package/dist/task/gate-deps.js +37 -1
  20. package/dist/task/launch-contract.d.ts +36 -1
  21. package/dist/task/launch-contract.js +80 -2
  22. package/dist/task/phases.d.ts +13 -1
  23. package/dist/task/phases.js +50 -11
  24. package/dist/task/prompts.js +2 -0
  25. package/dist/task/render-check.d.ts +32 -0
  26. package/dist/task/render-check.js +186 -0
  27. package/dist/task/requirements.d.ts +88 -0
  28. package/dist/task/requirements.js +331 -0
  29. package/dist/task/verify-reconcile.d.ts +36 -0
  30. package/dist/task/verify-reconcile.js +203 -0
  31. package/dist/task/write-guard.d.ts +52 -0
  32. package/dist/task/write-guard.js +112 -0
  33. package/package.json +1 -1
  34. package/dist/task/_ab.d.ts +0 -1
  35. package/dist/task/_ab.js +0 -68
  36. package/dist/task/task-file.d.ts +0 -14
  37. package/dist/task/task-file.js +0 -15
  38. package/dist/think-test/cli.d.ts +0 -1
  39. package/dist/think-test/cli.js +0 -98
  40. package/dist/think-test/client.d.ts +0 -26
  41. package/dist/think-test/client.js +0 -37
  42. package/dist/think-test/compressor.d.ts +0 -5
  43. package/dist/think-test/compressor.js +0 -25
  44. package/dist/think-test/judge.d.ts +0 -4
  45. package/dist/think-test/judge.js +0 -11
  46. package/dist/think-test/score.d.ts +0 -8
  47. package/dist/think-test/score.js +0 -22
  48. package/dist/think-test/serialize.d.ts +0 -19
  49. package/dist/think-test/serialize.js +0 -41
  50. package/dist/think-test/transcript.d.ts +0 -7
  51. package/dist/think-test/transcript.js +0 -41
  52. package/dist/think-test/transform.d.ts +0 -6
  53. package/dist/think-test/transform.js +0 -24
  54. package/dist/think-test/types.d.ts +0 -45
  55. package/dist/think-test/types.js +0 -1
@@ -0,0 +1,132 @@
1
+ /**
2
+ * decompose-fidelity — verbatim fidelity of plan derivations (mx5 run 11, goal B).
3
+ *
4
+ * The failure this closes: the design's §12 milestone lines 2 and 4 end in
5
+ * "guards + tests" / "contact + tests"; the decomposed titles carried everything
6
+ * BUT the "+ tests" suffix. A title is ALL a per-task pipeline ever sees, so a
7
+ * silently dropped constraint fragment vanishes from the whole run — the dropped
8
+ * tests were exactly the instrument that would have caught the shipped 404 bug.
9
+ * Decompose paraphrases freely and NOTHING compared a title to the spec line it
10
+ * derives from.
11
+ *
12
+ * Mechanism (contracts.ts pattern, applied to decompose itself): the decompose
13
+ * prompt asks each task line to cite its origin as a trailing
14
+ * `[source: "<verbatim spec line>"]`. The host then:
15
+ * 1. GROUNDS the quote — a citation that is not a (whitespace/case-normalised)
16
+ * substring of the source doc is fabricated and is stripped, never trusted;
17
+ * 2. deterministically detects DROPPED ADDITIVE FRAGMENTS: the `+`-joined
18
+ * trailing constraints of the cited line ("… + tests") whose words are
19
+ * absent from the title;
20
+ * 3. RE-ATTACHES the missing fragments to the title verbatim.
21
+ *
22
+ * Scope is deliberately the additive-suffix class (`+`-joined fragments): those
23
+ * are constraints by construction, so re-attachment can never inject noise that
24
+ * the cited line doesn't demand — worst case is redundancy with what the title
25
+ * already says, never fabrication. Whole-line paraphrase drift is NOT judged here
26
+ * (a title is a paraphrase by design); requirement-level coverage owns that.
27
+ * No similarity thresholds anywhere: grounding is exact normalised substring,
28
+ * presence is exact word membership (with a singular/plural `s` allowance).
29
+ */
30
+ import { normalise } from './contracts.js';
31
+ /** Trailing `[source: "…"]` clause a decompose line may carry (prompt asks for it last). */
32
+ const SOURCE_RE = /\s*\[source:\s*"(.+)"\s*\]\s*$/i;
33
+ /** Split a decompose title into its base and its GROUNDED source citation.
34
+ * An absent clause yields no source; a fabricated (ungrounded) one is stripped
35
+ * and dropped — exactly like keepGroundedContracts rejects a paraphrased quote. */
36
+ export function extractTitleSource(title, sourceDoc) {
37
+ const m = SOURCE_RE.exec(title);
38
+ if (!m)
39
+ return { base: title.trim() };
40
+ const base = title.slice(0, m.index).trim();
41
+ const quote = m[1].trim();
42
+ if (quote.length === 0 || !normalise(sourceDoc).includes(normalise(quote))) {
43
+ return { base };
44
+ }
45
+ return { base, source: quote };
46
+ }
47
+ /** Word tokens for presence checks: alphanumeric runs, lowercased. */
48
+ function words(s) {
49
+ return s.toLowerCase().match(/[a-z0-9]+/g) ?? [];
50
+ }
51
+ /** Is every word of `fragment` present in `titleWords`? A bare trailing `s`
52
+ * difference (test/tests) does not count as absence — a rule, not a threshold. */
53
+ function fragmentPresent(fragment, titleWords) {
54
+ return words(fragment).every(w => {
55
+ if (titleWords.has(w))
56
+ return true;
57
+ if (w.endsWith('s') && titleWords.has(w.slice(0, -1)))
58
+ return true;
59
+ return titleWords.has(w + 's');
60
+ });
61
+ }
62
+ /**
63
+ * The `+`-joined trailing constraint fragments of `sourceLine` whose words are
64
+ * absent from `title`. "2. **Auth** — sessions, login/logout/me, guards + tests."
65
+ * yields the fragment "tests"; a title that never mentions tests gets it back.
66
+ * Fragments before the first `+` are the task's body — a title paraphrases those
67
+ * freely and they are never judged here. A `+`-part is further split on commas
68
+ * ("+ Tailwind v4 tokens, nav, router" is three constraints), so a title missing
69
+ * one of them gets ONLY that one restored, not the whole phrase (measured live:
70
+ * whole-phrase restoration re-attached text the title already carried).
71
+ */
72
+ export function findDroppedPlusFragments(sourceLine, title) {
73
+ const parts = sourceLine.split('+');
74
+ if (parts.length < 2)
75
+ return [];
76
+ const titleWords = new Set(words(title));
77
+ const missing = [];
78
+ for (const raw of parts.slice(1)) {
79
+ for (const sub of raw.split(',')) {
80
+ // A fragment runs to the next `+`/comma; strip trailing sentence
81
+ // punctuation and markdown emphasis so "tests.**" compares as "tests".
82
+ const fragment = sub
83
+ .replace(/[*_`]/g, '')
84
+ .replace(/[.,;:!?)\]]+\s*$/, '')
85
+ .trim();
86
+ if (fragment.length === 0)
87
+ continue;
88
+ if (words(fragment).length === 0)
89
+ continue;
90
+ if (!fragmentPresent(fragment, titleWords))
91
+ missing.push(fragment);
92
+ }
93
+ }
94
+ return missing;
95
+ }
96
+ /**
97
+ * Reconcile decompose output against the source doc: ground each citation, strip
98
+ * the clause (its job ends here), and re-attach any dropped `+`-fragments to the
99
+ * title verbatim so downstream refine/compose — which see ONLY the title — get
100
+ * the constraint back. Titles without a citation pass through unchanged, so a
101
+ * model that never cites degrades to exactly the old behavior.
102
+ */
103
+ export function reconcileTitleSources(titles, sourceDoc) {
104
+ const out = [];
105
+ const restored = [];
106
+ let sourced = 0;
107
+ for (let i = 0; i < titles.length; i++) {
108
+ const { base, source } = extractTitleSource(titles[i], sourceDoc);
109
+ if (source === undefined) {
110
+ out.push(base);
111
+ continue;
112
+ }
113
+ sourced++;
114
+ const missing = findDroppedPlusFragments(source, base);
115
+ if (missing.length === 0) {
116
+ out.push(base);
117
+ continue;
118
+ }
119
+ restored.push({ index: i, fragments: missing, source });
120
+ out.push(`${base} — MUST also cover (restored from its spec line): ${missing.join(', ')}`);
121
+ }
122
+ return { titles: out, restored, sourced };
123
+ }
124
+ /** The decompose-prompt rule that makes titles citable (the belt half; the host
125
+ * grounding + restoration above is the lever). Kept here so prompt and parser
126
+ * can't drift apart. */
127
+ export const DECOMPOSE_SOURCE_RULE = '- When a task derives from a specific line of the feature/spec (a milestone, a'
128
+ + ' bullet, a requirement sentence), END that task\'s line with [source: "<that'
129
+ + ' line copied VERBATIM>"]. Copy exactly — a paraphrased or invented quote is'
130
+ + ' discarded host-side. Put the [source: …] clause after any [decisions: …]'
131
+ + ' clause. When the cited line carries additive constraints ("+ tests",'
132
+ + ' "+ docs"), those are PART of the task — keep them in the title itself.';
@@ -1,3 +1,4 @@
1
+ import { type TreeChangeSummary } from './write-guard.js';
1
2
  /** Same bounded-fix contract as lint-fix: edit in place, bash exists to RUN the
2
3
  * failing command (and the project's own tooling), not to mutate git state. */
3
4
  export declare const FINAL_FIX_TOOLS = "read,edit,bash";
@@ -79,13 +80,31 @@ export interface FinalFixDeps {
79
80
  /** Labels of every currently-discoverable gate command (static + integration),
80
81
  * for the shrink guard. Pure discovery — nothing is executed. */
81
82
  discoverLabels: (cwd: string) => string[];
82
- /** Discard the fix child's working-tree edits (shrink-guard trip only). Absent
83
+ /** Discard the fix child's working-tree edits (guard trips only). Absent
83
84
  * → the violation is still rejected, edits are left for inspection. */
84
85
  discard?: (cwd: string) => Promise<void>;
86
+ /** The fix child's tree changes (`git status --porcelain` shape) — diff capture
87
+ * for the log and the deletion guard's input. The tree was clean before the
88
+ * child ran (every task committed), so status IS the child's work. */
89
+ treeChanges?: () => Promise<TreeChangeSummary>;
90
+ /** The union of every task spec's frozen (Do-NOT-modify) paths — the whole-run
91
+ * write-deny set, since this child works across all slices at once. */
92
+ frozenPaths?: () => Promise<string[]>;
93
+ /** Restore the given frozen paths to HEAD; returns the files actually reverted
94
+ * (same mechanical deny the enforce pass carries — see frozen-path-guard.ts). */
95
+ revertFrozen?: (paths: string[]) => Promise<string[]>;
96
+ /** Deterministic probe scan over the child's ADDED lines (probe-gaming, F6):
97
+ * a fix written to game a check rather than meet it rejects the attempt —
98
+ * run 11's autofix replaced the typed client with a hand-written contract
99
+ * copy to green the lint. Findings are verbatim offending lines. */
100
+ probeScan?: () => Promise<string[]>;
101
+ /** Write a timestamped line to the gate debug log (guard events). */
102
+ log?: (msg: string) => void;
85
103
  }
86
104
  /**
87
- * Run one bounded final-gate fix attempt: snapshot discovery → child → shrink
88
- * guard → gate re-run. Never throws for an outcome; only a user cancel inside
105
+ * Run one bounded final-gate fix attempt: snapshot discovery → child → write-guard
106
+ * stack (diff capture → frozen-path revert → deletion guard → shrink guard → probe
107
+ * scan) → gate re-run. Never throws for an outcome; only a user cancel inside
89
108
  * runChild propagates (the caller's USER_CANCELLED path handles it).
90
109
  */
91
110
  export declare function runFinalGateAutofix(deps: FinalFixDeps): Promise<FinalFixResult>;
@@ -25,8 +25,26 @@
25
25
  * the child runs; any previously-discovered command that is no longer
26
26
  * discoverable afterwards rejects the attempt and discards its edits. A fix may
27
27
  * change what a command DOES, never make it disappear.
28
+ *
29
+ * WRITE-GUARD STACK (mx5 run 11): this child was added after the run-8 guard
30
+ * generation and inherited none of them — it ran `rm` on a sibling task's
31
+ * verified deliverable to satisfy a recorded debt claim and hand-copied a pinned
32
+ * contract to green the lint, with free bash and no trace. Every attempt now
33
+ * runs, deterministically and in order: frozen-path revert (when a freeze set is
34
+ * wired — see below) → deletion guard (a tracked file deleted without relocation
35
+ * rejects the attempt) → shrink guard (above) → probe-gaming scan over the added
36
+ * lines (a fix that SAYS it games a check rejects the attempt). Diff capture for
37
+ * every write-capable gate child lives at the gate-deps seam, keyed on the
38
+ * child's TOOLS, so the next write-capable kind cannot run invisibly either.
39
+ *
40
+ * The frozen-path deny is implemented but NOT wired by gate-deps: per-task
41
+ * frozen fences are task-SCOPED (they fence a task off a sibling's territory),
42
+ * and the measured union over the run-11 specs would have reverted the one
43
+ * legitimate whole-repo fix that run needed (migrate.ts — frozen by its own
44
+ * producing task). It activates only when a run-GLOBAL freeze source exists.
28
45
  */
29
46
  import { USER_CANCELLED } from './child-runner.js';
47
+ import { findForbiddenDeletions } from './write-guard.js';
30
48
  /** Same bounded-fix contract as lint-fix: edit in place, bash exists to RUN the
31
49
  * failing command (and the project's own tooling), not to mutate git state. */
32
50
  export const FINAL_FIX_TOOLS = 'read,edit,bash';
@@ -100,6 +118,10 @@ export function buildFinalFixPrompt(failReason) {
100
118
  ' command pass. Relocating or scoping a file the runner was never meant',
101
119
  ' to pick up (per the project’s own config) is a legitimate fix;',
102
120
  ' deleting it or marking it skipped is not.',
121
+ ' - Do NOT delete tracked files at all. Every tracked file is a completed',
122
+ ' task’s committed deliverable; a deletion is detected and the whole fix',
123
+ ' is rejected. A legitimate relocation keeps the file (same file name)',
124
+ ' elsewhere in the tree.',
103
125
  ' - Do NOT remove or rename the project’s own commands (its test/build/',
104
126
  ' lint scripts or targets). Making the gate unable to find the command',
105
127
  ' is detected and the whole fix is rejected.',
@@ -135,8 +157,9 @@ export function parseFinalFixMarker(text) {
135
157
  return { blocked: false, note: last[2].trim() || undefined };
136
158
  }
137
159
  /**
138
- * Run one bounded final-gate fix attempt: snapshot discovery → child → shrink
139
- * guard → gate re-run. Never throws for an outcome; only a user cancel inside
160
+ * Run one bounded final-gate fix attempt: snapshot discovery → child → write-guard
161
+ * stack (diff capture → frozen-path revert → deletion guard → shrink guard → probe
162
+ * scan) → gate re-run. Never throws for an outcome; only a user cancel inside
140
163
  * runChild propagates (the caller's USER_CANCELLED path handles it).
141
164
  */
142
165
  export async function runFinalGateAutofix(deps) {
@@ -151,6 +174,38 @@ export async function runFinalGateAutofix(deps) {
151
174
  throw err;
152
175
  return { ok: false, reason: `fix child failed: ${msg}` };
153
176
  }
177
+ const rejected = (what) => ({
178
+ ok: false,
179
+ reason: `${what} — edits ${deps.discard ? 'discarded' : 'REJECTED but left in the tree (no discard available)'}`
180
+ });
181
+ // (Diff capture — what the pass changed, durably — happens at the gate-deps
182
+ // seam for every write-capable child; here only the guards act on it.)
183
+ // FROZEN-PATH WRITE-DENY: undo the child's edits to any path a task spec
184
+ // froze, before anything downstream can act on them — same mechanical deny
185
+ // the enforce pass carries (prompt framing is A/B-proven insufficient).
186
+ // Non-fatal: the rest of the fix survives, only the frozen edits are undone.
187
+ if (deps.frozenPaths && deps.revertFrozen) {
188
+ const frozen = await deps.frozenPaths();
189
+ if (frozen.length > 0) {
190
+ const reverted = await deps.revertFrozen(frozen);
191
+ if (reverted.length > 0) {
192
+ deps.log?.(`final-fix FROZEN-PATH GUARD — reverted spec-frozen file(s) the fix pass modified: ${reverted.join(', ')}`);
193
+ }
194
+ }
195
+ }
196
+ // DELETION GUARD (post-revert state): a tracked file the pass deleted without
197
+ // relocating it is a committed deliverable destroyed — reject the attempt.
198
+ if (deps.treeChanges) {
199
+ const gone = findForbiddenDeletions(await deps.treeChanges());
200
+ if (gone.length > 0) {
201
+ if (deps.discard)
202
+ await deps.discard(deps.cwd);
203
+ const r = rejected(`fix pass DELETED tracked file(s) (${gone.join(', ')}) — a completed task's `
204
+ + `committed deliverable is not the fix child's to remove`);
205
+ deps.log?.(`final-fix DELETION GUARD — ${r.reason}`);
206
+ return r;
207
+ }
208
+ }
154
209
  // SHRINK GUARD: every gate command discoverable before the fix must still be
155
210
  // discoverable after it. A vanished command means the child "fixed" the gate
156
211
  // by removing the check — reject and (when possible) discard the edits.
@@ -159,11 +214,21 @@ export async function runFinalGateAutofix(deps) {
159
214
  if (vanished.length > 0) {
160
215
  if (deps.discard)
161
216
  await deps.discard(deps.cwd);
162
- return {
163
- ok: false,
164
- reason: `fix pass removed the gate's own command(s) (${vanished.join(', ')}) — `
165
- + `edits ${deps.discard ? 'discarded' : 'REJECTED but left in the tree (no discard available)'}`
166
- };
217
+ return rejected(`fix pass removed the gate's own command(s) (${vanished.join(', ')})`);
218
+ }
219
+ // PROBE SCAN (F6): added lines whose stated purpose is to make a check pass
220
+ // rather than meet the requirement reject the attempt — there is no verify
221
+ // child downstream of this pass to judge the finding, and the probe is
222
+ // FP-measured at 1 true hit in 50,735 added lines on the real corpus.
223
+ if (deps.probeScan) {
224
+ const findings = await deps.probeScan();
225
+ if (findings.length > 0) {
226
+ if (deps.discard)
227
+ await deps.discard(deps.cwd);
228
+ const r = rejected(`fix pass added CHECK-GAMING code (${findings.slice(0, 2).join('; ').slice(0, 300)})`);
229
+ deps.log?.(`final-fix PROBE-GAMING GUARD — ${r.reason}`);
230
+ return r;
231
+ }
167
232
  }
168
233
  const marker = parseFinalFixMarker(text);
169
234
  if (marker.blocked) {
@@ -1,10 +1,24 @@
1
1
  import { type HealthCommand } from './repo-health-check.js';
2
2
  import { type AcceptDebt } from './accept-debt.js';
3
+ import { type RenderOutcome } from './render-check.js';
3
4
  export interface FinalGateOutcome {
4
5
  /** true → statics and every runnable integration command passed (or nothing to run). */
5
6
  ok: boolean;
6
- /** On a fail: the exact command, its exit code, and the tail of its output. */
7
+ /**
8
+ * On a fail: the exact command, its exit code, and the tail of its output — the
9
+ * MECHANICAL failure only. The accept-debt note is deliberately NOT folded in
10
+ * here (mx5 run 11): this string seeds the final-gate AUTOFIX child's prompt,
11
+ * and a debt included there is read as an instruction — the run-11 fix child
12
+ * `rm`'d a sibling task's verified deliverable to satisfy a recorded claim. The
13
+ * child cannot act on text it never receives; debts travel in `debtNote`.
14
+ */
7
15
  reason: string;
16
+ /**
17
+ * Human-facing suffix listing the still-open accepted-defect claims (see
18
+ * buildAcceptDebtNote) — for the picker question and the trail, NEVER for the
19
+ * autofix seed. Absent when nothing is open.
20
+ */
21
+ debtNote?: string;
8
22
  /**
9
23
  * ACCEPT-despite-verify-FAIL debts still open at run end (mx5 run 4 B3 / run 8
10
24
  * TASK_0012): tasks the user blessed as-is despite a verify-FAIL that a
@@ -33,6 +47,9 @@ export declare function discoverLockfileChecks(cwd: string): HealthCommand[];
33
47
  export declare function discoverBootCommand(cwd: string): HealthCommand | null;
34
48
  type BootOutcome = {
35
49
  outcome: 'skip' | 'pass';
50
+ /** Set when the render check could not OBSERVE the served page (no browser,
51
+ * undeterminable port) — surfaced by the gate as an UNOBSERVED warning. */
52
+ renderNote?: string;
36
53
  } | {
37
54
  outcome: 'fail';
38
55
  detail: string;
@@ -60,6 +77,20 @@ export interface BootDeps {
60
77
  * real socket; the default probes ss/lsof + pgid.
61
78
  */
62
79
  groupHasListener?: (pgid: number) => boolean;
80
+ /**
81
+ * The (lowest) TCP port a listener owned by process group `pgid` is bound to,
82
+ * or null when it cannot be determined. Feeds the render check's URL; injected
83
+ * for tests, default probes ss/lsof + pgid.
84
+ */
85
+ groupListeningPort?: (pgid: number) => number | null;
86
+ /**
87
+ * Load the served page once in a headless browser and judge the RENDERED DOM
88
+ * (mx5 runs 8/11: curl cannot execute JS, so a blank-mount app passed every
89
+ * gate). Runs only for a served app, against the live listener, before the
90
+ * boot child is killed. Absent → the boot check behaves exactly as before;
91
+ * the gate wires runRenderCheck by default for served apps.
92
+ */
93
+ renderProbe?: (url: string) => RenderOutcome;
63
94
  }
64
95
  /**
65
96
  * Does the finished run stand up a listening HTTP server? Deterministic, from the
@@ -101,6 +132,22 @@ export declare function runBootCheck(cwd: string, [bin, args]: HealthCommand, gr
101
132
  * gaming the gate, not fixing the defect.
102
133
  */
103
134
  export declare function discoverGateCommandLabels(cwd: string): string[];
135
+ /**
136
+ * A non-zero exit whose output shows the EXTERNAL INFRASTRUCTURE a launch script
137
+ * talks to is absent HERE — a database/daemon that is not running or not
138
+ * installed — rather than a fault in the script itself. Applied ONLY to
139
+ * launch-contract scripts (a migrate/seed against no DB is an environment gap on
140
+ * this box; the same wording in a `test` run is a real failure the suite must own).
141
+ */
142
+ export declare const INFRA_GAP_OUTPUT_RE: RegExp;
143
+ /**
144
+ * The task whose commit INTRODUCED `rel` (oldest `--diff-filter=A` commit whose
145
+ * subject carries the pi-task `(TASK_nnnn)` suffix — both the task snapshot and the
146
+ * ENFORCE commit shapes match). Null when the file predates the run, was never
147
+ * committed, git is unavailable, or the adding commit is not a task commit — every
148
+ * unknown degrades to "no conflict claim".
149
+ */
150
+ export declare function taskThatIntroduced(cwd: string, rel: string): string | null;
104
151
  /**
105
152
  * Run the final gate: static analysis first, then the lockfile consistency
106
153
  * checks, then the discovered integration commands, then one boot exercise of