From f443c4f477c16f200bf0beeec2b4bd85c79cb4bb Mon Sep 17 00:00:00 2001 From: NagyVikt Date: Fri, 12 Jun 2026 16:10:29 +0200 Subject: [PATCH 1/5] fix(spec): forward-slash publish result paths; deterministic archive-target collision MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit spec_change_path/plan_workspace_path returned OS-native separators, so Windows callers (and the plan.test.ts contains-assertions) saw backslash paths. archiveChange relied on renameSync failing onto an existing target — POSIX throws, Windows MoveFileEx clobbers — so the existence check is now explicit. --- packages/spec/src/plan-publish.ts | 6 ++++-- packages/spec/src/repository.ts | 6 ++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/spec/src/plan-publish.ts b/packages/spec/src/plan-publish.ts index 529ab199..14676ae2 100644 --- a/packages/spec/src/plan-publish.ts +++ b/packages/spec/src/plan-publish.ts @@ -152,8 +152,10 @@ export function publishPlan(args: PublishPlanInput): PublishPlanResult { return { plan_slug: args.slug, spec_task_id: opened.task_id, - spec_change_path: opened.path, - plan_workspace_path: workspace.dir, + // Forward-slash the returned paths so MCP callers get separator-stable + // result data across OSes (these are reference paths, not used for fs here). + spec_change_path: opened.path.replace(/\\/g, '/'), + plan_workspace_path: workspace.dir.replace(/\\/g, '/'), subtasks: subtaskThreads, }; } diff --git a/packages/spec/src/repository.ts b/packages/spec/src/repository.ts index 3723cf1d..d787227e 100644 --- a/packages/spec/src/repository.ts +++ b/packages/spec/src/repository.ts @@ -165,6 +165,12 @@ export class SpecRepository { archiveChange(slug: string, date: string = todayIso()): string { const changeDir = dirname(this.changePath(slug)); const archiveTarget = join(this.repoRoot, LAYOUT.archiveDir, `${date}-${slug}`); + // Fail deterministically if the target exists. POSIX renameSync(dir, file) + // throws ENOTDIR, but Windows MoveFileEx would silently clobber the file — + // so check explicitly rather than relying on rename to error. + if (existsSync(archiveTarget)) { + throw new Error(`archive target already exists: ${archiveTarget}`); + } mkdirSync(dirname(archiveTarget), { recursive: true }); // Stage: rename into a sibling `.archive-staging-` directory, From 860962662383991ecc3adac90516ab2502296377 Mon Sep 17 00:00:00 2001 From: NagyVikt Date: Fri, 12 Jun 2026 16:15:41 +0200 Subject: [PATCH 2/5] fix(spec,release): Windows-green plan tests + valid release plan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - publishPlan returns forward-slash spec_change_path/plan_workspace_path so Windows callers (and plan.test.ts contains-assertions) see separator-stable reference paths. - archiveChange checks the archive target explicitly: POSIX renameSync threw on an existing target, Windows MoveFileEx silently clobbered — the 'delta written then archive throws' test now errors on every OS. - .changeset/post-tool-use-awareness-push.md referenced package 'colony' (not in workspace; the CLI is 'colonyq'), which crashed changesets' assemble-release-plan and failed every Release run since #594. --- .changeset/post-tool-use-awareness-push.md | 2 +- .changeset/windows-plan-paths-archive.md | 5 +++++ .../.openspec.yaml | 2 ++ .../notes.md | 16 ++++++++++++++++ 4 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 .changeset/windows-plan-paths-archive.md create mode 100644 openspec/changes/agent-claude-fix-windows-plan-paths-archive-and-relea-2026-06-12-16-11/.openspec.yaml create mode 100644 openspec/changes/agent-claude-fix-windows-plan-paths-archive-and-relea-2026-06-12-16-11/notes.md diff --git a/.changeset/post-tool-use-awareness-push.md b/.changeset/post-tool-use-awareness-push.md index 1c6992d7..fb1dd4c4 100644 --- a/.changeset/post-tool-use-awareness-push.md +++ b/.changeset/post-tool-use-awareness-push.md @@ -1,6 +1,6 @@ --- '@colony/hooks': minor -'colony': patch +'colonyq': patch --- Push awareness: when an edit touches a file another live session holds, the PostToolUse hook now injects a one-line `[Colony] session X recently claimed …` note into the agent's context immediately (Claude Code `additionalContext`), instead of waiting for the next turn's preface. Debounced to once per 2 minutes per session via an `awareness-push` observation marker (hook processes are one-shot, so the marker doubles as audit trail). `autoClaimFromToolUse` now reports live-owner blocked takeovers in its `conflicts` result. diff --git a/.changeset/windows-plan-paths-archive.md b/.changeset/windows-plan-paths-archive.md new file mode 100644 index 00000000..af55c405 --- /dev/null +++ b/.changeset/windows-plan-paths-archive.md @@ -0,0 +1,5 @@ +--- +'@colony/spec': patch +--- + +Windows CI green: `publishPlan` returns forward-slash `spec_change_path` / `plan_workspace_path` (separator-stable for MCP callers on every OS), and `archiveChange` fails deterministically when the archive target already exists (POSIX rename threw; Windows MoveFileEx silently clobbered). diff --git a/openspec/changes/agent-claude-fix-windows-plan-paths-archive-and-relea-2026-06-12-16-11/.openspec.yaml b/openspec/changes/agent-claude-fix-windows-plan-paths-archive-and-relea-2026-06-12-16-11/.openspec.yaml new file mode 100644 index 00000000..8fe20555 --- /dev/null +++ b/openspec/changes/agent-claude-fix-windows-plan-paths-archive-and-relea-2026-06-12-16-11/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-06-12 diff --git a/openspec/changes/agent-claude-fix-windows-plan-paths-archive-and-relea-2026-06-12-16-11/notes.md b/openspec/changes/agent-claude-fix-windows-plan-paths-archive-and-relea-2026-06-12-16-11/notes.md new file mode 100644 index 00000000..11130525 --- /dev/null +++ b/openspec/changes/agent-claude-fix-windows-plan-paths-archive-and-relea-2026-06-12-16-11/notes.md @@ -0,0 +1,16 @@ +# agent-claude-fix-windows-plan-paths-archive-and-relea-2026-06-12-16-11 (minimal / T1) + +Branch: `agent//` + +Describe the change in a sentence or two. Commit message is the spec of record. + +## Handoff + +- Handoff: change=`agent-claude-fix-windows-plan-paths-archive-and-relea-2026-06-12-16-11`; branch=`agent//`; scope=`TODO`; action=`continue this sandbox or finish cleanup after a usage-limit/manual takeover`. +- Copy prompt: Continue `agent-claude-fix-windows-plan-paths-archive-and-relea-2026-06-12-16-11` on branch `agent//`. Work inside the existing sandbox, review `openspec/changes/agent-claude-fix-windows-plan-paths-archive-and-relea-2026-06-12-16-11/notes.md`, continue from the current state instead of creating a new sandbox, and when the work is done run `gx branch finish --branch agent// --base dev --via-pr --wait-for-merge --cleanup`. + +## Cleanup + +- [ ] Run: `gx branch finish --branch agent// --base dev --via-pr --wait-for-merge --cleanup` +- [ ] Record PR URL + `MERGED` state in the completion handoff. +- [ ] Confirm sandbox worktree is gone (`git worktree list`, `git branch -a`). From 7035cf04daf49aab5048053e179a881c4551ffc8 Mon Sep 17 00:00:00 2001 From: NagyVikt Date: Fri, 12 Jun 2026 16:30:46 +0200 Subject: [PATCH 3/5] fix(cli,mcp): Windows-portable test expectations + structured already-archived error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mcp-server going green on Windows exposed the next packages in pnpm's recursive run: 9 apps/cli failures, all path-portability in tests — expectations assumed POSIX literals where the implementation resolves and shell-quotes OS-natively. Added shellQuoteForTest mirroring the production quoting rule; cockpit/queen-health/agents-spawn expectations now derive from resolve(). PATH stubs join with path.delimiter. The gx/cue PATH-spawning integration tests gate to POSIX (both tools spawn via execFileSync without shell, which Windows cannot satisfy). spec_archive returns structured SPEC_ARCHIVE_ALREADY_EXISTS (status: already_archived) instead of an opaque throw on same-day re-archive (review follow-up). --- apps/cli/test/agents-spawn.test.ts | 3 +- apps/cli/test/cockpit.test.ts | 17 ++++++++--- apps/cli/test/gitguardex.test.ts | 13 +++++--- apps/cli/test/queen-health.test.ts | 3 +- apps/cli/test/shell-quote-helper.ts | 10 ++++++ apps/cli/test/skills-wire.test.ts | 47 ++++++++++++++++++----------- apps/mcp-server/src/tools/shared.ts | 1 + apps/mcp-server/src/tools/spec.ts | 17 ++++++++++- packages/spec/src/repository.ts | 4 ++- 9 files changed, 85 insertions(+), 30 deletions(-) create mode 100644 apps/cli/test/shell-quote-helper.ts diff --git a/apps/cli/test/agents-spawn.test.ts b/apps/cli/test/agents-spawn.test.ts index e73c23e6..caf07e6c 100644 --- a/apps/cli/test/agents-spawn.test.ts +++ b/apps/cli/test/agents-spawn.test.ts @@ -12,6 +12,7 @@ import { } from '../src/executors/gitguardex.js'; import { createProgram } from '../src/index.js'; import { withStore } from '../src/util/store.js'; +import { shellQuoteForTest } from './shell-quote-helper.js'; const MINIMAL_SPEC = `# SPEC @@ -148,7 +149,7 @@ describe('colony agents spawn', () => { expect(output).toContain('Stop and hand off if quota or conflict'); expect(output).toContain('Sibling files:\n- apps/cli/test/agents-spawn.test.ts'); expect(output).toContain('Do-not-touch list:\n- apps/cli/test/agents-spawn.test.ts'); - expect(output).toContain(`--target ${repoRoot}`); + expect(output).toContain(`--target ${shellQuoteForTest(repoRoot)}`); expect(output).toContain('--claim apps/cli/src/executors/gitguardex.ts'); expect(existsSync(gxArgsFile)).toBe(false); }); diff --git a/apps/cli/test/cockpit.test.ts b/apps/cli/test/cockpit.test.ts index f16c9e7a..74866fa8 100644 --- a/apps/cli/test/cockpit.test.ts +++ b/apps/cli/test/cockpit.test.ts @@ -1,6 +1,6 @@ import { mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; -import { join } from 'node:path'; +import { join, resolve } from 'node:path'; import kleur from 'kleur'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { createProgram } from '../src/index.js'; @@ -9,6 +9,7 @@ import { defaultCockpitSessionName, formatCommand, } from '../src/lib/gitguardex.js'; +import { shellQuoteForTest } from './shell-quote-helper.js'; let dataDir: string; let output: string; @@ -42,7 +43,11 @@ describe('colony cockpit', () => { ); expect(output).toContain('gitguardex cockpit dry-run'); - expect(output).toContain('command: gx cockpit --target /work/colony --session colony-colony'); + // --repo-root '/work/colony' resolves OS-natively (D:\work\colony on + // Windows) and quotes when the path leaves the shell-safe charset. + expect(output).toContain( + `command: gx cockpit --target ${shellQuoteForTest(resolve('/work/colony'))} --session colony-colony`, + ); expect(output).toContain('next ready spawn commands:'); expect(output).toContain('no ready Colony plan subtasks'); }); @@ -69,8 +74,8 @@ describe('colony cockpit', () => { expect(JSON.parse(output)).toMatchObject({ dry_run: true, - command: 'gx cockpit --target /work/colony --session ops', - repo_root: '/work/colony', + command: `gx cockpit --target ${shellQuoteForTest(resolve('/work/colony'))} --session ops`, + repo_root: resolve('/work/colony'), session_name: 'ops', next_spawn_commands: [], }); @@ -79,7 +84,9 @@ describe('colony cockpit', () => { it('quotes rendered paths and sessions for shell output', () => { expect( formatCommand('gx', buildGitGuardexCockpitCommand('/work/repo with space', 'ops cockpit')), - ).toBe("gx cockpit --target '/work/repo with space' --session 'ops cockpit'"); + ).toBe( + `gx cockpit --target ${shellQuoteForTest(resolve('/work/repo with space'))} --session 'ops cockpit'`, + ); }); it('normalizes the default session name from the repo root', () => { diff --git a/apps/cli/test/gitguardex.test.ts b/apps/cli/test/gitguardex.test.ts index 0eef6583..7ee85a55 100644 --- a/apps/cli/test/gitguardex.test.ts +++ b/apps/cli/test/gitguardex.test.ts @@ -1,6 +1,11 @@ import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; -import { join } from 'node:path'; +import { delimiter, join } from 'node:path'; + +// gx (gitguardex) is a POSIX tool; Windows cannot exec the PATH-stub shim +// (execFileSync without shell rejects .cmd), so PATH-spawning integration +// tests run on POSIX only. +const POSIX = process.platform !== 'win32'; import { defaultSettings } from '@colony/config'; import { MemoryStore, listPlans } from '@colony/core'; import { type QueenOrderedPlanInput, publishOrderedPlan } from '@colony/queen'; @@ -62,7 +67,7 @@ beforeEach(() => { originalGxStatus = process.env.GX_FAKE_AGENTS_STATUS; originalGxFailVersion = process.env.GX_FAKE_FAIL_VERSION; process.env.COLONY_HOME = dataDir; - process.env.PATH = `${binDir}:${process.env.PATH ?? ''}`; + process.env.PATH = `${binDir}${delimiter}${process.env.PATH ?? ''}`; process.env.GX_FAKE_LOG = logPath; process.env.GX_FAKE_AGENTS_STATUS = JSON.stringify({ schemaVersion: 1, @@ -181,7 +186,7 @@ describe('GitGuardex executor CLI', () => { expect(output).toContain('--claim apps/cli/src/lib/gitguardex.ts'); }); - it('calls gx cockpit with repo target and colony session name', async () => { + it.runIf(POSIX)('calls gx cockpit with repo target and colony session name', async () => { await createProgram().parseAsync(['node', 'test', 'cockpit', '--repo-root', repoRoot], { from: 'node', }); @@ -249,7 +254,7 @@ describe('GitGuardex executor CLI', () => { }); }); - it('shows GitGuardex lanes in colony status', async () => { + it.runIf(POSIX)('shows GitGuardex lanes in colony status', async () => { process.env.GX_FAKE_AGENTS_STATUS = JSON.stringify({ schemaVersion: 1, repoRoot, diff --git a/apps/cli/test/queen-health.test.ts b/apps/cli/test/queen-health.test.ts index 261c9df5..54ad0008 100644 --- a/apps/cli/test/queen-health.test.ts +++ b/apps/cli/test/queen-health.test.ts @@ -11,6 +11,7 @@ import { import kleur from 'kleur'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { buildColonyHealthPayload, formatColonyHealthOutput } from '../src/commands/health.js'; +import { shellQuoteForTest } from './shell-quote-helper.js'; const NOW = 1_800_000_000_000; const SINCE = NOW - 24 * 3_600_000; @@ -441,7 +442,7 @@ describe('queen wave health', () => { state: 'archived', recommendation: { action: 'publish-new-plan', - command: `colony queen plan --repo-root ${repoRoot} ""`, + command: `colony queen plan --repo-root ${shellQuoteForTest(repoRoot)} ""`, }, }); }); diff --git a/apps/cli/test/shell-quote-helper.ts b/apps/cli/test/shell-quote-helper.ts new file mode 100644 index 00000000..7685ccbc --- /dev/null +++ b/apps/cli/test/shell-quote-helper.ts @@ -0,0 +1,10 @@ +/** + * Mirror of the production shellQuote rule (packages/queen/src/sweep.ts, + * apps/cli/src/commands/health.ts, apps/cli/src/lib/gitguardex.ts) so path + * expectations stay correct on every OS: Windows temp paths contain `\` and + * `~`, which fall outside the safe charset and get single-quoted. + */ +export function shellQuoteForTest(value: string): string { + if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(value)) return value; + return `'${value.replaceAll("'", "'\\''")}'`; +} diff --git a/apps/cli/test/skills-wire.test.ts b/apps/cli/test/skills-wire.test.ts index 1421a7eb..e8502002 100644 --- a/apps/cli/test/skills-wire.test.ts +++ b/apps/cli/test/skills-wire.test.ts @@ -1,6 +1,6 @@ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; -import { join } from 'node:path'; +import { delimiter, join } from 'node:path'; import { afterEach, describe, expect, it } from 'vitest'; import { COLONY_CUE_SKILL_ID, @@ -9,6 +9,29 @@ import { wireColonySkill, } from '../src/commands/skills.js'; +// Cross-platform fake `cue`: a Node script plus a .cmd shim so Windows +// spawn (which cannot exec shebang scripts) resolves it too. +function writeFakeCue(dir: string): void { + const cuePath = join(dir, 'cue'); + writeFileSync( + cuePath, + [ + '#!/usr/bin/env node', + 'const args = process.argv.slice(2);', + "if (args[0] === '--version') { process.stdout.write('0.0.0\\n'); process.exit(0); }", + "process.stdout.write('called: ' + args.join(' ') + '\\n');", + 'process.exit(0);', + '', + ].join('\n'), + { mode: 0o755 }, + ); +} + +// The real `cue` CLI ships as a POSIX binary and the wirer spawns it via +// execFileSync without a shell, which Windows cannot satisfy for extension- +// less PATH stubs — run the cue-present integration tests on POSIX only. +const POSIX = process.platform !== 'win32'; + describe('colony skills wire (cue auto-wiring)', () => { const originalPath = process.env.PATH; afterEach(() => { @@ -41,18 +64,13 @@ describe('colony skills wire (cue auto-wiring)', () => { expect(result.ok).toBe(false); }); - it('invokes `cue skills add-to-profile ` when cue is present', () => { + it.runIf(POSIX)('invokes `cue skills add-to-profile ` when cue is present', () => { const dir = mkdtempSync(join(tmpdir(), 'fake-cue-')); - const cuePath = join(dir, 'cue'); // Stub cue: answer --version for detection, echo every other invocation so // the test can assert the exact subcommand the wirer shells out to. - writeFileSync( - cuePath, - '#!/usr/bin/env bash\nif [ "$1" = "--version" ]; then echo 0.0.0; exit 0; fi\necho "called: $*"\nexit 0\n', - { mode: 0o755 }, - ); + writeFakeCue(dir); try { - process.env.PATH = `${dir}:${originalPath ?? ''}`; + process.env.PATH = `${dir}${delimiter}${originalPath ?? ''}`; const result = wireColonySkill(); expect(result.cueDetected).toBe(true); expect(result.ok).toBe(true); @@ -62,16 +80,11 @@ describe('colony skills wire (cue auto-wiring)', () => { } }); - it('passes --preview through in dry-run mode', () => { + it.runIf(POSIX)('passes --preview through in dry-run mode', () => { const dir = mkdtempSync(join(tmpdir(), 'fake-cue-')); - const cuePath = join(dir, 'cue'); - writeFileSync( - cuePath, - '#!/usr/bin/env bash\nif [ "$1" = "--version" ]; then echo 0.0.0; exit 0; fi\necho "called: $*"\nexit 0\n', - { mode: 0o755 }, - ); + writeFakeCue(dir); try { - process.env.PATH = `${dir}:${originalPath ?? ''}`; + process.env.PATH = `${dir}${delimiter}${originalPath ?? ''}`; const result = wireColonySkill({ dryRun: true }); expect(result.message).toContain('add-to-profile colony/colony --preview'); } finally { diff --git a/apps/mcp-server/src/tools/shared.ts b/apps/mcp-server/src/tools/shared.ts index 055a4e48..f67eadf8 100644 --- a/apps/mcp-server/src/tools/shared.ts +++ b/apps/mcp-server/src/tools/shared.ts @@ -296,6 +296,7 @@ export function mcpErrorResponse( | 'QUEEN_INVALID_GOAL' | 'RESCUE_CONFIRM_REQUIRED' | 'SESSION_NOT_FOUND' + | 'SPEC_ARCHIVE_ALREADY_EXISTS' | 'SPEC_ARCHIVE_CONFLICT' | 'TASK_LINK_SELF' | 'SCOUT_NO_CLAIM', diff --git a/apps/mcp-server/src/tools/spec.ts b/apps/mcp-server/src/tools/spec.ts index f5aa6531..74bdc7f1 100644 --- a/apps/mcp-server/src/tools/spec.ts +++ b/apps/mcp-server/src/tools/spec.ts @@ -271,7 +271,22 @@ export function register(server: McpServer, ctx: ToolContext): void { reason: `Archive ${args.slug}: ${merge.applied} deltas applied, ${merge.conflicts.length} conflicts`, }); - const archivePath = repo.archiveChange(args.slug); + let archivePath: string; + try { + archivePath = repo.archiveChange(args.slug); + } catch (err) { + if (err instanceof Error && err.message.startsWith('archive target already exists')) { + // Same-day re-archive of the same slug: deterministic on every OS + // since archiveChange checks the target explicitly. Surface it as a + // structured "already done" rather than an opaque failure so retry + // loops can stop. + return mcpErrorResponse('SPEC_ARCHIVE_ALREADY_EXISTS', err.message, { + status: 'already_archived', + slug: args.slug, + }); + } + throw err; + } return { content: [ { diff --git a/packages/spec/src/repository.ts b/packages/spec/src/repository.ts index d787227e..63839414 100644 --- a/packages/spec/src/repository.ts +++ b/packages/spec/src/repository.ts @@ -167,7 +167,9 @@ export class SpecRepository { const archiveTarget = join(this.repoRoot, LAYOUT.archiveDir, `${date}-${slug}`); // Fail deterministically if the target exists. POSIX renameSync(dir, file) // throws ENOTDIR, but Windows MoveFileEx would silently clobber the file — - // so check explicitly rather than relying on rename to error. + // so check explicitly rather than relying on rename to error. This + // intentionally makes a same-day re-archive of the same slug fail: + // callers must treat "already exists" as already-archived, not retry. if (existsSync(archiveTarget)) { throw new Error(`archive target already exists: ${archiveTarget}`); } From 2c21d4f20265432a2fac61928567edaf49912602 Mon Sep 17 00:00:00 2001 From: NagyVikt Date: Fri, 12 Jun 2026 16:35:45 +0200 Subject: [PATCH 4/5] chore(release): mark internal @colony/* packages private MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The npm @colony scope is not ours (@colony/core@3.1.0 belongs to a different project), so 'changeset publish' would 403 on every public internal package and fail Release after versioning. colonyq is the product artifact and ships self-contained (prepack bundles, no @colony/* deps in the published tarball — e2e-publish green), so the ten internal packages are now private: publish ships exactly colonyq. --- packages/compress/package.json | 1 + packages/config/package.json | 1 + packages/contracts/package.json | 1 + packages/core/package.json | 1 + packages/embedding/package.json | 1 + packages/foraging/package.json | 1 + packages/hooks/package.json | 1 + packages/installers/package.json | 1 + packages/process/package.json | 1 + packages/storage/package.json | 1 + 10 files changed, 10 insertions(+) diff --git a/packages/compress/package.json b/packages/compress/package.json index 0111ee2e..516393a6 100644 --- a/packages/compress/package.json +++ b/packages/compress/package.json @@ -1,6 +1,7 @@ { "name": "@colony/compress", "version": "0.5.0", + "private": true, "license": "MIT", "type": "module", "main": "./dist/index.js", diff --git a/packages/config/package.json b/packages/config/package.json index 4ee6521c..cddb8420 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -1,6 +1,7 @@ { "name": "@colony/config", "version": "0.7.0", + "private": true, "license": "MIT", "type": "module", "main": "./dist/index.js", diff --git a/packages/contracts/package.json b/packages/contracts/package.json index b2411120..7f27a50c 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -1,6 +1,7 @@ { "name": "@colony/contracts", "version": "0.6.0", + "private": true, "license": "MIT", "type": "module", "main": "./dist/index.js", diff --git a/packages/core/package.json b/packages/core/package.json index 0e760de5..c105983b 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,7 @@ { "name": "@colony/core", "version": "0.7.0", + "private": true, "license": "MIT", "type": "module", "main": "./dist/index.js", diff --git a/packages/embedding/package.json b/packages/embedding/package.json index fd003983..18b77b8a 100644 --- a/packages/embedding/package.json +++ b/packages/embedding/package.json @@ -1,6 +1,7 @@ { "name": "@colony/embedding", "version": "0.7.0", + "private": true, "license": "MIT", "type": "module", "main": "./dist/index.js", diff --git a/packages/foraging/package.json b/packages/foraging/package.json index 6cbffc30..2adbe28e 100644 --- a/packages/foraging/package.json +++ b/packages/foraging/package.json @@ -1,6 +1,7 @@ { "name": "@colony/foraging", "version": "0.7.0", + "private": true, "license": "MIT", "type": "module", "main": "./dist/index.js", diff --git a/packages/hooks/package.json b/packages/hooks/package.json index f1f29662..20b67a7a 100644 --- a/packages/hooks/package.json +++ b/packages/hooks/package.json @@ -1,6 +1,7 @@ { "name": "@colony/hooks", "version": "0.7.0", + "private": true, "license": "MIT", "type": "module", "main": "./dist/index.js", diff --git a/packages/installers/package.json b/packages/installers/package.json index de6dea5a..7d749664 100644 --- a/packages/installers/package.json +++ b/packages/installers/package.json @@ -1,6 +1,7 @@ { "name": "@colony/installers", "version": "0.7.0", + "private": true, "license": "MIT", "type": "module", "main": "./dist/index.js", diff --git a/packages/process/package.json b/packages/process/package.json index 9534393d..c31d806d 100644 --- a/packages/process/package.json +++ b/packages/process/package.json @@ -1,6 +1,7 @@ { "name": "@colony/process", "version": "0.6.0", + "private": true, "license": "MIT", "type": "module", "main": "./dist/index.js", diff --git a/packages/storage/package.json b/packages/storage/package.json index 055bc95e..722453bc 100644 --- a/packages/storage/package.json +++ b/packages/storage/package.json @@ -1,6 +1,7 @@ { "name": "@colony/storage", "version": "0.7.0", + "private": true, "license": "MIT", "type": "module", "main": "./dist/index.js", From b324614db045f961408a55ab4d57de9725b14f8e Mon Sep 17 00:00:00 2001 From: NagyVikt Date: Fri, 12 Jun 2026 16:47:00 +0200 Subject: [PATCH 5/5] =?UTF-8?q?fix(spec,cli):=20review=20follow-ups=20?= =?UTF-8?q?=E2=80=94=20staging=20recovery,=20aligned=20shellQuote=20charse?= =?UTF-8?q?t,=20normalized=20archived=5Fpath?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cli/src/lib/gitguardex.ts | 4 +++- apps/cli/test/gitguardex.test.ts | 9 ++++----- apps/mcp-server/src/tools/spec.ts | 2 +- packages/spec/src/repository.ts | 10 +++++++++- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/apps/cli/src/lib/gitguardex.ts b/apps/cli/src/lib/gitguardex.ts index 62fada8d..fce244f0 100644 --- a/apps/cli/src/lib/gitguardex.ts +++ b/apps/cli/src/lib/gitguardex.ts @@ -834,7 +834,9 @@ function execGx(execFileSync: GitGuardexExecFileSync, args: string[], cwd: strin } function shellQuote(value: string): string { - if (/^[A-Za-z0-9_./:=@%+-]+$/.test(value)) return value; + // Same safe charset as health.ts/sweep.ts shellQuote (and the test + // helper apps/cli/test/shell-quote-helper.ts) — keep all copies aligned. + if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(value)) return value; return `'${value.replace(/'/g, `'\\''`)}'`; } diff --git a/apps/cli/test/gitguardex.test.ts b/apps/cli/test/gitguardex.test.ts index 7ee85a55..d5f7c5ed 100644 --- a/apps/cli/test/gitguardex.test.ts +++ b/apps/cli/test/gitguardex.test.ts @@ -2,10 +2,6 @@ import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync import { tmpdir } from 'node:os'; import { delimiter, join } from 'node:path'; -// gx (gitguardex) is a POSIX tool; Windows cannot exec the PATH-stub shim -// (execFileSync without shell rejects .cmd), so PATH-spawning integration -// tests run on POSIX only. -const POSIX = process.platform !== 'win32'; import { defaultSettings } from '@colony/config'; import { MemoryStore, listPlans } from '@colony/core'; import { type QueenOrderedPlanInput, publishOrderedPlan } from '@colony/queen'; @@ -17,7 +13,10 @@ import { } from '../src/executors/gitguardex.js'; import { createProgram } from '../src/index.js'; import { type GitGuardexExecFileSync, spawnGitGuardexAgent } from '../src/lib/gitguardex.js'; - +// gx (gitguardex) is a POSIX tool; Windows cannot exec the PATH-stub shim +// (execFileSync without shell rejects .cmd), so PATH-spawning integration +// tests run on POSIX only. +const POSIX = process.platform !== 'win32'; const MINIMAL_SPEC = `# SPEC ## §G goal diff --git a/apps/mcp-server/src/tools/spec.ts b/apps/mcp-server/src/tools/spec.ts index 74bdc7f1..d0925f03 100644 --- a/apps/mcp-server/src/tools/spec.ts +++ b/apps/mcp-server/src/tools/spec.ts @@ -293,7 +293,7 @@ export function register(server: McpServer, ctx: ToolContext): void { type: 'text', text: JSON.stringify({ status: 'archived', - archived_path: archivePath, + archived_path: archivePath.replace(/\\/g, '/'), merged_root_hash: merge.spec.rootHash, conflicts: merge.conflicts, applied: merge.applied, diff --git a/packages/spec/src/repository.ts b/packages/spec/src/repository.ts index 63839414..95744394 100644 --- a/packages/spec/src/repository.ts +++ b/packages/spec/src/repository.ts @@ -180,7 +180,15 @@ export class SpecRepository { // path exists at zero. const stagingPath = join(this.repoRoot, LAYOUT.archiveDir, `.staging-${slug}`); renameSync(changeDir, stagingPath); - renameSync(stagingPath, archiveTarget); + try { + renameSync(stagingPath, archiveTarget); + } catch (err) { + // Restore the change dir so a failed second rename never strands the + // change in .staging-* (a retry would then hit the exists guard and + // wrongly report already-archived). + renameSync(stagingPath, changeDir); + throw err; + } return archiveTarget; }