From 54f091b6bb095f3b997c0cbb7156280465cdf454 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?JOS=CE=9E?= Date: Fri, 3 Jul 2026 13:02:20 -0400 Subject: [PATCH 01/12] feat(runtime): split headless host type --- .../src/IgniteElementFactory.ts | 26 +++++-------------- .../src/createComponentFactory.ts | 7 ++--- .../src/createProjectionFactory.ts | 6 ++--- packages/ignite-element/src/runtime/agent.ts | 2 +- 4 files changed, 15 insertions(+), 26 deletions(-) diff --git a/packages/ignite-element/src/IgniteElementFactory.ts b/packages/ignite-element/src/IgniteElementFactory.ts index d099b435..51751701 100644 --- a/packages/ignite-element/src/IgniteElementFactory.ts +++ b/packages/ignite-element/src/IgniteElementFactory.ts @@ -86,7 +86,7 @@ type FactoryOptions< eventTypes?: readonly string[]; createAdditionalArgs?: ( adapter: IgniteAdapter, - host?: HTMLElement, + host?: EventTarget, ) => AdditionalRenderArgs; resolveView?: (adapter: IgniteAdapter) => RuntimeView; createRenderStrategy?: RenderStrategyFactory; @@ -189,7 +189,7 @@ export default function igniteElementFactory< Event, RenderArgs > | null = null; - let runtimeHost: HTMLElement | null = null; + let runtimeHost: EventTarget | null = null; let lifecycleSequence = 0; let lifecycleInstanceSequence = 0; const lifecycleObservers = new Set< @@ -198,7 +198,7 @@ export default function igniteElementFactory< const createAdditionalArgs: ( adapter: IgniteAdapter, - host?: HTMLElement, + host?: EventTarget, ) => AdditionalRenderArgs = options?.createAdditionalArgs ?? ((_) => ({}) as AdditionalRenderArgs); @@ -327,23 +327,11 @@ export default function igniteElementFactory< sharedInstanceCount = 0; }; - // The headless agent runtime (getSchema/execute/on/watchView) only ever uses - // its host as an EventTarget — `on()` calls host.addEventListener/ - // removeEventListener and effect emits go through host.dispatchEvent. So in a - // non-DOM runtime (Node, edge) it does not need a real element: a bare - // EventTarget keeps the agent runtime fully DOM-free. The render path - // (createRuntimeDomBridge / the custom element) creates its own real element - // and still requires a DOM. - // - // The `as unknown as HTMLElement` cast is forced because the runtime host - // flows through the same `Host` generic the render path uses (typed - // HTMLElement for the JSX renderer's host.appendChild). It is safe here — no - // code path touches the runtime host with element-only APIs (verified) — but - // the proper fix is to split the agent-runtime host type (EventTarget) from - // the render-host `Host` generic so this cast disappears (tracked follow-up). - const createRuntimeHost = (): HTMLElement => + // The headless agent runtime only needs EventTarget APIs for `on()` and + // effect-emitted events. The DOM render path creates its own real element. + const createRuntimeHost = (): EventTarget => typeof document === "undefined" - ? (new EventTarget() as unknown as HTMLElement) + ? new EventTarget() : document.createElement("div"); const createRuntimeDomBridge = ( diff --git a/packages/ignite-element/src/createComponentFactory.ts b/packages/ignite-element/src/createComponentFactory.ts index 343733fb..0e50970c 100644 --- a/packages/ignite-element/src/createComponentFactory.ts +++ b/packages/ignite-element/src/createComponentFactory.ts @@ -41,7 +41,7 @@ export type ElementFactoryOptions< scope?: StateScope; createAdditionalArgs?: ( adapter: IgniteAdapter, - host?: HTMLElement, + host?: EventTarget, ) => AdditionalRenderArgs; createRenderStrategy?: RenderStrategyFactory; eventTypes?: readonly (keyof Events & string)[]; @@ -124,7 +124,7 @@ type BindProjectionToElementsOptions< }; const createDomEmit = ( - host: HTMLElement, + host: EventTarget, ): EmitFromEvents => { return ( type: Type, @@ -187,9 +187,10 @@ export function bindProjectionToElements< `[${errorPrefix}] Host element is required for projection.`, ); } + const renderHost = host as HTMLElement; return projection.createAdditionalArgs( adapter, - host, + renderHost, createDomEmit(host), ); }, diff --git a/packages/ignite-element/src/createProjectionFactory.ts b/packages/ignite-element/src/createProjectionFactory.ts index a9dac834..1ae0ae0e 100644 --- a/packages/ignite-element/src/createProjectionFactory.ts +++ b/packages/ignite-element/src/createProjectionFactory.ts @@ -20,7 +20,7 @@ import { facadeCleanupSymbol, } from "./runtime/effects"; -export type AdapterFactory = (( +export type AdapterFactory = (( host?: Host, ) => IgniteAdapter) & { scope?: StateScope; @@ -46,7 +46,7 @@ export type ProjectionFactoryOptions< >, Additional extends Record = Record, Events extends EventMap = EmptyEventMap, - Host = unknown, + Host = EventTarget, > = { scope?: StateScope; view?: FacadeViewCallback; @@ -87,7 +87,7 @@ export type ProjectionFactory< State, Event, RenderArgs extends BaseRenderArgs, - Host = unknown, + Host = EventTarget, Events extends EventMap = EmptyEventMap, ViewResult extends Record = Record, > = { diff --git a/packages/ignite-element/src/runtime/agent.ts b/packages/ignite-element/src/runtime/agent.ts index 83ec6f93..fefecc55 100644 --- a/packages/ignite-element/src/runtime/agent.ts +++ b/packages/ignite-element/src/runtime/agent.ts @@ -29,7 +29,7 @@ type RuntimeResources< > = { adapter: IgniteAdapter; additionalArgs: AdditionalArgs; - host: HTMLElement; + host: EventTarget; }; type AgentRuntimeOptions< From 97c298bfd677695b7f359df34410c13f3ee0956d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?JOS=CE=9E?= Date: Fri, 3 Jul 2026 13:02:30 -0400 Subject: [PATCH 02/12] test(runtime): cover eventtarget agent host --- .../src/tests/types/igniteCore.types.test.ts | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/packages/ignite-element/src/tests/types/igniteCore.types.test.ts b/packages/ignite-element/src/tests/types/igniteCore.types.test.ts index 82ca317e..03f385a1 100644 --- a/packages/ignite-element/src/tests/types/igniteCore.types.test.ts +++ b/packages/ignite-element/src/tests/types/igniteCore.types.test.ts @@ -5,7 +5,11 @@ import type { ActorWebReadModelSource as AdapterActorWebReadModelSource, ActorWebSource as AdapterActorWebSource, } from "@ignite-element/adapters/actor-web"; -import { command, commandMetadataSymbol } from "@ignite-element/core"; +import { + command, + commandMetadataSymbol, + type IgniteAdapter, +} from "@ignite-element/core"; import { makeAutoObservable } from "mobx"; import { describe, expect, expectTypeOf, it } from "vitest"; import { createMachine, type EventFrom, setup } from "xstate"; @@ -28,7 +32,6 @@ import * as actorWebPublic from "../../actor-web"; import { igniteCore as igniteCoreActorWebEntrypoint } from "../../actor-web"; import type { MobxEvent } from "../../adapters/MobxAdapter"; import type { XStateSnapshot } from "../../adapters/XStateAdapter"; -import counterStore, { counterSlice } from "../fixtures/reduxCounterStore"; import { igniteCore } from "../../IgniteCore"; import type { AdapterPack } from "../../IgniteElementFactory"; import type { @@ -73,6 +76,7 @@ import type { IgniteStoryTraceSnapshotEntry as ReduxIgniteStoryTraceSnapshotEntry, IgniteTestHelpers as ReduxIgniteTestHelpers, } from "../../redux"; +import { createAgentRuntime } from "../../runtime/agent"; import type { IgniteDomBridge, IgniteDomRoleExpectation, @@ -107,6 +111,7 @@ import { igniteCore as igniteCoreXState, test as xstateTest, } from "../../xstate"; +import counterStore, { counterSlice } from "../fixtures/reduxCounterStore"; type ActorWebShipmentContext = { shipmentId: string | null; @@ -196,6 +201,24 @@ const mobxCounterFactory = () => }); describe("igniteCore type inference", () => { + it("accepts an EventTarget host for the headless agent runtime", () => { + const adapter = {} as IgniteAdapter< + { count: number }, + { type: "INCREMENT" } + >; + const runtime = createAgentRuntime({ + eventTypes: [], + resolveRuntime: () => ({ + adapter, + additionalArgs: {}, + host: new EventTarget(), + }), + resolveView: () => ({}), + }); + + void runtime; + }); + it("re-exports IgniteCoreReturn from the xstate public entrypoint", () => { expectTypeOf< XStateIgniteCoreReturn< From e87957c48b07c81c647cc6147a42f04abac34755 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?JOS=CE=9E?= Date: Fri, 3 Jul 2026 13:02:39 -0400 Subject: [PATCH 03/12] chore(fas): track runtime host split task --- .fas/TASKS.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.fas/TASKS.md b/.fas/TASKS.md index e4be36e0..ab039f38 100644 --- a/.fas/TASKS.md +++ b/.fas/TASKS.md @@ -1638,8 +1638,8 @@ No active tasks. - Title: Split the agent-runtime host type (EventTarget) from the render-host Host generic so the headless runtime needs no HTMLE - Mode: single-agent -- Status: queued -- Owner: runtime +- Status: review +- Owner: reviewer - Brief: .fas/tasks/split-the-agent-runtime-host-type-eventtarget-from-the-rende.md - Automation mode: advisory From 3d47aeefefa862c699a84682f0ab50aa97cc960d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?JOS=CE=9E?= Date: Fri, 3 Jul 2026 13:04:02 -0400 Subject: [PATCH 04/12] chore(fas): complete runtime host split queue task --- .fas/queue/tasks.json | 50 +++++++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/.fas/queue/tasks.json b/.fas/queue/tasks.json index 28318240..5f7ee231 100644 --- a/.fas/queue/tasks.json +++ b/.fas/queue/tasks.json @@ -432,29 +432,6 @@ "deferredReason": "dependency_reopened: task-1782269396312", "invalidationReason": null }, - { - "id": "task-1782499303572", - "task": "Split the agent-runtime host type (EventTarget) from the render-host Host generic so the headless runtime needs no HTMLE", - "priority": "medium", - "source": "manual", - "details": "Phase A made the headless runtime DOM-free via createRuntimeHost returning a bare EventTarget cast to HTMLElement (IgniteElementFactory.ts). The cast is forced because the runtime host shares the render path's Host generic (typed HTMLElement for the JSX renderer host.appendChild). The agent runtime only uses host as an EventTarget (on() add/removeEventListener; effect emits via dispatchEvent) — verified no element-only API usage on the runtime host. Properly split the runtime-host type (EventTarget) from the render-host Host generic (HTMLElement) across createComponentFactory/createProjectionFactory/agent.ts so createRuntimeHost returns EventTarget honestly and the cast disappears. Found by CodeRabbit on the Phase A PR (major, accepted-with-justification + this follow-up).\nSee .fas/tasks/split-the-agent-runtime-host-type-eventtarget-from-the-rende.md", - "taskClassification": "self-improvement", - "selfImprovementCategory": "runtime-reliability", - "signalFingerprint": null, - "status": "queued", - "owner": "runtime", - "mode": "single-agent", - "dependsOn": [ - "task-1782494866385" - ], - "blocks": [ - "task-1782499305182" - ], - "branchBase": null, - "prDependencies": [], - "createdAt": "2026-06-26T18:41:43.572Z", - "updatedAt": "2026-06-26T18:41:43.572Z" - }, { "id": "task-1782499305182", "task": "igniteTools PR2 CodeRabbit follow-ups — bind runtime.execute, strict scalar value envelope, fix canExecute doc wording", @@ -464,7 +441,7 @@ "taskClassification": "self-improvement", "selfImprovementCategory": "runtime-reliability", "signalFingerprint": null, - "status": "deferred", + "status": "queued", "owner": "runtime", "mode": "single-agent", "dependsOn": [ @@ -477,7 +454,7 @@ "prDependencies": [], "createdAt": "2026-06-26T18:41:45.182Z", "updatedAt": "2026-06-26T18:41:45.182Z", - "deferredReason": "dependency_reopened: task-1782499303572", + "deferredReason": null, "invalidationReason": null } ], @@ -2784,6 +2761,29 @@ "workflowId": "direct-1783093106382", "branchName": "fas/ignite-ecosystem-bridge", "prStatus": "draft-only" + }, + { + "id": "task-1782499303572", + "task": "Split the agent-runtime host type (EventTarget) from the render-host Host generic so the headless runtime needs no HTMLE", + "priority": "medium", + "source": "manual", + "taskClassification": "self-improvement", + "selfImprovementCategory": "runtime-reliability", + "signalFingerprint": null, + "dependsOn": [ + "task-1782494866385" + ], + "blocks": [ + "task-1782499305182" + ], + "branchBase": null, + "prDependencies": [], + "prRef": null, + "completedAt": "2026-07-03T17:03:43.953Z", + "terminalOutcome": null, + "workflowId": "direct-1783097767418", + "branchName": "fas/ignite-ecosystem-followups", + "prStatus": "draft-only" } ], "pullRequests": [], From bb19f3d5ac2217891a0c95bbb514b35c7c1826c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?JOS=CE=9E?= Date: Fri, 3 Jul 2026 13:10:42 -0400 Subject: [PATCH 05/12] fix(tools): tighten scalar provider envelope --- .changeset/strict-scalar-tool-envelope.md | 5 ++++ docs/ignite-tools.md | 23 +++++++++++-------- .../src/tests/tools.anthropic.test.ts | 1 + .../src/tests/tools.scalar.test.ts | 22 +++++++++++++++++- .../ignite-element/src/tests/tools.test.ts | 2 +- .../ignite-element/src/tools/igniteTools.ts | 4 ++-- packages/ignite-element/src/tools/scalar.ts | 2 ++ 7 files changed, 46 insertions(+), 13 deletions(-) create mode 100644 .changeset/strict-scalar-tool-envelope.md diff --git a/.changeset/strict-scalar-tool-envelope.md b/.changeset/strict-scalar-tool-envelope.md new file mode 100644 index 00000000..1c50b16f --- /dev/null +++ b/.changeset/strict-scalar-tool-envelope.md @@ -0,0 +1,5 @@ +--- +"ignite-element": patch +--- + +Tighten igniteTools scalar provider envelopes by publishing `additionalProperties: false` on scalar wrappers and rejecting malformed `{ value, ...extra }` provider inputs as `InvalidInput`. diff --git a/docs/ignite-tools.md b/docs/ignite-tools.md index 80cf926a..48287409 100644 --- a/docs/ignite-tools.md +++ b/docs/ignite-tools.md @@ -83,14 +83,16 @@ because that is the command's true contract (`getSchema()` must not lie). So the wrap/unwrap lives only at the provider boundary, in shared pure helpers (`tools/scalar.ts`): -- `toProviderInputSchema(schema)` — wraps a scalar under a clean `value` key - (`{ type: "object", properties: { value: schema }, required: ["value"] }`); - object/no-arg schemas pass through unchanged. Adapters call it in `tools()`. -- `fromProviderInput(input, schema)` — unwraps the model's `{ value: x }` back to - `x`, **gated on the manifest schema being scalar** (collision-free: an object - command that legitimately has its own `value` field is never unwrapped). - Adapters call it in `toolCalls()`, which is why the port hands `toolCalls` the - manifest. +- `toProviderInputSchema(schema)` — wraps a scalar under a clean, strict `value` + key (`{ type: "object", properties: { value: schema }, required: ["value"], + additionalProperties: false }`); object/no-arg schemas pass through unchanged. + Adapters call it in `tools()`. +- `fromProviderInput(input, schema)` — unwraps the model's exact `{ value: x }` + back to `x`, **gated on the manifest schema being scalar** (collision-free: an + object command that legitimately has its own `value` field is never unwrapped). + Extra keys keep the provider object intact so `resolveCall` reports + `InvalidInput`. Adapters call it in `toolCalls()`, which is why the port hands + `toolCalls` the manifest. The constraint is universal across providers, so it is fixed once in the port + two helpers; the OpenAI/Ollama dialect reuses them verbatim. @@ -216,7 +218,10 @@ to the provider's `tool_result` (`is_error: true`) so the model can recover. - **typed-view** ✓ + **`getSchema().view`** ✓ (done) — typed manifest inputs + view grounding. - **`canExecute`** (`docs/can-execute.md`) — composes for availability-gated tools - by omitting unavailable commands from the manifest. Older runtimes without the + by omitting unavailable commands when `igniteTools(runtime)` builds the manifest + and by re-checking availability when `run()` routes a call. To publish a fresh + provider tool list after state changes, rebuild `igniteTools(runtime)` or + re-derive provider tools from a fresh manifest. Older runtimes without the optional method still offer all commands for compatibility. ## Alternatives considered diff --git a/packages/ignite-element/src/tests/tools.anthropic.test.ts b/packages/ignite-element/src/tests/tools.anthropic.test.ts index 9cac7163..7ee135ca 100644 --- a/packages/ignite-element/src/tests/tools.anthropic.test.ts +++ b/packages/ignite-element/src/tests/tools.anthropic.test.ts @@ -47,6 +47,7 @@ describe("anthropic.tools (neutral manifest -> Anthropic tool defs)", () => { type: "object", properties: { value: { type: "number", minimum: 3, maximum: 12 } }, required: ["value"], + additionalProperties: false, }, }); }); diff --git a/packages/ignite-element/src/tests/tools.scalar.test.ts b/packages/ignite-element/src/tests/tools.scalar.test.ts index e5ea0e2a..03b19cb6 100644 --- a/packages/ignite-element/src/tests/tools.scalar.test.ts +++ b/packages/ignite-element/src/tests/tools.scalar.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "vitest"; -import type { IgniteSchemaObject } from "../types/schema"; +import { isErr, resolveCall } from "../tools"; import { fromProviderInput, toProviderInputSchema } from "../tools/scalar"; +import type { IgniteSchemaObject } from "../types/schema"; // Every tool-calling provider (Anthropic / OpenAI / Ollama) requires OBJECT-shaped // tool inputs, but the neutral manifest carries SCALAR inputSchema for single-arg @@ -19,6 +20,7 @@ describe("toProviderInputSchema", () => { type: "object", properties: { value: scalar }, required: ["value"], + additionalProperties: false, }); }); @@ -31,6 +33,7 @@ describe("toProviderInputSchema", () => { type: "object", properties: { value: scalar }, required: ["value"], + additionalProperties: false, }); }); @@ -74,4 +77,21 @@ describe("fromProviderInput", () => { it("returns input unchanged for a scalar schema when the input is not a { value } envelope", () => { expect(fromProviderInput(7, { type: "number" })).toBe(7); }); + + it("rejects a scalar envelope with extra keys by leaving it for command validation", () => { + const input = { value: 7, extra: true }; + const schema: IgniteSchemaObject = { type: "number" }; + const providerInput = fromProviderInput(input, schema); + const result = resolveCall( + [{ name: "setLimit", inputSchema: schema, gated: false }], + "setLimit", + providerInput, + ); + + expect(providerInput).toBe(input); + expect(isErr(result)).toBe(true); + if (isErr(result)) { + expect(result.error.kind).toBe("InvalidInput"); + } + }); }); diff --git a/packages/ignite-element/src/tests/tools.test.ts b/packages/ignite-element/src/tests/tools.test.ts index 754c81e0..f1cf893e 100644 --- a/packages/ignite-element/src/tests/tools.test.ts +++ b/packages/ignite-element/src/tests/tools.test.ts @@ -430,7 +430,7 @@ describe("igniteTools (neutral, no dialect)", () => { } }); - it("binds runtime methods before calling execute and getView", async () => { + it("binds runtime.execute before storage and calls getView with runtime context", async () => { const component = new ThisBoundFakeComponent(); const { run } = igniteTools(component); const result = await run({ name: "setLimit", input: 7 }); diff --git a/packages/ignite-element/src/tools/igniteTools.ts b/packages/ignite-element/src/tools/igniteTools.ts index f691d90c..b0fe5c95 100644 --- a/packages/ignite-element/src/tools/igniteTools.ts +++ b/packages/ignite-element/src/tools/igniteTools.ts @@ -85,8 +85,8 @@ export function igniteTools( const schema = runtime.getSchema(); const manifest = buildManifest(schema, canExecute); - // The model supplies dynamic command names, so treat `execute` as the loose - // runtime contract at this boundary. + // The model supplies dynamic command names, so bind the runtime method before + // storing it and treat `execute` as the loose contract at this boundary. const execute = runtime.execute.bind(runtime) as unknown as ( name: string, payload?: unknown, diff --git a/packages/ignite-element/src/tools/scalar.ts b/packages/ignite-element/src/tools/scalar.ts index 4be77dad..74e35375 100644 --- a/packages/ignite-element/src/tools/scalar.ts +++ b/packages/ignite-element/src/tools/scalar.ts @@ -36,6 +36,7 @@ export function toProviderInputSchema( type: "object", properties: { [SCALAR_KEY]: schema }, required: [SCALAR_KEY], + additionalProperties: false, }; } @@ -54,6 +55,7 @@ export function fromProviderInput( schema !== undefined && schema.type !== "object" && isPlainObject(input) && + Object.keys(input).length === 1 && SCALAR_KEY in input ) { return input[SCALAR_KEY]; From 87a19e09d3a81d058d8111b588e3928b3284c7c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?JOS=CE=9E?= Date: Fri, 3 Jul 2026 13:10:56 -0400 Subject: [PATCH 06/12] chore(fas): track ignitetools pr2 follow-up task --- .fas/TASKS.md | 7 +++-- .fas/queue/tasks.json | 4 +-- ...abbit-follow-ups-bind-runtime-execute-s.md | 30 ++++++++++++++++++- 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/.fas/TASKS.md b/.fas/TASKS.md index ab039f38..1c640d6f 100644 --- a/.fas/TASKS.md +++ b/.fas/TASKS.md @@ -1647,10 +1647,13 @@ No active tasks. - Title: igniteTools PR2 CodeRabbit follow-ups — bind runtime.execute, strict scalar value envelope, fix canExecute doc wording - Mode: single-agent -- Status: queued -- Owner: runtime +- Status: review +- Owner: reviewer - Brief: .fas/tasks/ignitetools-pr2-coderabbit-follow-ups-bind-runtime-execute-s.md - Automation mode: advisory +- Verification lane: fast +- Policy sensitivity: standard +- Blast radius: cross-cutting ### Task: Fix example runtime test lanes before next igniteTools task diff --git a/.fas/queue/tasks.json b/.fas/queue/tasks.json index 5f7ee231..ddf470cd 100644 --- a/.fas/queue/tasks.json +++ b/.fas/queue/tasks.json @@ -441,7 +441,7 @@ "taskClassification": "self-improvement", "selfImprovementCategory": "runtime-reliability", "signalFingerprint": null, - "status": "queued", + "status": "processing", "owner": "runtime", "mode": "single-agent", "dependsOn": [ @@ -453,7 +453,7 @@ "branchBase": null, "prDependencies": [], "createdAt": "2026-06-26T18:41:45.182Z", - "updatedAt": "2026-06-26T18:41:45.182Z", + "updatedAt": "2026-07-03T17:04:35.699Z", "deferredReason": null, "invalidationReason": null } diff --git a/.fas/tasks/ignitetools-pr2-coderabbit-follow-ups-bind-runtime-execute-s.md b/.fas/tasks/ignitetools-pr2-coderabbit-follow-ups-bind-runtime-execute-s.md index 623bad47..7511e836 100644 --- a/.fas/tasks/ignitetools-pr2-coderabbit-follow-ups-bind-runtime-execute-s.md +++ b/.fas/tasks/ignitetools-pr2-coderabbit-follow-ups-bind-runtime-execute-s.md @@ -37,9 +37,37 @@ Three CodeRabbit findings on shipped PR2 code, out of the Phase A (DOM-free) sco - packages/ignite-element/src/tests/tools.test.ts - packages/ignite-element/src/tests/tools.scalar.test.ts - docs/ignite-tools.md +- .changeset/strict-scalar-tool-envelope.md +- packages/ignite-element/src/tests/tools.anthropic.test.ts ## Scope Amendments -- None. +- Type: implementation-scope +- Added at: 2026-07-03 +- Trigger: strict scalar envelope requires package changeset and Anthropic golden fixture update +- Reason: Acceptance criteria require a changeset, and adding additionalProperties:false changes the provider-facing Anthropic schema fixture. +- Evidence source: closeout-readiness +- Evidence: closeout-readiness | .fas/state/closeout-readiness/latest.json +- Accuracy signal: live ChangeSet after focused tests +- Follow-up needed: none + +- Type: implementation-scope +- Added at: 2026-07-03 +- Trigger: strict scalar wrapper changed provider schema +- Reason: The package patch changeset is required by acceptance criteria, and the Anthropic golden test must reflect additionalProperties:false emitted by the shared scalar helper. +- Added paths: .changeset/strict-scalar-tool-envelope.md, packages/ignite-element/src/tests/tools.anthropic.test.ts +- Evidence source: focused-verification +- Evidence: focused-verification | .fas/state/verification/validate-task-1783098491.log +- Accuracy signal: focused tests and typecheck passed before scope refresh +- Follow-up needed: none + +- Type: implementation-scope +- Added at: 2026-07-03 +- Trigger: explicit affected-file amendment recorded +- Reason: Refresh generated planning and task packet after adding changeset and Anthropic fixture to the task scope. +- Evidence source: task-brief +- Evidence: task-brief | .fas/tasks/ignitetools-pr2-coderabbit-follow-ups-bind-runtime-execute-s.md +- Accuracy signal: affected files now match live ChangeSet +- Follow-up needed: none ## Implementation plan - Convert the supplied context into a scoped implementation plan before editing. From f8fc66cc2f55f9b7e8ca9b9371855cd1a2e7c1c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?JOS=CE=9E?= Date: Fri, 3 Jul 2026 13:12:14 -0400 Subject: [PATCH 07/12] chore(fas): complete ignitetools pr2 follow-up --- .fas/queue/tasks.json | 52 +++++++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/.fas/queue/tasks.json b/.fas/queue/tasks.json index ddf470cd..957b0c65 100644 --- a/.fas/queue/tasks.json +++ b/.fas/queue/tasks.json @@ -391,7 +391,7 @@ "taskClassification": "self-improvement", "selfImprovementCategory": "runtime-reliability", "signalFingerprint": null, - "status": "deferred", + "status": "queued", "owner": "runtime", "mode": "single-agent", "dependsOn": [ @@ -404,7 +404,7 @@ "prDependencies": [], "createdAt": "2026-06-24T02:49:56.312Z", "updatedAt": "2026-06-24T02:49:56.312Z", - "deferredReason": "dependency_reopened: task-1782499305182", + "deferredReason": null, "invalidationReason": null }, { @@ -431,31 +431,6 @@ "updatedAt": "2026-06-24T02:50:14.711Z", "deferredReason": "dependency_reopened: task-1782269396312", "invalidationReason": null - }, - { - "id": "task-1782499305182", - "task": "igniteTools PR2 CodeRabbit follow-ups — bind runtime.execute, strict scalar value envelope, fix canExecute doc wording", - "priority": "medium", - "source": "manual", - "details": "Three CodeRabbit findings on shipped PR2 code, out of the Phase A (DOM-free) scope. (1) igniteTools.ts: const execute = runtime.execute is unbound — bind it via runtime.execute.bind(runtime) like canExecute already is, else an execute that relies on this breaks when run() calls it; the current fake runtime uses an arrow so the gap is uncovered — add a class/this-based fake runtime regression test. (2) tools/scalar.ts: the scalar value envelope is not strict — { value: 7, extra: true } unwraps to 7, bypassing resolveCall validation; add additionalProperties:false on encode (toProviderInputSchema) and reject extra keys on decode (fromProviderInput) so a malformed provider envelope surfaces InvalidInput; add tests. (3) docs/ignite-tools.md: the observation note overpromises that canExecute re-gates the tool list as state changes, but igniteTools snapshots the manifest once — reword to say rebuild igniteTools() (or re-derive tools) to publish a fresh canExecute-gated manifest.\nSee .fas/tasks/ignitetools-pr2-coderabbit-follow-ups-bind-runtime-execute-s.md", - "taskClassification": "self-improvement", - "selfImprovementCategory": "runtime-reliability", - "signalFingerprint": null, - "status": "processing", - "owner": "runtime", - "mode": "single-agent", - "dependsOn": [ - "task-1782499303572" - ], - "blocks": [ - "task-1782269396312" - ], - "branchBase": null, - "prDependencies": [], - "createdAt": "2026-06-26T18:41:45.182Z", - "updatedAt": "2026-07-03T17:04:35.699Z", - "deferredReason": null, - "invalidationReason": null } ], "completedTasks": [ @@ -2784,6 +2759,29 @@ "workflowId": "direct-1783097767418", "branchName": "fas/ignite-ecosystem-followups", "prStatus": "draft-only" + }, + { + "id": "task-1782499305182", + "task": "igniteTools PR2 CodeRabbit follow-ups — bind runtime.execute, strict scalar value envelope, fix canExecute doc wording", + "priority": "medium", + "source": "manual", + "taskClassification": "self-improvement", + "selfImprovementCategory": "runtime-reliability", + "signalFingerprint": null, + "dependsOn": [ + "task-1782499303572" + ], + "blocks": [ + "task-1782269396312" + ], + "branchBase": null, + "prDependencies": [], + "prRef": null, + "completedAt": "2026-07-03T17:11:46.866Z", + "terminalOutcome": null, + "workflowId": "direct-1783098275694", + "branchName": "fas/ignite-ecosystem-followups", + "prStatus": "draft-only" } ], "pullRequests": [], From b64426381b55c3f0d773b78b9664af7d2eb8579d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?JOS=CE=9E?= Date: Fri, 3 Jul 2026 13:15:20 -0400 Subject: [PATCH 08/12] chore(fas): defer actor-web address collapse --- .fas/TASKS.md | 2 +- .fas/queue/tasks.json | 6 +++--- ...e-ignite-actorwebaddress-tolerant-union-to-pure-strin.md | 5 ++++- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.fas/TASKS.md b/.fas/TASKS.md index 1c640d6f..634c7729 100644 --- a/.fas/TASKS.md +++ b/.fas/TASKS.md @@ -1562,7 +1562,7 @@ No active tasks. - Title: collapse ignite ActorWebAddress tolerant union to pure string once @actor-web/runtime publishes the opaque branded addre - Mode: single-agent -- Status: queued +- Status: deferred - Owner: runtime - Brief: .fas/tasks/collapse-ignite-actorwebaddress-tolerant-union-to-pure-strin.md - Automation mode: advisory diff --git a/.fas/queue/tasks.json b/.fas/queue/tasks.json index 957b0c65..3a0555c8 100644 --- a/.fas/queue/tasks.json +++ b/.fas/queue/tasks.json @@ -391,7 +391,7 @@ "taskClassification": "self-improvement", "selfImprovementCategory": "runtime-reliability", "signalFingerprint": null, - "status": "queued", + "status": "deferred", "owner": "runtime", "mode": "single-agent", "dependsOn": [ @@ -403,8 +403,8 @@ "branchBase": null, "prDependencies": [], "createdAt": "2026-06-24T02:49:56.312Z", - "updatedAt": "2026-06-24T02:49:56.312Z", - "deferredReason": null, + "updatedAt": "2026-07-03T17:13:42.000Z", + "deferredReason": "blocked-until-actor-web-runtime-branded-address: current @actor-web/runtime resolves to 0.1.0, which still uses the legacy object address", "invalidationReason": null }, { diff --git a/.fas/tasks/collapse-ignite-actorwebaddress-tolerant-union-to-pure-strin.md b/.fas/tasks/collapse-ignite-actorwebaddress-tolerant-union-to-pure-strin.md index f218fb9f..80cc4c38 100644 --- a/.fas/tasks/collapse-ignite-actorwebaddress-tolerant-union-to-pure-strin.md +++ b/.fas/tasks/collapse-ignite-actorwebaddress-tolerant-union-to-pure-strin.md @@ -48,7 +48,10 @@ Drop the object branch + the TODO(actor-web > 0.1.0) comment in packages/ignite- - Validate generated scope, acceptance criteria, and verification evidence before closeout to avoid workflow drift. ## Dependencies -- None known at task creation. +- Blocked until `@actor-web/runtime` publishes the branded string `ActorAddress` + and this repo bumps the installed/devDependency version. Current verification + on 2026-07-03 shows `@ignite-element/adapters` still resolves + `@actor-web/runtime@0.1.0`, which uses the legacy object address. ## Open questions - None captured at task creation. From f9b365a06a7c53d2ec9126b55b6bf6b3abbc6039 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?JOS=CE=9E?= Date: Fri, 3 Jul 2026 13:18:21 -0400 Subject: [PATCH 09/12] fix(actor-web): update command source warnings --- .../src/adapters/ActorWebAdapter.ts | 10 ++++--- .../tests/adapters/ActorWebAdapter.test.ts | 27 +++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/packages/ignite-adapters/src/adapters/ActorWebAdapter.ts b/packages/ignite-adapters/src/adapters/ActorWebAdapter.ts index f0f2f438..05b42bf1 100644 --- a/packages/ignite-adapters/src/adapters/ActorWebAdapter.ts +++ b/packages/ignite-adapters/src/adapters/ActorWebAdapter.ts @@ -286,7 +286,9 @@ function createSharedFactory< factory.resolveStateSnapshot = () => entry.snapshot(); factory.resolveCommandActor = () => entry.actor ?? - failInvariant("[ActorWebAdapter] Actor-Web commandSource is required."); + failInvariant( + "[ActorWebAdapter] Actor-Web command-capable source (one that exposes send()) is required.", + ); return factory; } @@ -324,7 +326,9 @@ function createIsolatedFactory< adapter, "[ActorWebAdapter] Unable to resolve actor for facade callbacks.", ).actor ?? - failInvariant("[ActorWebAdapter] Actor-Web commandSource is required.") + failInvariant( + "[ActorWebAdapter] Actor-Web command-capable source (one that exposes send()) is required.", + ) ); }; @@ -465,7 +469,7 @@ function createAdapterEntry< if (!commandSource) { console.warn( - "[ActorWebAdapter] Cannot send events without an Actor-Web commandSource.", + "[ActorWebAdapter] Cannot send events without an Actor-Web command-capable source (one that exposes send()).", ); return; } diff --git a/packages/ignite-element/src/tests/adapters/ActorWebAdapter.test.ts b/packages/ignite-element/src/tests/adapters/ActorWebAdapter.test.ts index ea2b7b98..ca2e4327 100644 --- a/packages/ignite-element/src/tests/adapters/ActorWebAdapter.test.ts +++ b/packages/ignite-element/src/tests/adapters/ActorWebAdapter.test.ts @@ -224,6 +224,33 @@ describe("ActorWebAdapter", () => { expect(close).toHaveBeenCalledTimes(1); }); + it("describes missing send() as a missing command-capable source", () => { + const warn = vi.spyOn(console, "warn").mockImplementation(() => undefined); + const source = createSource() as ReturnType & { + send?: unknown; + }; + const { send: _send, ...readModelSource } = source; + const adapterFactory = createActorWebAdapter( + readModelSource as unknown as ActorWebSource< + ShipmentContext, + ShipmentCommand, + ShipmentEmitted + >, + ); + const adapter = adapterFactory(); + + expect(() => adapterFactory.resolveCommandActor(adapter)).toThrow( + "Actor-Web command-capable source (one that exposes send()) is required.", + ); + + adapter.send({ type: "CREATE_SHIPMENT", shipmentId: "shipment-123" }); + + expect(warn).toHaveBeenCalledWith( + "[ActorWebAdapter] Cannot send events without an Actor-Web command-capable source (one that exposes send()).", + ); + adapter.stop(); + }); + it("dedupes the initial notification when upstream replays synchronously", () => { const source = createSource({ replayOnSubscribe: true, From f6cf71c61bd90f9e74822c22e7aa15d317575082 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?JOS=CE=9E?= Date: Fri, 3 Jul 2026 13:18:37 -0400 Subject: [PATCH 10/12] chore(fas): track actor-web warning wording task --- .fas/TASKS.md | 7 +++++-- ...r-dev-warning-wording-that-still-refere.md | 20 ++++++++++++++++++- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/.fas/TASKS.md b/.fas/TASKS.md index 634c7729..58c653f2 100644 --- a/.fas/TASKS.md +++ b/.fas/TASKS.md @@ -1571,9 +1571,12 @@ No active tasks. - Title: update ActorWebAdapter dev-warning wording that still references the removed commandSource config concept after the beta - Mode: single-agent -- Status: queued -- Owner: runtime +- Status: review +- Owner: reviewer - Brief: .fas/tasks/update-actorwebadapter-dev-warning-wording-that-still-refere.md +- Verification lane: fast +- Policy sensitivity: standard +- Blast radius: cross-cutting ### Task: add an observe() observation channel to the neutral igniteTools core (events + view stream) for the agent act-observe lo diff --git a/.fas/tasks/update-actorwebadapter-dev-warning-wording-that-still-refere.md b/.fas/tasks/update-actorwebadapter-dev-warning-wording-that-still-refere.md index 0745b189..e98c3e93 100644 --- a/.fas/tasks/update-actorwebadapter-dev-warning-wording-that-still-refere.md +++ b/.fas/tasks/update-actorwebadapter-dev-warning-wording-that-still-refere.md @@ -23,9 +23,27 @@ After commandSource was removed (PR #67), these messages still say 'commandSourc ## Affected files - packages/ignite-adapters/src/adapters/ActorWebAdapter.ts +- packages/ignite-element/src/tests/adapters/ActorWebAdapter.test.ts ## Scope Amendments -- None. +- Type: test-scope +- Added at: 2026-07-03 +- Trigger: warning wording needs runtime-facing regression coverage +- Reason: The adapter warning/error strings are exercised through the existing ignite-element ActorWebAdapter vitest suite. +- Added paths: packages/ignite-element/src/tests/adapters/ActorWebAdapter.test.ts +- Evidence source: focused-test +- Evidence: focused-test | packages/ignite-element/src/tests/adapters/ActorWebAdapter.test.ts +- Accuracy signal: focused test failed before implementation and passed after wording update +- Follow-up needed: none + +- Type: test-scope +- Added at: 2026-07-03 +- Trigger: explicit affected test path recorded +- Reason: Refresh generated planning and task packet after adding ActorWebAdapter.test.ts to task scope. +- Evidence source: task-brief +- Evidence: task-brief | .fas/tasks/update-actorwebadapter-dev-warning-wording-that-still-refere.md +- Accuracy signal: affected files now match live ChangeSet +- Follow-up needed: none ## Implementation plan - Convert the supplied context into a scoped implementation plan before editing. From 64b9eda890dd7d9a9ef5e8705f19c2ba06e07345 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?JOS=CE=9E?= Date: Fri, 3 Jul 2026 13:19:55 -0400 Subject: [PATCH 11/12] chore(fas): complete actor-web warning wording task --- .fas/queue/tasks.json | 50 +++++++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/.fas/queue/tasks.json b/.fas/queue/tasks.json index 3a0555c8..b43c5514 100644 --- a/.fas/queue/tasks.json +++ b/.fas/queue/tasks.json @@ -202,7 +202,7 @@ "prDependencies": [], "createdAt": "2026-06-18T17:54:24.107Z", "updatedAt": "2026-06-20T07:07:22.721Z", - "deferredReason": "dependency_reopened: task-1782269414711", + "deferredReason": null, "invalidationReason": null }, { @@ -406,31 +406,6 @@ "updatedAt": "2026-07-03T17:13:42.000Z", "deferredReason": "blocked-until-actor-web-runtime-branded-address: current @actor-web/runtime resolves to 0.1.0, which still uses the legacy object address", "invalidationReason": null - }, - { - "id": "task-1782269414711", - "task": "update ActorWebAdapter dev-warning wording that still references the removed commandSource config concept after the beta", - "priority": "low", - "source": "manual", - "details": "After commandSource was removed (PR #67), these messages still say 'commandSource' which no longer exists in the public API: failInvariant 'Actor-Web commandSource is required.' at packages/ignite-adapters/src/adapters/ActorWebAdapter.ts ~289 and ~327 (resolveCommandActor), and the send() console.warn 'Cannot send events without an Actor-Web commandSource.' ~468. Reword to reference 'a command-capable source (one that exposes send())'. Internal dev warnings only; behavior unchanged. Nit deferred from PR #67 babysit to avoid churning a green PR.\nSee .fas/tasks/update-actorwebadapter-dev-warning-wording-that-still-refere.md", - "taskClassification": "standard", - "selfImprovementCategory": null, - "signalFingerprint": null, - "status": "deferred", - "owner": "runtime", - "mode": "single-agent", - "dependsOn": [ - "task-1782269396312" - ], - "blocks": [ - "task-1781805264107" - ], - "branchBase": null, - "prDependencies": [], - "createdAt": "2026-06-24T02:50:14.711Z", - "updatedAt": "2026-06-24T02:50:14.711Z", - "deferredReason": "dependency_reopened: task-1782269396312", - "invalidationReason": null } ], "completedTasks": [ @@ -2782,6 +2757,29 @@ "workflowId": "direct-1783098275694", "branchName": "fas/ignite-ecosystem-followups", "prStatus": "draft-only" + }, + { + "id": "task-1782269414711", + "task": "update ActorWebAdapter dev-warning wording that still references the removed commandSource config concept after the beta", + "priority": "low", + "source": "manual", + "taskClassification": "standard", + "selfImprovementCategory": null, + "signalFingerprint": null, + "dependsOn": [ + "task-1782269396312" + ], + "blocks": [ + "task-1781805264107" + ], + "branchBase": null, + "prDependencies": [], + "prRef": null, + "completedAt": "2026-07-03T17:18:57.748Z", + "terminalOutcome": null, + "workflowId": "direct-1783098943999", + "branchName": "fas/ignite-ecosystem-followups", + "prStatus": "draft-only" } ], "pullRequests": [], From 9e12bdd142c51f9649316f62c3d6d3eba11ead9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?JOS=CE=9E?= Date: Fri, 3 Jul 2026 13:26:51 -0400 Subject: [PATCH 12/12] chore(fas): close ecosystem follow-up batch --- .fas/TASKS.md | 12 ++++++------ .fas/queue/tasks.json | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.fas/TASKS.md b/.fas/TASKS.md index 58c653f2..167c6043 100644 --- a/.fas/TASKS.md +++ b/.fas/TASKS.md @@ -1571,8 +1571,8 @@ No active tasks. - Title: update ActorWebAdapter dev-warning wording that still references the removed commandSource config concept after the beta - Mode: single-agent -- Status: review -- Owner: reviewer +- Status: done +- Owner: implementer - Brief: .fas/tasks/update-actorwebadapter-dev-warning-wording-that-still-refere.md - Verification lane: fast - Policy sensitivity: standard @@ -1641,8 +1641,8 @@ No active tasks. - Title: Split the agent-runtime host type (EventTarget) from the render-host Host generic so the headless runtime needs no HTMLE - Mode: single-agent -- Status: review -- Owner: reviewer +- Status: done +- Owner: implementer - Brief: .fas/tasks/split-the-agent-runtime-host-type-eventtarget-from-the-rende.md - Automation mode: advisory @@ -1650,8 +1650,8 @@ No active tasks. - Title: igniteTools PR2 CodeRabbit follow-ups — bind runtime.execute, strict scalar value envelope, fix canExecute doc wording - Mode: single-agent -- Status: review -- Owner: reviewer +- Status: done +- Owner: implementer - Brief: .fas/tasks/ignitetools-pr2-coderabbit-follow-ups-bind-runtime-execute-s.md - Automation mode: advisory - Verification lane: fast diff --git a/.fas/queue/tasks.json b/.fas/queue/tasks.json index b43c5514..67319389 100644 --- a/.fas/queue/tasks.json +++ b/.fas/queue/tasks.json @@ -187,7 +187,7 @@ "taskClassification": "self-improvement", "selfImprovementCategory": "developer-experience", "signalFingerprint": null, - "status": "deferred", + "status": "queued", "owner": "runtime", "mode": "single-agent", "dependsOn": [ @@ -2752,7 +2752,7 @@ "branchBase": null, "prDependencies": [], "prRef": null, - "completedAt": "2026-07-03T17:11:46.866Z", + "completedAt": "2026-07-03T17:25:38.445Z", "terminalOutcome": null, "workflowId": "direct-1783098275694", "branchName": "fas/ignite-ecosystem-followups",