fix(3808): codex adapter activates TEXT_MODE when request_user_input is unavailable (#84)
When Codex reports `request_user_input` as unavailable (Default mode), the
Codex skill adapter's Execute-mode-fallback section now explicitly instructs
the agent to append `--text` to `{{GSD_ARGS}}` to activate the workflow's
built-in TEXT_MODE branching. This ensures every AskUserQuestion gate is
handled consistently through the workflow's own text-mode mechanism rather
than ad-hoc plain-text fallback, and eliminates any path to silent-default
selection (#3018 / #3808).
Adds regression test bug-3808-codex-adapter-text-mode-fallback.test.cjs with
typed semantic-flag assertions covering gsd-plan-phase, gsd-discuss-phase,
gsd-execute-phase, and gsd-verify-work.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
5
.changeset/3808-codex-adapter-text-mode-fallback.md
Normal file
5
.changeset/3808-codex-adapter-text-mode-fallback.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 3808
|
||||
---
|
||||
**Codex adapter now activates TEXT_MODE when `request_user_input` is unavailable** — `bin/install.js:getCodexSkillAdapterHeader` previously told the agent only to "stop and present plain-text"; it now explicitly instructs the agent to append `--text` to `{{GSD_ARGS}}` so the workflow's built-in `TEXT_MODE` branching handles all `AskUserQuestion` gates consistently, eliminating silent-default selection in Codex Default mode (#3808).
|
||||
@@ -2579,7 +2579,7 @@ Multi-select workaround:
|
||||
- Codex has no \`multiSelect\`. Use sequential single-selects, or present a numbered freeform list asking the user to enter comma-separated numbers.
|
||||
|
||||
Execute mode fallback:
|
||||
- When \`request_user_input\` is rejected or unavailable, you MUST stop and present the questions as a plain-text numbered list, then wait for the user's reply. Do NOT pick a default and continue (#3018).
|
||||
- When \`request_user_input\` is rejected or unavailable, activate TEXT_MODE: append \`--text\` to \`{{GSD_ARGS}}\` so the workflow's built-in text-mode branching takes over. Present every \`AskUserQuestion\` call as a plain-text numbered list, then stop and wait for the user's reply. Do NOT pick a default and continue (#3018 / #3808).
|
||||
- You may only proceed without a user answer when one of these is true:
|
||||
(a) the invocation included an explicit non-interactive flag (\`--auto\` or \`--all\`),
|
||||
(b) the user has explicitly approved a specific default for this question, or
|
||||
|
||||
155
tests/bug-3808-codex-adapter-text-mode-fallback.test.cjs
Normal file
155
tests/bug-3808-codex-adapter-text-mode-fallback.test.cjs
Normal file
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* Regression test for bug #3808.
|
||||
*
|
||||
* When Codex runs in Default mode, `request_user_input` is reported as
|
||||
* unavailable. The Codex skill adapter must tell the agent to activate the
|
||||
* workflow's built-in TEXT_MODE mechanism (`--text` flag) rather than either:
|
||||
* (a) silently picking a default value — the #3018 failure mode, or
|
||||
* (b) ad-hoc plain-text fallback that bypasses the workflow's own branching.
|
||||
*
|
||||
* Workflows (e.g. plan-phase.md) already have TEXT_MODE logic:
|
||||
* "Set TEXT_MODE=true if `--text` is present in $ARGUMENTS OR text_mode
|
||||
* from init JSON is true."
|
||||
* The adapter must tell the agent to USE that mechanism when
|
||||
* `request_user_input` is unavailable instead of inventing its own fallback
|
||||
* or silently continuing with defaults.
|
||||
*
|
||||
* Test design: mirrors the typed-semantic-flag pattern from bug #3018 so that
|
||||
* prose rewording doesn't break tests as long as the semantics stay correct.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
process.env.GSD_TEST_MODE = '1';
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const path = require('node:path');
|
||||
|
||||
const INSTALL = require(path.join(__dirname, '..', 'bin', 'install.js'));
|
||||
const { getCodexSkillAdapterHeader } = INSTALL;
|
||||
|
||||
/**
|
||||
* Extract the "Execute mode fallback" section text from the adapter header.
|
||||
* Returns null if the section is missing. Section runs from the
|
||||
* "Execute mode fallback:" label up to the next heading or </codex_skill_adapter> tag.
|
||||
*/
|
||||
function extractExecuteModeFallback(header) {
|
||||
const m = header.match(/Execute mode fallback:\s*\n([\s\S]*?)(?=\n##\s|\n<\/codex_skill_adapter>)/);
|
||||
return m ? m[1].trim() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the Execute-mode-fallback section into a typed semantic-flag record.
|
||||
*
|
||||
* Flags for bug #3808 (TEXT_MODE activation):
|
||||
* activatesTextMode — does the prose tell the agent to activate TEXT_MODE / use --text?
|
||||
* instructsStop — does the prose tell the agent to stop/halt/wait?
|
||||
* presentsPlainText — does the prose mention plain-text / numbered-list presentation?
|
||||
* silentlyPicksDefaults — (anti-pattern) does the prose instruct silent-default picking?
|
||||
*/
|
||||
function parseExecuteModeFallbackFor3808(section) {
|
||||
if (!section || typeof section !== 'string') {
|
||||
return {
|
||||
ok: false,
|
||||
sectionLength: 0,
|
||||
activatesTextMode: false,
|
||||
instructsStop: false,
|
||||
presentsPlainText: false,
|
||||
silentlyPicksDefaults: false,
|
||||
};
|
||||
}
|
||||
|
||||
const lower = section.toLowerCase();
|
||||
|
||||
// (a) TEXT_MODE activation — adapter must tell the agent to use the workflow's
|
||||
// built-in text mode mechanism when request_user_input is unavailable.
|
||||
// Accept either: explicit "--text" flag mention OR "text_mode" / "text mode"
|
||||
// paired with context showing it is being SET/ACTIVATED (not just referenced).
|
||||
const mentionsTextFlag = section.includes('--text');
|
||||
const mentionsTextModeOn = /text_mode\s*=\s*true|set\s+text_mode|activate\s+text.?mode|enable\s+text.?mode|text.?mode.*active|text.?mode.*on\b/i.test(section);
|
||||
const activatesTextMode = mentionsTextFlag || mentionsTextModeOn;
|
||||
|
||||
// (b) STOP/WAIT directive — the agent must halt instead of proceeding silently.
|
||||
const instructsStop = /\b(stop|halt|wait)\b/.test(lower);
|
||||
|
||||
// (c) Plain-text fallback presentation.
|
||||
const presentsPlainText = /plain.?text|numbered list/.test(lower);
|
||||
|
||||
// Anti-pattern guard — the prose that caused #3018 and resurfaces in #3808.
|
||||
const silentlyPicksDefaults = /pick (a |the )?(reasonable|sensible|sane) default/i.test(section);
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
sectionLength: section.length,
|
||||
activatesTextMode,
|
||||
instructsStop,
|
||||
presentsPlainText,
|
||||
silentlyPicksDefaults,
|
||||
};
|
||||
}
|
||||
|
||||
describe('bug #3808: codex skill adapter activates TEXT_MODE when request_user_input is unavailable', () => {
|
||||
const SKILL_NAMES = ['gsd-plan-phase', 'gsd-discuss-phase', 'gsd-execute-phase', 'gsd-verify-work'];
|
||||
|
||||
test('getCodexSkillAdapterHeader is exported', () => {
|
||||
assert.equal(typeof getCodexSkillAdapterHeader, 'function');
|
||||
});
|
||||
|
||||
test('Execute mode fallback section exists for all key skills', () => {
|
||||
for (const skillName of SKILL_NAMES) {
|
||||
const header = getCodexSkillAdapterHeader(skillName);
|
||||
const section = extractExecuteModeFallback(header);
|
||||
assert.ok(section !== null && section.length > 0,
|
||||
`${skillName}: Execute mode fallback section must exist and have content`);
|
||||
}
|
||||
});
|
||||
|
||||
for (const skillName of SKILL_NAMES) {
|
||||
test(`${skillName}: fallback activates TEXT_MODE (--text flag or text_mode=true) when request_user_input is unavailable`, () => {
|
||||
const header = getCodexSkillAdapterHeader(skillName);
|
||||
const section = extractExecuteModeFallback(header);
|
||||
const parsed = parseExecuteModeFallbackFor3808(section);
|
||||
assert.equal(parsed.activatesTextMode, true,
|
||||
`${skillName}: fallback must instruct the agent to activate TEXT_MODE (mention --text flag or text_mode=true/active) when request_user_input is unavailable (#3808). Section was:\n${section}`);
|
||||
});
|
||||
|
||||
test(`${skillName}: fallback instructs STOP/WAIT (not silent continuation)`, () => {
|
||||
const header = getCodexSkillAdapterHeader(skillName);
|
||||
const section = extractExecuteModeFallback(header);
|
||||
const parsed = parseExecuteModeFallbackFor3808(section);
|
||||
assert.equal(parsed.instructsStop, true,
|
||||
`${skillName}: fallback must include stop/halt/wait instruction. Section was:\n${section}`);
|
||||
});
|
||||
|
||||
test(`${skillName}: fallback does NOT contain silent-default anti-pattern`, () => {
|
||||
const header = getCodexSkillAdapterHeader(skillName);
|
||||
const section = extractExecuteModeFallback(header);
|
||||
const parsed = parseExecuteModeFallbackFor3808(section);
|
||||
assert.equal(parsed.silentlyPicksDefaults, false,
|
||||
`${skillName}: regression — fallback must NOT instruct the agent to pick defaults autonomously (#3018 / #3808). Section was:\n${section}`);
|
||||
});
|
||||
}
|
||||
|
||||
test('typed semantic-record snapshot for gsd-plan-phase — full contract', () => {
|
||||
const section = extractExecuteModeFallback(getCodexSkillAdapterHeader('gsd-plan-phase'));
|
||||
const parsed = parseExecuteModeFallbackFor3808(section);
|
||||
assert.deepStrictEqual(
|
||||
{
|
||||
ok: parsed.ok,
|
||||
activatesTextMode: parsed.activatesTextMode,
|
||||
instructsStop: parsed.instructsStop,
|
||||
presentsPlainText: parsed.presentsPlainText,
|
||||
silentlyPicksDefaults: parsed.silentlyPicksDefaults,
|
||||
},
|
||||
{
|
||||
ok: true,
|
||||
activatesTextMode: true,
|
||||
instructsStop: true,
|
||||
presentsPlainText: true,
|
||||
silentlyPicksDefaults: false,
|
||||
},
|
||||
`gsd-plan-phase: full TEXT_MODE fallback contract violated (#3808). Section was:\n${section}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user