From bab4429cdacc615f25b4adc20684a1279bc71850 Mon Sep 17 00:00:00 2001 From: Chris Johnstone Date: Mon, 15 Jun 2026 14:47:16 +1200 Subject: [PATCH 1/6] Fix runtime script variable evaluation From 362a42c4326cd688565d2c3cb12cd797b5dfe88d Mon Sep 17 00:00:00 2001 From: Chris Johnstone Date: Mon, 15 Jun 2026 14:47:57 +1200 Subject: [PATCH 2/6] Fix runtime script variable evaluation From 66dc72030de23aada56e56a47b8a07432a15be87 Mon Sep 17 00:00:00 2001 From: Chris Johnstone Date: Mon, 15 Jun 2026 14:50:16 +1200 Subject: [PATCH 3/6] Fix runtime script variable evaluation --- .../AGENT_PROGRESS.md | 14 ++++ .../demo-api/WebSocket/runtime-lifecycle.yml | 14 ++-- .../demo-api/gRPC/runtime-unary-lifecycle.yml | 20 +++--- examples/demo-api/opencollection.yml | 2 + src/services/runtimeExecutionService.ts | 71 +++++++++++++++++-- test/grpcSupport.test.ts | 20 ++++-- test/runtimeExecutionService.test.ts | 70 ++++++++++++++++++ test/webSocketSupport.test.ts | 21 ++++-- 8 files changed, 204 insertions(+), 28 deletions(-) diff --git a/docs/open-collection-gap-analysis/AGENT_PROGRESS.md b/docs/open-collection-gap-analysis/AGENT_PROGRESS.md index 2dd2005..ddcefbc 100644 --- a/docs/open-collection-gap-analysis/AGENT_PROGRESS.md +++ b/docs/open-collection-gap-analysis/AGENT_PROGRESS.md @@ -474,6 +474,20 @@ Record cross-cutting decisions here so parallel agents do not rediscover them. Verified: `npm run compile` passed; targeted `npx vitest run test/webSocketSupport.test.ts test/grpcSupport.test.ts test/sendRequestTool.test.ts test/responseProvider.test.ts` passed 4 files/50 tests; `node scripts/validate-collection.js examples/demo-api` passed 44/44 files; `npm test` passed 22 files/428 tests; `npm run build` passed; `npx @vscode/vsce package --out $env:TEMP\missio-0.7.7-oc080-supervisor.vsix` passed; `code.cmd --install-extension $env:TEMP\missio-0.7.7-oc080-supervisor.vsix --force` installed `missio.missio@0.7.7`. Next: OC-080 is safe to move forward; keep OC-110 as the separate runtime authoring UX follow-up. +- 2026-06-15 14:43 NZT - Codex: Claimed an OC-080 runtime lifecycle regression audit for script-source `{{variable}}` evaluation before WebSocket and unary gRPC request mutation. + GitButler: `feature/runtime-script-variable-evaluation` (bl). `but status -fv` shows only unrelated generated PDF asset churn `qp`/`ko` in `zz`; preserve those and route only runtime lifecycle/test/demo/ledger changes. + Coverage: add failing coverage proving before-request script source can interpolate base environment variables, request runtime variables, and variables mutated by earlier scripts before setting WebSocket headers/body and gRPC metadata/message; keep unresolved placeholders literal for diagnostics; keep unsafe interpolated script execution inside the existing VM sandbox; re-run WebSocket/gRPC runtime lifecycle, runtime service, Copilot tool output, response provider, demo collection validation, compile, full suite, and build. + Changed: `docs/open-collection-gap-analysis/AGENT_PROGRESS.md`. + Verified: `but status -fv`, OC-080 task page, existing OC-080 tests, and demo WebSocket/gRPC runtime fixtures reviewed; implementation tests not run yet. + Next: add the regression tests, patch `RuntimeExecutionService` interpolation at script compile time, and update demo runtime fixtures to exercise the user-verifiable path. + +- 2026-06-15 14:52 NZT - Codex: Completed the OC-080 regression fix for runtime variable evaluation in script source before non-HTTP request mutation. + GitButler: owned unassigned IDs before commit are `s1`, `r1`, `j1`, `g2`, `k1`-`p1`, `w1`-`z1`, and `t1`-`v1` for `feature/runtime-script-variable-evaluation` (bl). Parallel/unrelated IDs `qp` and `ko` are existing generated PDF asset churn and were not touched for this slice. + Coverage: `RuntimeExecutionService` now recursively resolves request runtime variable values against base/extra variables, interpolates script source from the currently visible variable state before each VM compile, preserves unresolved placeholders, and keeps interpolated source inside the existing no-filesystem/no-process VM sandbox. WebSocket and unary gRPC lifecycle tests now prove earlier before-request script mutations can feed later script-source placeholders that mutate headers, metadata, and messages. Demo WebSocket/gRPC runtime requests now exercise the same user-verifiable path. + Changed: `src/services/runtimeExecutionService.ts`, `test/runtimeExecutionService.test.ts`, `test/webSocketSupport.test.ts`, `test/grpcSupport.test.ts`, `examples/demo-api/WebSocket/runtime-lifecycle.yml`, `examples/demo-api/gRPC/runtime-unary-lifecycle.yml`, `examples/demo-api/opencollection.yml`, and this ledger. + Verified: reproducing `npx.cmd vitest run test/runtimeExecutionService.test.ts test/webSocketSupport.test.ts test/grpcSupport.test.ts` failed before the implementation with 6 expected regression failures; after the fix it passed 3 files/55 tests and a final post-format rerun also passed 3 files/55 tests. `npx.cmd vitest run test/sendRequestTool.test.ts test/responseProvider.test.ts` passed 2 files/13 tests. `node scripts/validate-collection.js examples/demo-api` passed 47/47 files. `npm.cmd run compile` passed before the full run and again after the final formatting tweak. `npm.cmd test` passed 25 files/467 tests. `npm.cmd run build` first hit sandbox access denial writing generated media files, then passed with approved escalation. + Next: commit only the owned runtime lifecycle regression slice to `feature/runtime-script-variable-evaluation` and leave `qp`/`ko` uncommitted. + ### OC-110 Runtime Authoring UX - 2026-06-15 09:15 NZT - Codex: Claimed OC-110 and recorded the coverage plan before implementation. diff --git a/examples/demo-api/WebSocket/runtime-lifecycle.yml b/examples/demo-api/WebSocket/runtime-lifecycle.yml index bbb2a13..4c010eb 100644 --- a/examples/demo-api/WebSocket/runtime-lifecycle.yml +++ b/examples/demo-api/WebSocket/runtime-lifecycle.yml @@ -12,17 +12,21 @@ runtime: type: bearer token: "{{demoToken}}" variables: + - name: runtimeSocketUserName + value: "{{socketUser}} Runtime" - name: runtimeSocketCount - value: "7" + value: "{{runtimeSocketCountSeed}}" scripts: - type: before-request code: |- - missio.variables.set("runtimeSocketUser", "Ada Runtime"); + missio.variables.set("runtimeSocketUser", "{{runtimeSocketUserName}}"); + - type: before-request + code: |- missio.request.headers.set("X-Demo-Client", "missio-demo"); - missio.request.headers.set("X-Runtime-Header", "scripted-websocket"); + missio.request.headers.set("X-Runtime-Header", "scripted-{{socketMessageId}}"); missio.request.body = { - user: missio.variables.get("runtimeSocketUser"), - count: Number(missio.variables.get("runtimeSocketCount")) + user: "{{runtimeSocketUser}}", + count: Number("{{runtimeSocketCount}}") }; - type: after-response code: |- diff --git a/examples/demo-api/gRPC/runtime-unary-lifecycle.yml b/examples/demo-api/gRPC/runtime-unary-lifecycle.yml index 4bd2950..0adc1c7 100644 --- a/examples/demo-api/gRPC/runtime-unary-lifecycle.yml +++ b/examples/demo-api/gRPC/runtime-unary-lifecycle.yml @@ -25,29 +25,33 @@ runtime: type: bearer token: "{{grpcToken}}" variables: + - name: grpcRuntimeNameSeed + value: "{{grpcName}} Runtime" - name: grpcRuntimeUserId value: "77" scripts: - type: before-request code: |- - missio.variables.set("grpcRuntimeName", "Runtime Ada"); - missio.variables.set("grpcRuntimeTrace", "runtime-" + missio.variables.get("grpcRequestId")); - missio.request.metadata.set("x-demo-request", "script-" + missio.variables.get("grpcRequestId")); + missio.variables.set("grpcRuntimeName", "{{grpcRuntimeNameSeed}}"); + - type: before-request + code: |- + missio.variables.set("grpcRuntimeTrace", "runtime-{{grpcRequestId}}"); + missio.request.metadata.set("x-demo-request", "script-{{grpcRequestId}}"); missio.request.body = { - name: missio.variables.get("grpcRuntimeName"), - userId: Number(missio.variables.get("grpcRuntimeUserId")), - trace: { requestId: missio.variables.get("grpcRuntimeTrace") } + name: "{{grpcRuntimeName}}", + userId: Number("{{grpcRuntimeUserId}}"), + trace: { requestId: "{{grpcRuntimeTrace}}" } }; - type: after-response code: |- console.info("runtime grpc request", response.json().requestId); - type: tests code: |- - test("runtime grpc message echoed", () => assert(response.json().name === "Runtime Ada")); + test("runtime grpc message echoed", () => assert(response.json().name === "Ada Runtime")); assertions: - expression: res.body.name operator: equals - value: Runtime Ada + value: Ada Runtime - expression: res.body.userId operator: equals value: "77" diff --git a/examples/demo-api/opencollection.yml b/examples/demo-api/opencollection.yml index 854e6d5..bd75d01 100644 --- a/examples/demo-api/opencollection.yml +++ b/examples/demo-api/opencollection.yml @@ -52,6 +52,8 @@ config: value: Ada - name: socketMessageId value: ws-demo-001 + - name: runtimeSocketCountSeed + value: "7" - name: demoToken value: demo-token color: charts.blue diff --git a/src/services/runtimeExecutionService.ts b/src/services/runtimeExecutionService.ts index 13177cb..0a8e4a9 100644 --- a/src/services/runtimeExecutionService.ts +++ b/src/services/runtimeExecutionService.ts @@ -1,4 +1,5 @@ import * as vm from 'vm'; +import { randomUUID } from 'crypto'; import type { Action, ActionPhase, @@ -30,6 +31,7 @@ import type { WebSocketMessageVariant, WebSocketRequest, } from '../models/types'; +import { varPatternGlobal } from '../models/varPattern'; export type RuntimeVariableResolver = ( collection: MissioCollection, @@ -100,7 +102,11 @@ export class RuntimeExecutionService { request: RuntimeCapableRequest, extraVariables?: Map, ): Promise | undefined> { - const requestVariables = variablesToMap(runtimeConfig(request)?.variables); + const requestVariables = resolveVariableMapValues( + variablesToMap(runtimeConfig(request)?.variables), + new Map(), + extraVariables, + ); if (requestVariables.size === 0 && (!extraVariables || extraVariables.size === 0)) { return undefined; } @@ -175,9 +181,14 @@ export class RuntimeExecutionService { const baseVariables = this._variableResolver ? await this._variableResolver(collection, folderDefaults, environmentName) : new Map(); + const requestVariables = resolveVariableMapValues( + variablesToMap(runtime?.variables), + baseVariables, + extraVariables, + ); const variables: RuntimeVariableScopes = { base: new Map(baseVariables), - request: variablesToMap(runtime?.variables), + request: requestVariables, runtime: new Map(extraVariables), }; const state: RuntimeState = { request: clonedRequest, variables, result }; @@ -249,9 +260,10 @@ export class RuntimeExecutionService { name: 'missio-runtime', codeGeneration: { strings: false, wasm: false }, }); - const compiled = new vm.Script(script.code, { - filename: `missio-${script.type}.js`, - }); + const compiled = new vm.Script( + interpolateRuntimeTemplate(script.code, buildVisibleVariables(state.variables)), + { filename: `missio-${script.type}.js` }, + ); compiled.runInContext(context, { timeout: 1000, displayErrors: true }); } @@ -483,6 +495,55 @@ function variablesToMap(variables: Variable[] | undefined): Map return map; } +function resolveVariableMapValues( + variables: Map, + baseVariables: Map, + extraVariables?: Map, +): Map { + const resolved = new Map(variables); + for (let pass = 0; pass < 10; pass++) { + let changed = false; + const visible = new Map(baseVariables); + mergeInto(visible, resolved); + if (extraVariables) mergeInto(visible, extraVariables); + + for (const [key, value] of resolved) { + const next = value.replace(varPatternGlobal(), (match, name) => { + const ref = String(name).trim(); + if (ref === key) return match; + const builtin = resolveRuntimeBuiltin(ref); + if (builtin !== undefined) return builtin; + return visible.has(ref) ? visible.get(ref)! : match; + }); + if (next !== value) { + resolved.set(key, next); + changed = true; + } + } + + if (!changed) break; + } + return resolved; +} + +function interpolateRuntimeTemplate(template: string, variables: Map): string { + return template.replace(varPatternGlobal(), (match, name) => { + const key = String(name).trim(); + const builtin = resolveRuntimeBuiltin(key); + if (builtin !== undefined) return builtin; + return variables.has(key) ? variables.get(key)! : match; + }); +} + +function resolveRuntimeBuiltin(name: string): string | undefined { + switch (name) { + case '$guid': return randomUUID(); + case '$timestamp': return String(Math.floor(Date.now() / 1000)); + case '$randomInt': return String(Math.floor(Math.random() * 1001)); + default: return undefined; + } +} + function resolveVariableValue(value: Variable['value']): string | undefined { if (value === undefined || value === null) return undefined; if (typeof value === 'string') return value; diff --git a/test/grpcSupport.test.ts b/test/grpcSupport.test.ts index b77dab6..415eba9 100644 --- a/test/grpcSupport.test.ts +++ b/test/grpcSupport.test.ts @@ -356,6 +356,8 @@ describe('gRPC execution', () => { const collection = makeCollection(); const localEnv = collection.data.config?.environments?.find(env => env.name === 'LOCAL'); localEnv?.variables?.push({ name: 'grpcBaseUrl', value: address }); + localEnv?.variables?.push({ name: 'grpcRuntimeNameSeed', value: 'Runtime Ada' }); + localEnv?.variables?.push({ name: 'grpcRuntimeUserIdSeed', value: '77' }); await environmentService.setActiveEnvironment(collection.id, 'LOCAL'); const grpcClient = new GrpcClient(environmentService); const runtime = new RuntimeExecutionService((runtimeCollection, folderDefaults, environmentName) => @@ -375,15 +377,21 @@ describe('gRPC execution', () => { }, runtime: { auth: { type: 'bearer' as const, token: '{{grpcToken}}' }, - variables: [{ name: 'grpcRuntimeUserId', value: '77' }], + variables: [ + { name: 'grpcRuntimeUserId', value: '{{grpcRuntimeUserIdSeed}}' }, + { name: 'grpcRuntimeNameSeeded', value: '{{grpcRuntimeNameSeed}}' }, + ], scripts: [ + { + type: 'before-request' as const, + code: 'missio.variables.set("grpcRuntimeName", "{{grpcRuntimeNameSeeded}}");', + }, { type: 'before-request' as const, code: [ - 'missio.variables.set("grpcRuntimeName", "Runtime Ada");', - 'missio.variables.set("grpcRuntimeTrace", "runtime-" + missio.variables.get("grpcRequestId"));', - 'missio.request.metadata.set("x-demo-request", "script-" + missio.variables.get("grpcRequestId"));', - 'missio.request.body = { name: missio.variables.get("grpcRuntimeName"), userId: Number(missio.variables.get("grpcRuntimeUserId")), trace: { requestId: missio.variables.get("grpcRuntimeTrace") } };', + 'missio.variables.set("grpcRuntimeTrace", "runtime-{{grpcRequestId}}");', + 'missio.request.metadata.set("x-demo-request", "script-{{grpcRequestId}}");', + 'missio.request.body = { name: "{{grpcRuntimeName}}", userId: Number("{{grpcRuntimeUserId}}"), trace: { requestId: "{{grpcRuntimeTrace}}" } };', ].join('\n'), }, { @@ -736,7 +744,7 @@ describe('gRPC integration surfaces', () => { const body = JSON.parse(response.body); expect(body).toMatchObject({ - name: 'Runtime Ada', + name: 'Ada Runtime', userId: 77, requestMetadata: 'script-trace-123', }); diff --git a/test/runtimeExecutionService.test.ts b/test/runtimeExecutionService.test.ts index 9e47001..9d1170a 100644 --- a/test/runtimeExecutionService.test.ts +++ b/test/runtimeExecutionService.test.ts @@ -120,6 +120,76 @@ describe('RuntimeExecutionService lifecycle', () => { expect(response.runtime?.logs.map(log => log.message)).toEqual(['before base', 'after abc123']); }); + it('evaluates runtime variable values and interpolates script source before execution', async () => { + const service = new RuntimeExecutionService(async () => new Map([ + ['owner', 'Ada'], + ['headerSeed', 'scripted'], + ])); + const request: HttpRequest = { + http: { + method: 'POST', + url: 'http://127.0.0.1/runtime', + }, + runtime: { + variables: [ + { name: 'requestHeader', value: '{{headerSeed}}' }, + { name: 'runtimeOwner', value: '{{owner}} Runtime' }, + { name: 'combined', value: '{{runtimeOwner}}/{{requestHeader}}' }, + { name: 'unresolved', value: '{{missingToken}}' }, + ], + scripts: [ + { + type: 'before-request', + code: [ + 'missio.request.setHeader("X-Combined", "{{combined}}");', + 'missio.request.setHeader("X-Unresolved", "{{unresolved}}");', + 'missio.variables.set("lateHeader", "late-{{requestHeader}}");', + ].join('\n'), + }, + { + type: 'before-request', + code: 'missio.request.setHeader("X-Late", "{{lateHeader}}");', + }, + ], + }, + }; + + const prepared = await service.prepareHttpRequest(request, makeCollection()); + + expect(prepared.request.http?.headers?.find(header => header.name === 'X-Combined')?.value) + .toBe('Ada Runtime/scripted'); + expect(prepared.request.http?.headers?.find(header => header.name === 'X-Late')?.value) + .toBe('late-scripted'); + expect(prepared.request.http?.headers?.find(header => header.name === 'X-Unresolved')?.value) + .toBe('{{missingToken}}'); + expect(prepared.extraVariables?.get('combined')).toBe('Ada Runtime/scripted'); + expect(prepared.extraVariables?.get('unresolved')).toBe('{{missingToken}}'); + expect(prepared.runtime.variableMutations).toEqual([ + { scope: 'runtime', name: 'lateHeader', value: 'late-scripted', source: 'script' }, + ]); + }); + + it('keeps interpolated script source inside the runtime sandbox', async () => { + const service = new RuntimeExecutionService(async () => new Map([ + ['unsafeScript', 'require("fs").readFileSync("package.json", "utf8");'], + ])); + const request: HttpRequest = { + http: { method: 'GET', url: 'http://127.0.0.1/runtime' }, + runtime: { + scripts: [{ type: 'before-request', code: '{{unsafeScript}}' }], + }, + }; + + await expect(service.prepareHttpRequest(request, makeCollection())) + .rejects.toBeInstanceOf(RuntimeExecutionError); + + try { + await service.prepareHttpRequest(request, makeCollection()); + } catch (error) { + expect((error as RuntimeExecutionError).runtime.errors[0].message).toMatch(/require is not defined/); + } + }); + it('blocks filesystem and process access from before-request scripts', async () => { const service = new RuntimeExecutionService(); const request: HttpRequest = { diff --git a/test/webSocketSupport.test.ts b/test/webSocketSupport.test.ts index 804d251..20020c0 100644 --- a/test/webSocketSupport.test.ts +++ b/test/webSocketSupport.test.ts @@ -391,6 +391,9 @@ describe('WebSocket execution lifecycle', () => { wsBaseUrl: fixture.baseUrl, tenant: 'nz', token: 'token-abc', + runtimeCountSeed: '7', + runtimeHeaderSeed: 'scripted', + runtimeUserSeed: 'Ada Runtime', }); const client = new WebSocketClient(envService); const runtime = new RuntimeExecutionService((collection, folderDefaults, environmentName) => @@ -410,15 +413,22 @@ describe('WebSocket execution lifecycle', () => { }, runtime: { auth: { type: 'bearer', token: '{{token}}' }, - variables: [{ name: 'runtimeCount', value: '7' }], + variables: [ + { name: 'runtimeCount', value: '{{runtimeCountSeed}}' }, + { name: 'runtimeHeader', value: '{{runtimeHeaderSeed}}' }, + { name: 'runtimeUserName', value: '{{runtimeUserSeed}}' }, + ], scripts: [ + { + type: 'before-request', + code: 'missio.variables.set("runtimeUser", "{{runtimeUserName}}");', + }, { type: 'before-request', code: [ - 'missio.variables.set("runtimeUser", "Ada Runtime");', 'missio.request.headers.set("X-Demo-Client", "missio-demo");', - 'missio.request.headers.set("X-Runtime-Header", "scripted");', - 'missio.request.body = { user: missio.variables.get("runtimeUser"), count: Number(missio.variables.get("runtimeCount")) };', + 'missio.request.headers.set("X-Runtime-Header", "{{runtimeHeader}}");', + 'missio.request.body = { user: "{{runtimeUser}}", count: Number("{{runtimeCount}}") };', ].join('\n'), }, { @@ -1168,6 +1178,9 @@ describe('WebSocket demo fixtures', () => { const envService = makeEnvService({ wsBaseUrl: fixture.baseUrl, demoToken: 'token-abc', + socketUser: 'Ada', + socketMessageId: 'ws-demo-001', + runtimeSocketCountSeed: '7', tenant: 'nz', }); const request = parseYaml(fs.readFileSync(path.join(demoRoot, 'WebSocket', 'runtime-lifecycle.yml'), 'utf-8')) as WebSocketRequest; From 38de32deb6a801191ef44dc3ef97a8fd9402711f Mon Sep 17 00:00:00 2001 From: Chris Johnstone Date: Mon, 15 Jun 2026 14:52:40 +1200 Subject: [PATCH 4/6] Record runtime script variable fix --- docs/open-collection-gap-analysis/AGENT_PROGRESS.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/open-collection-gap-analysis/AGENT_PROGRESS.md b/docs/open-collection-gap-analysis/AGENT_PROGRESS.md index ddcefbc..96460b9 100644 --- a/docs/open-collection-gap-analysis/AGENT_PROGRESS.md +++ b/docs/open-collection-gap-analysis/AGENT_PROGRESS.md @@ -488,6 +488,12 @@ Record cross-cutting decisions here so parallel agents do not rediscover them. Verified: reproducing `npx.cmd vitest run test/runtimeExecutionService.test.ts test/webSocketSupport.test.ts test/grpcSupport.test.ts` failed before the implementation with 6 expected regression failures; after the fix it passed 3 files/55 tests and a final post-format rerun also passed 3 files/55 tests. `npx.cmd vitest run test/sendRequestTool.test.ts test/responseProvider.test.ts` passed 2 files/13 tests. `node scripts/validate-collection.js examples/demo-api` passed 47/47 files. `npm.cmd run compile` passed before the full run and again after the final formatting tweak. `npm.cmd test` passed 25 files/467 tests. `npm.cmd run build` first hit sandbox access denial writing generated media files, then passed with approved escalation. Next: commit only the owned runtime lifecycle regression slice to `feature/runtime-script-variable-evaluation` and leave `qp`/`ko` uncommitted. +- 2026-06-15 14:59 NZT - Codex: Committed the OC-080 runtime variable/script-source interpolation regression fix. + GitButler: `feature/runtime-script-variable-evaluation` (bl) is stacked on `feature/oc-140-websocket-lifecycle-ux`; implementation/demo/test/ledger commit is `429a563` (`Fix runtime script variable evaluation`). Earlier no-change commits `c61131d` and `05fa416` remain in the branch history from the pre-stack routing attempts. + Changed: ledger status only after `429a563`. + Verified: `but status -fv` after the implementation commit shows only unrelated generated PDF asset churn `qp`/`ko` in `zz`; no OC-080 runtime lifecycle changes remain unassigned. + Next: OC-080 regression follow-up is ready for review from `429a563`; preserve `qp`/`ko` for the PDF asset owner. + ### OC-110 Runtime Authoring UX - 2026-06-15 09:15 NZT - Codex: Claimed OC-110 and recorded the coverage plan before implementation. From a9ab46c0bbfd249c94f9d1ba3839c6d87b91882e Mon Sep 17 00:00:00 2001 From: Chris Johnstone Date: Mon, 15 Jun 2026 14:55:07 +1200 Subject: [PATCH 5/6] Refresh OC-080 runtime lifecycle board --- docs/open-collection-gap-analysis/AGENT_PROGRESS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/open-collection-gap-analysis/AGENT_PROGRESS.md b/docs/open-collection-gap-analysis/AGENT_PROGRESS.md index 96460b9..6dd7a39 100644 --- a/docs/open-collection-gap-analysis/AGENT_PROGRESS.md +++ b/docs/open-collection-gap-analysis/AGENT_PROGRESS.md @@ -65,7 +65,7 @@ Use full branch names for stacking existing branches with `but move