Skip to content
Open
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 docs/when-to-use-mods.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ The following table summarizes candidate opportunities for additive mods across

| Skill Family | Candidate Mod Enhancement | Fallback Behaviour (Without Mod) |
|---|---|---|
| **`family:setup`** | Instant lockfile drift notification on `session.start`; zero-token `/magpie-status` command; GPG hardware touch toast. | Skill pre-flight step performs comparison in prompt context; standard CLI status output. |
| **`family:setup`** | Instant lockfile drift notification on `session.start` (Pilot 1); zero-token `/magpie-status` command (Pilot 2); GPG hardware touch toast. | Skill pre-flight step performs comparison in prompt context; standard CLI status output. |
| **`family:utilities`** | Zero-token `/magpie-skills` command; live token-usage band during skill authoring and evals. | Standard skill execution via model turn. |
| **`family:security`** | Deterministic pre-commit/pre-push grep for unredacted CVE references; local tracker queue dashboard pane. | Explicit grep check instruction in `security-issue-fix` workflow step. |
| **`family:pr-management`** | Interactive review pane with accept/skip/edit buttons for code review findings; triage sweep progress band. | Sequential `AskUserQuestion` prompts or consolidated review summary markdown. |
Expand Down
34 changes: 20 additions & 14 deletions plugins/magpie-setup/hooks/drift-check.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,13 +205,16 @@ ref: v1.0.0
on('fs.exists', (_: any, e: any) => ({
value: true,
}));
on('fs.read', (_: any, e: any) => ({
value: e.path.endsWith('.apache-magpie.lock')
? LOCK_BELOW_FLOOR
: e.path.endsWith('.claude-plugin/plugin.json')
? JSON.stringify({ name: 'magpie-setup', version: '0.9.0' })
: '',
}));
on('fs.read', (_: any, e: any) => {
const p = (e.path || '').replace(/\\/g, '/');
return {
value: p.endsWith('.apache-magpie.lock')
? LOCK_BELOW_FLOOR
: p.endsWith('.claude-plugin/plugin.json')
? JSON.stringify({ name: 'magpie-setup', version: '0.9.0' })
: '',
};
});

on('ui.render', () => ({ type: 'Box', children: [] }));
on('session.start', () => ({ cwd: '/work' }));
Expand All @@ -236,13 +239,16 @@ ref: v1.0.0
on('fs.exists', (_: any, e: any) => ({
value: true,
}));
on('fs.read', (_: any, e: any) => ({
value: e.path.endsWith('.apache-magpie.lock')
? LOCK_SATISFYING_FLOOR
: e.path.endsWith('.claude-plugin/plugin.json')
? JSON.stringify({ name: 'magpie-setup', version: '0.9.0' })
: '',
}));
on('fs.read', (_: any, e: any) => {
const p = (e.path || '').replace(/\\/g, '/');
return {
value: p.endsWith('.apache-magpie.lock')
? LOCK_SATISFYING_FLOOR
: p.endsWith('.claude-plugin/plugin.json')
? JSON.stringify({ name: 'magpie-setup', version: '0.9.0' })
: '',
};
});

on('ui.render', () => ({ type: 'Box', children: [] }));
on('session.start', () => ({ cwd: '/work' }));
Expand Down
42 changes: 40 additions & 2 deletions plugins/magpie-setup/hooks/drift-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
// under the License.

import { atom, read, update } from 'claude-code';
import { executeMagpieStatus } from './status-command.ts';

export const driftNotice = atom(
{ plugin: 'magpie-setup', key: 'driftNotice' },
Expand Down Expand Up @@ -495,8 +496,19 @@ export async function checkSetupDrift(
* Event hook registration entry point for Claude Code.
*/
export function register(on: any): void {
// 1. Hook session.start to check for lockfile drift
// 1. Hook session.start to check for lockfile drift and register slash commands
on('session.start', async ($: any, e: any, next: any) => {
try {
await $.command.register({
name: 'magpie-status',
description: 'Print Apache Magpie adoption and health dashboard (zero-token)',
argumentHint: '[--format md|json]',
immediate: true,
});
} catch {
// Safe no-op on command register exception
}

try {
const workspaceDir = await $.session.root();
const pluginDir = $.plugin.root;
Expand Down Expand Up @@ -526,7 +538,33 @@ export function register(on: any): void {
}
});

// 2. Hook ui.render to display AbovePrompt drift banner if detected
// 2. Hook command.run on magpie-status (zero-token status dashboard)
on('command.run', { command: 'magpie-status' }, async ($: any, e: any) => {
try {
const workspaceDir = await $.session.root();
const pluginDir = $.plugin.root;
const scriptPath = joinPath(
pluginDir,
'skills',
'status',
'scripts',
'collect_status.py'
);

return await executeMagpieStatus(
(argv, init) => $.process.run(argv, init),
scriptPath,
workspaceDir,
e.args
);
} catch (err: any) {
return {
text: `⚠️ Apache Magpie: Unexpected error executing /magpie-status: ${err?.message || String(err)}`,
};
}
});

// 3. Hook ui.render to display AbovePrompt drift banner if detected
on('ui.render', { component: 'AbovePrompt' }, async ($: any, e: any, next: any) => {
const notice = await read($, driftNotice);
if (!notice) {
Expand Down
258 changes: 258 additions & 0 deletions plugins/magpie-setup/hooks/status-command.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
// SPDX-License-Identifier: Apache-2.0
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

import { describe, test } from 'claude-code/testing';
import { executeMagpieStatus, type ProcessRunner, type ProcessRunResult } from './status-command.ts';

function assert(condition: any, message?: string): void {
if (!condition) {
throw new Error(message || 'Assertion failed');
}
}

function assertStrictEqual(actual: any, expected: any, message?: string): void {
if (actual !== expected) {
throw new Error(message || `Expected ${JSON.stringify(expected)} but got ${JSON.stringify(actual)}`);
}
}

function assertDeepStrictEqual(actual: any, expected: any): void {
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
throw new Error(`Expected deep equality: ${JSON.stringify(expected)} !== ${JSON.stringify(actual)}`);
}
}

describe('magpie-status mod command (Pilot 2)', () => {
const workspaceDir = '/test/workspace';
const scriptPath = '/test/plugin/skills/status/scripts/collect_status.py';
const MOCK_MD_OUTPUT = '## apache-magpie adoption — test\n\n**verdict:** ✔ healthy';
const MOCK_JSON_OUTPUT = '{\n "verdict": "healthy"\n}';

describe('executeMagpieStatus logic', () => {
test('invokes python3 with default markdown format and workspace repo', async () => {
const calls: Array<{ argv: readonly string[]; init?: any }> = [];
const runner: ProcessRunner = async (argv, init) => {
calls.push({ argv, init });
return { exitCode: 0, stdout: MOCK_MD_OUTPUT, stderr: '' };
};

const result = await executeMagpieStatus(runner, scriptPath, workspaceDir);
assertStrictEqual(result.text, MOCK_MD_OUTPUT);
assertStrictEqual(calls.length, 1);
assertDeepStrictEqual(calls[0]?.argv, [
'python3',
scriptPath,
'--format',
'md',
'--repo',
workspaceDir,
]);
assertStrictEqual(calls[0]?.init?.cwd, workspaceDir);
assertStrictEqual(calls[0]?.init?.env?.PYTHONIOENCODING, 'utf-8');
});

test('honors --format json flag and requests pretty-printed JSON', async () => {
const calls: Array<{ argv: readonly string[] }> = [];
const runner: ProcessRunner = async (argv) => {
calls.push({ argv });
return { exitCode: 0, stdout: MOCK_JSON_OUTPUT, stderr: '' };
};

const result = await executeMagpieStatus(runner, scriptPath, workspaceDir, '--format json');
assertStrictEqual(result.text, MOCK_JSON_OUTPUT);
assertStrictEqual(calls.length, 1);
assertDeepStrictEqual(calls[0]?.argv, [
'python3',
scriptPath,
'--format',
'json',
'--pretty',
'--repo',
workspaceDir,
]);
});

test('honors --format=json syntax and case-insensitivity', async () => {
const calls: Array<{ argv: readonly string[] }> = [];
const runner: ProcessRunner = async (argv) => {
calls.push({ argv });
return { exitCode: 0, stdout: MOCK_JSON_OUTPUT, stderr: '' };
};

const result1 = await executeMagpieStatus(runner, scriptPath, workspaceDir, '--format=json');
assertStrictEqual(result1.text, MOCK_JSON_OUTPUT);

const result2 = await executeMagpieStatus(runner, scriptPath, workspaceDir, '--FORMAT JSON');
assertStrictEqual(result2.text, MOCK_JSON_OUTPUT);
assertStrictEqual(calls.length, 2);
});

test('honors --json flag alias', async () => {
const calls: Array<{ argv: readonly string[] }> = [];
const runner: ProcessRunner = async (argv) => {
calls.push({ argv });
return { exitCode: 0, stdout: MOCK_JSON_OUTPUT, stderr: '' };
};

const result = await executeMagpieStatus(runner, scriptPath, workspaceDir, '--json');
assertStrictEqual(result.text, MOCK_JSON_OUTPUT);
assertStrictEqual(calls.length, 1);
assertDeepStrictEqual(calls[0]?.argv, [
'python3',
scriptPath,
'--format',
'json',
'--pretty',
'--repo',
workspaceDir,
]);
});

test('falls back cleanly to python when python3 command throws (e.g. ENOENT)', async () => {
const calls: string[][] = [];
const runner: ProcessRunner = async (argv) => {
calls.push([...argv]);
if (argv[0] === 'python3') {
throw new Error('spawn python3 ENOENT');
}
return { exitCode: 0, stdout: MOCK_MD_OUTPUT, stderr: '' };
};

const result = await executeMagpieStatus(runner, scriptPath, workspaceDir);
assertStrictEqual(result.text, MOCK_MD_OUTPUT);
assertStrictEqual(calls.length, 2);
assertStrictEqual(calls[0]?.[0], 'python3');
assertStrictEqual(calls[1]?.[0], 'python');
assertDeepStrictEqual(calls[1]?.slice(1), [
scriptPath,
'--format',
'md',
'--repo',
workspaceDir,
]);
});

test('falls back to python when python3 exits with non-zero exit code', async () => {
const calls: string[][] = [];
const runner: ProcessRunner = async (argv) => {
calls.push([...argv]);
if (argv[0] === 'python3') {
return { exitCode: 127, stdout: '', stderr: 'python3 not found' };
}
return { exitCode: 0, stdout: MOCK_MD_OUTPUT, stderr: '' };
};

const result = await executeMagpieStatus(runner, scriptPath, workspaceDir);
assertStrictEqual(result.text, MOCK_MD_OUTPUT);
assertStrictEqual(calls.length, 2);
assertStrictEqual(calls[0]?.[0], 'python3');
assertStrictEqual(calls[1]?.[0], 'python');
});

test('returns actionable diagnostic error when both python3 and python fail', async () => {
const runner: ProcessRunner = async () => {
return { exitCode: 1, stdout: '', stderr: 'No module named internal_magpie' };
};

const result = await executeMagpieStatus(runner, scriptPath, workspaceDir);
assert(result.text.includes('Unable to execute deterministic status collector'));
assert(result.text.includes('No module named internal_magpie'));
assert(result.text.includes('/magpie-setup status'));
});

test('prefers python error when python3 exits with 127 (not found)', async () => {
const runner: ProcessRunner = async (argv) => {
if (argv[0] === 'python3') {
return { exitCode: 127, stdout: '', stderr: 'python3: command not found' };
}
return { exitCode: 1, stdout: '', stderr: 'SyntaxError: invalid syntax in collect_status.py' };
};

const result = await executeMagpieStatus(runner, scriptPath, workspaceDir);
assert(result.text.includes('SyntaxError: invalid syntax in collect_status.py'));
});
});

describe('Claude Code hook registration and /magpie-status command execution', () => {
test('registers and executes /magpie-status returning stdout in session transcript', async ($: any, on: any) => {
const registeredCommands: any[] = [];
on('command.register', (_: any, e: any) => {
registeredCommands.push(e);
return { value: undefined };
});

on('session.start', () => ({ cwd: '/test/workspace' }));
on('session.root', () => ({ value: '/test/workspace' }));
on('fs.exists', () => ({ value: false }));
on('fs.read', () => ({ value: '' }));
on('ui.render', () => ({ type: 'Box', children: [] }));

on('process.run', (_: any, e: any) => {
return {
value: {
exitCode: 0,
stdout: MOCK_MD_OUTPUT,
stderr: '',
},
};
});

await $.session.start({ surface: 'terminal', isInteractive: true, cwd: '/test/workspace' });

// Verify that magpie-status was registered with immediate: true
const reg = registeredCommands.find((c) => c.name === 'magpie-status');
assert(reg, 'Expected magpie-status command to be registered');
assertStrictEqual(reg.name, 'magpie-status');
assertStrictEqual(reg.immediate, true);

// Verify command execution
const answer = await $.command.run({ command: 'magpie-status', args: '' });
assertStrictEqual(answer.text, MOCK_MD_OUTPUT);
});

test('executes /magpie-status with --format json returning formatted JSON', async ($: any, on: any) => {
on('command.register', () => ({ value: undefined }));
on('session.start', () => ({ cwd: '/test/workspace' }));
on('session.root', () => ({ value: '/test/workspace' }));
on('fs.exists', () => ({ value: false }));
on('fs.read', () => ({ value: '' }));
on('ui.render', () => ({ type: 'Box', children: [] }));

let receivedArgv: readonly string[] = [];
on('process.run', (_: any, e: any) => {
receivedArgv = e.argv || [];
return {
value: {
exitCode: 0,
stdout: MOCK_JSON_OUTPUT,
stderr: '',
},
};
});

await $.session.start({ surface: 'terminal', isInteractive: true, cwd: '/test/workspace' });

const answer = await $.command.run({ command: 'magpie-status', args: '--format json' });
assertStrictEqual(answer.text, MOCK_JSON_OUTPUT);
assert(receivedArgv.includes('--format'), 'Expected argv to include --format');
assert(receivedArgv.includes('json'), 'Expected argv to include json');
assert(receivedArgv.includes('--pretty'), 'Expected argv to include --pretty');
});
});
});
Loading