@pome-sh/cli 0.16.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -5
- package/dist/build-info.json +3 -3
- package/dist/src/cli/checks-add.d.ts +2 -7
- package/dist/src/cli/checks-add.js +10 -24
- package/dist/src/cli/eval.js +12 -10
- package/dist/src/cli/main.js +1 -1
- package/dist/src/cli/vocabulary-skew.d.ts +64 -0
- package/dist/src/cli/vocabulary-skew.js +187 -0
- package/dist/src/hosted/evalResultView.d.ts +1 -1
- package/dist/src/hosted/evalResultView.js +22 -5
- package/dist/src/runner/groupRender.d.ts +12 -2
- package/dist/src/runner/groupRender.js +32 -8
- package/dist/src/runner/runTaskHosted.d.ts +8 -1
- package/dist/src/runner/runTaskHosted.js +11 -1
- package/dist/src/runner/runTrialGroup.js +15 -2
- package/dist/src/twin/githubCloneAdapter.d.ts +1 -0
- package/node_modules/@pome-sh/twin-github/FIDELITY.md +14 -0
- package/node_modules/@pome-sh/twin-github/dist/src/check-pulls.d.ts +4 -0
- package/node_modules/@pome-sh/twin-github/dist/src/check-pulls.js +93 -0
- package/node_modules/@pome-sh/twin-github/dist/src/check-pulls.js.map +1 -1
- package/node_modules/@pome-sh/twin-github/dist/src/check-state.d.ts +1 -0
- package/node_modules/@pome-sh/twin-github/dist/src/check-state.js.map +1 -1
- package/node_modules/@pome-sh/twin-github/dist/src/checks.d.ts +3 -0
- package/node_modules/@pome-sh/twin-github/dist/src/checks.js +2 -1
- package/node_modules/@pome-sh/twin-github/dist/src/checks.js.map +1 -1
- package/node_modules/@pome-sh/twin-github/dist/src/db.js +46 -4
- package/node_modules/@pome-sh/twin-github/dist/src/db.js.map +1 -1
- package/node_modules/@pome-sh/twin-github/dist/src/domain/github-domain.d.ts +3 -0
- package/node_modules/@pome-sh/twin-github/dist/src/domain/github-domain.js +49 -1
- package/node_modules/@pome-sh/twin-github/dist/src/domain/github-domain.js.map +1 -1
- package/node_modules/@pome-sh/twin-github/dist/src/domain/issues.js +15 -5
- package/node_modules/@pome-sh/twin-github/dist/src/domain/issues.js.map +1 -1
- package/node_modules/@pome-sh/twin-github/dist/src/serializers.d.ts +1 -1
- package/node_modules/@pome-sh/twin-github/dist/src/serializers.js +8 -2
- package/node_modules/@pome-sh/twin-github/dist/src/serializers.js.map +1 -1
- package/node_modules/@pome-sh/twin-github/package.json +1 -1
- package/node_modules/@pome-sh/twin-linear/CHANGELOG.md +101 -0
- package/node_modules/@pome-sh/twin-linear/dist/src/db.d.ts +6 -0
- package/node_modules/@pome-sh/twin-linear/dist/src/db.js +53 -4
- package/node_modules/@pome-sh/twin-linear/dist/src/db.js.map +1 -1
- package/node_modules/@pome-sh/twin-linear/dist/src/domain/agents.d.ts +16 -13
- package/node_modules/@pome-sh/twin-linear/dist/src/domain/agents.js +16 -12
- package/node_modules/@pome-sh/twin-linear/dist/src/domain/agents.js.map +1 -1
- package/node_modules/@pome-sh/twin-linear/dist/src/domain/comments.js +1 -1
- package/node_modules/@pome-sh/twin-linear/dist/src/domain/comments.js.map +1 -1
- package/node_modules/@pome-sh/twin-linear/dist/src/domain/documents.js +8 -8
- package/node_modules/@pome-sh/twin-linear/dist/src/domain/documents.js.map +1 -1
- package/node_modules/@pome-sh/twin-linear/dist/src/domain/issues.js +20 -17
- package/node_modules/@pome-sh/twin-linear/dist/src/domain/issues.js.map +1 -1
- package/node_modules/@pome-sh/twin-linear/dist/src/domain/labels.js +1 -1
- package/node_modules/@pome-sh/twin-linear/dist/src/domain/labels.js.map +1 -1
- package/node_modules/@pome-sh/twin-linear/dist/src/domain/linear-domain.d.ts +4 -17
- package/node_modules/@pome-sh/twin-linear/dist/src/domain/linear-domain.js.map +1 -1
- package/node_modules/@pome-sh/twin-linear/dist/src/domain/normalize.d.ts +12 -2
- package/node_modules/@pome-sh/twin-linear/dist/src/domain/normalize.js +28 -5
- package/node_modules/@pome-sh/twin-linear/dist/src/domain/normalize.js.map +1 -1
- package/node_modules/@pome-sh/twin-linear/dist/src/domain/projects-cycles.js +1 -1
- package/node_modules/@pome-sh/twin-linear/dist/src/domain/projects-cycles.js.map +1 -1
- package/node_modules/@pome-sh/twin-linear/dist/src/domain/rows.d.ts +3 -3
- package/node_modules/@pome-sh/twin-linear/dist/src/domain/rows.js +4 -3
- package/node_modules/@pome-sh/twin-linear/dist/src/domain/rows.js.map +1 -1
- package/node_modules/@pome-sh/twin-linear/dist/src/graphql/formatters.js +3 -3
- package/node_modules/@pome-sh/twin-linear/dist/src/graphql/formatters.js.map +1 -1
- package/node_modules/@pome-sh/twin-linear/dist/src/graphql/mutation-inputs.d.ts +30 -12
- package/node_modules/@pome-sh/twin-linear/dist/src/graphql/mutation-inputs.js +33 -28
- package/node_modules/@pome-sh/twin-linear/dist/src/graphql/mutation-inputs.js.map +1 -1
- package/node_modules/@pome-sh/twin-linear/dist/src/graphql/resolvers.js +2 -2
- package/node_modules/@pome-sh/twin-linear/dist/src/graphql/resolvers.js.map +1 -1
- package/node_modules/@pome-sh/twin-linear/dist/src/graphql/schema.js +31 -13
- package/node_modules/@pome-sh/twin-linear/dist/src/graphql/schema.js.map +1 -1
- package/node_modules/@pome-sh/twin-linear/dist/src/state.js +4 -4
- package/node_modules/@pome-sh/twin-linear/dist/src/state.js.map +1 -1
- package/node_modules/@pome-sh/twin-linear/dist/src/types.d.ts +10 -4
- package/node_modules/@pome-sh/twin-linear/dist/src/types.js.map +1 -1
- package/node_modules/@pome-sh/twin-linear/fixtures/README.md +1 -0
- package/node_modules/@pome-sh/twin-linear/fixtures/linear-introspection.json +66 -0
- package/node_modules/@pome-sh/twin-linear/package.json +2 -1
- package/node_modules/@pome-sh/twin-stripe/FIDELITY.md +8 -0
- package/node_modules/@pome-sh/twin-stripe/dist/src/idempotency.d.ts +23 -1
- package/node_modules/@pome-sh/twin-stripe/dist/src/idempotency.js +40 -6
- package/node_modules/@pome-sh/twin-stripe/dist/src/idempotency.js.map +1 -1
- package/node_modules/@pome-sh/twin-stripe/dist/src/routes/_helpers.js +5 -0
- package/node_modules/@pome-sh/twin-stripe/dist/src/routes/_helpers.js.map +1 -1
- package/node_modules/@pome-sh/twin-stripe/dist/src/twin.js +7 -1
- package/node_modules/@pome-sh/twin-stripe/dist/src/twin.js.map +1 -1
- package/node_modules/@pome-sh/twin-stripe/package.json +1 -1
- package/package.json +4 -4
- package/tasks/14-stripe-refund-retry.md +17 -14
package/README.md
CHANGED
|
@@ -64,22 +64,30 @@ code is the verdict**. Gate CI on it directly.
|
|
|
64
64
|
| Exit code | Meaning |
|
|
65
65
|
| --- | --- |
|
|
66
66
|
| `0` | pass (hosted/scored run), or trace captured (`--local`, not scored) |
|
|
67
|
-
| `1` | ran and scored **below** the pass threshold |
|
|
67
|
+
| `1` | ran and scored **below** the pass threshold, **or ran `INCOMPLETE`** |
|
|
68
68
|
| `2` | twin / orchestration error (network, 5xx, twin spawn failed) |
|
|
69
69
|
| `3` | auth error (401/403) — `pome login` again, or set `POME_API_KEY` in CI |
|
|
70
70
|
| `4` | quota exceeded (402/429) |
|
|
71
71
|
| `5` | usage error (bad flags, missing task file) |
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
Three rules CI must honor:
|
|
74
74
|
|
|
75
75
|
- **`--local` is not a verdict.** A `--local` run captures a raw trace and never
|
|
76
76
|
scores, so its exit `0` means "trace captured," not "passed." Never gate CI on
|
|
77
77
|
a `--local` exit code — score it later with `pome eval <run-dir>`.
|
|
78
|
+
- **`INCOMPLETE` shares exit `1`, and it is not the agent's failure.** A run
|
|
79
|
+
whose criteria could not all be graded exits `1` rather than mapping its
|
|
80
|
+
partial score to a code — a run whose checks never ran is not a green CI
|
|
81
|
+
signal. The cost is stated rather than hidden: **`1` cannot tell "the agent
|
|
82
|
+
regressed" from "we could not grade it."** Read the verdict word printed
|
|
83
|
+
beside the score (`INCOMPLETE` vs a sub-threshold number) to separate them.
|
|
78
84
|
- **Trial groups map as a whole.** `pome run -n k` (k>1) collapses the whole
|
|
79
85
|
group to one code: `0` = at least one trial completed and every completed
|
|
80
|
-
trial passed; `1` = at least one completed trial failed its threshold
|
|
81
|
-
no trial completed. Errored trials are
|
|
82
|
-
|
|
86
|
+
trial passed; `1` = at least one completed trial failed its threshold **or was
|
|
87
|
+
incomplete**; `2` = no trial completed. Errored and incomplete trials are
|
|
88
|
+
excluded from the verdict fraction (`3 of 4 passed · 1 incomplete`) so neither
|
|
89
|
+
is counted as a pass nor charged to the agent as a loss — but a group holding
|
|
90
|
+
one cannot exit `0`.
|
|
83
91
|
|
|
84
92
|
## Development
|
|
85
93
|
|
package/dist/build-info.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"package": "pome-sh",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"git_sha": "
|
|
5
|
-
"build_time": "2026-
|
|
3
|
+
"version": "0.18.0",
|
|
4
|
+
"git_sha": "285d48dcd654088387b40e8d36a7a2bf8ffbb599",
|
|
5
|
+
"build_time": "2026-08-03T13:39:43.286Z"
|
|
6
6
|
}
|
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
import { type InteractiveSeams } from "./agent-resolver.js";
|
|
2
|
-
|
|
2
|
+
import { type RemoteVocabulary } from "./vocabulary-skew.js";
|
|
3
|
+
export interface RemoteChecks extends RemoteVocabulary {
|
|
3
4
|
twin: string;
|
|
4
|
-
digest: string;
|
|
5
|
-
checks: Array<{
|
|
6
|
-
id: string;
|
|
7
|
-
template: string;
|
|
8
|
-
substrate: string;
|
|
9
|
-
}>;
|
|
10
5
|
}
|
|
11
6
|
export type HandshakeResult = {
|
|
12
7
|
kind: "match";
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
* the cloud resolves them from a different one. Before writing, ask the cloud
|
|
9
9
|
* for the vocabulary it GRADES against and compare digests.
|
|
10
10
|
* equal → write, silently
|
|
11
|
-
* different → REFUSE, and name
|
|
11
|
+
* different → REFUSE, and name what moved (`vocabulary-skew.ts`, F-1137 —
|
|
12
|
+
* a check, a field of one, or the sdk that compiled it, but
|
|
13
|
+
* never an empty list)
|
|
12
14
|
* unreachable → write from the local pin with a named note on stderr
|
|
13
15
|
* Offline authoring is real, and the degraded path's worst case is bounded: a
|
|
14
16
|
* stale sentence that keeps a template's literal segments fails at finalize as
|
|
@@ -22,6 +24,7 @@ import { resolveSeams } from "./agent-resolver.js";
|
|
|
22
24
|
import { SUBSTRATE_HELP, checksFor, displaySentence, findCheck, localDigest, pinnedVersion, twinOf, } from "./checks.js";
|
|
23
25
|
import { resolveCredentials } from "./credentials.js";
|
|
24
26
|
import { auditCodeCriteria, formatBindingReport } from "./criterion-binding.js";
|
|
27
|
+
import { explainSkew, formatSkewRefusal } from "./vocabulary-skew.js";
|
|
25
28
|
export function parseArgFlags(pairs) {
|
|
26
29
|
const args = {};
|
|
27
30
|
for (const pair of pairs) {
|
|
@@ -47,29 +50,12 @@ export async function handshake(twin, fetchRemote) {
|
|
|
47
50
|
}
|
|
48
51
|
if (localDigest(twin) === remote.digest)
|
|
49
52
|
return { kind: "match" };
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
else if (there.get(id) !== template) {
|
|
57
|
-
moved.push(`${id} — the sentence differs\n here: ${template}\n cloud: ${there.get(id)}`);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
for (const id of there.keys()) {
|
|
61
|
-
if (!here.has(id))
|
|
62
|
-
moved.push(`${id} — the cloud has it, this CLI does not`);
|
|
63
|
-
}
|
|
64
|
-
return {
|
|
65
|
-
kind: "skew",
|
|
66
|
-
message: `Refusing to write: this CLI and the cloud disagree about ${twin}'s vocabulary, so a ` +
|
|
67
|
-
`sentence written here might not be graded there.\n` +
|
|
68
|
-
moved.map((line) => ` - ${line}`).join("\n") +
|
|
69
|
-
`\n\n local @pome-sh/twin-${twin} ${pin}` +
|
|
70
|
-
`\n Update with \`npm i -g @pome-sh/cli@latest\`. If this CLI is already current, the ` +
|
|
71
|
-
`cloud is behind — that is a deploy, not something you can fix here.`,
|
|
72
|
-
};
|
|
53
|
+
// F-1137 — the taxonomy lives in `vocabulary-skew.ts` and is NON-EMPTY by
|
|
54
|
+
// construction, so this refusal cannot name the disagreement and then list
|
|
55
|
+
// nothing. Which is what it did whenever the skew was in `substrate` or in the
|
|
56
|
+
// compiled pattern: the two fields `checksDigest` hashes that the old
|
|
57
|
+
// id-and-template diff never looked at.
|
|
58
|
+
return { kind: "skew", message: formatSkewRefusal(twin, explainSkew(twin, remote)) };
|
|
73
59
|
}
|
|
74
60
|
async function liveFetch(creds, twin) {
|
|
75
61
|
const res = await fetch(`${creds.apiBaseUrl}/v1/checks?twin=${encodeURIComponent(twin)}`, {
|
package/dist/src/cli/eval.js
CHANGED
|
@@ -296,7 +296,7 @@ export async function runEval(options) {
|
|
|
296
296
|
return client.finalize(sid, {
|
|
297
297
|
stopReason: "eval_upload",
|
|
298
298
|
// /finalize's schema requires an integer exit_code, so `null` (agent
|
|
299
|
-
// timed out, or meta.json lacked the field)
|
|
299
|
+
// timed out, or meta.json lacked the field) is not a legal value.
|
|
300
300
|
// Send -1 as the explicit "unknown" sentinel — never a fabricated 0,
|
|
301
301
|
// which would report a clean agent exit the trace can't vouch for.
|
|
302
302
|
exitCode: artifacts.meta.exitCode ?? -1,
|
|
@@ -340,14 +340,16 @@ export async function runEval(options) {
|
|
|
340
340
|
// never persisted next to the trace. Local artifacts stay trace-only (no
|
|
341
341
|
// score.json), and the verdict lives in the cloud (see the dashboard URL).
|
|
342
342
|
const score = scoreFromFinalizeResponse(finalized);
|
|
343
|
-
// Exit-code policy —
|
|
344
|
-
//
|
|
345
|
-
//
|
|
346
|
-
//
|
|
347
|
-
//
|
|
348
|
-
//
|
|
349
|
-
//
|
|
350
|
-
//
|
|
343
|
+
// Exit-code policy — the full FDRS-591/611 A5 guard: exit 0 ONLY when the run
|
|
344
|
+
// was evaluated, every criterion was judged (can_pass), AND the score clears
|
|
345
|
+
// the threshold. An INCOMPLETE verdict (any criterion not evaluated) exits 1.
|
|
346
|
+
//
|
|
347
|
+
// F-925 retired the divergence that used to be documented here. `pome run`
|
|
348
|
+
// mapped the raw cloud score because "pre-FDRS-618 cloud builds don't emit
|
|
349
|
+
// criteria_results" — but `scoreFromFinalizeResponse` already handles that
|
|
350
|
+
// case (`hasCriteriaResults ? … : true`), so the guard degrades to score-only
|
|
351
|
+
// for exactly those builds on its own. The divergence was protecting a case
|
|
352
|
+
// its own helper already protected, and the two commands now agree.
|
|
351
353
|
const exitCode = scoreStatus(score, EVAL_PASS_THRESHOLD) === "pass" ? 0 : 1;
|
|
352
354
|
return {
|
|
353
355
|
taskName,
|
|
@@ -403,7 +405,7 @@ export async function runEvalCommand(runDirArg, opts) {
|
|
|
403
405
|
});
|
|
404
406
|
// Same verdict shape as hosted `pome run`: LABEL, score line, cloud URL.
|
|
405
407
|
const status = scoreStatus(result.score, EVAL_PASS_THRESHOLD);
|
|
406
|
-
const label = status === "pass" ? "PASS" : status === "fail" ? "FAIL" : "
|
|
408
|
+
const label = status === "pass" ? "PASS" : status === "fail" ? "FAIL" : "INCOMPLETE";
|
|
407
409
|
console.error(`${label} ${result.taskName}`);
|
|
408
410
|
console.error(` ${runScoreLine(result.score, EVAL_PASS_THRESHOLD, "cloud score")}`);
|
|
409
411
|
if (result.score.results.length > 0) {
|
package/dist/src/cli/main.js
CHANGED
|
@@ -660,7 +660,7 @@ export function createProgram() {
|
|
|
660
660
|
agentVersion: options.agentVersion,
|
|
661
661
|
});
|
|
662
662
|
const status = scoreStatus(result.score, result.scenario.config.passThreshold);
|
|
663
|
-
const label = status === "pass" ? "PASS" : status === "fail" ? "FAIL" : "
|
|
663
|
+
const label = status === "pass" ? "PASS" : status === "fail" ? "FAIL" : "INCOMPLETE";
|
|
664
664
|
console.error(`${label} ${result.scenario.title}`);
|
|
665
665
|
console.error(` ${runScoreLine(result.score, result.scenario.config.passThreshold, "cloud score")}`);
|
|
666
666
|
console.error(` local: ${result.artifacts.runDir}`);
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** One check as `GET /v1/checks` publishes it. */
|
|
2
|
+
export interface RemoteCheck {
|
|
3
|
+
id: string;
|
|
4
|
+
template: string;
|
|
5
|
+
substrate: string;
|
|
6
|
+
/** The compiled regex source. Optional because a control plane that does not
|
|
7
|
+
* publish it must degrade to a named class, never to a crash or to silence. */
|
|
8
|
+
pattern?: string;
|
|
9
|
+
/** Ordered as the template names them — the compiled pattern wraps one group
|
|
10
|
+
* per slot in that order, so the ORDER is part of what the digest sees. */
|
|
11
|
+
params?: Array<{
|
|
12
|
+
name: string;
|
|
13
|
+
pattern: string;
|
|
14
|
+
}>;
|
|
15
|
+
}
|
|
16
|
+
export interface RemoteVocabulary {
|
|
17
|
+
digest: string;
|
|
18
|
+
checks: RemoteCheck[];
|
|
19
|
+
}
|
|
20
|
+
export type SkewFinding = {
|
|
21
|
+
kind: "only_here";
|
|
22
|
+
check: string;
|
|
23
|
+
} | {
|
|
24
|
+
kind: "only_there";
|
|
25
|
+
check: string;
|
|
26
|
+
} | {
|
|
27
|
+
kind: "template";
|
|
28
|
+
check: string;
|
|
29
|
+
here: string;
|
|
30
|
+
there: string;
|
|
31
|
+
} | {
|
|
32
|
+
kind: "substrate";
|
|
33
|
+
check: string;
|
|
34
|
+
here: string;
|
|
35
|
+
there: string;
|
|
36
|
+
} | {
|
|
37
|
+
kind: "params";
|
|
38
|
+
check: string;
|
|
39
|
+
here: string;
|
|
40
|
+
there: string;
|
|
41
|
+
} | {
|
|
42
|
+
kind: "pattern";
|
|
43
|
+
check: string;
|
|
44
|
+
here: string;
|
|
45
|
+
there: string;
|
|
46
|
+
paramsCompared: boolean;
|
|
47
|
+
} | {
|
|
48
|
+
kind: "pattern_generation";
|
|
49
|
+
check: null;
|
|
50
|
+
here: string;
|
|
51
|
+
there: string;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Every way this CLI's vocabulary for `twin` differs from the one the cloud
|
|
55
|
+
* published, as at least one named finding.
|
|
56
|
+
*
|
|
57
|
+
* Ordered by check id, so the readout is a property of the difference rather
|
|
58
|
+
* than of either side's declaration order.
|
|
59
|
+
*/
|
|
60
|
+
export declare function explainSkew(twin: string, remote: RemoteVocabulary): [SkewFinding, ...SkewFinding[]];
|
|
61
|
+
/** The refusal an author reads. Names the twin pin always, and the sdk pin only
|
|
62
|
+
* when the sdk is what the findings implicate — a pin nobody needs is noise in a
|
|
63
|
+
* message someone is reading because they are already blocked. */
|
|
64
|
+
export declare function formatSkewRefusal(twin: string, findings: readonly SkewFinding[]): string;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* F-1137 — WHY do this CLI and the cloud disagree about a twin's vocabulary?
|
|
4
|
+
*
|
|
5
|
+
* `checksDigest` hashes three fields per check: `id`, `substrate`, and the
|
|
6
|
+
* COMPILED pattern (`packages/sdk/src/checks.ts`). The refusal in `checks-add.ts`
|
|
7
|
+
* used to build its "which check moved" list from `id` and `template` — so a skew
|
|
8
|
+
* that moved only `substrate`, or only `buildPattern`'s output while every
|
|
9
|
+
* template was byte-identical, refused correctly and then named NOTHING. A named
|
|
10
|
+
* refusal that names nothing, in exactly the two cases the digest was widened to
|
|
11
|
+
* catch, during a situation that already blocks the author.
|
|
12
|
+
*
|
|
13
|
+
* The fix is a taxonomy with no silent branch: `explainSkew` returns a NON-EMPTY
|
|
14
|
+
* list by construction, and the type says so. The class names are pome-cloud's
|
|
15
|
+
* (`apps/control-plane/src/services/vocabulary-parity.ts`, F-1136) so the two
|
|
16
|
+
* surfaces that explain the same disagreement stay greppable against each other.
|
|
17
|
+
*
|
|
18
|
+
* This side can say more than the cloud-side monitor can. `GET /v1/checks`
|
|
19
|
+
* publishes the compiled `pattern` and the parameter patterns (F-1074 Phase 3),
|
|
20
|
+
* which the CLI's own `checks --json` does not — so a generator-only skew is
|
|
21
|
+
* localisable to a check HERE, and only falls back to the unlocalised
|
|
22
|
+
* `pattern_generation` class when the control plane published nothing to diff.
|
|
23
|
+
*
|
|
24
|
+
* `description` and `params[].example` are compared by nothing here on purpose:
|
|
25
|
+
* `checksDigest` does not hash them, so a difference in either cannot be what
|
|
26
|
+
* moved the digest, and comparing them could only manufacture false findings.
|
|
27
|
+
*/
|
|
28
|
+
import { checkPattern, templateSlots } from "@pome-sh/sdk/checks";
|
|
29
|
+
import { checksFor, localDigest, pinnedVersion } from "./checks.js";
|
|
30
|
+
function paramsHere(def) {
|
|
31
|
+
return templateSlots(def.template)
|
|
32
|
+
.params.map((name) => `${name}=${def.params[name].pattern}`)
|
|
33
|
+
.join(", ");
|
|
34
|
+
}
|
|
35
|
+
function paramsThere(check) {
|
|
36
|
+
return (check.params ?? []).map((p) => `${p.name}=${p.pattern}`).join(", ");
|
|
37
|
+
}
|
|
38
|
+
/** The fields BOTH sides always publish. Returns empty when the declarations
|
|
39
|
+
* agree — which is the precondition for reading anything into the compiled
|
|
40
|
+
* pattern, since a moved template moves the pattern too and reporting both
|
|
41
|
+
* would bury the cause under its own consequence. */
|
|
42
|
+
function declarationSkew(def, check) {
|
|
43
|
+
const findings = [];
|
|
44
|
+
if (def.template !== check.template) {
|
|
45
|
+
findings.push({ kind: "template", check: def.id, here: def.template, there: check.template });
|
|
46
|
+
}
|
|
47
|
+
if (def.substrate !== check.substrate) {
|
|
48
|
+
findings.push({
|
|
49
|
+
kind: "substrate",
|
|
50
|
+
check: def.id,
|
|
51
|
+
here: def.substrate,
|
|
52
|
+
there: check.substrate,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
if (check.params !== undefined && paramsHere(def) !== paramsThere(check)) {
|
|
56
|
+
findings.push({
|
|
57
|
+
kind: "params",
|
|
58
|
+
check: def.id,
|
|
59
|
+
here: paramsHere(def),
|
|
60
|
+
there: paramsThere(check),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return findings;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Every way this CLI's vocabulary for `twin` differs from the one the cloud
|
|
67
|
+
* published, as at least one named finding.
|
|
68
|
+
*
|
|
69
|
+
* Ordered by check id, so the readout is a property of the difference rather
|
|
70
|
+
* than of either side's declaration order.
|
|
71
|
+
*/
|
|
72
|
+
export function explainSkew(twin, remote) {
|
|
73
|
+
const here = new Map(checksFor(twin).map((def) => [def.id, def]));
|
|
74
|
+
const there = new Map(remote.checks.map((check) => [check.id, check]));
|
|
75
|
+
const findings = [];
|
|
76
|
+
for (const id of [...new Set([...here.keys(), ...there.keys()])].sort()) {
|
|
77
|
+
const def = here.get(id);
|
|
78
|
+
const check = there.get(id);
|
|
79
|
+
if (!check) {
|
|
80
|
+
findings.push({ kind: "only_here", check: id });
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (!def) {
|
|
84
|
+
findings.push({ kind: "only_there", check: id });
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const declaration = declarationSkew(def, check);
|
|
88
|
+
if (declaration.length > 0) {
|
|
89
|
+
findings.push(...declaration);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const compiled = checkPattern(def).source;
|
|
93
|
+
if (check.pattern !== undefined && check.pattern !== compiled) {
|
|
94
|
+
findings.push({
|
|
95
|
+
kind: "pattern",
|
|
96
|
+
check: id,
|
|
97
|
+
here: compiled,
|
|
98
|
+
there: check.pattern,
|
|
99
|
+
paramsCompared: check.params !== undefined,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const [first, ...rest] = findings;
|
|
104
|
+
if (!first) {
|
|
105
|
+
return [
|
|
106
|
+
{
|
|
107
|
+
kind: "pattern_generation",
|
|
108
|
+
check: null,
|
|
109
|
+
here: localDigest(twin),
|
|
110
|
+
there: remote.digest,
|
|
111
|
+
},
|
|
112
|
+
];
|
|
113
|
+
}
|
|
114
|
+
return [first, ...rest];
|
|
115
|
+
}
|
|
116
|
+
function pair(here, there) {
|
|
117
|
+
return [` here: ${here}`, ` cloud: ${there}`];
|
|
118
|
+
}
|
|
119
|
+
// One bullet per class, and the compiler holds the line: the annotated return
|
|
120
|
+
// type plus an exhaustive switch means a class added without a bullet is
|
|
121
|
+
// `error TS2366`, not a line that renders `undefined`. Do NOT add a `default:` —
|
|
122
|
+
// it would buy back the silence this module exists to remove.
|
|
123
|
+
function bullet(finding) {
|
|
124
|
+
switch (finding.kind) {
|
|
125
|
+
case "only_here":
|
|
126
|
+
return [` - ${finding.check} — this CLI has it, the cloud does not`];
|
|
127
|
+
case "only_there":
|
|
128
|
+
return [` - ${finding.check} — the cloud has it, this CLI does not`];
|
|
129
|
+
case "template":
|
|
130
|
+
return [
|
|
131
|
+
` - ${finding.check} — the sentence differs`,
|
|
132
|
+
...pair(finding.here, finding.there),
|
|
133
|
+
];
|
|
134
|
+
case "substrate":
|
|
135
|
+
return [
|
|
136
|
+
` - ${finding.check} — the substrate differs, so the two sides grade it against`,
|
|
137
|
+
` different evidence`,
|
|
138
|
+
...pair(finding.here, finding.there),
|
|
139
|
+
];
|
|
140
|
+
case "params":
|
|
141
|
+
return [
|
|
142
|
+
` - ${finding.check} — the parameters differ`,
|
|
143
|
+
...pair(`[${finding.here}]`, `[${finding.there}]`),
|
|
144
|
+
];
|
|
145
|
+
case "pattern":
|
|
146
|
+
return [
|
|
147
|
+
...(finding.paramsCompared
|
|
148
|
+
? [
|
|
149
|
+
` - ${finding.check} — same sentence and parameters, different compiled`,
|
|
150
|
+
` pattern: the two sides' @pome-sh/sdk compile one declaration differently,`,
|
|
151
|
+
` which is a buildPattern change and not a vocabulary change`,
|
|
152
|
+
]
|
|
153
|
+
: [
|
|
154
|
+
` - ${finding.check} — same sentence, different compiled pattern, and no`,
|
|
155
|
+
` parameter patterns published: either a parameter type or @pome-sh/sdk`,
|
|
156
|
+
` buildPattern moved`,
|
|
157
|
+
]),
|
|
158
|
+
...pair(finding.here, finding.there),
|
|
159
|
+
];
|
|
160
|
+
case "pattern_generation":
|
|
161
|
+
return [
|
|
162
|
+
` - the digests differ, but every field the cloud published matches this CLI's`,
|
|
163
|
+
` declarations. That makes it an @pome-sh/sdk difference — buildPattern, or what`,
|
|
164
|
+
` checksDigest hashes — rather than a vocabulary change`,
|
|
165
|
+
...pair(finding.here, finding.there),
|
|
166
|
+
];
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/** The refusal an author reads. Names the twin pin always, and the sdk pin only
|
|
170
|
+
* when the sdk is what the findings implicate — a pin nobody needs is noise in a
|
|
171
|
+
* message someone is reading because they are already blocked. */
|
|
172
|
+
export function formatSkewRefusal(twin, findings) {
|
|
173
|
+
const sdkImplicated = findings.some((f) => f.kind === "pattern" || f.kind === "pattern_generation");
|
|
174
|
+
return [
|
|
175
|
+
`Refusing to write: this CLI and the cloud disagree about ${twin}'s vocabulary, so a ` +
|
|
176
|
+
`sentence written here might not be graded there.`,
|
|
177
|
+
...findings.flatMap(bullet),
|
|
178
|
+
"",
|
|
179
|
+
` local @pome-sh/twin-${twin} ${pinnedVersion(`@pome-sh/twin-${twin}`)}` +
|
|
180
|
+
(sdkImplicated ? `, @pome-sh/sdk ${pinnedVersion("@pome-sh/sdk")}` : ""),
|
|
181
|
+
...(sdkImplicated
|
|
182
|
+
? [` The cloud publishes no sdk version here, so that is the only pin this CLI can name.`]
|
|
183
|
+
: []),
|
|
184
|
+
` Update with \`npm i -g @pome-sh/cli@latest\`. If this CLI is already current, the ` +
|
|
185
|
+
`cloud is behind — that is a deploy, not something you can fix here.`,
|
|
186
|
+
].join("\n");
|
|
187
|
+
}
|
|
@@ -29,7 +29,7 @@ export type Score = {
|
|
|
29
29
|
judge_tokens_out: number | null;
|
|
30
30
|
};
|
|
31
31
|
export declare function outcomeOf(result: CriterionResult): CriterionOutcome;
|
|
32
|
-
export type ScoreStatus = "pass" | "fail" | "
|
|
32
|
+
export type ScoreStatus = "pass" | "fail" | "incomplete";
|
|
33
33
|
export declare function scoreStatus(score: Score, passThreshold: number): ScoreStatus;
|
|
34
34
|
export declare function taskPassed(score: Score, passThreshold: number): boolean;
|
|
35
35
|
export declare function markerFor(outcome: CriterionOutcome): string;
|
|
@@ -31,9 +31,20 @@ export function outcomeOf(result) {
|
|
|
31
31
|
// Encodes the A5 guard: a run is only a PASS when it was evaluated, every
|
|
32
32
|
// required criterion was evaluated (can_pass), AND satisfaction cleared the
|
|
33
33
|
// threshold. PURE — no computation of the score itself.
|
|
34
|
+
//
|
|
35
|
+
// F-932 renamed the third state from `unevaluated` to `incomplete` and CHANGED
|
|
36
|
+
// NOTHING ELSE HERE. The guard is the one place the CLI refuses to inflate a
|
|
37
|
+
// partial run into a pass — the same refusal pome-cloud added server-side in
|
|
38
|
+
// F-925 — so the rename must not become a loosening.
|
|
39
|
+
//
|
|
40
|
+
// One rule, two repos: `can_pass` is false for ANY abstention
|
|
41
|
+
// (`uploadAndFinalize.ts`), and pome-cloud's `isRunIncomplete` says
|
|
42
|
+
// `notEvaluated > 0` over the same `criteria_results`. Deliberately NOT read
|
|
43
|
+
// from the wire's `all_skipped`, which is the narrower every-abstained
|
|
44
|
+
// predicate and would loosen this guard.
|
|
34
45
|
export function scoreStatus(score, passThreshold) {
|
|
35
46
|
if (!score.evaluated || !score.can_pass)
|
|
36
|
-
return "
|
|
47
|
+
return "incomplete";
|
|
37
48
|
return score.satisfaction >= passThreshold ? "pass" : "fail";
|
|
38
49
|
}
|
|
39
50
|
export function taskPassed(score, passThreshold) {
|
|
@@ -55,14 +66,14 @@ export function markerFor(outcome) {
|
|
|
55
66
|
// Multi-twin (M3): the per-criterion bracket for terminal display —
|
|
56
67
|
// `[code]` / `[model]`, plus the `:<twin>` suffix when the criterion attributes
|
|
57
68
|
// to a specific twin (so a `[code:slack]`/`[model:github]` marker survives into the
|
|
58
|
-
//
|
|
69
|
+
// INCOMPLETE / criteria list). A bare (primary-twin) criterion renders `[code]`
|
|
59
70
|
// unchanged.
|
|
60
71
|
export function criterionMarkerLabel(criterion) {
|
|
61
72
|
return criterion.twin ? `[${criterion.type}:${criterion.twin}]` : `[${criterion.type}]`;
|
|
62
73
|
}
|
|
63
74
|
// Multi-twin (M3): when the cloud could not evaluate a criterion for a
|
|
64
75
|
// twin-related reason (a twin-tagged criterion, or a `no_matching_predicate`
|
|
65
|
-
// skip), name the twin inline so the
|
|
76
|
+
// skip), name the twin inline so the INCOMPLETE line explains WHICH twin's timeline
|
|
66
77
|
// came up empty. Returns "" when there's nothing twin-specific to add.
|
|
67
78
|
export function twinSkipSuffix(result) {
|
|
68
79
|
const twin = result.criterion.twin;
|
|
@@ -79,8 +90,14 @@ export function scoreCountsSummary(score) {
|
|
|
79
90
|
}
|
|
80
91
|
export function runScoreLine(score, passThreshold, unevaluatedNumericLabel) {
|
|
81
92
|
const status = scoreStatus(score, passThreshold);
|
|
82
|
-
if (status === "
|
|
83
|
-
|
|
93
|
+
if (status === "incomplete") {
|
|
94
|
+
// Leads with the COUNT, which is the fact the reader needs and the same
|
|
95
|
+
// fact the cloud's own header now states. The old copy said "cannot pass",
|
|
96
|
+
// which is a verdict about the AGENT for a gap in the GRADER — the exact
|
|
97
|
+
// inversion F-925 exists to stop, one surface over.
|
|
98
|
+
const notEvaluated = score.skipped + score.errored;
|
|
99
|
+
const total = score.total_required + notEvaluated;
|
|
100
|
+
return `score: incomplete — ${notEvaluated} of ${total} criteria not evaluated; ${scoreCountsSummary(score)}; ${unevaluatedNumericLabel}: ${score.satisfaction}/100`;
|
|
84
101
|
}
|
|
85
102
|
return `score: ${score.satisfaction}/100`;
|
|
86
103
|
}
|
|
@@ -1,9 +1,19 @@
|
|
|
1
|
+
import type { ScoreStatus } from "../hosted/evalResultView.js";
|
|
1
2
|
export type TrialRow = {
|
|
2
3
|
kind: "completed";
|
|
3
4
|
/** Cloud-authoritative satisfaction score, 0-100. */
|
|
4
5
|
score: number;
|
|
5
|
-
/**
|
|
6
|
-
|
|
6
|
+
/**
|
|
7
|
+
* F-925 — three states, not a boolean. `incomplete` means the trial ran
|
|
8
|
+
* and finalized but at least one criterion never produced a verdict, so
|
|
9
|
+
* it is neither a pass nor the agent's failure. It was `passed: boolean`
|
|
10
|
+
* fed from `exitCode === 0`, which counted a 100/100 run with 3 of 4
|
|
11
|
+
* criteria skipped as a clean passing trial.
|
|
12
|
+
*
|
|
13
|
+
* Typed as the CLI's own `ScoreStatus` rather than a second enum, so the
|
|
14
|
+
* trial line and the single-run headline cannot drift apart.
|
|
15
|
+
*/
|
|
16
|
+
verdict: ScoreStatus;
|
|
7
17
|
seconds: number;
|
|
8
18
|
/** Failing-criteria summary ("a · b"), absent when none were reported. */
|
|
9
19
|
note?: string;
|
|
@@ -45,20 +45,40 @@ export function trialRowLine(n, row) {
|
|
|
45
45
|
if (row.kind === "errored") {
|
|
46
46
|
return `trial ${n} ⚠ ${"errored".padEnd(16)}${row.reason} — excluded`;
|
|
47
47
|
}
|
|
48
|
-
|
|
48
|
+
// A dash for the ungradable trial: it ran, and it asserts nothing. Reusing
|
|
49
|
+
// ✗ would make a grader gap look like the agent's failure at a glance, which
|
|
50
|
+
// is the whole reading F-925 removes.
|
|
51
|
+
const mark = row.verdict === "pass" ? "✓" : row.verdict === "incomplete" ? "–" : "✗";
|
|
49
52
|
const base = `trial ${n} ${mark} ${String(row.score).padEnd(9)}${row.seconds.toFixed(1)}s`;
|
|
50
53
|
return row.note ? `${base} ${row.note}` : base;
|
|
51
54
|
}
|
|
52
55
|
export function groupSummaryLines(input) {
|
|
53
56
|
const completed = input.rows.filter((r) => r.kind === "completed");
|
|
54
|
-
const passed = completed.filter((r) => r.
|
|
57
|
+
const passed = completed.filter((r) => r.verdict === "pass").length;
|
|
58
|
+
const incomplete = completed.filter((r) => r.verdict === "incomplete").length;
|
|
59
|
+
// F-925 — the fraction's denominator is the GRADED trials. A trial that
|
|
60
|
+
// finalized but could not be fully graded leaves both the numerator and the
|
|
61
|
+
// denominator, so a 5-trial set with one of them reads "3 of 4", never the
|
|
62
|
+
// "4 of 5" that counted it as a pass.
|
|
63
|
+
const graded = completed.length - incomplete;
|
|
55
64
|
const errored = input.rows.length - completed.length;
|
|
56
65
|
const lines = ["─────"];
|
|
57
|
-
// The fraction counts
|
|
58
|
-
// excluded, never silently folded into the denominator.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
66
|
+
// The fraction counts GRADED trials only; incomplete and errored trials are
|
|
67
|
+
// named and excluded, never silently folded into the denominator. They stay
|
|
68
|
+
// two clauses rather than one: "the trial died" is ours to retry, "the trial
|
|
69
|
+
// ran and could not be graded" is a grader gap, and a reader told the wrong
|
|
70
|
+
// one goes looking in the wrong place.
|
|
71
|
+
let fraction;
|
|
72
|
+
if (graded === 0) {
|
|
73
|
+
fraction =
|
|
74
|
+
completed.length === 0 ? "no trials completed" : "no trials could be graded";
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
fraction = `${passed} of ${graded} passed`;
|
|
78
|
+
}
|
|
79
|
+
if (incomplete > 0) {
|
|
80
|
+
fraction += ` · ${incomplete} incomplete, excluded from the fraction`;
|
|
81
|
+
}
|
|
62
82
|
if (errored > 0) {
|
|
63
83
|
fraction += ` · ${errored} errored, excluded from the fraction`;
|
|
64
84
|
}
|
|
@@ -85,7 +105,11 @@ export function groupExitCode(rows) {
|
|
|
85
105
|
const completed = rows.filter((r) => r.kind === "completed");
|
|
86
106
|
if (completed.length === 0)
|
|
87
107
|
return 2;
|
|
88
|
-
|
|
108
|
+
// F-925 — an ungradable trial is not a pass, so a group holding one cannot
|
|
109
|
+
// exit 0: green here would tell CI the set was verified when part of it was
|
|
110
|
+
// never checked. It stays 1 rather than 2 even when EVERY trial was
|
|
111
|
+
// incomplete — `2` means nothing completed, and these completed.
|
|
112
|
+
return completed.every((r) => r.verdict === "pass") ? 0 : 1;
|
|
89
113
|
}
|
|
90
114
|
/** Modal failed-criterion text across the group's completed trials, as the
|
|
91
115
|
* short phrase the "start there" line renders. */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type HostedClient } from "../hosted/client.js";
|
|
2
2
|
import type { CreateSessionResponse } from "../types/shared.js";
|
|
3
3
|
import type { Task } from "../task/taskSchema.js";
|
|
4
|
-
import type
|
|
4
|
+
import { type Score, type ScoreStatus } from "../hosted/evalResultView.js";
|
|
5
5
|
import type { RunArtifacts } from "../recorder/artifacts.js";
|
|
6
6
|
export interface RunTaskHostedOptions {
|
|
7
7
|
taskPath: string;
|
|
@@ -44,6 +44,13 @@ export interface RunTaskHostedResult {
|
|
|
44
44
|
cloudDashboardUrl: string;
|
|
45
45
|
artifacts: RunArtifacts;
|
|
46
46
|
score: Score;
|
|
47
|
+
/**
|
|
48
|
+
* F-925 — the run's own three-state verdict, computed ONCE here and carried
|
|
49
|
+
* out so a caller never re-derives it. `exitCode` cannot express it: 1 means
|
|
50
|
+
* both "failed" and "could not be graded", and a trial group needs to tell
|
|
51
|
+
* them apart to keep the ungradable one out of its fraction.
|
|
52
|
+
*/
|
|
53
|
+
verdict: ScoreStatus;
|
|
47
54
|
exitCode: number;
|
|
48
55
|
/** Wall time from run start to post-agent state capture — the same value
|
|
49
56
|
* reported to /finalize as duration_ms. FDRS-636 renders it as the trial
|
|
@@ -13,6 +13,7 @@ import { ensureMcpSuffix } from "../cli/session.js";
|
|
|
13
13
|
import { redactJsonl, scoreFromFinalizeResponse, uploadRunBlobs, } from "../hosted/uploadAndFinalize.js";
|
|
14
14
|
import { resolveRunAgentIdentity } from "../cli/agent-identity.js";
|
|
15
15
|
import { HostedOrchError, HostedTrialError } from "../hosted/errors.js";
|
|
16
|
+
import { scoreStatus, } from "../hosted/evalResultView.js";
|
|
16
17
|
/** Build the agent subprocess env for a hosted run.
|
|
17
18
|
*
|
|
18
19
|
* Two layers, in this order:
|
|
@@ -491,7 +492,15 @@ export async function runTaskHosted(options) {
|
|
|
491
492
|
// Pre-finalize agent failures (auth, quota, twin spawn, exec
|
|
492
493
|
// errors) take other code paths via thrown HostedAuthError /
|
|
493
494
|
// HostedQuotaError / HostedOrchError and never reach this line.
|
|
494
|
-
|
|
495
|
+
// F-925 — the score alone is not the verdict. A 100/100 run with a
|
|
496
|
+
// criterion that never ran is `incomplete`, and exiting 0 on it is how
|
|
497
|
+
// "the fix passed" came to mean "the check never ran". `scoreStatus`
|
|
498
|
+
// applies the A5 guard `pome eval` has always applied; the FDRS-618
|
|
499
|
+
// compat this used to preserve is already preserved inside
|
|
500
|
+
// `scoreFromFinalizeResponse`, which sets can_pass true when the
|
|
501
|
+
// response carries no criteria_results at all.
|
|
502
|
+
const verdict = scoreStatus(score, scenario.config.passThreshold);
|
|
503
|
+
const exitCode = verdict === "pass" ? 0 : 1;
|
|
495
504
|
// 11. FDRS-644 — cache the CLOUD verdict payload next to the raw trace
|
|
496
505
|
// (verdict.json, provenance-labeled `source: "cloud-finalize"`).
|
|
497
506
|
// Not a local score: evaluation stayed in the cloud; this records
|
|
@@ -528,6 +537,7 @@ export async function runTaskHosted(options) {
|
|
|
528
537
|
cloudDashboardUrl: finalized.dashboard_url,
|
|
529
538
|
artifacts,
|
|
530
539
|
score,
|
|
540
|
+
verdict,
|
|
531
541
|
exitCode,
|
|
532
542
|
durationMs,
|
|
533
543
|
};
|