clearotron 0.3.0 → 0.3.1-beta.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/bin/example.mjs +28 -15
- package/bin/onboard.mjs +7 -1
- package/bin/start.mjs +13 -4
- package/build-info.json +2 -2
- package/docs/DELIVERY.md +20 -2
- package/driver/CHANGELOG.md +10 -0
- package/driver/band-size.mjs +66 -0
- package/driver/corrections-feedforward.mjs +128 -5
- package/driver/demo-container.mjs +83 -6
- package/driver/gateway.mjs +3 -3
- package/driver/package.json +1 -1
- package/driver/pipeline-knockout.mjs +9 -9
- package/driver/pipeline.mjs +113 -28
- package/driver/portal-service.mjs +8 -1
- package/driver/repair-composers.mjs +8 -0
- package/driver/stages.mjs +13 -1
- package/driver/suite-census.json +38 -14
- package/mcp-server/CHANGELOG.md +6 -0
- package/mcp-server/lib/ops.mjs +10 -1
- package/mcp-server/package.json +1 -1
- package/mcp-server/server.mjs +13 -2
- package/package.json +1 -1
- package/portal-ui/package.json +1 -1
- package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/scripts/release-version.mjs +84 -4
- package/shared/trigger-lane.mjs +66 -0
- package/shared/wsl.mjs +20 -3
|
@@ -1142,10 +1142,10 @@ export async function knockoutInner(ctx, job, opts = {}) {
|
|
|
1142
1142
|
writeRunStatus(ctx, { state: "failed", failedStage, reason: shortReason, reasonTruncated, reasonFull, terminalKind });
|
|
1143
1143
|
recordRunConsumption(ctx, { phase: "failed", tokens: stampTokenRollup(run.runDir, "failed") });
|
|
1144
1144
|
rollupStatus(run.studioRoot);
|
|
1145
|
-
//
|
|
1146
|
-
//
|
|
1147
|
-
//
|
|
1148
|
-
//
|
|
1145
|
+
// OWED EITHER WAY, mirroring pipelineInner: the flag is armed whichever lane wrote the packet,
|
|
1146
|
+
// because mark_sent is the only clear and it settles a failure.json on the same evidence a delivery
|
|
1147
|
+
// needs. The marker is NOT duplicated — the primary lane's own packet lands `<runId>.failed.pending`
|
|
1148
|
+
// and the *.pending watch matches it; a second `<runId>.pending` would outlive every settle.
|
|
1149
1149
|
let failPingSent = false;
|
|
1150
1150
|
try {
|
|
1151
1151
|
const rich = buildFailurePacket({
|
|
@@ -1160,14 +1160,14 @@ export async function knockoutInner(ctx, job, opts = {}) {
|
|
|
1160
1160
|
writeFileSync(driverDir(run.runDir, "failure.json"), JSON.stringify(packet, null, 2) + "\n");
|
|
1161
1161
|
failPingSent = Boolean(writeOutboxPacket(`${runId}.failed`, packet));
|
|
1162
1162
|
if (!failPingSent) {
|
|
1163
|
-
// a fresh notice supersedes an older send's skip-guards (a resumed-then-failed-again run must
|
|
1164
|
-
// still notify — the .sent/receipts invariant, clearance parity)
|
|
1165
|
-
try { rmSync(join(run.runDir, ".sent")); } catch { /* none */ }
|
|
1166
|
-
try { rmSync(driverDir(run.runDir, "send-receipts.json"), { force: true }); } catch { /* none */ }
|
|
1167
|
-
writeRunStatus(ctx, { sendPending: true });
|
|
1168
1163
|
mkdirSync(config.outboxDir, { recursive: true });
|
|
1169
1164
|
writeFileSync(join(config.outboxDir, `${runId}.pending`), `${agent}\n`);
|
|
1170
1165
|
}
|
|
1166
|
+
// a fresh notice supersedes an older send's skip-guards (a resumed-then-failed-again run must
|
|
1167
|
+
// still notify — the .sent/receipts invariant, clearance parity)
|
|
1168
|
+
try { rmSync(join(run.runDir, ".sent")); } catch { /* none */ }
|
|
1169
|
+
try { rmSync(driverDir(run.runDir, "send-receipts.json"), { force: true }); } catch { /* none */ }
|
|
1170
|
+
writeRunStatus(ctx, { sendPending: true });
|
|
1171
1171
|
} catch (nfErr) { note(`knockout failure-notice write skipped (${String(nfErr?.message ?? nfErr).slice(0, 100)})`); }
|
|
1172
1172
|
note(`=== KNOCKOUT ${terminalKind === REFUSAL_TERMINAL_KIND ? "REFUSED" : "FAILED"} ${run.codename} at ${failedStage}: ${shortReason} ===\n`);
|
|
1173
1173
|
// `codename`: this lane's terminals reach the SAME CLI exit as the clearance lane's, and the
|
package/driver/pipeline.mjs
CHANGED
|
@@ -31,14 +31,14 @@ import { buildRunContext, deriveSlug, kebab } from "./phase0.mjs";
|
|
|
31
31
|
import { paths, STAGES, axisTier, decideAxes, assertTierSanity, assertEffectiveTier, lines, AGENT_WHATSAPP, whatsappRouting,
|
|
32
32
|
chainEntries, stageOrdinal, stageInputs, stageOutputs, dependencyOrder, REGISTER_AXES, REGISTER_ENUMERATE_TOOL,
|
|
33
33
|
buildEscalationFollowup, buildEnvelopeCloseFollowup, buildFrameReopenFollowup,
|
|
34
|
-
buildFrameReopenRetryMessage, thinkingFor, composeFollowup, stampDispatchBlocks, recordEmptyReturn, nothingFound, nothingToRead, PROVIDER_META, proseRungDirective, inquiryRungDirective } from "./stages.mjs";
|
|
34
|
+
buildFrameReopenRetryMessage, thinkingFor, composeFollowup, stampDispatchBlocks, recordEmptyReturn, nothingFound, nothingToRead, PROVIDER_META, proseRungDirective, inquiryRungDirective } from "./stages.mjs"; import { bandSizeForStage } from "./band-size.mjs";
|
|
35
35
|
import { IDENTITY_FILE as REPORT_IDENTITY_FILE } from "./report-overview-record.mjs";
|
|
36
36
|
import { dispatchRows, clearedSignatures } from "./seat-attempts.mjs";
|
|
37
37
|
import { CONTEXT_DERIVATIONS, DISPATCH_EXTRAS, INLINE_CONTEXT, sandboxManifest, sandboxGaps, derivationsFor } from "./stage-context.mjs"; // — what a stage is actually handed
|
|
38
38
|
import { parseVerdict, countCitedDefects, parseCorrectionKinds, parseCorrections, validators, findReviewerCoherenceFlags, verdictHardenedTo } from "./verify.mjs";
|
|
39
39
|
import { readAcceptedFlags } from "./narrative-refutation-record.mjs"; // T3b — the typed flags, not the re-parse
|
|
40
40
|
import { evidenceClaimViolations, evidenceClaimTable } from "./evidence-claim-invariant.mjs"; //
|
|
41
|
-
import { buildCorrectionsApplied, correctionsWorklist, correctionsAppliedTable, correctionScope, scopeDrift, unresolvedFlags } from "./corrections-feedforward.mjs";
|
|
41
|
+
import { buildCorrectionsApplied, correctionsWorklist, correctionsAppliedTable, correctionScope, scopeDrift, unresolvedFlags, reportLines, linesOf, REPORT_LINE_KEY, REPORT_LINE_LABEL } from "./corrections-feedforward.mjs";
|
|
42
42
|
import { parseCoverageLedgerJson, parseCoverageLedgerFull, deriveCoverageStatus, classTokensFromScopeText, coerceToolAbsenceDeferred, applyTaintDeferred, decideRegisterGap, splitDeferredByCloseability, coverageLedgerTableRows, coverageUnitLabel, NON_MATERIAL_AXES, COVERAGE_STATUSES } from "./coverage-ledger.mjs";
|
|
43
43
|
import { receiptSettled, readEnvelopeDecision, settleReceipt, settledDeferralsSection } from "./envelope-settle.mjs";
|
|
44
44
|
import { readRegisterTaint, readActiveTaintAxes } from "./register-taint.mjs";
|
|
@@ -4321,7 +4321,7 @@ async function stageOnce(name, ctx, opts = {}) {
|
|
|
4321
4321
|
stallSec: def.stallSec, // per-stage stall override (heavy stages); undefined → global CLEAROTRON_STALL_MS
|
|
4322
4322
|
expectFile: out,
|
|
4323
4323
|
validate: def.validate,
|
|
4324
|
-
runDir: P.runDir,
|
|
4324
|
+
runDir: P.runDir, bandSize: bandSizeForStage(name, P),
|
|
4325
4325
|
// ── — THE PRESENTING SIDE, RECOMPOSED PER ATTEMPT ────────────────────────────────────────
|
|
4326
4326
|
//
|
|
4327
4327
|
// A stage that declares `refreshCtx` is saying its dispatch text depends on state the run CHANGES
|
|
@@ -5741,9 +5741,9 @@ export function rollbackCorrectivePass(P, runDir, pre, fail) { // @internal
|
|
|
5741
5741
|
* event so e2e can assert exactly that, and a non-zero count is a report of the fix not holding rather
|
|
5742
5742
|
* than of this backstop working.
|
|
5743
5743
|
*
|
|
5744
|
-
* @returns {null | {restoredFindings: {ordinal: *, mark: *}[], restoredKeys: string[], leftRemoved: {ordinal: *, mark: *}[]}}
|
|
5744
|
+
* @returns {null | {restoredFindings: {ordinal: *, mark: *}[], restoredKeys: string[], restoredRows: {register: string, key: string}[], leftRemoved: {ordinal: *, mark: *}[]}}
|
|
5745
5745
|
*/
|
|
5746
|
-
export function repairUnnamedRemovals(P, runDir, pre, namedOrdinals, namedMarks) { // @internal
|
|
5746
|
+
export function repairUnnamedRemovals(P, runDir, pre, namedOrdinals, namedMarks, namedLines = []) { // @internal
|
|
5747
5747
|
if (!pre?.raw) return null; // nothing held — nothing to compare against
|
|
5748
5748
|
let preDoc = null, postDoc = null, postRaw = null;
|
|
5749
5749
|
try { preDoc = JSON.parse(pre.raw); } catch { return null; }
|
|
@@ -5769,10 +5769,38 @@ export function repairUnnamedRemovals(P, runDir, pre, namedOrdinals, namedMarks)
|
|
|
5769
5769
|
// covered the day it exists — a list of key names here would be a second place to remember.
|
|
5770
5770
|
const restoredKeys = Object.keys(preDoc).filter((k) => !(k in postDoc));
|
|
5771
5771
|
|
|
5772
|
-
|
|
5772
|
+
// ROWS OF A REGISTER, NOT ONLY WHOLE REGISTERS. A pass rewriting a coverage note or an action sends the
|
|
5773
|
+
// complete array in its PATCH, and a patch REPLACES the stored register — so a row the seat left out is
|
|
5774
|
+
// a row deleted, and nothing downstream objects unless the slice was a limited one. A row no flag named
|
|
5775
|
+
// is restored whole, keyed as `reportLines` keys it; a row the reviewer named stays out, as a named
|
|
5776
|
+
// finding does.
|
|
5777
|
+
const lineKeys = new Set(namedLines ?? []);
|
|
5778
|
+
const restoredRows = [];
|
|
5779
|
+
const rowsBack = {};
|
|
5780
|
+
for (const [reg, keyOf] of Object.entries(REPORT_LINE_KEY)) {
|
|
5781
|
+
if (!Array.isArray(preDoc[reg]) || !Array.isArray(postDoc[reg])) continue;
|
|
5782
|
+
const have = new Set(postDoc[reg].map(keyOf));
|
|
5783
|
+
const back = preDoc[reg].filter((r) => !have.has(keyOf(r)) && !lineKeys.has(keyOf(r)));
|
|
5784
|
+
if (!back.length) continue;
|
|
5785
|
+
rowsBack[reg] = back;
|
|
5786
|
+
for (const r of back) restoredRows.push({ register: reg, key: keyOf(r), label: REPORT_LINE_LABEL[reg](r) });
|
|
5787
|
+
}
|
|
5788
|
+
|
|
5789
|
+
if (!restoredFindings.length && !restoredKeys.length && !restoredRows.length) return null;
|
|
5773
5790
|
|
|
5774
5791
|
const merged = { ...postDoc };
|
|
5775
5792
|
for (const k of restoredKeys) merged[k] = preDoc[k];
|
|
5793
|
+
for (const [reg, back] of Object.entries(rowsBack)) {
|
|
5794
|
+
// In the snapshot's order, the post-pass version of every row it kept, then any row the pass added.
|
|
5795
|
+
const keyOf = REPORT_LINE_KEY[reg];
|
|
5796
|
+
const post = new Map(postDoc[reg].map((r) => [keyOf(r), r]));
|
|
5797
|
+
const restored = new Set(back);
|
|
5798
|
+
const inPre = new Set(preDoc[reg].map(keyOf));
|
|
5799
|
+
merged[reg] = [
|
|
5800
|
+
...preDoc[reg].flatMap((r) => restored.has(r) ? [r] : post.has(keyOf(r)) ? [post.get(keyOf(r))] : []),
|
|
5801
|
+
...postDoc[reg].filter((r) => !inPre.has(keyOf(r))),
|
|
5802
|
+
];
|
|
5803
|
+
}
|
|
5776
5804
|
if (restoredFindings.length) {
|
|
5777
5805
|
// Back in the order the reviewer read them, by ordinal — a restored finding appended to the end
|
|
5778
5806
|
// would be a different document from the one that was correct.
|
|
@@ -5785,6 +5813,7 @@ export function repairUnnamedRemovals(P, runDir, pre, namedOrdinals, namedMarks)
|
|
|
5785
5813
|
ts: new Date().toISOString(),
|
|
5786
5814
|
restoredFindings: restoredFindings.map(brief),
|
|
5787
5815
|
restoredKeys,
|
|
5816
|
+
restoredRows,
|
|
5788
5817
|
leftRemoved: leftRemoved.map(brief),
|
|
5789
5818
|
};
|
|
5790
5819
|
atomicWrite(P.findings, JSON.stringify(merged, null, 2) + "\n");
|
|
@@ -5811,7 +5840,22 @@ export function repairUnnamedRemovals(P, runDir, pre, namedOrdinals, namedMarks)
|
|
|
5811
5840
|
*/
|
|
5812
5841
|
export function restoredFindingsTable(repair) { // @internal
|
|
5813
5842
|
const rows = repair?.restoredFindings ?? [];
|
|
5814
|
-
|
|
5843
|
+
// A RESTORED ROW IS THE DRIVER'S DOING TOO, and this read only findings — so a coverage note or an
|
|
5844
|
+
// action the driver put back reached the reviewer as the pass's own work, which is the one reading
|
|
5845
|
+
// this section exists to prevent. Same repair, same snapshot, same sentence (found in review).
|
|
5846
|
+
const lines = repair?.restoredRows ?? [];
|
|
5847
|
+
if (!rows.length && !lines.length) return "";
|
|
5848
|
+
if (!rows.length) return [
|
|
5849
|
+
"RESTORED BY THE DRIVER, NOT BY THE AUTHOR — read these before anything else.",
|
|
5850
|
+
"The corrective pass removed the line(s) below and no flag of yours named them, so the driver put them",
|
|
5851
|
+
"back exactly as you last read them. They carry NONE of this round's corrections, and they are not the",
|
|
5852
|
+
"author's judgment about anything. If a removal here was in fact right, say so in your review and name",
|
|
5853
|
+
"the ground — that is the statement the pass failed to make.",
|
|
5854
|
+
"",
|
|
5855
|
+
...lines.map((l) => ` ${l.label} — restored: removed by the corrective pass with no flag naming it`),
|
|
5856
|
+
"",
|
|
5857
|
+
"These lines MOVED, whatever the scope note above says about what did not.",
|
|
5858
|
+
].join("\n");
|
|
5815
5859
|
return [
|
|
5816
5860
|
"RESTORED BY THE DRIVER, NOT BY THE AUTHOR — read these before anything else.",
|
|
5817
5861
|
"The corrective pass removed the finding(s) below and no flag of yours named them, so the driver put",
|
|
@@ -5820,6 +5864,7 @@ export function restoredFindingsTable(repair) { // @internal
|
|
|
5820
5864
|
"and name the ground — that is the statement the pass failed to make.",
|
|
5821
5865
|
"",
|
|
5822
5866
|
...rows.map((f) => ` #${f.ordinal} ${f.mark} — restored: removed by the corrective pass with no flag naming it`),
|
|
5867
|
+
...lines.map((l) => ` ${l.label} — restored: removed by the corrective pass with no flag naming it`),
|
|
5823
5868
|
"",
|
|
5824
5869
|
"These rows MOVED, whatever the scope note above says about findings that did not.",
|
|
5825
5870
|
].join("\n");
|
|
@@ -5838,6 +5883,35 @@ function correctionNamedOrdinals(P) {
|
|
|
5838
5883
|
return out;
|
|
5839
5884
|
}
|
|
5840
5885
|
|
|
5886
|
+
/**
|
|
5887
|
+
* The non-finding lines the review's flags name or quote, as `reportLines` keys — matched against the
|
|
5888
|
+
* PRE-corrective document, for the reason the names are: a removed row is absent from the post file.
|
|
5889
|
+
*
|
|
5890
|
+
* ── THIS JOIN DECIDES A RESTORATION, AND ITS TWO FAILURE DIRECTIONS ARE OPPOSITE ─────────────────────
|
|
5891
|
+
*
|
|
5892
|
+
* `linesOf` applies no correction anywhere — but these keys become `namedLines`, and `repairUnnamedRemovals`
|
|
5893
|
+
* keeps a removed row OUT when its key is among them. So a prose match decides whether a row the pass
|
|
5894
|
+
* deleted comes back:
|
|
5895
|
+
*
|
|
5896
|
+
* a MISS → the row is restored although the reviewer asked for it to go: the driver overrides a
|
|
5897
|
+
* judgment that was made, and the reviewer sees it named as restored and can say so again.
|
|
5898
|
+
* a FALSE HIT → an accidental deletion stays deleted: the row leaves the client's report and the
|
|
5899
|
+
* restored-lines section says nothing, because the driver believed it was asked for.
|
|
5900
|
+
*
|
|
5901
|
+
* The second is the worse one and it is the same bet the backstop already makes by mark for findings.
|
|
5902
|
+
* What keeps it bounded is that the reviewer is told: every restored row prints in the re-read section,
|
|
5903
|
+
* so an override is visible, and a flag whose wording missed still leaves its own row in the observation
|
|
5904
|
+
* table as unresolved. `namesLine` wants the label's shape — "the coverage line for <area>" — so a flag
|
|
5905
|
+
* writing "the US coverage note" misses unless it also quoted eight running words of the line.
|
|
5906
|
+
*/
|
|
5907
|
+
function correctionNamedLines(P, doc0) {
|
|
5908
|
+
try {
|
|
5909
|
+
const review = existsSync(P.seniorEyeReview) ? readFileSync(P.seniorEyeReview, "utf8") : "";
|
|
5910
|
+
const lines = reportLines(doc0);
|
|
5911
|
+
return [...new Set(parseCorrections(review).flatMap((r) => linesOf(r?.text, lines).map((l) => l.key)))];
|
|
5912
|
+
} catch { return []; } // no review read — every row removal is then unnamed, and restored
|
|
5913
|
+
}
|
|
5914
|
+
|
|
5841
5915
|
/**
|
|
5842
5916
|
* @param {*} doc0 the findings document to match the review's names against. DEFAULTS to the file on
|
|
5843
5917
|
* disk, which is right for the staleness check — but WRONG for judging a removal: a finding that was
|
|
@@ -6082,12 +6156,9 @@ function plainRegisterExtra(ctx) {
|
|
|
6082
6156
|
const visible = [];
|
|
6083
6157
|
const add = (where, v) => { const t = String(v ?? "").trim(); if (t) visible.push({ where, text: t }); };
|
|
6084
6158
|
for (const f of doc.findings ?? []) add(`conflict ${f?.ordinal ?? "?"}'s one sentence`, f?.net);
|
|
6085
|
-
|
|
6086
|
-
|
|
6087
|
-
for (const
|
|
6088
|
-
const v = doc.markAssessment?.[k];
|
|
6089
|
-
add(`the mark assessment's ${k}`, typeof v === "string" ? v : v?.read);
|
|
6090
|
-
}
|
|
6159
|
+
// The lines that are not a finding, labelled by `reportLines` — the labels the corrective cycle's
|
|
6160
|
+
// observation joins a flag back to, so what the reviewer is handed and what the driver reads agree.
|
|
6161
|
+
for (const l of reportLines(doc)) add(l.label, l.text);
|
|
6091
6162
|
|
|
6092
6163
|
const hits = [];
|
|
6093
6164
|
for (const { where, text } of visible) {
|
|
@@ -7534,6 +7605,8 @@ export function buildReviewerOpenPointsSection(reviewMd, appliedRows = null) {
|
|
|
7534
7605
|
"findings-removed": "the finding this named was removed rather than corrected",
|
|
7535
7606
|
"not-entity-scoped": "the run could not check whether this was addressed",
|
|
7536
7607
|
"not-checkable": "the run could not check whether this was addressed",
|
|
7608
|
+
"line-unchanged": "the line this named was left as written",
|
|
7609
|
+
"line-removed": "the line this named was removed rather than corrected",
|
|
7537
7610
|
};
|
|
7538
7611
|
const items = rows.map((r) => {
|
|
7539
7612
|
const bits = [];
|
|
@@ -12186,7 +12259,7 @@ async function pipelineInner(job, opts = {}) {
|
|
|
12186
12259
|
let preDocForNames = null;
|
|
12187
12260
|
try { preDocForNames = preCorrective ? parseFindingsJsonLenient(preCorrective.raw) : null; } catch { /* fall back to the file */ }
|
|
12188
12261
|
const repaired = repairUnnamedRemovals(P, run.runDir, preCorrective,
|
|
12189
|
-
correctionNamedOrdinals(P), correctionNamedSet(P, preDocForNames));
|
|
12262
|
+
correctionNamedOrdinals(P), correctionNamedSet(P, preDocForNames), correctionNamedLines(P, preDocForNames));
|
|
12190
12263
|
correctiveRepair = repaired; // carried to the reviewer's re-read, which must know these are the DRIVER's
|
|
12191
12264
|
if (repaired) {
|
|
12192
12265
|
// A DEFECT SIGNAL, not a success. After the schema fix a corrective pass sends a targeted edit
|
|
@@ -12194,12 +12267,14 @@ async function pipelineInner(job, opts = {}) {
|
|
|
12194
12267
|
// primary fix not holding, never this backstop working.
|
|
12195
12268
|
runLog(run.runDir, { event: "corrective-unnamed-removal-repaired", defect: true,
|
|
12196
12269
|
restored: repaired.restoredFindings.length, keys: repaired.restoredKeys.length,
|
|
12270
|
+
rows: repaired.restoredRows.length,
|
|
12197
12271
|
leftRemoved: repaired.leftRemoved.length,
|
|
12198
12272
|
findings: repaired.restoredFindings.map((f) => `${f.ordinal}:${f.mark}`) });
|
|
12199
12273
|
note(`[corrections] the corrective pass removed ${repaired.restoredFindings.length} finding(s) `
|
|
12200
12274
|
+ `no flag named — restored whole from the pre-corrective snapshot: `
|
|
12201
12275
|
+ `${repaired.restoredFindings.map((f) => `#${f.ordinal} ${f.mark}`).join(", ")}`
|
|
12202
12276
|
+ (repaired.restoredKeys.length ? `; and ${repaired.restoredKeys.length} top-level register(s): ${repaired.restoredKeys.join(", ")}` : "")
|
|
12277
|
+
+ (repaired.restoredRows.length ? `; and ${repaired.restoredRows.length} row(s): ${repaired.restoredRows.map((r) => r.key).join(", ")}` : "")
|
|
12203
12278
|
+ (repaired.leftRemoved.length ? `. ${repaired.leftRemoved.length} removal(s) the reviewer DID name stay removed.` : "")
|
|
12204
12279
|
+ " The reviewer re-reads the repaired document before it ships.");
|
|
12205
12280
|
}
|
|
@@ -15247,9 +15322,9 @@ async function pipelineInner(job, opts = {}) {
|
|
|
15247
15322
|
// rides the outbox like every other event (the engine has no message tool; a chat-ping stage on it
|
|
15248
15323
|
// would burn a turn and fail) — the never-silent guarantee, headless form (docs/DELIVERY.md
|
|
15249
15324
|
// run-failed packet; the level-triggered *.pending watch keeps it alive until ack_event).
|
|
15250
|
-
// failPingSent records
|
|
15251
|
-
//
|
|
15252
|
-
//
|
|
15325
|
+
// failPingSent records which lane WROTE the packet. It no longer decides whether the notice is
|
|
15326
|
+
// owed: a failure arms sendPending either way (below), because mark_sent is the only clear and it
|
|
15327
|
+
// settles a failure.json on the same evidence a delivery needs. ONE SEND PATH, ruled 2026-09-12.
|
|
15253
15328
|
let failPingSent = false;
|
|
15254
15329
|
{
|
|
15255
15330
|
try {
|
|
@@ -15289,27 +15364,37 @@ async function pipelineInner(job, opts = {}) {
|
|
|
15289
15364
|
// no longer silence it ( F9: teal-keystone died silently on exactly that). Best-effort —
|
|
15290
15365
|
// packet-write trouble must never mask the original failure; the .failed sentinel + status.json
|
|
15291
15366
|
// remain the durable record either way.
|
|
15292
|
-
|
|
15293
|
-
|
|
15367
|
+
//
|
|
15368
|
+
// OWED EITHER WAY, AND EXACTLY ONE MARKER. A failed run used to read sendPending:false whenever the
|
|
15369
|
+
// packet write SUCCEEDED, so the owed-run scan could not see it and the notice waited on a reader of
|
|
15370
|
+
// the outbox instead. Now the flag is armed on both lanes. What is NOT duplicated is the marker: the
|
|
15371
|
+
// primary lane's packet IS one, because writeOutboxPacket("<runId>.failed") lands
|
|
15372
|
+
// `<runId>.failed.pending` and the *.pending watch already matches it. Arming a second
|
|
15373
|
+
// `<runId>.pending` beside it would leave a file mark_sent does not clear — it clears `<id>.pending`
|
|
15374
|
+
// for each id the run was known by, not this suffixed form — and the sweep re-arms off whatever is
|
|
15375
|
+
// on disk, which is how a permanent SEND PENDING was born. So: the flag always, the packet and the
|
|
15376
|
+
// marker only when nothing wrote them.
|
|
15377
|
+
try {
|
|
15378
|
+
if (!failPingSent) {
|
|
15294
15379
|
const packet = buildFailurePacket({
|
|
15295
15380
|
runId: `${run.slug}-${run.date}-${run.codename}`, agent, job, failedStage, shortReason, // canonical runId form (charter P1 §3)
|
|
15296
15381
|
reasonVerbatim: String(reason).slice(0, 1000), sig: failSig.sig, failClass, terminalKind,
|
|
15297
15382
|
repairs: attemptedRepairs ?? [], priorAttempts, whatsappTo: AGENT_WHATSAPP[agent] ?? null,
|
|
15298
15383
|
reasonDetail: reasonDetailField, reasonQuantity: quantity, // — the BACKSTOP notice carries it too
|
|
15299
15384
|
});
|
|
15300
|
-
// same per-send invariant as the delivery handoff: a fresh notice supersedes an older send's
|
|
15301
|
-
// .sent marker AND its per-channel receipts (e.g. a resumed run that fails again must still
|
|
15302
|
-
// notify on every channel, not be skip-guarded by the earlier send's receipts).
|
|
15303
|
-
try { rmSync(join(run.runDir, ".sent")); } catch { /* none */ }
|
|
15304
|
-
try { rmSync(driverDir(run.runDir, "send-receipts.json"), { force: true }); } catch { /* none */ }
|
|
15305
15385
|
writeFileSync(driverDir(run.runDir, "failure.json"), JSON.stringify(packet, null, 2) + "\n");
|
|
15306
|
-
writeRunStatus(ctx, { sendPending: true });
|
|
15307
15386
|
mkdirSync(config.outboxDir, { recursive: true });
|
|
15308
15387
|
writeFileSync(join(config.outboxDir, `${packet.runId}.pending`), `${agent}\n`);
|
|
15309
|
-
note(`failure notice: handoff → _driver/failure.json + outbox marker (completion-watch sends it)`);
|
|
15310
|
-
} catch (nfErr) {
|
|
15311
|
-
note(`failure-notice packet write skipped (${String(nfErr?.message ?? nfErr).slice(0, 100)}) — .failed + status.json remain the record`);
|
|
15312
15388
|
}
|
|
15389
|
+
// same per-send invariant as the delivery handoff: a fresh notice supersedes an older send's
|
|
15390
|
+
// .sent marker AND its per-channel receipts (e.g. a resumed run that fails again must still
|
|
15391
|
+
// notify on every channel, not be skip-guarded by the earlier send's receipts).
|
|
15392
|
+
try { rmSync(join(run.runDir, ".sent")); } catch { /* none */ }
|
|
15393
|
+
try { rmSync(driverDir(run.runDir, "send-receipts.json"), { force: true }); } catch { /* none */ }
|
|
15394
|
+
writeRunStatus(ctx, { sendPending: true });
|
|
15395
|
+
note(`failure notice OWED (${failPingSent ? "event lane wrote the packet" : "handoff wrote the packet"}) — mark_sent is the only clear`);
|
|
15396
|
+
} catch (nfErr) {
|
|
15397
|
+
note(`failure-notice arming skipped (${String(nfErr?.message ?? nfErr).slice(0, 100)}) — .failed + status.json remain the record`);
|
|
15313
15398
|
}
|
|
15314
15399
|
note(`=== FAILED ${run.codename} at ${failedStage}: ${reason} ===\n`);
|
|
15315
15400
|
// `codename` matches the three sibling terminals (postpone, recovery park, cancelled). Without it
|
|
@@ -4337,8 +4337,15 @@ const PORT = PORT_CHOICE.port;
|
|
|
4337
4337
|
try {
|
|
4338
4338
|
const res = await fetch(new URL("/mcp", MCP_URL), { method: "GET", redirect: "manual",
|
|
4339
4339
|
signal: AbortSignal.timeout(2500) });
|
|
4340
|
+
// THE BODY IS READ BECAUSE THE HEADER DOES NOT SEPARATE THE TWO DOORS. A proxy-fronted door and
|
|
4341
|
+
// a key door both refuse with 401 and no `www-authenticate`; each names the credential it wants
|
|
4342
|
+
// in the body, and that is the only thing on the wire that tells them apart. Bounded and
|
|
4343
|
+
// best-effort: a door that sends nothing readable leaves this null and the verdict reports
|
|
4344
|
+
// rather than judges.
|
|
4345
|
+
let body = null;
|
|
4346
|
+
if (res.status === 401) { try { body = (await res.text()).slice(0, 400); } catch { body = null; } }
|
|
4340
4347
|
probe = { ok: res.status < 500, status: res.status, error: null,
|
|
4341
|
-
challenge: res.headers.get("www-authenticate") };
|
|
4348
|
+
challenge: res.headers.get("www-authenticate"), body };
|
|
4342
4349
|
} catch (e) { probe = { ok: false, status: null, error: String(e?.cause?.code ?? e?.name ?? e?.message ?? e) }; }
|
|
4343
4350
|
// `verbs` IS DELIBERATELY NOT PASSED. That check short-circuits ahead of the probe, so handing it
|
|
4344
4351
|
// in would answer a question about the TOKEN where a question about the DOOR was asked — and the
|
|
@@ -207,6 +207,14 @@ export const REPAIR_COMPOSERS = [
|
|
|
207
207
|
`IF YOU REMOVE THE SUPPORT FOR A CLAIM, THE CLAIM GOES WITH IT. Deleting the evidence and leaving the sentence standing does not make it safer, it makes it unsupported and therefore stronger than the record allows. A run was refused for exactly this.`,
|
|
208
208
|
`1. Correct the flagged narrative sections — send back ONLY the sections a flag names, and every section the flags do not name is left out and comes back byte-identical;`,
|
|
209
209
|
`2. mirror every correction into the finding objects the flags name (owner names, levels/composites, meters). A finding the review KILLS is not deleted: keep its object, set "disposition": "withdrawn" and add "withdrawn_reason": "<the review flag that killed it>". Add a top-level "corrections": {"applied": true, "note": "<one line per flagged entity: corrected / withdrawn / no-change-because-…>"}.`,
|
|
210
|
+
// THE LINES THAT ARE NOT A FINDING. A coverage note, an action and the mark assessment carry no
|
|
211
|
+
// ordinal, so step 2 cannot reach them, and nothing said where a rewrite of one goes: seven such
|
|
212
|
+
// flags survived one clearance on the test box, each recorded as naming nothing (2026-09-11). The
|
|
213
|
+
// channel already existed — a PATCH copies a top-level register of the findings document through
|
|
214
|
+
// — and this names it. The coverage-account rule above forbids NEW claims about what was searched;
|
|
215
|
+
// rewording a note keeps its claims, and the text says so, or the seat reads the two as a conflict
|
|
216
|
+
// and does nothing, which is what it did.
|
|
217
|
+
`3. A flag about a line the reader sees that is NOT a finding — a coverage row's "note", an action's "text", or the mark assessment's "distinctiveness" or "connotation" — is applied in the same PATCH: send the complete top-level \`coverage\` array (every row, the unflagged ones exactly as you last sent them), the complete \`actions\` array, or the complete \`mark_assessment\`, with the flagged line rewritten. A row left out of the array is a row deleted, so send every row. Rewording a coverage note is NOT reopening the coverage account: keep its area, its state and every fact it states, and change only how it says them. A flagged line you leave as written is named in the "corrections" note with the reason, as the flag names it — for example: the coverage line for "<area>": no-change-because …`,
|
|
210
218
|
// ── CONVERTED. `editRepairTail` stood here and ordered targeted Edits of
|
|
211
219
|
// both files. The seat holds no Edit for either any more — but the ECONOMICS that argued for a
|
|
212
220
|
// targeted edit are preserved rather than lost, which is why the call has a patch shape at all:
|
package/driver/stages.mjs
CHANGED
|
@@ -1276,7 +1276,7 @@ export const STAGES = {
|
|
|
1276
1276
|
},
|
|
1277
1277
|
"Campaign shape (stated) — the intake's campaign facts retyped from the `Stated campaign shape` line the dispatch already carries": {
|
|
1278
1278
|
class: "mechanical:code-rendered", tokens: [],
|
|
1279
|
-
why: "CLASS ALIGNED WITH `## Instructed scope` in this same stage — same mechanism, same job object, same retyping of a driver-held value, so the same discharge: code stamps the line rather than the model retyping it. Not pre-bound: pre-bound means the driver writes the value into a FORM before dispatch, and nothing here does that. The driver holds job.campaignShape and puts it in the dispatch; the model retypes it into the frame. The dispatch itself calls it \"facts, not judgment\". Same shape as `## Instructed scope` eight elements above, which is already mechanical. NOTE FOR E7: this is derivable-and-not-derived, and it is NOT covered by frame_scope_missing — instructed-scope.json carries marks/classes/jurisdictions/goods/customer (pipeline.mjs
|
|
1279
|
+
why: "CLASS ALIGNED WITH `## Instructed scope` in this same stage — same mechanism, same job object, same retyping of a driver-held value, so the same discharge: code stamps the line rather than the model retyping it. Not pre-bound: pre-bound means the driver writes the value into a FORM before dispatch, and nothing here does that. The driver holds job.campaignShape and puts it in the dispatch; the model retypes it into the frame. The dispatch itself calls it \"facts, not judgment\". Same shape as `## Instructed scope` eight elements above, which is already mechanical. NOTE FOR E7: this is derivable-and-not-derived, and it is NOT covered by frame_scope_missing — instructed-scope.json carries marks/classes/jurisdictions/goods/customer (written at intake by `pipelineInner()` in `pipeline.mjs`) and no campaign field, so nothing joins the frame's copy back to the intake value.",
|
|
1280
1280
|
},
|
|
1281
1281
|
"Campaign shape (inferred) — the inference where intake stated none, and the decision to label the line `(inferred — not stated in the request)`": {
|
|
1282
1282
|
class: "judgment", tokens: [],
|
|
@@ -4664,3 +4664,15 @@ export function recordEmptyReturn(built, id, { parts, ids, empty }) {
|
|
|
4664
4664
|
export function dispatchBlockWhy(ctx, stage, id) {
|
|
4665
4665
|
return ctx?.dispatchBlocks?.[stage]?.empty?.find((x) => x.id === id)?.why ?? null;
|
|
4666
4666
|
}
|
|
4667
|
+
|
|
4668
|
+
// The stages whose dispatch hands the seat the band reading layer. Declared as data so the driver can
|
|
4669
|
+
// ask "does this stage read the band" without composing its message, and pinned by a test that DERIVES
|
|
4670
|
+
// the same set from which stage functions reference `BAND_READING_CONTRACT` — the declaration is
|
|
4671
|
+
// convenience, the derivation is the authority.
|
|
4672
|
+
//
|
|
4673
|
+
// AT THE FOOT OF THIS FILE ON PURPOSE, not beside the contract it belongs to. Every line inserted above
|
|
4674
|
+
// a cited line silently mis-points every citation below it, and this file is cited by line in twenty
|
|
4675
|
+
// places; the citation check only fails when a cited span goes entirely blank, so a citation shifted
|
|
4676
|
+
// onto a different real line passes while describing the wrong code. Adding below the last cited line
|
|
4677
|
+
// is the one placement that cannot do that. See band-size.mjs for what reads this.
|
|
4678
|
+
export const BAND_READING_STAGES = new Set(["placement-inquiry", "register-digest", "synthesis"]);
|
package/driver/suite-census.json
CHANGED
|
@@ -69,6 +69,12 @@
|
|
|
69
69
|
"skips": 0,
|
|
70
70
|
"todos": 0
|
|
71
71
|
},
|
|
72
|
+
"a-budget-is-recorded-beside-the-band-it-ran-against.test.mjs": {
|
|
73
|
+
"tests": 9,
|
|
74
|
+
"asserts": 30,
|
|
75
|
+
"skips": 0,
|
|
76
|
+
"todos": 0
|
|
77
|
+
},
|
|
72
78
|
"a-cancel-marker-names-its-actor.test.mjs": {
|
|
73
79
|
"tests": 14,
|
|
74
80
|
"asserts": 34,
|
|
@@ -171,6 +177,12 @@
|
|
|
171
177
|
"skips": 0,
|
|
172
178
|
"todos": 0
|
|
173
179
|
},
|
|
180
|
+
"a-correction-to-a-line-that-is-not-a-finding-is-observed.test.mjs": {
|
|
181
|
+
"tests": 7,
|
|
182
|
+
"asserts": 33,
|
|
183
|
+
"skips": 0,
|
|
184
|
+
"todos": 0
|
|
185
|
+
},
|
|
174
186
|
"a-credential-file-is-written-one-way.test.mjs": {
|
|
175
187
|
"tests": 4,
|
|
176
188
|
"asserts": 13,
|
|
@@ -231,6 +243,12 @@
|
|
|
231
243
|
"skips": 0,
|
|
232
244
|
"todos": 0
|
|
233
245
|
},
|
|
246
|
+
"a-demo-sample-that-cannot-be-read-is-named.test.mjs": {
|
|
247
|
+
"tests": 7,
|
|
248
|
+
"asserts": 35,
|
|
249
|
+
"skips": 4,
|
|
250
|
+
"todos": 0
|
|
251
|
+
},
|
|
234
252
|
"a-demo-says-what-it-is-not-what-it-lacks.test.mjs": {
|
|
235
253
|
"tests": 5,
|
|
236
254
|
"asserts": 32,
|
|
@@ -2980,8 +2998,8 @@
|
|
|
2980
2998
|
"todos": 0
|
|
2981
2999
|
},
|
|
2982
3000
|
"pipeline.mock.test.mjs": {
|
|
2983
|
-
"tests":
|
|
2984
|
-
"asserts":
|
|
3001
|
+
"tests": 94,
|
|
3002
|
+
"asserts": 792,
|
|
2985
3003
|
"skips": 0,
|
|
2986
3004
|
"todos": 0
|
|
2987
3005
|
},
|
|
@@ -3335,7 +3353,7 @@
|
|
|
3335
3353
|
},
|
|
3336
3354
|
"reading-the-demo-does-not-edit-the-repository.test.mjs": {
|
|
3337
3355
|
"tests": 2,
|
|
3338
|
-
"asserts":
|
|
3356
|
+
"asserts": 13,
|
|
3339
3357
|
"skips": 1,
|
|
3340
3358
|
"todos": 0
|
|
3341
3359
|
},
|
|
@@ -3616,8 +3634,8 @@
|
|
|
3616
3634
|
"todos": 0
|
|
3617
3635
|
},
|
|
3618
3636
|
"release-pipeline.test.mjs": {
|
|
3619
|
-
"tests":
|
|
3620
|
-
"asserts":
|
|
3637
|
+
"tests": 93,
|
|
3638
|
+
"asserts": 356,
|
|
3621
3639
|
"skips": 0,
|
|
3622
3640
|
"todos": 0
|
|
3623
3641
|
},
|
|
@@ -3887,7 +3905,7 @@
|
|
|
3887
3905
|
},
|
|
3888
3906
|
"runner.failure-backstop.test.mjs": {
|
|
3889
3907
|
"tests": 3,
|
|
3890
|
-
"asserts":
|
|
3908
|
+
"asserts": 25,
|
|
3891
3909
|
"skips": 0,
|
|
3892
3910
|
"todos": 0
|
|
3893
3911
|
},
|
|
@@ -4235,7 +4253,7 @@
|
|
|
4235
4253
|
},
|
|
4236
4254
|
"start-command.test.mjs": {
|
|
4237
4255
|
"tests": 27,
|
|
4238
|
-
"asserts":
|
|
4256
|
+
"asserts": 121,
|
|
4239
4257
|
"skips": 0,
|
|
4240
4258
|
"todos": 0
|
|
4241
4259
|
},
|
|
@@ -4455,6 +4473,12 @@
|
|
|
4455
4473
|
"skips": 0,
|
|
4456
4474
|
"todos": 0
|
|
4457
4475
|
},
|
|
4476
|
+
"the-boot-line-says-which-door-it-found.test.mjs": {
|
|
4477
|
+
"tests": 5,
|
|
4478
|
+
"asserts": 21,
|
|
4479
|
+
"skips": 0,
|
|
4480
|
+
"todos": 0
|
|
4481
|
+
},
|
|
4458
4482
|
"the-bundle-ships-no-commentary.test.mjs": {
|
|
4459
4483
|
"tests": 2,
|
|
4460
4484
|
"asserts": 3,
|
|
@@ -4510,8 +4534,8 @@
|
|
|
4510
4534
|
"todos": 0
|
|
4511
4535
|
},
|
|
4512
4536
|
"the-corrective-pass-names-what-it-removes.test.mjs": {
|
|
4513
|
-
"tests":
|
|
4514
|
-
"asserts":
|
|
4537
|
+
"tests": 12,
|
|
4538
|
+
"asserts": 48,
|
|
4515
4539
|
"skips": 0,
|
|
4516
4540
|
"todos": 0
|
|
4517
4541
|
},
|
|
@@ -5146,8 +5170,8 @@
|
|
|
5146
5170
|
"todos": 0
|
|
5147
5171
|
},
|
|
5148
5172
|
"under-wsl-the-windows-binary-on-path-is-not-the-engine.test.mjs": {
|
|
5149
|
-
"tests":
|
|
5150
|
-
"asserts":
|
|
5173
|
+
"tests": 9,
|
|
5174
|
+
"asserts": 31,
|
|
5151
5175
|
"skips": 0,
|
|
5152
5176
|
"todos": 0
|
|
5153
5177
|
},
|
|
@@ -5505,8 +5529,8 @@
|
|
|
5505
5529
|
"todos": 0
|
|
5506
5530
|
},
|
|
5507
5531
|
"delivery-state.test.mjs": {
|
|
5508
|
-
"tests":
|
|
5509
|
-
"asserts":
|
|
5532
|
+
"tests": 12,
|
|
5533
|
+
"asserts": 31,
|
|
5510
5534
|
"skips": 0,
|
|
5511
5535
|
"todos": 0
|
|
5512
5536
|
},
|
|
@@ -5596,7 +5620,7 @@
|
|
|
5596
5620
|
},
|
|
5597
5621
|
"ops.test.mjs": {
|
|
5598
5622
|
"tests": 38,
|
|
5599
|
-
"asserts":
|
|
5623
|
+
"asserts": 164,
|
|
5600
5624
|
"skips": 0,
|
|
5601
5625
|
"todos": 0
|
|
5602
5626
|
},
|
package/mcp-server/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# trademark-artifacts-mcp
|
|
2
2
|
|
|
3
|
+
## 0.3.1-beta.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- d9798db: Fixed: A clearance that stops now always records that its notice is still owed, so a failure cannot be passed over as already handled.
|
|
8
|
+
|
|
3
9
|
## 0.3.0
|
|
4
10
|
|
|
5
11
|
No changes in this release.
|
package/mcp-server/lib/ops.mjs
CHANGED
|
@@ -643,8 +643,17 @@ export function markSent(args = {}) {
|
|
|
643
643
|
const legacy = run.slug && run.codename ? `${run.slug}-${run.codename}` : null;
|
|
644
644
|
const forms = [...new Set([packet.runId, run.runId, args.runId, legacy]
|
|
645
645
|
.filter((id) => id != null && String(id).trim() !== "").map(sanitize))];
|
|
646
|
+
// BOTH SUFFIXES, because a failure's marker IS its packet. The delivery lane drops `<id>.pending`;
|
|
647
|
+
// the failure lane writes its packet through the outbox as `<id>.failed.pending`, which the same
|
|
648
|
+
// *.pending watch matches. Clearing only the first left the failure form on disk after a settle, the
|
|
649
|
+
// sweep re-armed off it, and the run stayed owed forever — the permanent SEND PENDING the pipelines'
|
|
650
|
+
// old comment feared, arriving by the one route nobody had named. A missing form is a no-op, as
|
|
651
|
+
// before. (How many were stranded when this was found is a fact about one box on one day; it lives on
|
|
652
|
+
// the issue, not here, because a count in a comment rots and the mechanism does not.)
|
|
646
653
|
for (const id of forms) {
|
|
647
|
-
|
|
654
|
+
for (const suffix of [".pending", ".failed.pending"]) {
|
|
655
|
+
try { rmSync(join(config.outboxDir, `${id}${suffix}`), { force: true }); } catch { /* best-effort */ }
|
|
656
|
+
}
|
|
648
657
|
}
|
|
649
658
|
};
|
|
650
659
|
const sentPath = join(run.runDir, ".sent");
|
package/mcp-server/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trademark-artifacts-mcp",
|
|
3
|
-
"version": "0.3.0",
|
|
3
|
+
"version": "0.3.1-beta.0",
|
|
4
4
|
"license": "AGPL-3.0-only",
|
|
5
5
|
"private": true,
|
|
6
6
|
"description": "MCP server to interrogate clearotron trademark-clearance runs — list/read artifacts, trace the full decision flow, telemetry/cost, coverage, single-run search, and a gated single-step what-if. Imports the clearotron-driver read-only; touches no driver/template/deploy files.",
|
package/mcp-server/server.mjs
CHANGED
|
@@ -205,10 +205,21 @@ const tools = {
|
|
|
205
205
|
brief({ runId }) {
|
|
206
206
|
return { _note: BRIEFING_NOTE, ...buildBrief(mustRun(runId)) };
|
|
207
207
|
},
|
|
208
|
-
|
|
208
|
+
// ── ASKING FOR OWED RUNS IS A COMPLETENESS QUESTION, NOT A BROWSING ONE ──────────────────────────
|
|
209
|
+
//
|
|
210
|
+
// The default cap of 50 is right for "show me the recent runs" and wrong for "which runs still owe
|
|
211
|
+
// their requester a notice". An integrator polls the second to decide what to send; a cap silently
|
|
212
|
+
// drops the fifty-first, and the run it dropped is indistinguishable from a run that owes nothing.
|
|
213
|
+
// Nothing in the reply says a page was cut, so the caller cannot tell the difference either.
|
|
214
|
+
//
|
|
215
|
+
// So `sendPending: true` returns ALL of them unless the caller names a limit itself. Every other call
|
|
216
|
+
// keeps the cap it has always had: an explicit limit is still obeyed, and an unfiltered list is still
|
|
217
|
+
// 50, so no existing caller changes.
|
|
218
|
+
list_runs({ agent, state, slug, mark, sendPending, limit } = {}) {
|
|
209
219
|
let out = enumerateRuns({ agent, state, slug, mark }).map(runSummary);
|
|
210
220
|
if (sendPending === true || sendPending === false) out = out.filter((r) => r.sendPending === sendPending);
|
|
211
|
-
|
|
221
|
+
const cap = limit ?? (sendPending === true ? out.length : 50);
|
|
222
|
+
return out.slice(0, cap);
|
|
212
223
|
},
|
|
213
224
|
list_profiles() {
|
|
214
225
|
// Each customer carries its PROJECTS (engagements) so intake can resolve a projectKey too. A bad
|
package/package.json
CHANGED
package/portal-ui/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "portal-ui",
|
|
3
3
|
"private": true,
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "0.3.0",
|
|
5
|
+
"version": "0.3.1-beta.0",
|
|
6
6
|
"license": "AGPL-3.0-only",
|
|
7
7
|
"description": "The unified trademark portal UI. One address, one login: who you are decides what you see. Built as a static bundle, served by driver/portal-service.mjs — the browser never reaches profile-service or recipe-service.",
|
|
8
8
|
"engines": {
|