Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"plugins": [
{
"name": "genie",
"version": "5.260712.1",
"version": "5.260712.2",
"source": "./plugins/genie",
"description": "Human-AI partnership for Claude Code. Share a terminal, orchestrate workers, evolve together. Brainstorm ideas, wish them into plans, make with parallel agents, ship as one team. A coding genie that grows with your project."
}
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@automagik/genie",
"version": "5.260712.1",
"version": "5.260712.2",
"description": "Collaborative terminal toolkit for human + AI workflows. NOTE: npm distribution discontinued 2026-05-09 — install via `curl -fsSL https://raw.githubusercontent.com/automagik-dev/genie/main/install.sh | bash` (cosign + SLSA verified). See https://automagik.dev/genie/release-process",
"license": "MIT",
"type": "module",
Expand Down
5 changes: 2 additions & 3 deletions plugins/genie/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
{
"name": "genie",
"version": "5.260712.1",
"version": "5.260712.2",
"description": "Human-AI partnership for Claude Code. Share a terminal, orchestrate workers, evolve together. Brainstorm ideas, turn them into wishes, execute with /work, validate with /review, and ship as one team.",
"author": {
"name": "Namastex Labs"
},
"repository": "https://github.com/automagik-dev/genie",
"license": "MIT",
"keywords": ["workflow", "orchestration", "collaboration", "claude-code", "tmux", "agents"],
"hooks": "./hooks/hooks.json"
"keywords": ["workflow", "orchestration", "collaboration", "claude-code", "tmux", "agents"]
}
2 changes: 1 addition & 1 deletion plugins/genie/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "genie",
"version": "5.260712.1",
"version": "5.260712.2",
"description": "Plan, execute, review, and ship software with Genie workflows in Codex.",
"author": {
"name": "Namastex Labs",
Expand Down
2 changes: 1 addition & 1 deletion plugins/genie/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "genie-plugin",
"version": "5.260712.1",
"version": "5.260712.2",
"private": true,
"description": "Runtime dependencies for genie bundled CLIs",
"license": "MIT",
Expand Down
120 changes: 120 additions & 0 deletions src/lib/runtime-integrations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,126 @@ describe('durable integration consent and Claude payload provenance', () => {
expect(retry).toBeNull();
expect(retryCalls).toEqual(['claude plugin list --json']);
});

test('Claude disabled-state restore tolerates an already-disabled plugin', () => {
const root = mkdtempSync(join(tmpdir(), 'genie-claude-already-disabled-'));
const statePath = join(root, 'refresh.json');
const result = convergeClaudePlugin({
command: 'claude',
bundleRoot: root,
expectedVersion: VERSION,
installIfAbsent: false,
statePath,
verifyClaudePayload: () => undefined,
runner(_command, args) {
const call = args.join(' ');
if (call === 'plugin list --json') {
return {
exitCode: 0,
stdout: JSON.stringify([{ id: 'genie@automagik', enabled: false, version: VERSION }]),
stderr: '',
};
}
if (call === 'plugin disable genie@automagik') {
return {
exitCode: 1,
stdout: '',
stderr: '✘ Failed to disable plugin "genie@automagik": Plugin "genie@automagik" is already disabled',
};
}
return { exitCode: 0, stdout: '{}', stderr: '' };
},
});

expect(result?.ok).toBe(true);
expect(result?.preservedDisabled).toBe(true);
expect(existsSync(statePath)).toBe(false);
});

test('Claude stale planned intent defers to a live enabled plugin instead of re-disabling it', () => {
const root = mkdtempSync(join(tmpdir(), 'genie-claude-stale-planned-intent-'));
const statePath = join(root, 'refresh.json');
writeFileSync(
statePath,
`${JSON.stringify({
schemaVersion: 4,
runtime: 'claude',
installed: true,
enabled: false,
createdAt: new Date().toISOString(),
phase: 'planned',
})}\n`,
);
const calls: string[] = [];
const result = convergeClaudePlugin({
command: 'claude',
bundleRoot: root,
expectedVersion: VERSION,
installIfAbsent: false,
statePath,
verifyClaudePayload: () => undefined,
runner(_command, args) {
const call = args.join(' ');
calls.push(call);
if (call === 'plugin list --json') {
return {
exitCode: 0,
stdout: JSON.stringify([{ id: 'genie@automagik', enabled: true, version: VERSION }]),
stderr: '',
};
}
return { exitCode: 0, stdout: '{}', stderr: '' };
},
});

expect(result?.ok).toBe(true);
expect(result?.preservedDisabled).toBe(false);
expect(calls).not.toContain('plugin disable genie@automagik');
expect(existsSync(statePath)).toBe(false);
});

test('Codex stale planned intent defers to a live enabled plugin instead of re-disabling it', () => {
const root = mkdtempSync(join(tmpdir(), 'genie-codex-stale-planned-intent-'));
const statePath = join(root, 'refresh.json');
const configPath = join(root, 'config.toml');
writeFileSync(configPath, '[plugins."genie@automagik"]\nenabled = true\n');
writeFileSync(
statePath,
`${JSON.stringify({
schemaVersion: 4,
runtime: 'codex',
installed: true,
enabled: false,
createdAt: new Date().toISOString(),
phase: 'planned',
})}\n`,
);
const result = convergeCodexPlugin({
command: 'codex',
bundleRoot: root,
expectedVersion: VERSION,
installIfAbsent: false,
statePath,
configPath,
codexHome: root,
verifyCodexPayload: () => undefined,
runner(_command, args) {
if (args.join(' ') === 'plugin list --json') {
return {
exitCode: 0,
stdout: JSON.stringify({ installed: [{ pluginId: 'genie@automagik', enabled: true, version: VERSION }] }),
stderr: '',
};
}
return { exitCode: 0, stdout: '{}', stderr: '' };
},
});

expect(result?.ok).toBe(true);
expect(result?.preservedDisabled).toBe(false);
expect(readFileSync(configPath, 'utf8')).toContain('enabled = true');
expect(existsSync(statePath)).toBe(false);
});
});

describe('codex repair convergence (stale marketplace root / stale installed plugin)', () => {
Expand Down
24 changes: 23 additions & 1 deletion src/lib/runtime-integrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2167,6 +2167,22 @@ function settleFailedRefreshIntent(
return markRefreshAmbiguous(options.statePath, intent);
}

/**
* A leftover planned-phase intent means the previous run settled with the
* plugin installed, so the live enabled state — which the user may have
* changed since — is authoritative, not the file's snapshot. Later phases
* keep the file's word: mid-recovery the plugin can be legitimately absent
* or transiently misconfigured, and only the intent knows the user's state.
*/
function reconcilePlannedIntentWithLiveState(
intent: RefreshIntent | null,
before: RuntimePluginState,
): RefreshIntent | null {
if (intent === null || intent.phase !== 'planned' || !before.installed) return intent;
const liveEnabled = before.enabled === true;
return intent.enabled === liveEnabled ? intent : { ...intent, enabled: liveEnabled };
}

function integrationFailure(runtime: RuntimeName, error: unknown): IntegrationResult {
return {
runtime,
Expand Down Expand Up @@ -2273,6 +2289,8 @@ function performCodexPluginConvergence(
if (progress.intent !== null) clearRefreshIntent(options.statePath);
return false;
}
progress.intent = reconcilePlannedIntentWithLiveState(progress.intent, before);
progress.desiredEnabled = progress.intent?.enabled ?? null;

progress.hookReviewRequired = codexHookReviewRequired(options, before);

Expand Down Expand Up @@ -2464,6 +2482,8 @@ export function convergeClaudePlugin(options: ConvergePluginOptions): Integratio
if (intent !== null) clearRefreshIntent(options.statePath);
return null;
}
intent = reconcilePlannedIntentWithLiveState(intent, before);
desiredEnabled = intent?.enabled ?? null;
intent ??= plannedRefreshIntent('claude', before.installed ? before.enabled === true : true);
desiredEnabled = intent.enabled;
writeRefreshIntent(options.statePath, intent);
Expand Down Expand Up @@ -2520,7 +2540,9 @@ export function convergeClaudePlugin(options: ConvergePluginOptions): Integratio

if (desiredEnabled === false) {
try {
runChecked(options.runner, options.command, ['plugin', 'disable', 'genie@automagik'], false, timeoutMs);
// "already disabled" is the desired end state, not a failure; the list
// check below verifies the restore either way.
runChecked(options.runner, options.command, ['plugin', 'disable', 'genie@automagik'], true, timeoutMs);
const restored = requireClaudePluginState(
runChecked(options.runner, options.command, ['plugin', 'list', '--json'], false, timeoutMs).stdout,
'after restoring disabled state',
Expand Down
Loading