bareloop 0.46.0 → 0.46.1
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/CHANGELOG.md +26 -0
- package/bareloop.context.md +8 -6
- package/package.json +1 -1
- package/src/authorreadout.js +47 -12
- package/src/panel/authorroutes.js +3 -2
- package/src/panel/authorsession.js +5 -7
- package/src/panel/index.html +13 -1
- package/src/panel/schedulestart.js +5 -0
- package/src/panel/server.js +9 -9
- package/src/panel/status.js +11 -0
- package/src/stagewords.js +18 -17
- package/types/authorreadout.d.ts +7 -0
- package/types/panel/status.d.ts +8 -0
- package/types/stagewords.d.ts +16 -15
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,32 @@ All notable changes to bareloop are documented here. Format:
|
|
|
5
5
|
[SemVer](https://semver.org/spec/v2.0.0.html). Pre-1.0: **minor** = a ladder rung or
|
|
6
6
|
feature lands, **patch** = docs, fixes, scaffolding.
|
|
7
7
|
|
|
8
|
+
## [0.46.1] - 2026-10-24
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **A blank Base URL in the New job readiness line reads "Type a Base URL."** It showed `[✗] no-base-url — may be flaky, not blocking`; it now shows the server's own sentence (owned by `src/modellist.js`).
|
|
13
|
+
|
|
14
|
+
- **The Audit learnings header counts distinct hints across the draft and the first replan.** It counted only the draft block (3 at draft, 5 at replan read "3 hints"). A hint handed at both draft and replan counts once (identity = ledger row id).
|
|
15
|
+
|
|
16
|
+
- **A scheduled fire refused at the run-start fetch shows its status word on the schedule card.** The card read `last 10:30 · run-4` with no word because the run list's events were not passed to `summarizeRow`; the refused row now carries its `failed` status.
|
|
17
|
+
|
|
18
|
+
- **Every drafting stop reads in plain words from one table.** `confirm-abandoned` and `confirm-restart` printed `Stopped: <code>`, and a code outside an 8-entry table fell back to `Drafting stopped (<code>).`. `STOP_HEADS` (with `STOP_TODO` where "draft again" is wrong) in `src/authorreadout.js` now covers about 20 stop codes and `authorsession` routes every stop through `draftStopText`; an unknown code gets a generic sentence, never the raw code. The confirm-abandoned stop now ends "nothing more was spent" (it promised a figure that reads "unknown" when spend is incomplete). Wording signed by hamr 2026-10-23.
|
|
19
|
+
|
|
20
|
+
- **One `isReusableGreen` decides every Reuse-eligibility check.** The picker, `getStartFrom`, the start gate and prefill flag in `authorroutes`, and the Ended block each spelled it their own way, and the Ended block still treated an already-green as not reusable. An already-green now ends "Nothing to do." like a green, and a scheduled already-green run is held for Accept like a green. Scheduling itself still needs a real green (`greenFor` in `src/schedules.js` is unchanged).
|
|
21
|
+
|
|
22
|
+
- **A watched-folder file whose name holds a line break or ends in whitespace is refused.** A newline in the name became extra Inputs lines in the fire's card text, and a trailing space trimmed to a path that does not exist. `perFireSpec` now refuses it through the ordinary refused-fire path ("Rename the file. Nothing started."; one refused row, no session) and never rewrites the name. A leading space is harmless and not refused.
|
|
23
|
+
|
|
24
|
+
- **`scripts/hermetic.mjs` refuses a test path that does not exist.** A mistyped path was skipped and the run exited 0, so a green count could miss a file. A path-looking argument that does not exist now exits 1 with a plain message and runs nothing; `npm test` with no arguments, flags and globs are untouched. The value of a value-taking flag (`--test-name-pattern "a/b"`, `--test-reporter`, `--import`, `-r`, ...) is never checked as a path; a boolean flag followed by a mistyped path is still refused.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- **Linear-time trailing trims in the reuse-preprobe `normPath` (`scripts/`, not shipped).** The `[.,;:)\]}'"`]+$` and `\/+$` trims were the quadratic class (about 1.2 s CPU on 30000 trailing dots); same results on 1.5M random strings, 0 mismatches.
|
|
29
|
+
|
|
30
|
+
- **`stageQuestionText`'s JSDoc now sits on the function, not on `GENRE_WHAT`.** Comments and declaration order only.
|
|
31
|
+
|
|
32
|
+
- **Tests strengthened (test-only, no source change).** A seeded differential test pins the linear `stringLiteralsOf` and `staticImportsOf` scanners against the old regexes; `buildCards`' card-level secret mask, the judged-strike trend number (read from the real `runDeclaredStages`), the Revise-at-menu spend figure (waits for the unwind, runs a second draft), and money-as-read in symbol-first and far-apart layouts each have a test that fails on a mutant.
|
|
33
|
+
|
|
8
34
|
## [0.46.0] - 2026-10-23
|
|
9
35
|
|
|
10
36
|
### Added
|
package/bareloop.context.md
CHANGED
|
@@ -627,7 +627,7 @@ a step, or test it without a provider.
|
|
|
627
627
|
`error: 'truncated:max_tokens'`, neutral `stopReason: 'max_tokens'`) is a FAILED TRY on `askStructured`'s retry ladder with the
|
|
628
628
|
reason "the AI's answer was cut off at the output limit" (`axis: 'output-truncated'`) — NOT `provider-red`; all attempts stay
|
|
629
629
|
booked, `AUTHOR_MAX_TOKENS` is never raised, and only a real transport failure stays `provider-red`. `draftStopText`
|
|
630
|
-
(`src/authorreadout.js`) words
|
|
630
|
+
(`src/authorreadout.js`) words EVERY drafting stop from one table (`STOP_HEADS` for the sentence, `STOP_TODO` where "draft again" is the wrong advice; a code the table lacks reads one generic sentence, never the raw code); the panel puts it on the failed step. (2)
|
|
631
631
|
`authorClose({ offerQuestions })` (default `false`; the CLI never sets it) offers the optional `questions` property on the LAST
|
|
632
632
|
try of the full ladder only, and only when the two tries before it both failed — validation OR the seed read (hamr ruled 2026-10-10; this DIFFERS
|
|
633
633
|
from fwdloop's validation-only rule): a seed-read failure is a work stage that instrument-stopped or a close with no work stage
|
|
@@ -3074,7 +3074,9 @@ Enforcement is **local only** — wired into `npm test` (see `package.json`'s `t
|
|
|
3074
3074
|
script), never into `.github/workflows/*` (CI already runs `npm test`, so the rule is
|
|
3075
3075
|
enforced there without an ask-first CI edit). `npm test` itself now runs `node --test`
|
|
3076
3076
|
through `scripts/hermetic.mjs`, which strips this machine's `HOME`/git identity so a
|
|
3077
|
-
test that leans on either fails locally instead of only on CI (F136).
|
|
3077
|
+
test that leans on either fails locally instead of only on CI (F136). It also refuses a
|
|
3078
|
+
test path that does not exist (`node --test` skips one silently, so a mistyped file could hide
|
|
3079
|
+
behind a green count); the separate value of a value-taking flag is never read as a path. The check itself lives outside the
|
|
3078
3080
|
published package, at `scripts/prompt-commit-check.mjs` (pure decision logic in
|
|
3079
3081
|
`scripts/promptcommitlib.mjs`, `scripts/` is not in `package.json`'s `files`, so none of
|
|
3080
3082
|
this ships):
|
|
@@ -3889,7 +3891,7 @@ No model is called anywhere in learnings (a test walks the import closure). Pure
|
|
|
3889
3891
|
- **Cards:** `buildCards({ runid, events, audit, workdir, spec, model, questions })` → `{ cards, suspect, thin, noise }`. FIXED = a check red then the same check passes later in the run; NEVER FIXED = still red at run end. Excluded and counted: noise runs (provider-red, close-crashed, worker-crash, no close spoke), CHECK-SUSPECT (a no-suppressions hit on a prose comment), TOO-THIN (no error kind and no fix files). Written files come from `attempt-diff` events and the gate audit's allow write/edit rows relative to the tree.
|
|
3890
3892
|
- **Fold:** `cardsMatch` / `foldCards`: same job + check, a shared error kind, Jaccard >= 60% on flagged-text tokens (a fit on known cards, not a proof). Code-less cards (kind `other`) fold on the text rule alone, only when both token sets are non-empty (R1, 2026-10-23: UNTESTED on real data; the 60% was fitted on cards with codes). `kindAgrees` is the one kind test.
|
|
3891
3893
|
- **Ledger** `<config home>/learn/ledger.json` (mode 600, atomic, secrets masked): rows `{ id, lesson, count, sample, models, workflow, place, level, trust, runIds, firstSeen, lastSeen, specHash }`, plus `pending` (held rubric cards), `suspect`, `health`. There is no check-wide set-aside (R2, 2026-10-23): a lesson seen in several jobs climbs by the climb rules and is read at its level; all-jobs hints come last. `countLedger` (+1 per run per row), `holdPending` / `releasePending` (rubric cards count only after the human Accept; `releaseRun` is called from the door's accept path), `climb` (the level of a lesson, derived at read time by `readableRows` from the rows not decayed at that moment: it can go back down when siblings age past 56 days; the file stores `workflow` only and ignores an older stored level). The ledger keeps EVERY row (nothing is deleted, no `expired` flag is stored; hamr 2026-10-23). Memory is the filter: `readableRows` / `renderHints` skip a row last seen more than 56 days ago (`isDecayed`) and a workflow row of another signed hash (`otherJobVersion`); a lesson seen again counts +1 on its old row. Old files with `status`/`expiredBy` still load; the fields are ignored and dropped at the next write.
|
|
3892
|
-
- **Reading:** `hintsForRun({ home, spec, specHash })` renders once at run start; `runJob(..., { learnHints: { text, ids } })` → `runPlan` hands it to the PLANNER at the draft and the first replan only, and emits a `learnings` spine record with the exact text. `renderHints(ledger, { workflow, place, specHash, now })` is the one render (this job, then this place, then all-jobs, highest count first, 2,000 characters, a hint whole or not at all; all-jobs hints last); a FIXED hint adds `What changed:` (first 3 `+`/`-` lines of the stored diff, 150 chars each, then `(+N more)`; never on NEVER FIXED) and a repeated path in a hint reads as its basename (R3, R5, 2026-10-23); the spine's recorded text is byte-equal to a fresh `renderHints(...).text` from the ledger as it stood. The Audit run detail carries `learnings` (read-only).
|
|
3894
|
+
- **Reading:** `hintsForRun({ home, spec, specHash })` renders once at run start; `runJob(..., { learnHints: { text, ids } })` → `runPlan` hands it to the PLANNER at the draft and the first replan only, and emits a `learnings` spine record with the exact text. `renderHints(ledger, { workflow, place, specHash, now })` is the one render (this job, then this place, then all-jobs, highest count first, 2,000 characters, a hint whole or not at all; all-jobs hints last); a FIXED hint adds `What changed:` (first 3 `+`/`-` lines of the stored diff, 150 chars each, then `(+N more)`; never on NEVER FIXED) and a repeated path in a hint reads as its basename (R3, R5, 2026-10-23); the spine's recorded text is byte-equal to a fresh `renderHints(...).text` from the ledger as it stood. The Audit run detail carries `learnings` (read-only); its header counts DISTINCT hints (the same ledger row handed at the draft and the first replan counts once).
|
|
3893
3895
|
|
|
3894
3896
|
### Settings — the keys file, `config.json`, the monthly $ limit, the Providers tab (PANEL-BUILD.md P4a, `src/keysfile.js`, `src/config.js`, `src/monthly.js`, `src/providerrows.js`, `src/panel/settingsroutes.js`)
|
|
3895
3897
|
|
|
@@ -3976,7 +3978,7 @@ above 0, or `null`/blank to clear) — an empty or key-less body is a `400`, nev
|
|
|
3976
3978
|
only the shell holds when config already names it (`keys.<ENV>` or `models.<ENV>`; its status reads `found in shell`) — nothing is hardcoded, there is no add-row and no remove-row control. A missing `.env`
|
|
3977
3979
|
is created (mode 600, dir 700) on the first Providers read with two comment lines and no key. A value of `null` counts as SET and means "no real key" (the provider is handed the placeholder
|
|
3978
3980
|
`no-key-needed`, never the word `null`; the row shows `set in file`). A key whose value has a space or a line break shows `The key has a space or a line break in it. Fix it in your keys file, then
|
|
3979
|
-
reload keys.` Each row: the env NAME + status, an **API shape** select (Anthropic / OpenAI-compatible / Gemini), the typed **Base URL** (empty = no run, no list, `type the Base URL`), the **Old max_tokens**
|
|
3981
|
+
reload keys.` Each row: the env NAME + status, an **API shape** select (Anthropic / OpenAI-compatible / Gemini), the typed **Base URL** (empty = no run, no list, `type the Base URL`; the Test line reads the server's own sentence `Type a Base URL.`), the **Old max_tokens**
|
|
3980
3982
|
tick (applies to `openai-api` only), **Tokens** (the key's spend), `[ Test ] [ ⟳ Refresh ]`, and its model lines: a **Use** tick, the model name, **In / Out $/1M** boxes, tokens (`—` when no spend row names the model).
|
|
3981
3983
|
A listed id from Refresh is shown unticked and is not saved until touched; `+ type a model name` adds a line unticked. Saves are on change; success reads `saved — applies from the next model call`;
|
|
3982
3984
|
every refusal is a plain sentence in `say` (`Fill both prices to tick this model.`, `Fill both price boxes or clear both.`, `A price must be a number, 0 or more.`, …). **Open keys folder** (in the KEYS strip,
|
|
@@ -4439,12 +4441,12 @@ Check what the person asked for, not what the worker chose to write. bareloop ne
|
|
|
4439
4441
|
- **Only the panel fires.** `createPanelServer` runs one 500 ms unref'd scheduler (poll only, never `fs.watch`); the CLI never fires. Panel off = nothing fires; slots missed collapse to ONE catch-up (marked when more than 2 minutes late). `createPanelServer` test seams: `scheduleNow`, `schedulerIntervalMs` (0 = no timer), `sessionDeps`, `acceptWaitMs`.
|
|
4440
4442
|
- **Files live under `<config home>/triggers/<job>/`**: `schedule.json` (the current signed schedule) and ONE append-only `ledger.jsonl` (every row carries `sig`, the spec hash it was written under). A run that a schedule started carries `trigger.json` beside its `resolved-spec.json`.
|
|
4441
4443
|
- **Setting one is two clicks, both $0** (`POST /api/author/schedule-prepare`, then `/api/author/schedule` with the hash). It needs a real green run of the job's CURRENT workflow (a rubric job also an honoured Accept). A file trigger is for plain-files jobs only. Cancel is never refused. A reuse never clears a schedule: Reuse workflow (the one door, v1.101) keeps the origin's name while no job box is changed and switches the Job name to the next free `<base>-N` once one is; a changed job is a new workflow that needs its own green before it can be scheduled.
|
|
4442
|
-
- **A fire goes busy, then monthly limit, then `wouldAccept`, then the one starter** (the same session start and `signRun` as a Reuse the person clicked, with `--review-door`). Busy, monthly and refused fires consume nothing and are told once per `key|outcome|why`, so they retry. A run is busy while its pid is alive AND its process started no later than 5 s after its run row was written (a recycled pid is not that run). A run waiting at its review door is not busy.
|
|
4444
|
+
- **A fire goes busy, then monthly limit, then `wouldAccept`, then the one starter** (the same session start and `signRun` as a Reuse the person clicked, with `--review-door`). Busy, monthly and refused fires consume nothing and are told once per `key|outcome|why`, so they retry. A file fire whose name holds a line break or ends in whitespace is refused at `perFireSpec` (the card text is `label: value` lines, so it would read back as another Inputs line); the person is told to rename the file. A run is busy while its pid is alive AND its process started no later than 5 s after its run row was written (a recycled pid is not that run). A run waiting at its review door is not busy.
|
|
4443
4445
|
- **An install-needed refusal backs off, then gives up.** When a fire's copy needs packages, that job's next attempt waits 1, 2, 3, 5, 8, 13, then 21 minutes (in memory: a restart begins at the first wait); a wait that would pass 60 minutes in total is not taken: the schedule writes ONE `gave-up` row and is paused (nothing fires; files still queue as `arrived`), and the job shows in Needs you with the pill `Check` (`listGaveUp`). Signing the schedule again writes `resumed` and clears it. Constants: `INSTALL_RETRY_STEPS_MS`, `INSTALL_GIVE_UP_MS` (`src/panel/schedules.js`).
|
|
4444
4446
|
- **A scheduled green is held until accepted**: `getStartFrom` reads its outcome as `green-held` and the Reuse list skips it, until the spine holds an honoured accept.
|
|
4445
4447
|
- **Accept** (`POST /api/runs/<id>/accept`) runs the engine's own door path (`run-u --spec <signed spec> --door <id> --decide accept --approve <hash>`): the mechanical stages re-run, a red is `door-accept-red`, the run's verdict is untouched. It shows only while `doorWaiting(events)` (`src/reviewdoor.js`) is true; the same answer fills the runs list's Needs-you `Accept`.
|
|
4446
4448
|
- **Every word the page shows** comes from `src/schedulewords.js` (`SCHEDULE_WORDS` is the one button table the server sends).
|
|
4447
|
-
- **The schedule is shown on three tabs** (v1.102): the Run tab's one summary block (ENDED dated, NEXT, SCHEDULE with `last`/`next`; row and frame red when the last fire started nothing), the Job tab's Started and Schedule rows (`getRunJob`), and the Audit's own folded `schedule` section on every run of a scheduled job (v1.104: `detail.scheduleSection` `{title, sign, word, head, rows}`, drawn `[sign] word schedule · head` with the sign and word both from the status table, v1.106 Part 4; a run a cancelled schedule started keeps `detail.startedAudit`). `/api/runs` `scheduled[]` and `gaveUp[]` carry `runNo` so the small lists read `job · run-N`. `trigger.json` carries `triggerKind`, `sessionId`, `originRunid`, `sig`, `lateMs`; each signing appends a `signed` ledger row naming its origin run.
|
|
4449
|
+
- **The schedule is shown on three tabs** (v1.102): the Run tab's one summary block (ENDED dated, NEXT, SCHEDULE with `last`/`next`; row and frame red when the last fire started nothing; `last` wears the run's status word even for a fire refused at the run-start fetch, which has no spine, only a settled event), the Job tab's Started and Schedule rows (`getRunJob`), and the Audit's own folded `schedule` section on every run of a scheduled job (v1.104: `detail.scheduleSection` `{title, sign, word, head, rows}`, drawn `[sign] word schedule · head` with the sign and word both from the status table, v1.106 Part 4; a run a cancelled schedule started keeps `detail.startedAudit`). `/api/runs` `scheduled[]` and `gaveUp[]` carry `runNo` so the small lists read `job · run-N`. `trigger.json` carries `triggerKind`, `sessionId`, `originRunid`, `sig`, `lateMs`; each signing appends a `signed` ledger row naming its origin run.
|
|
4448
4450
|
|
|
4449
4451
|
|
|
4450
4452
|
## The Audit's tries, checks and answered questions (PRD v1.106 Part 5)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bareloop",
|
|
3
|
-
"version": "0.46.
|
|
3
|
+
"version": "0.46.1",
|
|
4
4
|
"description": "Workflows that earn their own design, with receipts — agent-authored workflows for repeated, long, verifiable jobs that improve across runs under an un-gameable gate.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
package/src/authorreadout.js
CHANGED
|
@@ -656,6 +656,50 @@ export function proposalStopText({ stop, reds, cases = null, spend = null, sourc
|
|
|
656
656
|
/** the reason inside a `scout-absent` red's detail: `the survey is ABSENT (<reason>) — an absent facts object ...` (src/authorflow.js) */
|
|
657
657
|
const SCOUT_REASON_RE = /^the survey is [^(]*\((.*)\) — an absent facts/s;
|
|
658
658
|
|
|
659
|
+
/**
|
|
660
|
+
* ONE owner of the plain sentence for every drafting stop code (hamr 2026-10-09: "a user should get a reason they can
|
|
661
|
+
* understand and do something about"). What happened; `STOP_TODO` below overrides the default "draft again" where that is wrong.
|
|
662
|
+
* Used by `draftStopText` for every stop the panel and the CLI show; a code not listed reads `STOP_UNKNOWN`, never the raw code.
|
|
663
|
+
* @type {Record<string, string>}
|
|
664
|
+
*/
|
|
665
|
+
export const STOP_HEADS = {
|
|
666
|
+
'provider-red': 'The AI provider failed before a usable plan came back — a connection or service fault, not a problem with your job.',
|
|
667
|
+
'artifact-red': 'The AI answered, but its answer could not be used as a plan.',
|
|
668
|
+
'cap-halt': 'Drafting stopped on your cost cap.',
|
|
669
|
+
'pricing-red': 'Drafting stopped: the cost of a call could not be priced, so the cap cannot be enforced.',
|
|
670
|
+
'max-revisions': 'The AI used all its tries without a plan that passed the checks.',
|
|
671
|
+
precheck: 'Drafting stopped before any call: something it needs was missing.',
|
|
672
|
+
unchanged: 'Drafting stopped: the AI sent the same plan twice.',
|
|
673
|
+
'scout-absent': 'Drafting stopped: the first look at your repo or input did not complete.',
|
|
674
|
+
refused: 'Drafting stopped: something in the job or its inputs is not allowed.',
|
|
675
|
+
'interview-incomplete': 'Drafting stopped: the job form was not complete, so there was nothing to draft from.',
|
|
676
|
+
'calibration-missing': 'Drafting stopped: the practice cases that prove the rubric were missing, so the check could not be trusted.',
|
|
677
|
+
'invalid-set': 'Drafting stopped: the rubric and its practice cases do not fit together, so they could not be graded.',
|
|
678
|
+
'broken-close': 'Drafting stopped: the final check the model wrote could not run.',
|
|
679
|
+
'listing-absent': 'Drafting stopped: the list of your files was not available, so the checks could not name any.',
|
|
680
|
+
'listing-conflict': 'Drafting stopped: the list of your files was given twice and the two did not agree.',
|
|
681
|
+
'listing-unreadable': 'Drafting stopped: your repo\'s file list could not be read.',
|
|
682
|
+
'non-code-source': 'Drafting stopped: this source is not a code repo, and this kind of job only works on code.',
|
|
683
|
+
'authoring-failed': 'Drafting stopped for a reason that was not recorded.',
|
|
684
|
+
'confirm-abandoned': 'Drafting stopped: the confirm step ended before you answered, so nothing was signed.',
|
|
685
|
+
'confirm-restart': 'Drafting stopped: you chose to start over, so nothing was signed.',
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
/** the "What to do" for the stops where "draft again" is not the right advice */
|
|
689
|
+
const STOP_TODO = /** @type {Record<string, string>} */ ({
|
|
690
|
+
refused: 'change what the message above names, then draft again.',
|
|
691
|
+
'interview-incomplete': 'fill in every line of the job form, then draft again.',
|
|
692
|
+
'calibration-missing': 'draft again; if it stops the same way twice, make your PASS/FAIL examples more concrete.',
|
|
693
|
+
'invalid-set': 'draft again; if it stops the same way twice, make your PASS/FAIL examples more concrete.',
|
|
694
|
+
'non-code-source': 'point the job at a code repo, or use a folder or file job instead.',
|
|
695
|
+
'listing-unreadable': 'check the repo is a readable git repo with at least one commit, then draft again.',
|
|
696
|
+
'confirm-abandoned': 'start a new draft when you are ready; nothing more was spent.',
|
|
697
|
+
'confirm-restart': 'change the answers you want and start the draft again.',
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
/** a stop code this table does not know: still plain words, never the code */
|
|
701
|
+
const STOP_UNKNOWN = 'Drafting stopped for a reason this screen has no wording for.';
|
|
702
|
+
|
|
659
703
|
/**
|
|
660
704
|
* @param {{stop: string, reds?: PlainRed[]|null, refusal?: {detail?: string}|null,
|
|
661
705
|
* spend?: {knownUsd?: number|null, spendComplete?: boolean|null}|null, wholeJob?: boolean}} o `wholeJob` - a rubric job on the person's
|
|
@@ -674,23 +718,13 @@ export function draftStopText({ stop, reds = null, refusal = null, spend = null,
|
|
|
674
718
|
const detail = (Array.isArray(reds) ? reds : []).find((r) => r?.code === 'fit-stopped')?.detail;
|
|
675
719
|
if (detail) return `Stopped before drafting: the page does not show everything the job asks for: ${redactSecrets(String(detail))}. Change the job or the page and draft again. Spent so far: ${spent ?? 'unknown'}.`;
|
|
676
720
|
}
|
|
677
|
-
|
|
678
|
-
const heads = {
|
|
679
|
-
'provider-red': 'The AI provider failed before a usable plan came back — a connection or service fault, not a problem with your job.',
|
|
680
|
-
'artifact-red': 'The AI answered, but its answer could not be used as a plan.',
|
|
681
|
-
'cap-halt': 'Drafting stopped on your cost cap.',
|
|
682
|
-
'pricing-red': 'Drafting stopped: the cost of a call could not be priced, so the cap cannot be enforced.',
|
|
683
|
-
'max-revisions': 'The AI used all its tries without a plan that passed the checks.',
|
|
684
|
-
precheck: 'Drafting stopped before any call: something it needs was missing.',
|
|
685
|
-
unchanged: 'Drafting stopped: the AI sent the same plan twice.',
|
|
686
|
-
'scout-absent': 'Drafting stopped: the first look at your repo or input did not complete.',
|
|
687
|
-
};
|
|
721
|
+
const heads = STOP_HEADS;
|
|
688
722
|
const redList = Array.isArray(reds) ? reds : [];
|
|
689
723
|
// F217: the first look at the repo failing (a dropped connection, a timeout) arrives as a `precheck` stop with a
|
|
690
724
|
// `scout-absent` red. That is NOT "something it needs was missing", and its reason sits past the 60-char quote bound
|
|
691
725
|
// behind a fixed lead-in, so the person saw neither. Word it as the scout's stop and quote the reason in full (scrubbed).
|
|
692
726
|
const scoutRed = stop === 'precheck' ? redList.find((r) => r?.code === 'scout-absent') : undefined;
|
|
693
|
-
const head = scoutRed ? heads['scout-absent'] : (heads[stop]
|
|
727
|
+
const head = scoutRed ? heads['scout-absent'] : (Object.hasOwn(heads, stop) ? heads[stop] : STOP_UNKNOWN);
|
|
694
728
|
/** @type {string[]} */
|
|
695
729
|
const said = scoutRed
|
|
696
730
|
? [SCOUT_REASON_RE.exec(String(scoutRed.detail ?? ''))?.[1]
|
|
@@ -705,6 +739,7 @@ export function draftStopText({ stop, reds = null, refusal = null, spend = null,
|
|
|
705
739
|
}
|
|
706
740
|
lines.push(stop === 'cap-halt' || stop === 'pricing-red'
|
|
707
741
|
? 'What to do: raise the cap or fix the price in Settings, then draft again.'
|
|
742
|
+
: Object.hasOwn(STOP_TODO, stop) ? `What to do: ${STOP_TODO[stop]}`
|
|
708
743
|
: (wholeJob ? 'What to do: review the whole job - every line and its ~ rules, and the inputs - and submit it again.' : 'What to do: draft again.'));
|
|
709
744
|
return lines.join('\n');
|
|
710
745
|
}
|
|
@@ -25,6 +25,7 @@ import { readFileSync } from 'node:fs';
|
|
|
25
25
|
import { planMatchesClose } from '../rubricstep.js';
|
|
26
26
|
import { createSession, validateJobCard, validateReuseCard, jobBoxesChanged, buildReuseSpec, cardFields, nextFreeJobName, startDestinationProblem, STEP_LABELS } from './authorsession.js';
|
|
27
27
|
import { parseInputs } from '../jobblock.js';
|
|
28
|
+
import { isReusableGreen } from './status.js';
|
|
28
29
|
import { proveInputFiles, looksLikeRubricInput, webFetchState } from '../source.js';
|
|
29
30
|
import { validateJob } from '../job.js';
|
|
30
31
|
|
|
@@ -282,7 +283,7 @@ export function createAuthorRoutes(opts) {
|
|
|
282
283
|
ok: true, origin: pre.origin, card, from: pre.from, note: pre.note,
|
|
283
284
|
// ONE door (v1.101): `green` = the origin earned a real green (an imported job counts), so an unchanged job reuses the signed plan;
|
|
284
285
|
// `nextName` = the Job name box once a job box changes (the box shows the origin's name until then). Nothing is locked.
|
|
285
|
-
green: pre.origin.importId !== undefined || pre.outcome
|
|
286
|
+
green: pre.origin.importId !== undefined || isReusableGreen(pre.outcome),
|
|
286
287
|
nextName: nextFreeJobName(pre.origin.job, opts.runJobNames ? opts.runJobNames() : [], { jobsDir: opts.jobsDir }),
|
|
287
288
|
specHash: pre.specHash, workflowKey: pre.workflowKey, trackRecord: pre.trackRecord,
|
|
288
289
|
line: pre.line, specNamesModel: pre.specNamesModel, specWorker: pre.specWorker,
|
|
@@ -357,7 +358,7 @@ export function createAuthorRoutes(opts) {
|
|
|
357
358
|
// a scheduled run's green waits for the person's Accept (quarantine): its plan is neither reused nor re-drafted until then
|
|
358
359
|
if (pre.outcome === 'green-held') { send(409, { ok: false, error: 'This run was started by a schedule and is waiting for your Accept. Accept it first, then it can be reused.' }); return true; }
|
|
359
360
|
card.jobName = pre.origin.job; // an unchanged job keeps its name; a typed one is ignored
|
|
360
|
-
if (fromImport !== null || pre.outcome
|
|
361
|
+
if (fromImport !== null || isReusableGreen(pre.outcome)) {
|
|
361
362
|
const rv = validateReuseCard(card, { rows });
|
|
362
363
|
if (!rv.ok) { send(400, { ok: false, error: rv.error }); return true; }
|
|
363
364
|
const spec = buildReuseSpec(pre.spec, card, rows);
|
|
@@ -1146,13 +1146,11 @@ export function createSession(card, deps = {}) {
|
|
|
1146
1146
|
const no = state.steps.findLastIndex((/** @type {any} */ x) => x.status === 'running');
|
|
1147
1147
|
appendDraftLog(outDir, { kind: 'step-reds', no: no === -1 ? state.steps.length - 1 : no, stop: authored.stop ?? 'authoring-failed', reds: redsRecord(stopReds), at: new Date().toISOString() });
|
|
1148
1148
|
}
|
|
1149
|
-
refuse(
|
|
1150
|
-
|
|
1151
|
-
:
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
wholeJob: RUBRIC_FILES,
|
|
1155
|
-
}), authored.stop === 'confirm-abandoned' ? 'abandoned' : 'refused');
|
|
1149
|
+
refuse(draftStopText({
|
|
1150
|
+
stop: String(authored.stop ?? 'authoring-failed'), reds: stopReds, refusal: authored.refusal,
|
|
1151
|
+
spend: { knownUsd: state.draftSpentUsd, spendComplete: state.draftSpendComplete },
|
|
1152
|
+
wholeJob: RUBRIC_FILES,
|
|
1153
|
+
}), authored.stop === 'confirm-abandoned' ? 'abandoned' : 'refused');
|
|
1156
1154
|
return;
|
|
1157
1155
|
}
|
|
1158
1156
|
|
package/src/panel/index.html
CHANGED
|
@@ -2910,7 +2910,17 @@
|
|
|
2910
2910
|
});
|
|
2911
2911
|
});
|
|
2912
2912
|
if(sum){
|
|
2913
|
-
|
|
2913
|
+
// count DISTINCT hints across the draft and replan blocks: the same ledger row (ids[i], parallel to hints[i]) handed twice is one hint
|
|
2914
|
+
var seen = {};
|
|
2915
|
+
var first = 0;
|
|
2916
|
+
items.forEach(function(it){
|
|
2917
|
+
var hs = Array.isArray(it.hints) ? it.hints : [];
|
|
2918
|
+
var byId = Array.isArray(it.ids) && it.ids.length === hs.length;
|
|
2919
|
+
hs.forEach(function(h, i){
|
|
2920
|
+
var key = byId ? "id:" + it.ids[i] : "n:" + it.phase + ":" + h.n;
|
|
2921
|
+
if(!seen[key]){ seen[key] = true; first += 1; }
|
|
2922
|
+
});
|
|
2923
|
+
});
|
|
2914
2924
|
var label = items.length ? head + " \u00B7 " + first + (first === 1 ? " hint" : " hints") : head;
|
|
2915
2925
|
// no arrow (hamr 2026-10-23): the state chip leads; aria-expanded carries open/closed. The same `[✓] word` state markup the schedule/step headers wear (stateDotHTML), then the plain-words title
|
|
2916
2926
|
var paint = function(){ sum.innerHTML = stateDotHTML(glyphClass("\u2713"), "learnings", "audit-learnings-state") + " " + escapeXml(label); };
|
|
@@ -4939,6 +4949,8 @@
|
|
|
4939
4949
|
var suffix = reach.modelListed === false ? " (model not listed)" : "";
|
|
4940
4950
|
return { keyOk: true, ok: true, text: "[✓] " + r.envKey + " found · " + modelId + " reachable" + suffix };
|
|
4941
4951
|
}
|
|
4952
|
+
// a blank Base URL is nothing to probe, not flakiness: say the server's own plain sentence (src/modellist.js owns it)
|
|
4953
|
+
if(reach.status === "no-base-url" && reach.note) return { keyOk: true, ok: false, text: "[✗] " + reach.note };
|
|
4942
4954
|
return { keyOk: true, ok: false, text: reachStatusText(reach.status) };
|
|
4943
4955
|
}
|
|
4944
4956
|
function refreshModelStatus(){
|
|
@@ -46,6 +46,11 @@ export function perFireSpec(schedule, cause) {
|
|
|
46
46
|
if (jobSpecHash(schedule.spec) !== schedule.specHash) return { ok: false, why: 'The saved schedule no longer matches what you signed. Nothing started.' };
|
|
47
47
|
const spec = JSON.parse(JSON.stringify(schedule.spec));
|
|
48
48
|
if (cause.kind === 'file') {
|
|
49
|
+
// the card text is `label: value` lines: a name holding a line break would read back as ANOTHER Inputs line, and edge whitespace
|
|
50
|
+
// is trimmed to a path that does not exist (a leading space in the file name sits mid-path and is harmless). Refused with its reason, never rewritten.
|
|
51
|
+
if (/[\r\n]/.test(cause.path) || cause.path !== cause.path.trim()) {
|
|
52
|
+
return { ok: false, why: `The file name ${JSON.stringify(cause.path)} has a line break or a space at its end, which the job form cannot carry. Rename the file. Nothing started.` };
|
|
53
|
+
}
|
|
49
54
|
const first = Array.isArray(spec.inputs) ? spec.inputs[0] : null;
|
|
50
55
|
if (!first) return { ok: false, why: 'The saved schedule has no input for the file to replace. Nothing started.' };
|
|
51
56
|
spec.inputs = [{ ...first, value: cause.path }, ...spec.inputs.slice(1)];
|
package/src/panel/server.js
CHANGED
|
@@ -68,7 +68,7 @@ import { SHAPES } from '../providershapes.js';
|
|
|
68
68
|
import { resolveProvider } from '../providers.js';
|
|
69
69
|
import { readResume, checkpointAgeGate, CHECKPOINT_OUTCOMES } from '../reuse.js';
|
|
70
70
|
import {
|
|
71
|
-
statusFor, partStatusFor, GOAL_MET_LINE, doorViewOf, ACTOR_MARKER, DOOR_WORDS,
|
|
71
|
+
statusFor, partStatusFor, GOAL_MET_LINE, doorViewOf, ACTOR_MARKER, DOOR_WORDS, isReusableGreen,
|
|
72
72
|
} from './status.js';
|
|
73
73
|
import { thinkingJobText } from '../thinking.js';
|
|
74
74
|
import { judgedFilesOf, lineRules, stepFileRel, proseNotCheckedItems } from '../rubricstep.js';
|
|
@@ -542,7 +542,7 @@ function endedBase(summary, death, o) {
|
|
|
542
542
|
if (o.destinationRefused) {
|
|
543
543
|
// Reuse workflow only on a run that earned its green (hamr 2026-10-08, as below); every ending offers Reuse workflow
|
|
544
544
|
// (withReuseAction adds it) and is told to change the job
|
|
545
|
-
const earned = outcome
|
|
545
|
+
const earned = isReusableGreen(outcome);
|
|
546
546
|
return {
|
|
547
547
|
reason: 'Goal met, but the output could not be delivered.',
|
|
548
548
|
next: earned ? 'Fix the destination, then Reuse workflow.' : CHANGE,
|
|
@@ -550,10 +550,9 @@ function endedBase(summary, death, o) {
|
|
|
550
550
|
actions: earned ? REUSE : [],
|
|
551
551
|
};
|
|
552
552
|
}
|
|
553
|
-
//
|
|
554
|
-
//
|
|
555
|
-
|
|
556
|
-
if (outcome !== 'green') return { reason: 'Goal met.', next: CHANGE, line: GOAL_MET_LINE, actions: [] };
|
|
553
|
+
// a reusable green (isReusableGreen: green or already-green, hamr 2026-10-15) says nothing is left to do; every ending offers
|
|
554
|
+
// Reuse workflow (withReuseAction adds it). `satisfied` is a close VERDICT, never a run outcome, so it is not one.
|
|
555
|
+
if (!isReusableGreen(outcome)) return { reason: 'Goal met.', next: CHANGE, line: GOAL_MET_LINE, actions: [] };
|
|
557
556
|
return { reason: 'Goal met.', next: 'Nothing to do.', line: GOAL_MET_LINE, actions: REUSE };
|
|
558
557
|
}
|
|
559
558
|
if (outcome === 'cap-halt') {
|
|
@@ -1530,7 +1529,8 @@ function scheduleExtrasFor(runid, job, rows, nos, opts, view) {
|
|
|
1530
1529
|
}
|
|
1531
1530
|
const got = hit ?? fallback;
|
|
1532
1531
|
const no = got ? nos.get(got.r.runid) : undefined;
|
|
1533
|
-
|
|
1532
|
+
// events: a fire refused at the run-start fetch has no spine, only its settled event; without them it has no status word
|
|
1533
|
+
const word = got ? summarizeRow(got.r, Date.now(), undefined, readRunList(opts).events).status?.word : undefined;
|
|
1534
1534
|
lastWords = `last ${dateTimeWords(Number(lastRow.at))}${no ? ` · run-${no}` : ''}${word ? ` · ${word}` : ''}${got?.rec.catchUp === true ? ' · catch-up' : ''}`;
|
|
1535
1535
|
} else {
|
|
1536
1536
|
lastWords = `last ${dateTimeWords(Number(lastRow.at))} · ${skippedWords(lastRow, true)}`;
|
|
@@ -3514,7 +3514,7 @@ export function getStartFrom(runid, opts = {}) {
|
|
|
3514
3514
|
// T D.2: a SCHEDULED run's green reads `green-held` until the person accepts it, so that same refusal holds it unchanged
|
|
3515
3515
|
outcome: (() => {
|
|
3516
3516
|
const o = legsOf(records).at(-1)?.outcome ?? null;
|
|
3517
|
-
return o
|
|
3517
|
+
return isReusableGreen(o) && heldScheduled(records, triggerRecordOf(row)) ? 'green-held' : o;
|
|
3518
3518
|
})(),
|
|
3519
3519
|
card,
|
|
3520
3520
|
from: 'signed job',
|
|
@@ -3579,7 +3579,7 @@ export function listReuseJobs(opts = {}) {
|
|
|
3579
3579
|
try { records = parseJsonl(row.spine).records; } catch { continue; }
|
|
3580
3580
|
const outcome = legsOf(records).at(-1)?.outcome ?? null;
|
|
3581
3581
|
// a real green or an already-green (hamr 2026-10-15: if it is already fixed and nothing is to be done, it is green and can be reused)
|
|
3582
|
-
if (outcome
|
|
3582
|
+
if (!isReusableGreen(outcome)) continue;
|
|
3583
3583
|
if (heldScheduled(records, triggerRecordOf(row))) continue; // T D.2: a scheduled green waits for the person's Accept
|
|
3584
3584
|
const signed = signedSpecForRun(row, records);
|
|
3585
3585
|
if (!signed) continue;
|
package/src/panel/status.js
CHANGED
|
@@ -20,6 +20,17 @@ import { capKindOf } from '../legs.js';
|
|
|
20
20
|
/** the sentence a passed run carries as its reason (the Ended block's `line`) */
|
|
21
21
|
export const GOAL_MET_LINE = 'goal met';
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* ONE spelling of "this run's outcome is a green that can be reused" (the Reuse picker, the Reuse start/prefill, the Ended block's
|
|
25
|
+
* next line). hamr 2026-10-15: an `already-green` (fixed before any work) counts. `satisfied` is a close verdict, never a run outcome.
|
|
26
|
+
* The SCHEDULE gate (schedules.js greenFor) deliberately stays on a real `green` only (T3).
|
|
27
|
+
* @param {string|null|undefined} outcome
|
|
28
|
+
* @returns {boolean}
|
|
29
|
+
*/
|
|
30
|
+
export function isReusableGreen(outcome) {
|
|
31
|
+
return outcome === 'green' || outcome === 'already-green';
|
|
32
|
+
}
|
|
33
|
+
|
|
23
34
|
/** @typedef {'running'|'waiting'|'passed'|'failed'|'capped'|'stopped'|'died'} StatusKey */
|
|
24
35
|
|
|
25
36
|
/** the table: key -> sign + word */
|
package/src/stagewords.js
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
// src/panel/server.js (verbatim) so the Audit/step map AND the learnings cards read the same sentence from the same
|
|
3
3
|
// code — a second spelling is how two screens come to disagree about one check. Pure; no file IO, no model.
|
|
4
4
|
|
|
5
|
+
/** the genre-owned noun for the count a TYPES-genre typecheck stage measures (see {@link stageQuestionText}) */
|
|
6
|
+
export const GENRE_WHAT = Object.freeze({ TYPES: 'type errors' });
|
|
7
|
+
|
|
8
|
+
/** `cmd`+`args`, as a comparable key — the one signal (besides `kind`
|
|
9
|
+
* itself) that two stages measure the SAME command's output, used to tell a
|
|
10
|
+
* genuine typecheck stage apart from the TYPES genre's other `lower-is-
|
|
11
|
+
* better`/`baseline: 0` stage (the failing-test-count half of `suite-green`'s
|
|
12
|
+
* "two assertions"), which shares that stage's own shape but not its
|
|
13
|
+
* population. `null` for a stage whose `cmd` isn't a non-empty string —
|
|
14
|
+
* never matches anything, so it never falsely suppresses a real typecheck
|
|
15
|
+
* question.
|
|
16
|
+
* @param {any} params @returns {string|null} */
|
|
17
|
+
export function cmdKey(params) {
|
|
18
|
+
if (!params || typeof params.cmd !== 'string' || params.cmd.length === 0) return null;
|
|
19
|
+
const args = Array.isArray(params.args) ? params.args : [];
|
|
20
|
+
return JSON.stringify([params.cmd, args]);
|
|
21
|
+
}
|
|
22
|
+
|
|
5
23
|
/**
|
|
6
24
|
* THE PLAIN-QUESTION OWNER (plain-checks build, 2026-09-28, hamr's ruling:
|
|
7
25
|
* the "checks N/M" headline is WITHDRAWN — "confusing, reads like 6 failed
|
|
@@ -64,23 +82,6 @@
|
|
|
64
82
|
* @param {{kind: string|null, params: any, genre: string|null, suiteCmdKeys: Set<string>}} o
|
|
65
83
|
* @returns {string|null}
|
|
66
84
|
*/
|
|
67
|
-
export const GENRE_WHAT = Object.freeze({ TYPES: 'type errors' });
|
|
68
|
-
|
|
69
|
-
/** `cmd`+`args`, as a comparable key — the one signal (besides `kind`
|
|
70
|
-
* itself) that two stages measure the SAME command's output, used to tell a
|
|
71
|
-
* genuine typecheck stage apart from the TYPES genre's other `lower-is-
|
|
72
|
-
* better`/`baseline: 0` stage (the failing-test-count half of `suite-green`'s
|
|
73
|
-
* "two assertions"), which shares that stage's own shape but not its
|
|
74
|
-
* population. `null` for a stage whose `cmd` isn't a non-empty string —
|
|
75
|
-
* never matches anything, so it never falsely suppresses a real typecheck
|
|
76
|
-
* question.
|
|
77
|
-
* @param {any} params @returns {string|null} */
|
|
78
|
-
export function cmdKey(params) {
|
|
79
|
-
if (!params || typeof params.cmd !== 'string' || params.cmd.length === 0) return null;
|
|
80
|
-
const args = Array.isArray(params.args) ? params.args : [];
|
|
81
|
-
return JSON.stringify([params.cmd, args]);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
85
|
export function stageQuestionText({
|
|
85
86
|
kind, params, genre, suiteCmdKeys,
|
|
86
87
|
}) {
|
package/types/authorreadout.d.ts
CHANGED
|
@@ -303,6 +303,13 @@ export function redsRecord(reds: PlainRed[], { cases, signing }?: {
|
|
|
303
303
|
}): Record<string, any>[];
|
|
304
304
|
/** the stops this module explains in plain words */
|
|
305
305
|
export const PLAIN_PROPOSAL_STOPS: readonly string[];
|
|
306
|
+
/**
|
|
307
|
+
* ONE owner of the plain sentence for every drafting stop code (hamr 2026-10-09: "a user should get a reason they can
|
|
308
|
+
* understand and do something about"). What happened; `STOP_TODO` below overrides the default "draft again" where that is wrong.
|
|
309
|
+
* Used by `draftStopText` for every stop the panel and the CLI show; a code not listed reads `STOP_UNKNOWN`, never the raw code.
|
|
310
|
+
* @type {Record<string, string>}
|
|
311
|
+
*/
|
|
312
|
+
export const STOP_HEADS: Record<string, string>;
|
|
306
313
|
/** codes the declaration gates (validateJob, validateCloseDecl) raise: one plain sentence covers the family */
|
|
307
314
|
export const DECLARATION_CODES: readonly string[];
|
|
308
315
|
export type PlainRed = {
|
package/types/panel/status.d.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ONE spelling of "this run's outcome is a green that can be reused" (the Reuse picker, the Reuse start/prefill, the Ended block's
|
|
3
|
+
* next line). hamr 2026-10-15: an `already-green` (fixed before any work) counts. `satisfied` is a close verdict, never a run outcome.
|
|
4
|
+
* The SCHEDULE gate (schedules.js greenFor) deliberately stays on a real `green` only (T3).
|
|
5
|
+
* @param {string|null|undefined} outcome
|
|
6
|
+
* @returns {boolean}
|
|
7
|
+
*/
|
|
8
|
+
export function isReusableGreen(outcome: string | null | undefined): boolean;
|
|
1
9
|
/**
|
|
2
10
|
* The status of one run.
|
|
3
11
|
* @param {{outcome: string|null|undefined, died?: boolean, category?: string|null, moneyHalt?: boolean}} o
|
package/types/stagewords.d.ts
CHANGED
|
@@ -8,21 +8,6 @@
|
|
|
8
8
|
* question.
|
|
9
9
|
* @param {any} params @returns {string|null} */
|
|
10
10
|
export function cmdKey(params: any): string | null;
|
|
11
|
-
export function stageQuestionText({ kind, params, genre, suiteCmdKeys, }: {
|
|
12
|
-
kind: any;
|
|
13
|
-
params: any;
|
|
14
|
-
genre: any;
|
|
15
|
-
suiteCmdKeys: any;
|
|
16
|
-
}): string | null;
|
|
17
|
-
/**
|
|
18
|
-
* Stage name to its plain question, off a signed spec's `closeDecl.stages` — the one pass that gathers every
|
|
19
|
-
* `command-exit` stage's cmd+args FIRST (so the failing-test-count half of `suite-green`'s pair can be told from a
|
|
20
|
-
* genuine typecheck stage) and then asks {@link stageQuestionText} per stage. `null` for a stage with no honest
|
|
21
|
-
* question; an empty map for a spec with no declared stages.
|
|
22
|
-
* @param {any} spec
|
|
23
|
-
* @returns {Map<string, string|null>}
|
|
24
|
-
*/
|
|
25
|
-
export function stageQuestionsByName(spec: any): Map<string, string | null>;
|
|
26
11
|
/**
|
|
27
12
|
* THE PLAIN-QUESTION OWNER (plain-checks build, 2026-09-28, hamr's ruling:
|
|
28
13
|
* the "checks N/M" headline is WITHDRAWN — "confusing, reads like 6 failed
|
|
@@ -85,6 +70,22 @@ export function stageQuestionsByName(spec: any): Map<string, string | null>;
|
|
|
85
70
|
* @param {{kind: string|null, params: any, genre: string|null, suiteCmdKeys: Set<string>}} o
|
|
86
71
|
* @returns {string|null}
|
|
87
72
|
*/
|
|
73
|
+
export function stageQuestionText({ kind, params, genre, suiteCmdKeys, }: {
|
|
74
|
+
kind: string | null;
|
|
75
|
+
params: any;
|
|
76
|
+
genre: string | null;
|
|
77
|
+
suiteCmdKeys: Set<string>;
|
|
78
|
+
}): string | null;
|
|
79
|
+
/**
|
|
80
|
+
* Stage name to its plain question, off a signed spec's `closeDecl.stages` — the one pass that gathers every
|
|
81
|
+
* `command-exit` stage's cmd+args FIRST (so the failing-test-count half of `suite-green`'s pair can be told from a
|
|
82
|
+
* genuine typecheck stage) and then asks {@link stageQuestionText} per stage. `null` for a stage with no honest
|
|
83
|
+
* question; an empty map for a spec with no declared stages.
|
|
84
|
+
* @param {any} spec
|
|
85
|
+
* @returns {Map<string, string|null>}
|
|
86
|
+
*/
|
|
87
|
+
export function stageQuestionsByName(spec: any): Map<string, string | null>;
|
|
88
|
+
/** the genre-owned noun for the count a TYPES-genre typecheck stage measures (see {@link stageQuestionText}) */
|
|
88
89
|
export const GENRE_WHAT: Readonly<{
|
|
89
90
|
TYPES: "type errors";
|
|
90
91
|
}>;
|