mirror of
https://github.com/glittercowboy/get-shit-done
synced 2026-04-25 17:25:23 +02:00
feat(executor): add cross-AI execution hook (step 2.5) in execute-phase (#1875)
Add optional cross-AI delegation step that lets execute-phase delegate plans to external AI runtimes via stdin-based prompt delivery. Activated by --cross-ai flag, plan frontmatter cross_ai: true, or config key workflow.cross_ai_execution. Adds 3 config keys, template defaults, and 18 tests. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -27,6 +27,7 @@ const VALID_CONFIG_KEYS = new Set([
|
||||
'workflow.code_review_depth',
|
||||
'git.branching_strategy', 'git.base_branch', 'git.phase_branch_template', 'git.milestone_branch_template', 'git.quick_branch_template',
|
||||
'planning.commit_docs', 'planning.search_gitignored',
|
||||
'workflow.cross_ai_execution', 'workflow.cross_ai_command', 'workflow.cross_ai_timeout',
|
||||
'workflow.subagent_timeout',
|
||||
'hooks.context_warnings',
|
||||
'features.thinking_partner',
|
||||
|
||||
@@ -11,7 +11,10 @@
|
||||
"security_asvs_level": 1,
|
||||
"security_block_on": "high",
|
||||
"discuss_mode": "discuss",
|
||||
"research_before_questions": false
|
||||
"research_before_questions": false,
|
||||
"cross_ai_execution": false,
|
||||
"cross_ai_command": "",
|
||||
"cross_ai_timeout": 300
|
||||
},
|
||||
"planning": {
|
||||
"commit_docs": true,
|
||||
|
||||
@@ -57,6 +57,8 @@ Parse `$ARGUMENTS` before loading any context:
|
||||
- First positional token → `PHASE_ARG`
|
||||
- Optional `--wave N` → `WAVE_FILTER`
|
||||
- Optional `--gaps-only` keeps its current meaning
|
||||
- Optional `--cross-ai` → `CROSS_AI_FORCE=true` (force all plans through cross-AI execution)
|
||||
- Optional `--no-cross-ai` → `CROSS_AI_DISABLED=true` (disable cross-AI for this run, overrides config and frontmatter)
|
||||
|
||||
If `--wave` is absent, preserve the current behavior of executing all incomplete waves in the phase.
|
||||
</step>
|
||||
@@ -243,6 +245,77 @@ Report:
|
||||
```
|
||||
</step>
|
||||
|
||||
<step name="cross_ai_delegation">
|
||||
**Optional step 2.5 — Delegate plans to an external AI runtime.**
|
||||
|
||||
This step runs after plan discovery and before normal wave execution. It identifies plans
|
||||
that should be delegated to an external AI command and executes them via stdin-based prompt
|
||||
delivery. Plans handled here are removed from the execute_waves plan list so the normal
|
||||
executor skips them.
|
||||
|
||||
**Activation logic:**
|
||||
|
||||
1. If `CROSS_AI_DISABLED` is true (`--no-cross-ai` flag): skip this step entirely.
|
||||
2. If `CROSS_AI_FORCE` is true (`--cross-ai` flag): mark ALL incomplete plans for cross-AI execution.
|
||||
3. Otherwise: check each plan's frontmatter for `cross_ai: true` AND verify config
|
||||
`workflow.cross_ai_execution` is `true`. Plans matching both conditions are marked for cross-AI.
|
||||
|
||||
```bash
|
||||
CROSS_AI_ENABLED=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-get workflow.cross_ai_execution --default false 2>/dev/null)
|
||||
CROSS_AI_CMD=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-get workflow.cross_ai_command --default "" 2>/dev/null)
|
||||
CROSS_AI_TIMEOUT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-get workflow.cross_ai_timeout --default 300 2>/dev/null)
|
||||
```
|
||||
|
||||
**If no plans are marked for cross-AI:** Skip to execute_waves.
|
||||
|
||||
**If plans are marked but `cross_ai_command` is empty:** Error — tell user to set
|
||||
`workflow.cross_ai_command` via `gsd-tools.cjs config-set workflow.cross_ai_command "<command>"`.
|
||||
|
||||
**For each cross-AI plan (sequentially):**
|
||||
|
||||
1. **Construct the task prompt** from the plan file:
|
||||
- Extract `<objective>` and `<tasks>` sections from the PLAN.md
|
||||
- Append PROJECT.md context (project name, description, tech stack)
|
||||
- Format as a self-contained execution prompt
|
||||
|
||||
2. **Check for dirty working tree before execution:**
|
||||
```bash
|
||||
if ! git diff --quiet HEAD 2>/dev/null; then
|
||||
echo "WARNING: dirty working tree detected — the external AI command may produce uncommitted changes that conflict with existing modifications"
|
||||
fi
|
||||
```
|
||||
|
||||
3. **Run the external command** from the project root, writing the prompt to stdin.
|
||||
Never shell-interpolate the prompt — always pipe via stdin to prevent injection:
|
||||
```bash
|
||||
echo "$TASK_PROMPT" | timeout "${CROSS_AI_TIMEOUT}s" ${CROSS_AI_CMD} > "$CANDIDATE_SUMMARY" 2>"$ERROR_LOG"
|
||||
EXIT_CODE=$?
|
||||
```
|
||||
|
||||
4. **Evaluate the result:**
|
||||
|
||||
**Success (exit 0 + valid summary):**
|
||||
- Read `$CANDIDATE_SUMMARY` and validate it contains meaningful content
|
||||
(not empty, has at least a heading and description — a valid SUMMARY.md structure)
|
||||
- Write it as the plan's SUMMARY.md file
|
||||
- Update STATE.md plan status to complete
|
||||
- Update ROADMAP.md progress
|
||||
- Mark plan as handled — skip it in execute_waves
|
||||
|
||||
**Failure (non-zero exit or invalid summary):**
|
||||
- Display the error output and exit code
|
||||
- Warn: "The external command may have left uncommitted changes or partial edits
|
||||
in the working tree. Review `git status` and `git diff` before proceeding."
|
||||
- Offer three choices:
|
||||
- **retry** — run the same plan through cross-AI again
|
||||
- **skip** — fall back to normal executor for this plan (re-add to execute_waves list)
|
||||
- **abort** — stop execution entirely, preserve state for resume
|
||||
|
||||
5. **After all cross-AI plans processed:** Remove successfully handled plans from the
|
||||
incomplete plan list so execute_waves skips them. Any skipped-to-fallback plans remain
|
||||
in the list for normal executor processing.
|
||||
</step>
|
||||
|
||||
<step name="execute_waves">
|
||||
Execute each selected wave in sequence. Within a wave: parallel if `PARALLELIZATION=true`, sequential if `false`.
|
||||
|
||||
|
||||
173
tests/cross-ai-execution.test.cjs
Normal file
173
tests/cross-ai-execution.test.cjs
Normal file
@@ -0,0 +1,173 @@
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const CONFIG_PATH = path.join(__dirname, '..', 'get-shit-done', 'bin', 'lib', 'config.cjs');
|
||||
const EXECUTE_PHASE_PATH = path.join(__dirname, '..', 'get-shit-done', 'workflows', 'execute-phase.md');
|
||||
const CONFIG_TEMPLATE_PATH = path.join(__dirname, '..', 'get-shit-done', 'templates', 'config.json');
|
||||
|
||||
describe('cross-AI execution', () => {
|
||||
|
||||
describe('config keys', () => {
|
||||
test('workflow.cross_ai_execution is in VALID_CONFIG_KEYS', () => {
|
||||
const { VALID_CONFIG_KEYS } = require(CONFIG_PATH);
|
||||
assert.ok(VALID_CONFIG_KEYS.has('workflow.cross_ai_execution'),
|
||||
'VALID_CONFIG_KEYS must include workflow.cross_ai_execution');
|
||||
});
|
||||
|
||||
test('workflow.cross_ai_command is in VALID_CONFIG_KEYS', () => {
|
||||
const { VALID_CONFIG_KEYS } = require(CONFIG_PATH);
|
||||
assert.ok(VALID_CONFIG_KEYS.has('workflow.cross_ai_command'),
|
||||
'VALID_CONFIG_KEYS must include workflow.cross_ai_command');
|
||||
});
|
||||
|
||||
test('workflow.cross_ai_timeout is in VALID_CONFIG_KEYS', () => {
|
||||
const { VALID_CONFIG_KEYS } = require(CONFIG_PATH);
|
||||
assert.ok(VALID_CONFIG_KEYS.has('workflow.cross_ai_timeout'),
|
||||
'VALID_CONFIG_KEYS must include workflow.cross_ai_timeout');
|
||||
});
|
||||
});
|
||||
|
||||
describe('config template defaults', () => {
|
||||
test('config template has cross_ai_execution default', () => {
|
||||
const template = JSON.parse(fs.readFileSync(CONFIG_TEMPLATE_PATH, 'utf-8'));
|
||||
assert.strictEqual(template.workflow.cross_ai_execution, false,
|
||||
'cross_ai_execution should default to false');
|
||||
});
|
||||
|
||||
test('config template has cross_ai_command default', () => {
|
||||
const template = JSON.parse(fs.readFileSync(CONFIG_TEMPLATE_PATH, 'utf-8'));
|
||||
assert.strictEqual(template.workflow.cross_ai_command, '',
|
||||
'cross_ai_command should default to empty string');
|
||||
});
|
||||
|
||||
test('config template has cross_ai_timeout default', () => {
|
||||
const template = JSON.parse(fs.readFileSync(CONFIG_TEMPLATE_PATH, 'utf-8'));
|
||||
assert.strictEqual(template.workflow.cross_ai_timeout, 300,
|
||||
'cross_ai_timeout should default to 300 seconds');
|
||||
});
|
||||
});
|
||||
|
||||
describe('execute-phase.md cross-AI step', () => {
|
||||
let content;
|
||||
|
||||
test('execute-phase.md has a cross-AI execution step', () => {
|
||||
content = fs.readFileSync(EXECUTE_PHASE_PATH, 'utf-8');
|
||||
assert.ok(content.includes('<step name="cross_ai_delegation">'),
|
||||
'execute-phase.md must have a step named cross_ai_delegation');
|
||||
});
|
||||
|
||||
test('cross-AI step appears between discover_and_group_plans and execute_waves', () => {
|
||||
content = content || fs.readFileSync(EXECUTE_PHASE_PATH, 'utf-8');
|
||||
const discoverIdx = content.indexOf('<step name="discover_and_group_plans">');
|
||||
const crossAiIdx = content.indexOf('<step name="cross_ai_delegation">');
|
||||
const executeIdx = content.indexOf('<step name="execute_waves">');
|
||||
assert.ok(discoverIdx < crossAiIdx, 'cross_ai_delegation must come after discover_and_group_plans');
|
||||
assert.ok(crossAiIdx < executeIdx, 'cross_ai_delegation must come before execute_waves');
|
||||
});
|
||||
|
||||
test('cross-AI step handles --cross-ai flag', () => {
|
||||
content = content || fs.readFileSync(EXECUTE_PHASE_PATH, 'utf-8');
|
||||
assert.ok(content.includes('--cross-ai'),
|
||||
'execute-phase.md must reference --cross-ai flag');
|
||||
});
|
||||
|
||||
test('cross-AI step handles --no-cross-ai flag', () => {
|
||||
content = content || fs.readFileSync(EXECUTE_PHASE_PATH, 'utf-8');
|
||||
assert.ok(content.includes('--no-cross-ai'),
|
||||
'execute-phase.md must reference --no-cross-ai flag');
|
||||
});
|
||||
|
||||
test('cross-AI step uses stdin-based prompt delivery', () => {
|
||||
content = content || fs.readFileSync(EXECUTE_PHASE_PATH, 'utf-8');
|
||||
// The step must describe piping prompt via stdin, not shell interpolation
|
||||
assert.ok(content.includes('stdin'),
|
||||
'cross-AI step must describe stdin-based prompt delivery');
|
||||
});
|
||||
|
||||
test('cross-AI step validates summary output', () => {
|
||||
content = content || fs.readFileSync(EXECUTE_PHASE_PATH, 'utf-8');
|
||||
// The step must describe validating the captured summary
|
||||
const crossAiSection = content.substring(
|
||||
content.indexOf('<step name="cross_ai_delegation">'),
|
||||
content.indexOf('</step>', content.indexOf('<step name="cross_ai_delegation">')) + '</step>'.length
|
||||
);
|
||||
assert.ok(
|
||||
crossAiSection.includes('SUMMARY') && crossAiSection.includes('valid'),
|
||||
'cross-AI step must validate the summary output'
|
||||
);
|
||||
});
|
||||
|
||||
test('cross-AI step warns about dirty working tree', () => {
|
||||
content = content || fs.readFileSync(EXECUTE_PHASE_PATH, 'utf-8');
|
||||
const crossAiSection = content.substring(
|
||||
content.indexOf('<step name="cross_ai_delegation">'),
|
||||
content.indexOf('</step>', content.indexOf('<step name="cross_ai_delegation">')) + '</step>'.length
|
||||
);
|
||||
assert.ok(
|
||||
crossAiSection.includes('dirty') || crossAiSection.includes('uncommitted') || crossAiSection.includes('working tree'),
|
||||
'cross-AI step must warn about dirty/uncommitted changes from external command'
|
||||
);
|
||||
});
|
||||
|
||||
test('cross-AI step reads cross_ai_command from config', () => {
|
||||
content = content || fs.readFileSync(EXECUTE_PHASE_PATH, 'utf-8');
|
||||
const crossAiSection = content.substring(
|
||||
content.indexOf('<step name="cross_ai_delegation">'),
|
||||
content.indexOf('</step>', content.indexOf('<step name="cross_ai_delegation">')) + '</step>'.length
|
||||
);
|
||||
assert.ok(
|
||||
crossAiSection.includes('cross_ai_command'),
|
||||
'cross-AI step must read cross_ai_command from config'
|
||||
);
|
||||
});
|
||||
|
||||
test('cross-AI step reads cross_ai_timeout from config', () => {
|
||||
content = content || fs.readFileSync(EXECUTE_PHASE_PATH, 'utf-8');
|
||||
const crossAiSection = content.substring(
|
||||
content.indexOf('<step name="cross_ai_delegation">'),
|
||||
content.indexOf('</step>', content.indexOf('<step name="cross_ai_delegation">')) + '</step>'.length
|
||||
);
|
||||
assert.ok(
|
||||
crossAiSection.includes('cross_ai_timeout'),
|
||||
'cross-AI step must read cross_ai_timeout from config'
|
||||
);
|
||||
});
|
||||
|
||||
test('cross-AI step handles failure with retry/skip/abort', () => {
|
||||
content = content || fs.readFileSync(EXECUTE_PHASE_PATH, 'utf-8');
|
||||
const crossAiSection = content.substring(
|
||||
content.indexOf('<step name="cross_ai_delegation">'),
|
||||
content.indexOf('</step>', content.indexOf('<step name="cross_ai_delegation">')) + '</step>'.length
|
||||
);
|
||||
assert.ok(crossAiSection.includes('retry'), 'cross-AI step must offer retry on failure');
|
||||
assert.ok(crossAiSection.includes('skip'), 'cross-AI step must offer skip on failure');
|
||||
assert.ok(crossAiSection.includes('abort'), 'cross-AI step must offer abort on failure');
|
||||
});
|
||||
|
||||
test('cross-AI step skips normal executor for handled plans', () => {
|
||||
content = content || fs.readFileSync(EXECUTE_PHASE_PATH, 'utf-8');
|
||||
const crossAiSection = content.substring(
|
||||
content.indexOf('<step name="cross_ai_delegation">'),
|
||||
content.indexOf('</step>', content.indexOf('<step name="cross_ai_delegation">')) + '</step>'.length
|
||||
);
|
||||
assert.ok(
|
||||
crossAiSection.includes('skip') && (crossAiSection.includes('executor') || crossAiSection.includes('execute_waves')),
|
||||
'cross-AI step must describe skipping normal executor for cross-AI handled plans'
|
||||
);
|
||||
});
|
||||
|
||||
test('parse_args step includes --cross-ai and --no-cross-ai', () => {
|
||||
content = content || fs.readFileSync(EXECUTE_PHASE_PATH, 'utf-8');
|
||||
const parseArgsSection = content.substring(
|
||||
content.indexOf('<step name="parse_args"'),
|
||||
content.indexOf('</step>', content.indexOf('<step name="parse_args"')) + '</step>'.length
|
||||
);
|
||||
assert.ok(parseArgsSection.includes('--cross-ai'),
|
||||
'parse_args step must parse --cross-ai flag');
|
||||
assert.ok(parseArgsSection.includes('--no-cross-ai'),
|
||||
'parse_args step must parse --no-cross-ai flag');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user