From 94c9c1425acac17abb501472b223cb5470124885 Mon Sep 17 00:00:00 2001 From: Kurt Overmier Date: Mon, 1 Jun 2026 17:05:44 -0500 Subject: [PATCH 1/4] fix(adf): validate `patch --ops` at the CLI boundary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A malformed `--ops` element (e.g. `[null]` or a non-object) crashed the before-capture pass with an uncaught TypeError instead of a clean error, because `captureOpBefore` ran before `applyPatches` validated the ops. Surfaced by sweeping local `.charter/telemetry` across org repos — a live TypeError in `adf patch --ops` had gone unreported for months. Validate the parsed ops against a Zod `PatchOperationArraySchema` right after JSON.parse, before any capture. Malformed ops now produce a clean CLIError naming the offending `ops[i]`; the target file is left untouched. The schema lives in the CLI (keeps `@stackbilt/adf` zero-dep) with a compile-time drift guard asserting `z.infer` stays mutually assignable with adf's public `PatchOperation` type — so the two cannot silently diverge. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/cli/package.json | 3 +- packages/cli/src/__tests__/adf-patch.test.ts | 27 +++++++++ packages/cli/src/commands/adf.ts | 18 ++++-- packages/cli/src/schemas/patch-ops.ts | 60 ++++++++++++++++++++ 4 files changed, 102 insertions(+), 6 deletions(-) create mode 100644 packages/cli/src/schemas/patch-ops.ts diff --git a/packages/cli/package.json b/packages/cli/package.json index 29a729d..eb99194 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -62,7 +62,8 @@ "@stackbilt/policies": "workspace:*", "@stackbilt/surface": "workspace:*", "@stackbilt/types": "workspace:*", - "@stackbilt/validate": "workspace:*" + "@stackbilt/validate": "workspace:*", + "zod": "^3.24.1" }, "license": "Apache-2.0", "author": "Stackbilt LLC" diff --git a/packages/cli/src/__tests__/adf-patch.test.ts b/packages/cli/src/__tests__/adf-patch.test.ts index a9f7023..cd3c2a2 100644 --- a/packages/cli/src/__tests__/adf-patch.test.ts +++ b/packages/cli/src/__tests__/adf-patch.test.ts @@ -163,6 +163,33 @@ describe('adf patch JSON changes array', () => { expect(fs.readFileSync(file, 'utf-8')).not.toContain('Prefer immutability'); }); + it('malformed ops: null array element is rejected cleanly, not as a TypeError', async () => { + const dir = tmpDir(); + const file = path.join(dir, 'core.adf'); + fs.writeFileSync(file, METRIC_ADF); + + // Previously crashed in the before-capture pass with an uncaught + // "Cannot read properties of null (reading 'op')" TypeError. + await expect(adfCommand(baseOptions, ['patch', file, '--ops', '[null]'])).rejects.toMatchObject({ + name: 'CLIError', + message: expect.stringContaining('Invalid --ops operation'), + }); + // The file must be left untouched when validation fails. + expect(fs.readFileSync(file, 'utf-8')).toBe(METRIC_ADF); + }); + + it('malformed ops: non-object and unknown-op elements are rejected with CLIError', async () => { + const dir = tmpDir(); + const file = path.join(dir, 'core.adf'); + fs.writeFileSync(file, METRIC_ADF); + + for (const bad of ['[123]', '[{"section":"x","index":0}]', '[{"op":"FROBNICATE","section":"x"}]']) { + await expect(adfCommand(baseOptions, ['patch', file, '--ops', bad])).rejects.toMatchObject({ + name: 'CLIError', + }); + } + }); + it('error: returns patched:false with error message, no changes', async () => { const dir = tmpDir(); const file = path.join(dir, 'core.adf'); diff --git a/packages/cli/src/commands/adf.ts b/packages/cli/src/commands/adf.ts index 331123e..a3f52c3 100644 --- a/packages/cli/src/commands/adf.ts +++ b/packages/cli/src/commands/adf.ts @@ -29,6 +29,7 @@ import { NAMED_MODULE_SCAFFOLDS, NAMED_MODULE_DEFAULT_TRIGGERS, } from './adf-named-scaffolds'; +import { PatchOperationArraySchema } from '../schemas/patch-ops'; // Re-export named-scaffold registry for programmatic consumers and tests. export { @@ -634,17 +635,24 @@ function adfPatch(options: CLIOptions, args: string[]): number { const rawOps = opsFile ? readFlagFile(opsFile, '--ops-file') : opsJson!; - let ops: PatchOperation[]; + let parsed: unknown; try { - ops = JSON.parse(rawOps); - if (!Array.isArray(ops)) { - throw new Error('ops must be an array'); - } + parsed = JSON.parse(rawOps); } catch (e: unknown) { const msg = e instanceof Error ? e.message : String(e); throw new CLIError(`Invalid --ops JSON: ${msg}`); } + // Validate structure at the boundary so malformed ops surface as a clean + // error here rather than crashing the before-capture pass below. + const validation = PatchOperationArraySchema.safeParse(parsed); + if (!validation.success) { + const issue = validation.error.issues[0]; + const where = issue?.path.length ? ` at ops[${issue.path.join('.')}]` : ''; + throw new CLIError(`Invalid --ops operation${where}: ${issue?.message ?? 'does not match a known patch operation'}`); + } + const ops: PatchOperation[] = validation.data; + const input = fs.readFileSync(filePath, 'utf-8'); const doc = parseAdf(input); diff --git a/packages/cli/src/schemas/patch-ops.ts b/packages/cli/src/schemas/patch-ops.ts new file mode 100644 index 0000000..5839c59 --- /dev/null +++ b/packages/cli/src/schemas/patch-ops.ts @@ -0,0 +1,60 @@ +/** + * Zod schema for `adf patch --ops` input. + * + * Validates externally-supplied patch operations at the CLI boundary (the + * `--ops` JSON / `--ops-file` contents) before they reach the patch engine. + * Without this, a malformed op (e.g. a `null` array element) crashed the + * before-capture pass with an uncaught `TypeError` instead of a clean error. + * + * The schema is the runtime authority; `@stackbilt/adf` remains the source of + * truth for the `PatchOperation` *type*. The compile-time guard at the bottom + * fails the build if the two ever drift, without making adf depend on zod. + */ + +import { z } from 'zod'; +import type { PatchOperation } from '@stackbilt/adf'; + +const AdfContentSchema = z.discriminatedUnion('type', [ + z.object({ type: z.literal('text'), value: z.string() }), + z.object({ type: z.literal('list'), items: z.array(z.string()) }), + z.object({ + type: z.literal('map'), + entries: z.array(z.object({ key: z.string(), value: z.string() })), + }), + z.object({ + type: z.literal('metric'), + entries: z.array( + z.object({ + key: z.string(), + value: z.number(), + ceiling: z.number(), + unit: z.string(), + }) + ), + }), +]); + +export const PatchOperationSchema = z.discriminatedUnion('op', [ + z.object({ op: z.literal('ADD_BULLET'), section: z.string(), value: z.string() }), + z.object({ op: z.literal('REPLACE_BULLET'), section: z.string(), index: z.number(), value: z.string() }), + z.object({ op: z.literal('REMOVE_BULLET'), section: z.string(), index: z.number() }), + z.object({ + op: z.literal('ADD_SECTION'), + key: z.string(), + decoration: z.string().nullable().optional(), + content: AdfContentSchema, + weight: z.enum(['load-bearing', 'advisory']).optional(), + }), + z.object({ op: z.literal('REPLACE_SECTION'), key: z.string(), content: AdfContentSchema }), + z.object({ op: z.literal('REMOVE_SECTION'), key: z.string() }), + z.object({ op: z.literal('UPDATE_METRIC'), section: z.string(), key: z.string(), value: z.number() }), +]); + +export const PatchOperationArraySchema = z.array(PatchOperationSchema); + +// Compile-time drift guard: the inferred schema type and adf's public +// `PatchOperation` must be mutually assignable. If either side changes without +// the other, one of these aliases fails to resolve and the build breaks. +type AssertAssignable = A; +type _SchemaMatchesType = AssertAssignable, PatchOperation>; +type _TypeMatchesSchema = AssertAssignable>; From 9c2300ddf9904ae90f7553313244be1b5df0894b Mon Sep 17 00:00:00 2001 From: Kurt Overmier Date: Mon, 1 Jun 2026 17:05:57 -0500 Subject: [PATCH 2/4] fix(cli): surface clean errors from `serve` and `hook install` Two error-path gaps found via local telemetry triage: - `serve`: when `--ai-dir` (or its manifest) is missing, the error now echoes the resolved path. Repos showed `serve --name --ai-dir` rejected and retried-then-abandoned because the message never said which path was checked. - `hook install`: filesystem failures creating/writing the hook file, and git failures resolving the hooks dir, escaped as raw Errors. Both are now wrapped into a CLIError with an actionable message. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../cli/src/__tests__/hook-git-error.test.ts | 36 ++++++++++++++++++ packages/cli/src/__tests__/hook.test.ts | 15 ++++++++ .../cli/src/__tests__/serve-context.test.ts | 28 +++++++++++++- packages/cli/src/commands/hook.ts | 37 ++++++++++++++----- packages/cli/src/commands/serve.ts | 7 ++-- 5 files changed, 109 insertions(+), 14 deletions(-) create mode 100644 packages/cli/src/__tests__/hook-git-error.test.ts diff --git a/packages/cli/src/__tests__/hook-git-error.test.ts b/packages/cli/src/__tests__/hook-git-error.test.ts new file mode 100644 index 0000000..6e43c79 --- /dev/null +++ b/packages/cli/src/__tests__/hook-git-error.test.ts @@ -0,0 +1,36 @@ +import { describe, it, expect, vi } from 'vitest'; +import type { CLIOptions } from '../index'; +import { hookCommand } from '../commands/hook'; + +// Simulate a git environment where the repo check passes but resolving the +// hooks directory fails (e.g. `git rev-parse --git-dir` errors). This path +// previously escaped as a raw `Error`; it must now surface as a clean CLIError. +// hook.ts consumes only these three helpers from git-helpers. +vi.mock('../git-helpers', () => ({ + isGitRepo: () => true, + runGit: (args: string[]) => { + throw Object.assign(new Error(`git ${args.join(' ')} failed`), { + stderr: 'fatal: not a git repository', + }); + }, + getGitErrorMessage: (err: unknown) => { + const e = err as Error & { stderr?: string }; + return e?.stderr?.trim() || e?.message || 'Unknown git error.'; + }, +})); + +const baseOptions: CLIOptions = { + configPath: '.charter', + format: 'text', + ciMode: false, + yes: false, +}; + +describe('hook install — git resolution failure', () => { + it('surfaces a CLIError (not a raw Error) when the hooks dir cannot be resolved', async () => { + await expect(hookCommand(baseOptions, ['install', '--commit-msg'])).rejects.toMatchObject({ + name: 'CLIError', + message: expect.stringContaining('Could not resolve git hooks directory'), + }); + }); +}); diff --git a/packages/cli/src/__tests__/hook.test.ts b/packages/cli/src/__tests__/hook.test.ts index 826f07f..3312793 100644 --- a/packages/cli/src/__tests__/hook.test.ts +++ b/packages/cli/src/__tests__/hook.test.ts @@ -57,6 +57,21 @@ describe('hookCommand', () => { expect(content).toContain('echo "custom"'); }); + it('surfaces a CLIError (not a raw fs Error) when the hook file cannot be written', async () => { + vi.spyOn(console, 'log').mockImplementation(() => {}); + + // Point the hooks dir at an existing regular file so creating the hooks + // directory fails for real — no fs mocking, exercises the actual guard. + const blocker = path.join(tempDir, 'blocker'); + fs.writeFileSync(blocker, 'i am a file, not a directory'); + execFileSync('git', ['config', 'core.hooksPath', blocker], { stdio: 'ignore' }); + + await expect(hookCommand(baseOptions, ['install', '--commit-msg'])).rejects.toMatchObject({ + name: 'CLIError', + message: expect.stringContaining('Could not write git hook'), + }); + }); + it('hook print --claude returns 0 and outputs UserPromptSubmit config', async () => { const logs: string[] = []; vi.spyOn(console, 'log').mockImplementation((...args: unknown[]) => { diff --git a/packages/cli/src/__tests__/serve-context.test.ts b/packages/cli/src/__tests__/serve-context.test.ts index 10fe3ed..c4a42e6 100644 --- a/packages/cli/src/__tests__/serve-context.test.ts +++ b/packages/cli/src/__tests__/serve-context.test.ts @@ -5,7 +5,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import type { CLIOptions } from '../index'; import { EXIT_CODE } from '../index'; import { CLIError } from '../index'; -import { loadCharterContextSnapshot } from '../commands/serve'; +import { loadCharterContextSnapshot, serveCommand } from '../commands/serve'; const contextRefreshCommandMock = vi.hoisted(() => vi.fn()); vi.mock('../commands/context-refresh', () => ({ @@ -102,3 +102,29 @@ describe('loadCharterContextSnapshot', () => { expect((result.snapshot as { version: number }).version).toBe(1); }); }); + +describe('serveCommand startup guards', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('names the resolved --ai-dir path when the directory is missing', async () => { + const missing = path.join(makeTempDir(), 'does-not-exist'); + vi.spyOn(process.stdout, 'write').mockImplementation(() => true); + + await expect(serveCommand(baseOptions, ['--ai-dir', missing])).rejects.toMatchObject({ + name: 'CLIError', + message: expect.stringContaining(path.resolve(missing)), + }); + }); + + it('names the resolved manifest path when manifest.adf is missing', async () => { + const dir = makeTempDir(); // exists, but contains no manifest.adf + vi.spyOn(process.stdout, 'write').mockImplementation(() => true); + + await expect(serveCommand(baseOptions, ['--ai-dir', dir])).rejects.toMatchObject({ + name: 'CLIError', + message: expect.stringContaining(path.join(path.resolve(dir), 'manifest.adf')), + }); + }); +}); diff --git a/packages/cli/src/commands/hook.ts b/packages/cli/src/commands/hook.ts index 5865dd6..da649ab 100644 --- a/packages/cli/src/commands/hook.ts +++ b/packages/cli/src/commands/hook.ts @@ -8,7 +8,7 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import type { CLIOptions } from '../index'; import { CLIError, EXIT_CODE } from '../index'; -import { runGit, isGitRepo } from '../git-helpers'; +import { runGit, isGitRepo, getGitErrorMessage } from '../git-helpers'; interface HookInstallResult { status: 'INSTALLED' | 'SKIPPED'; @@ -183,12 +183,9 @@ function installCommitMsgHook(force: boolean): HookInstallResult { reason: 'existing commit-msg hook is not managed by Charter', }; } - } else { - fs.mkdirSync(hooksDir, { recursive: true }); } - fs.writeFileSync(hookPath, COMMIT_MSG_HOOK_CONTENT); - setExecutableBit(hookPath); + writeHookFile(hookPath, hooksDir, COMMIT_MSG_HOOK_CONTENT, exists); return { status: 'INSTALLED', @@ -214,12 +211,9 @@ function installPreCommitHook(force: boolean): HookInstallResult { reason: 'existing pre-commit hook is not managed by Charter', }; } - } else { - fs.mkdirSync(hooksDir, { recursive: true }); } - fs.writeFileSync(hookPath, PRE_COMMIT_HOOK_CONTENT); - setExecutableBit(hookPath); + writeHookFile(hookPath, hooksDir, PRE_COMMIT_HOOK_CONTENT, exists); return { status: 'INSTALLED', @@ -227,13 +221,36 @@ function installPreCommitHook(force: boolean): HookInstallResult { }; } +/** + * Create the hooks dir (when new) and write the hook file. Any filesystem + * failure (missing/unwritable hooks dir, permissions) surfaces as a clean + * CLIError rather than a raw Error escaping to the top-level handler. + */ +function writeHookFile(hookPath: string, hooksDir: string, content: string, exists: boolean): void { + try { + if (!exists) { + fs.mkdirSync(hooksDir, { recursive: true }); + } + fs.writeFileSync(hookPath, content); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + throw new CLIError(`Could not write git hook to ${hookPath.replace(/\\/g, '/')}: ${msg}`); + } + setExecutableBit(hookPath); +} + function resolveHooksDir(): string { const configuredPath = getGitConfig('core.hooksPath'); if (configuredPath && configuredPath.trim().length > 0) { return path.resolve(configuredPath.trim()); } - const gitDir = runGit(['rev-parse', '--git-dir']).trim(); + let gitDir: string; + try { + gitDir = runGit(['rev-parse', '--git-dir']).trim(); + } catch (err) { + throw new CLIError(`Could not resolve git hooks directory: ${getGitErrorMessage(err)}`); + } return path.resolve(gitDir, 'hooks'); } diff --git a/packages/cli/src/commands/serve.ts b/packages/cli/src/commands/serve.ts index e3d8880..46eb424 100644 --- a/packages/cli/src/commands/serve.ts +++ b/packages/cli/src/commands/serve.ts @@ -74,14 +74,15 @@ export async function serveCommand(options: CLIOptions, args: string[]): Promise const customName = getFlag(args, '--name'); if (!fs.existsSync(aiDir)) { - const errMsg = `No .ai/ directory found. Run: charter init`; + const errMsg = `No .ai/ directory found at ${aiDir}. Run: charter init (or pass --ai-dir )`; if (transport === 'stdio') { process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32000, message: `charter serve: ${errMsg}` } }) + '\n'); } throw new CLIError(errMsg); } - if (!fs.existsSync(path.join(aiDir, 'manifest.adf'))) { - const errMsg = `.ai/manifest.adf not found. Run: charter adf init`; + const manifestPath = path.join(aiDir, 'manifest.adf'); + if (!fs.existsSync(manifestPath)) { + const errMsg = `ADF manifest not found at ${manifestPath}. Run: charter adf init`; if (transport === 'stdio') { process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32000, message: `charter serve: ${errMsg}` } }) + '\n'); } From ee3e45d41a2d0a0dbd5e2b213ef727f184517e9e Mon Sep 17 00:00:00 2001 From: Kurt Overmier Date: Thu, 11 Jun 2026 16:38:35 -0500 Subject: [PATCH 3/4] fix(docs-sync): skip gracefully in standalone OSS checkouts Adds --allow-missing-config (docs:check/sync) and --allow-missing-target-root (docs:oss:check) so CI passes in repos without a sibling docs-site checkout. Also fixes AGENT_DX_FEEDBACK_008 tracked-issues metadata shape. Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 8 ++++++++ PUBLISHING.md | 2 ++ README.md | 7 ++++++- package.json | 6 +++--- scripts/docs-sync.mjs | 23 ++++++++++++++++++++++- 5 files changed, 41 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f8abf2..e211f2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,14 @@ The format is based on Keep a Changelog and follows Semantic Versioning. - On-demand modules are listed in the output (path + triggers) but their bodies are NOT inlined — preserving progressive disclosure. - Pure compiler core in `@stackbilt/adf` (`compiler.ts`) following the Zod-core-out architecture; Zod validation at the CLI boundary in `adf-compile.ts`. +### Changed + +- Docs sync validation now supports standalone OSS checkouts: `docs:check` skips an absent local `.docsync.json`, while `docs:oss:check` skips an absent sibling docs-site checkout instead of failing before source validation can run. + +### Fixed + +- `papers-lint` metadata for `AGENT_DX_FEEDBACK_008.md` now uses the required YAML-list shape for tracked issues. + ## [1.0.0] - 2026-05-23 **Breaking:** `run`, `architect`, `scaffold`, and `login` commands removed from `@stackbilt/cli`. The `stackbilt` binary alias is also removed. These commands have moved to [`@stackbilt/build`](https://www.npmjs.com/package/@stackbilt/build) — install it with `npm install -g @stackbilt/build`. Migration tracked in [RFC #112](https://github.com/Stackbilt-dev/charter/issues/112). diff --git a/PUBLISHING.md b/PUBLISHING.md index e140581..d71e11f 100644 --- a/PUBLISHING.md +++ b/PUBLISHING.md @@ -39,6 +39,8 @@ npm whoami ```bash pnpm install pnpm run clean +pnpm run docs:check +pnpm run docs:oss:check pnpm run typecheck pnpm run build pnpm run test diff --git a/README.md b/README.md index d672459..9528a72 100644 --- a/README.md +++ b/README.md @@ -292,7 +292,12 @@ Charter is built as a monorepo. Individual packages are published to npm and usa ## Development ```bash -pnpm install && pnpm run build && pnpm run test +pnpm install +pnpm run docs:check +pnpm run docs:oss:check +pnpm run typecheck +pnpm run build +pnpm run test ``` Full publish workflow: see [PUBLISHING.md](./PUBLISHING.md). diff --git a/package.json b/package.json index 57ebf7f..fdf9208 100644 --- a/package.json +++ b/package.json @@ -15,10 +15,10 @@ "test:coverage": "bash -lc \"pnpm exec vitest run --coverage\"", "scorecard:generate": "node scripts/generate-scorecard.mjs", "scorecard:validate": "node scripts/validate-scorecard.mjs", - "docs:sync": "node scripts/docs-sync.mjs --write && node scripts/papers-lint.mjs", - "docs:check": "node scripts/docs-sync.mjs --check && node scripts/papers-lint.mjs", + "docs:sync": "node scripts/docs-sync.mjs --write --allow-missing-config && node scripts/papers-lint.mjs", + "docs:check": "node scripts/docs-sync.mjs --check --allow-missing-config && node scripts/papers-lint.mjs", "docs:oss:sync": "node scripts/docs-sync.mjs --write --config .docsync.oss.json", - "docs:oss:check": "node scripts/docs-sync.mjs --check --config .docsync.oss.json", + "docs:oss:check": "node scripts/docs-sync.mjs --check --config .docsync.oss.json --allow-missing-target-root", "docs:oss:auto": "node scripts/docs-oss-auto-sync.mjs --config .docsync.oss.json", "docs:oss:auto:dry-run": "node scripts/docs-oss-auto-sync.mjs --config .docsync.oss.json --dry-run --no-push", "publish:check": "node scripts/assert-packages-publishable.mjs", diff --git a/scripts/docs-sync.mjs b/scripts/docs-sync.mjs index 5ad80b3..9ea7d50 100644 --- a/scripts/docs-sync.mjs +++ b/scripts/docs-sync.mjs @@ -4,6 +4,8 @@ import process from 'node:process'; const CHECK_MODE = process.argv.includes('--check'); const WRITE_MODE = process.argv.includes('--write'); +const ALLOW_MISSING_CONFIG = process.argv.includes('--allow-missing-config'); +const ALLOW_MISSING_TARGET_ROOT = process.argv.includes('--allow-missing-target-root'); const CONFIG_FLAG_INDEX = process.argv.indexOf('--config'); const CONFIG_ARG = CONFIG_FLAG_INDEX >= 0 ? process.argv[CONFIG_FLAG_INDEX + 1] : null; @@ -13,13 +15,22 @@ if (CONFIG_FLAG_INDEX >= 0 && (!CONFIG_ARG || CONFIG_ARG.startsWith('--'))) { } if (!CHECK_MODE && !WRITE_MODE) { - console.error('Usage: node scripts/docs-sync.mjs --check|--write [--config ]'); + console.error('Usage: node scripts/docs-sync.mjs --check|--write [--config ] [--allow-missing-config] [--allow-missing-target-root]'); process.exit(2); } const cwd = process.cwd(); const configPath = CONFIG_ARG ? path.resolve(cwd, CONFIG_ARG) : path.join(cwd, '.docsync.json'); +async function exists(filePath) { + try { + await fs.access(filePath); + return true; + } catch { + return false; + } +} + async function readJson(filePath) { const raw = await fs.readFile(filePath, 'utf8'); return JSON.parse(raw); @@ -63,6 +74,11 @@ async function loadSnippet(source, snippetFile) { } async function main() { + if (ALLOW_MISSING_CONFIG && !(await exists(configPath))) { + console.log(`docs-sync: skipped; config not found at ${path.relative(cwd, configPath) || configPath}`); + return; + } + const config = await readJson(configPath); const configDir = path.dirname(configPath); const sourceRoot = path.resolve(configDir, config.source.localRoot); @@ -70,6 +86,11 @@ async function main() { const failures = []; const updates = []; + if (ALLOW_MISSING_TARGET_ROOT && !(await exists(targetRoot))) { + console.log(`docs-sync: skipped; target root not found at ${path.relative(cwd, targetRoot) || targetRoot}`); + return; + } + for (const mapping of config.mappings) { const targetPath = path.resolve(targetRoot, mapping.targetFile); const targetRaw = await fs.readFile(targetPath, 'utf8'); From 1351608610688b033cb5540d16366e0652a4bb9d Mon Sep 17 00:00:00 2001 From: Kurt Overmier Date: Thu, 11 Jun 2026 16:42:18 -0500 Subject: [PATCH 4/4] chore: sync lockfile with main (tsx 4.22.4, zod peer resolution) Co-Authored-By: Claude Sonnet 4.6 --- pnpm-lock.yaml | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 701848e..9e113e3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -51,7 +51,7 @@ importers: dependencies: '@modelcontextprotocol/sdk': specifier: ^1.29.0 - version: 1.29.0(zod@4.3.6) + version: 1.29.0(zod@3.25.76) '@stackbilt/adf': specifier: workspace:* version: link:../adf @@ -82,6 +82,9 @@ importers: '@stackbilt/validate': specifier: workspace:* version: link:../validate + zod: + specifier: ^3.24.1 + version: 3.25.76 packages/core: dependencies: @@ -1175,9 +1178,6 @@ packages: zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} - zod@4.3.6: - resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} - snapshots: '@esbuild/aix-ppc64@0.27.7': @@ -1342,7 +1342,7 @@ snapshots: '@jridgewell/sourcemap-codec@1.5.5': {} - '@modelcontextprotocol/sdk@1.29.0(zod@4.3.6)': + '@modelcontextprotocol/sdk@1.29.0(zod@3.25.76)': dependencies: '@hono/node-server': 1.19.12(hono@4.12.9) ajv: 8.18.0 @@ -1359,8 +1359,8 @@ snapshots: json-schema-typed: 8.0.2 pkce-challenge: 5.0.1 raw-body: 3.0.2 - zod: 4.3.6 - zod-to-json-schema: 3.25.2(zod@4.3.6) + zod: 3.25.76 + zod-to-json-schema: 3.25.2(zod@3.25.76) transitivePeerDependencies: - supports-color @@ -2063,10 +2063,8 @@ snapshots: wrappy@1.0.2: {} - zod-to-json-schema@3.25.2(zod@4.3.6): + zod-to-json-schema@3.25.2(zod@3.25.76): dependencies: - zod: 4.3.6 + zod: 3.25.76 zod@3.25.76: {} - - zod@4.3.6: {}