feat: read the engine's unfulfilled-keys report instead of diffing declared vs delivered - #1308
ralphstodomingo wants to merge 16 commits into
Conversation
…clared vs delivered The MCP catalog now keeps the `_meta` of a server's last tools/list page per client, exposed as `MCP.listMeta(name)`. On attach, the gaps come from the engine's `ai.altimate/unfulfilled` report, grouped by reason in the toast and headless line with the engine's detail (e.g. `spawn docker ENOENT`); `no-bridge` entries stay out of the missing set as before. The attached outcome carries the full report. `MIN_ENGINE_VERSION` moves to 0.7.2, the first engine that emits it; an engine that sends none claims no gaps rather than inventing them. Closes #1307 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GHBUvb843k1R7UAGi8Ya9b
Env-guarded (`ALTIMATE_ENGINE_E2E_ROOT`), skipped otherwise: spawns a built engine over stdio the way the overlay does, against a fake Altimate API and a real second MCP server, and reads the `ai.altimate/unfulfilled` report through `MCP.listMeta` into the attach toast text. The engine is a node shebang script, so the test spawns node rather than the bun test runner. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GHBUvb843k1R7UAGi8Ya9b
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: trueThanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Marker Guard flagged the changed lines in the upstream-shared catalog; the single-line marker comments did not count as a wrapped block. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GHBUvb843k1R7UAGi8Ya9b
Engine → altimate-code, through the real attach pathThis runs the CLI's production attach code under a real instance with no model turn: the binding cache, the Declared by the workspace: Result — release candidate (head
|
Custom (tenant-created) integrations carry numeric ids; the parser treated the whole report as malformed over that one field and the attach announced no gaps at all. Take the id as a string. Found by the engine-to-CLI run against a local backend with a custom MCP integration. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GHBUvb843k1R7UAGi8Ya9b
|
One more commit on this head, |
Multi-model review — client halfReviewed jointly with AltimateAI/altimate-mcp-engine#248 as one feature. The wire contract between the two halves agrees exactly: meta key, all four field names, all six reason spellings, and the empty-array-vs-absent distinction match. Major1. Tools and their report are not committed atomically across a refresh
Consequences:
This bites C3 ("the catalog keeps the report across the paths that list tools") on the Suggested: accumulate Minor2. The headline and the gap line can disagree (raw vs sanitized key space)
So a served The undercount itself pre-dates this PR — the old Suggested: normalize both sides to one key space before the 3.
Suggested: group by 4. "Last page's
Suggested: pick a rule (last-page-authoritative vs any-page), implement it inside the completed snapshot, and add a first-page-only case. 5. Truncation is not redaction
6.
Verified sound
|
…hat a gap is with its own detail Answers the multi-model review of the unfulfilled report, client half. - the tools of a listing and its _meta are committed in one statement (State.meta beside State.defs) and read through one accessor, MCP.snapshot(name): a refresh that is pending or that failed leaves the last good pair standing, and the overlay can no longer pair one listing's tools with another's report - the catalog commits _meta when a listing completes — the last page that carries one wins, a listing with none clears it — instead of clearing at the start - served counts compare the declared keys in the catalog's sanitised key space, so the headline cannot undercount a served tool whose raw key the MCP layer renamed - the missing line groups by reason AND integration, so one integration's error is never printed as another's - spawn-failed reads 'server could not be started or reached', which is what the engine records under it Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GHBUvb843k1R7UAGi8Ya9b
Re-review disposition —
|
|
@codex review Scoped review against the claims below (head Claims
Residuals (already accepted)
|
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 45a8d02c8c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…er counts a reported key Answers the Codex round on the report fixes: - McpCatalog.defsWithMeta returns the listing and its _meta as one value, and every commit of a listing stores that pair — not a per-client value another refresh may have overwritten while this one was awaiting - served counts exclude keys the engine reports unfulfilled, so two raw keys that sanitise to one catalog name cannot both count as served - parseUnfulfilled rejects an entry whose detail is present but not a string, failing closed like the other fields Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GHBUvb843k1R7UAGi8Ya9b
|
@codex review Scoped falsification round on the fixes since the last round ( Fix claims
Accepted residuals
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 820147aee2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Two raw keys that sanitise to one catalog name are one callable tool however many the engine lists; served and extension counts are the number of distinct sanitised entries that are present and unreported. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GHBUvb843k1R7UAGi8Ya9b
|
@codex review Final scoped falsification round (round 3 of 3) on the fix since the last round ( Fix claim
Accepted residuals
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9452109654
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…nd extension groups Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GHBUvb843k1R7UAGi8Ya9b
Disposition after three Codex rounds —
|
…kers Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GHBUvb843k1R7UAGi8Ya9b
…w as bare Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GHBUvb843k1R7UAGi8Ya9b
…-meta # Conflicts: # packages/opencode/src/altimate/workspace/engine-overlay.ts # packages/opencode/src/altimate/workspace/engine-types.ts # packages/opencode/test/altimate/workspace/engine-overlay.test.ts
There was a problem hiding this comment.
Consensus review (Claude + Codex + Gemini + MiniMax + Qwen + MiMo, 6/8 configured participants, quorum met)
Superseded — see the follow-up review below requesting changes.
1 MAJOR and 4 MINOR findings posted as inline comments (Nits omitted per request — see the full write-up for those plus positive observations, missing-test notes, and the disagreements the panel investigated and rejected during convergence).
Full review with attribution: reviews/pr-1308-consensus-review.md in the team's review archive.
| const id = `${u.reason} ${u.integrationId}` | ||
| const group = groups.get(id) ?? { reason: u.reason, keys: [] } | ||
| group.keys.push(u.key) | ||
| if (group.detail === undefined && u.detail) group.detail = u.detail |
There was a problem hiding this comment.
MAJOR (Security) — u.detail (the engine's raw spawn-error / connection-failure text for a custom MCP integration) is captured here and later only length-truncated (DETAIL_CHARS = 60) before reaching the attach toast, never content-redacted. This can contain command args, URLs with embedded credentials, tokens, or paths. The same unredacted value also reaches log.info in full at engine-overlay.ts:716.
This codebase already has the right pattern for exactly this class of data — mcp/index.ts:232 wraps subprocess stderr with Telemetry.maskString(...) before it reaches logs/status. detail should go through the same masking (plus stripping control characters) before it's stored on group.detail here, so both the toast and the log inherit the redaction.
Flagged by: Codex — verified against the existing maskString precedent.
There was a problem hiding this comment.
Fixed in two places.
- Engine (the source): since 0.7.3, which this PR now requires, the engine sends
detailonly for known error shapes, such asspawn docker ENOENT,connect ECONNREFUSED host:portorInvalid URL. Anything else arrives asdetails in the engine log, and a spawn failure is reduced to a plain command name. - Client (19f8eae):
parseUnfulfillednow collapses control characters and runsTelemetry.maskString, the same treatment as subprocess stderr inmcp/index.ts. Because this happens at parse time, the toast andlog.infoboth carry the masked text. Test: "a detail is masked and flattened before anything shows or logs it".
One limit, recorded as R4: maskString masks paths and known key shapes, but a short bare token passes through it. The guarantee that no configuration reaches the client comes from the engine side.
| : `${outcome.available} integration tools available.`, | ||
| variant: missing && missing.length > 0 ? "warning" : "info", | ||
| message: `${headline}${describeMissing(missingReport ?? [])}${describeExtensionServed(extServed)}`, | ||
| variant: missingReport !== undefined && missingReport.length > 0 ? "warning" : "info", |
There was a problem hiding this comment.
MINOR (Logic) — variant is warning only when missingReport.length > 0. But served < declared.keys.length can also happen with an empty report — e.g. two raw keys that sanitize to the same catalog entry ("1 of 2 declared integration tools available" currently renders as info). The report is authoritative about reasons; the client is authoritative about what's actually callable post-sanitization, and severity should reflect the latter. The absent-report case is already tested and asserts info (today's deliberate behavior) — the collision case specifically has no variant assertion.
Flagged by: Codex.
There was a problem hiding this comment.
Fixed in 19f8eae. The toast is now a warning when a gap is reported, and also when a report is present and fewer declared tools are callable than were declared. The collision case (two raw keys that sanitise to one catalog entry) asserts warning. The no-report case stays info, as before. The no-bridge test now declares only the served keys, so nothing but its no-bridge entries could affect the variant.
| const signature = `attached:${workspace.key}:${outcome.available}:${outcome.declared ?? "?"}:${(missing ?? []).join(",")}:${extServed}` | ||
| // A gap whose reason changed (a connection fixed, a binary still absent) | ||
| // is a new verdict too, so the reasons are in the signature. | ||
| const gaps = (missingReport ?? []).map((u) => `${u.key}=${u.reason}`).join(",") |
There was a problem hiding this comment.
MINOR (Logic) — the re-announcement signature is built from ${u.key}=${u.reason} only. If a spawn-failed detail changes from e.g. spawn docker ENOENT to a different actionable error while reason stays spawn-failed, the outcome updates but the toast doesn't re-fire — the user is left with stale remediation text. Consider folding integrationId/detail into the signature too.
Flagged by: Codex.
There was a problem hiding this comment.
Fixed in 19f8eae. The signature now carries integration, key, reason and detail, so a changed error text under the same reason re-announces. Test: "a gap whose error text changed under the same reason is announced again".
| /** The engine's report out of a tools/list `_meta`. Undefined when there is | ||
| * none, or it is malformed: the caller then knows nothing about gaps, which | ||
| * is not the same as knowing there are none. */ | ||
| export function parseUnfulfilled(meta: Record<string, unknown> | undefined): Unfulfilled[] | undefined { |
There was a problem hiding this comment.
MINOR (Design/Robustness) — a report of N valid entries plus 1 malformed one is discarded wholesale (return undefined on the first bad entry), and the toast silently falls back to showing no gap information at all. This is deliberate and tested (the file's own comment: "Undefined... is not the same as knowing there are none") and the failure mode is honest rather than dangerous — but there's currently no diagnostic signal when it happens. Worth a logWarning on discard, purely for diagnosability.
Flagged by: MiMo (downgraded from an initial MAJOR — confirmed as Minor by 2 convergence reviewers given the fail-closed behavior is intentional and tested).
There was a problem hiding this comment.
Added in 19f8eae. When a report is present but malformed and gets dropped, the attach logs a warning. It fires once per verdict, and it logs only the workspace, never the report content.
| yield* mcp.remove("datamate") | ||
| expect(yield* mcp.listMeta("datamate")).toBeUndefined() | ||
| } finally { | ||
| api.close() |
There was a problem hiding this comment.
MINOR (Testing) — isolatedHome() (line 170) creates a temp dir via mkdtempSync that's never removed here in finally — only api.close() is called. Leaves config/log directories behind on every enabled run, especially in CI. Consider an rmSync(home, { recursive: true, force: true }) alongside api.close().
Flagged independently by 2 reviewers: Codex + MiMo.
There was a problem hiding this comment.
Fixed in 19f8eae. home is removed in finally next to api.close(). I re-ran it against the published 0.7.3, and no temporary HOME was left behind.
sahrizvi
left a comment
There was a problem hiding this comment.
Requesting changes
Reversing the earlier approve on this consensus review. The MAJOR finding in the inline comments above — engine-reported detail strings (spawn errors / connection failures from arbitrary custom MCP integrations, which can contain credentials, tokens, or paths) reach both log.info and the attach toast unredacted, bypassing this codebase's own existing Telemetry.maskString convention (mcp/index.ts:232) — should be fixed before merge.
The 4 MINOR findings are not blocking but worth addressing in this PR or a fast follow-up.
Full review with attribution: reviews/pr-1308-consensus-review.md in the team's review archive.
|
Closing while the workspaces line is re-scoped with the team (this is section WL-2 of the write-up shared internally). The branch stays as it is; this reopens unchanged once that section is signed off. |
Datamate 0.7.2 was released without the `ai.altimate/unfulfilled` report, which now ships in 0.7.3. `MIN_ENGINE_VERSION` moves to 0.7.3 so a 0.7.2 engine no longer clears the floor and announces no gaps. The tests that pin the floor and the overlay harness's default engine version follow. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0172qrhMa5TQgETASi5hxMqD
Engine → altimate-code through the real attach path, on 0.7.3The same harness as the earlier evidence comment ( Release candidate: engine
Floor negative: a build of the engine at AltimateAI/altimate-mcp-engine#248's head, versioned
The harness's expected toast text was updated for the |
…what is callable - Mask each gap's `detail` when the report is parsed (control characters collapsed, then `Telemetry.maskString`, the same treatment as subprocess stderr), so the attach toast and the attach log both carry the masked text. - Warn when fewer declared tools are callable than were declared, even with an empty report (two raw keys that sanitise to one catalog entry). With no report nothing is claimed, so that case stays `info`. - Re-announce when a gap's integration or error text changes under the same reason, so the toast never keeps stale remediation. - Log a warning when a report is present but malformed and is dropped. - The real-engine e2e test removes its temporary HOME. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0172qrhMa5TQgETASi5hxMqD
…empty it `bun test` runs the suite in one process and `mock.module` is process-wide. `mcp.test.ts` mocks `@modelcontextprotocol/sdk/client/index.js` with a client whose `listTools` returns no tools, so when it ran first the catalog `_meta` tests listed nothing. They now drive `McpCatalog` through a scripted client (the only method the catalog calls is `listTools`). The SDK's own `_meta` passthrough stays covered by the real-engine e2e test. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0172qrhMa5TQgETASi5hxMqD
|
@codex review Scope: the changes since |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 87efc09a07
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // from. (multi-model review) | ||
| const gaps = JSON.stringify((missingReport ?? []).map((u) => [u.integrationId, u.key, u.reason, u.detail ?? ""])) | ||
| const signature = `attached:${workspace.key}:${outcome.available}:${outcome.declared ?? "?"}:${gaps}:${extServed}` | ||
| if (rec.announced === signature) return |
There was a problem hiding this comment.
Log malformed reports before deduplicating announcements
C1 is false when a session first observes no report (or a valid empty report) and a later reconciliation receives a malformed report with the same inventory. Both states produce the same gaps value, so this return runs before the malformed-report warning and the malformed metadata is silently discarded. Move the validation warning ahead of announcement deduplication, or include report validity in the signature, so the first transition to malformed metadata is always diagnosed.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Confirmed and fixed in 4aa8e68. The session record now remembers whether the last report was malformed. The warning is checked before the announcement dedupe and logged once per transition into the malformed state, with no second toast. Test: "a report that turns malformed is logged once per transition, without a second toast". It walks empty, then malformed twice, then empty, then malformed again, and fails with the warning placed after the dedupe.
…e announcement A malformed report can share its announcement signature with an earlier empty or absent one, so the warning placed after the dedupe never fired for that transition. The session record now remembers whether the last report was malformed; the warning is checked before the dedupe and logged once per transition into that state, without a second toast. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0172qrhMa5TQgETASi5hxMqD
|
@codex review Scope: only |
|
Codex Review: Didn't find any major issues. Keep it up! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
Issue for this PR
Closes #1307
Type of change
What does this PR do?
When a workspace is bound, the attach toast says "N of M declared integration tools available" and lists what is declared but absent. Until now that list came from a client-side diff: fetch the workspace's allowlist from the API, subtract the tool names the engine served. A diff can name keys, never reasons — an expired Jira token, an MCP server whose binary is not installed, an integration the tenant removed from the catalog, an extension tool with no VS Code window, and a key the provider does not offer all read the same.
@altimateai/datamate0.7.3 (AltimateAI/altimate-mcp-engine#248, released by AltimateAI/altimate-mcp-engine#250) reports every declared-but-unserved key with a reason under_meta["ai.altimate/unfulfilled"]on eachtools/listresponse. This PR reads it:_metaof a server's lasttools/listpage per client.paginatekeeps only each page's items, so the result object — the only carrier of_meta— was dropped. A listing starts with none; any page that carries one sets it; a listing without one clears it. Exposed asMCP.listMeta(name)(undefined while not connected).no-bridgeentries stay out of the "missing" line, as absent extension tools without an IDE were already treated as expected; every other reason is named, grouped, with the engine's detail (e.g.spawn docker ENOENT) —Declared but not available — no usable connection: jira_search_issues; server could not be started or reached (spawn docker ENOENT): gh_list_prs, gh_create_pr.The detail is masked when the report is parsed (control characters collapsed, thenTelemetry.maskString, as subprocess stderr is), so the toast and the attach log carry the masked text. The "N of M" headline still counts declared keys that are present, and the toast is a warning when a gap is reported or fewer declared tools are callable than declared. The attached outcome carries the full report for later surfaces._meta(nothing at or above the floor does) yields an outcome with neithermissingnorunfulfilled, and a toast with no gap line — not "all served".MIN_ENGINE_VERSION), the first engine that emits the report (0.7.2 was released without it).@altimateai/datamate@0.7.3is on npm aslatest.The client no longer reads what it cannot know: the allowlist lookup (
declared()) is kept only for the headline's denominator and the extension-tool count, and a report without a reachable allowlist still names the gaps.Claims
missingandunfulfilledexist on the outcome only when the engine sent a well-formed report; a malformed or absent_metayields neither, and a malformed one is logged as a warning once per transition into that state, even when the announcement itself is unchanged (tests: "an engine that sends no report is not read as having no gaps", "a report that turns malformed is logged once per transition, without a second toast";parseUnfulfilledcases).no-bridgenever counts as missing, and every other reason does — includingunknown-keyon an extension key while a bridge is connected (reportedMissing; test "no-bridge entries in the report are expected, never missing").tools/list_changedrefresh, the post-OAuth reconnect all go throughMcpCatalog.defs→listTools, which is the only writer (catalog-list-meta.test.tscovers first page, multi-page, and clearing)._metais retained per client but read only fordatamate; tool conversion and the storeddefsare unchanged.parseUnfulfilledcollapses control characters and appliesTelemetry.maskString, so the toast andlog.infoboth carry the masked text (test "a detail is masked and flattened before anything shows or logs it").warningwhen a gap is reported, or when a report is present and fewer declared tools are callable than declared (two raw keys that sanitise to one catalog entry); with no report nothing is claimed and it staysinfo(the collision test assertswarning, the no-report testinfo).Residuals
MIN_ENGINE_VERSIONcomment.Telemetry.maskStringis the codebase's general masking, not a secret detector: it masks paths and known key shapes, and a short bare token passes through it. What keeps configuration out ofdetailis the engine at the floor, which sends only known error shapes and reduces a spawn failure to a plain command name.How did you verify your code works?
bun run typecheckclean; prettier clean on the files this PR touches (the files that were already non-conforming onmainare left as they were).test/altimate/workspaceandtest/mcpon the branch merged with currentmain: 964 pass, 6 fail. The 6 (5mcp.headers, 1oauth-auto-connect) fail the same way withmain'ssrc/mcpswapped in, so they are environmental, not this change.New tests: 6 attach cases (reasons in the toast, no-bridge exclusion, no-report, report-without-allowlist, reason-change re-announce, the existing inventory case now stating the engine's report, error-text re-announce, and the collision case's
warning),describeMissing/parseUnfulfilled/reportedMissingunit cases (including detail masking), 6 catalog cases through a scripted client (other test files mock the MCP SDK process-wide, so a real SDK client here would list nothing; the SDK's_metapassthrough is covered by the real-engine e2e run).End to end through the real MCP service (
test/mcp/engine-unfulfilled.e2e.test.ts, env-guarded, skipped in CI): the published@altimateai/datamate@0.7.3(a cleannpm i) is spawned over stdio byMCP.addexactly as the overlay spawns it, against a fake Altimate API, a real second MCP server and a missing binary;MCP.listMeta("datamate")returns the five-entry report with the expected reasons and the toast text readsDeclared but not available — no usable connection: jira_search_issues; not offered by the integration: ghost; server could not be started or reached (spawn altimate-e2e-missing-binary ENOENT): whatever; no longer in the catalog: retired_tool.— 1 pass, and the run leaves no temporary HOME behind. Run it withALTIMATE_ENGINE_E2E_ROOT=<engine checkout with dist/> bun test test/mcp/engine-unfulfilled.e2e.test.tsfrompackages/opencode.Engine → CLI through the real attach path (evidence):
bootstrap+beforeTurnon a bound directory against the published@altimateai/datamate@0.7.3(a cleannpm i, on PATH asdatamate). Settled outcomeattachedwithdeclared: 5,missing: [jira_search_issues, ghost, whatever, retired_tool], the full report incl. theno-bridgeentry, and the exact toast text; 8/8 checks. A 0.7.2 build (the report present, the version that shipped without it) is refused asengine-too-old; 2/2.Screenshots / recordings
Not a UI change beyond toast text; the exact strings are asserted in the tests above.
Checklist
🤖 Generated with Claude Code
https://claude.ai/code/session_01GHBUvb843k1R7UAGi8Ya9b
Appendix — complexity delta (altimate-code: engine unfulfilled report)
e8c21c2af7→93c879af8· only functions this diff touches · advisory, not a gate.✅ No touched function changed in complexity (12 touched, 4 new, all under 10).
ℹ️ How to read these numbers
Cognitive (Sonar spec) counts breaks in linear reading flow — each
if/loop/catch/ternary/boolean-operator switch adds 1, and nesting makes every further break cost more. It approximates how much you must hold in your head to follow the function: 0–5 trivial · 6–10 easy · 11–15 moderate (15 = Sonar's recommended per-function cap) · 16–25 hard to follow · >25 needs decomposition.CCN (cyclomatic) counts independent paths — also the minimum number of test cases for full branch coverage of the function.
Only functions this diff touches are measured, as deltas — pre-existing complexity is not counted against this change. Rising numbers aren't automatically wrong; they're where review attention should go. Test files excluded.
Summary by cubic
Closes #1307. Workspace attach now reads the engine’s
ai.altimate/unfulfilledreport instead of diffing declared and delivered tools, so missing-tool notices include actionable reasons and details rather than only tool names.Attach behavior
no-bridgeentries; every other valid reason is reported.missingandunfulfilledunset when the report is absent; logs a warning once when it is malformed.MIN_ENGINE_VERSIONto 0.7.3, which requires@altimateai/datamate0.7.3 or newer.MCP catalog
tools/list_metaand exposes it asMCP.listMeta(name).MCP.snapshot(name)to prevent mismatched refreshes._metaclears it.Written for commit 4aa8e68. Summary will update on new commits.