mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-05 01:50:27 +08:00
refactor(cli): change from env var injection to direct prompt concatenation
- Replace $PROTO/$TMPL environment variable injection with systemRules/roles direct concatenation - Append rules to END of prompt instead of prepending - Change prompt field name from RULES to CONSTRAINTS in all prompts - Default to universal-rigorous-style template when --rule not specified - Update all .claude documentation, agents, commands, and skills - Add streaming_content type support for Gemini delta messages Breaking: Prompts now use CONSTRAINTS field instead of RULES
This commit is contained in:
@@ -551,6 +551,8 @@ async function execAction(positionalPrompt: string | undefined, options: CliExec
|
||||
}
|
||||
|
||||
// Priority: 1. --file, 2. --prompt/-p option, 3. positional argument
|
||||
// Note: On Windows, quoted arguments like -p "say hello" may be split into
|
||||
// -p "say" and positional "hello". We merge them back together.
|
||||
let finalPrompt: string | undefined;
|
||||
|
||||
if (file) {
|
||||
@@ -569,7 +571,8 @@ async function execAction(positionalPrompt: string | undefined, options: CliExec
|
||||
}
|
||||
} else if (optionPrompt) {
|
||||
// Use --prompt/-p option (preferred for multi-line)
|
||||
finalPrompt = optionPrompt;
|
||||
// Merge with positional argument if Windows split the quoted string
|
||||
finalPrompt = positionalPrompt ? `${optionPrompt} ${positionalPrompt}` : optionPrompt;
|
||||
} else {
|
||||
// Fall back to positional argument
|
||||
finalPrompt = positionalPrompt;
|
||||
@@ -586,20 +589,21 @@ async function execAction(positionalPrompt: string | undefined, options: CliExec
|
||||
|
||||
const prompt_to_use = finalPrompt || '';
|
||||
|
||||
// Load rules templates (will be passed as env vars)
|
||||
// Load rules templates (concatenation mode - directly prepend to prompt)
|
||||
// Default to universal-rigorous-style if --rule not specified
|
||||
const effectiveRule = rule || 'universal-rigorous-style';
|
||||
let rulesEnv: { PROTO?: string; TMPL?: string } = {};
|
||||
let systemRules = ''; // Protocol content
|
||||
let roles = ''; // Template content
|
||||
try {
|
||||
const { loadProtocol, loadTemplate } = await import('../tools/template-discovery.js');
|
||||
const proto = loadProtocol(mode);
|
||||
const tmpl = loadTemplate(effectiveRule);
|
||||
if (proto) rulesEnv.PROTO = proto;
|
||||
if (tmpl) rulesEnv.TMPL = tmpl;
|
||||
if (proto) systemRules = proto;
|
||||
if (tmpl) roles = tmpl;
|
||||
if (debug) {
|
||||
console.log(chalk.gray(` Rule loaded: ${effectiveRule}${!rule ? ' (default)' : ''}`));
|
||||
console.log(chalk.gray(` PROTO(${proto ? proto.length : 0} chars) + TMPL(${tmpl ? tmpl.length : 0} chars)`));
|
||||
console.log(chalk.gray(` Use $PROTO and $TMPL in your prompt to reference them`));
|
||||
console.log(chalk.gray(` systemRules(${systemRules.length} chars) + roles(${roles.length} chars)`));
|
||||
console.log(chalk.gray(` Rules will be prepended to prompt automatically`));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`Error loading rule template: ${error instanceof Error ? error.message : error}`));
|
||||
@@ -728,6 +732,24 @@ async function execAction(positionalPrompt: string | undefined, options: CliExec
|
||||
}
|
||||
}
|
||||
|
||||
// Concatenate systemRules and roles to the end of prompt (if loaded)
|
||||
// Format: [USER_PROMPT]\n[SYSTEM_RULES]\n[ROLES]
|
||||
if (systemRules || roles) {
|
||||
const parts: string[] = [actualPrompt];
|
||||
if (systemRules) {
|
||||
parts.push(`=== SYSTEM RULES ===\n${systemRules}`);
|
||||
}
|
||||
if (roles) {
|
||||
parts.push(`=== ROLES ===\n${roles}`);
|
||||
}
|
||||
actualPrompt = parts.join('\n\n');
|
||||
|
||||
if (debug) {
|
||||
console.log(chalk.gray(` Prompt structure: USER_PROMPT(${prompt_to_use.length}) + SYSTEM_RULES(${systemRules.length}) + ROLES(${roles.length})`));
|
||||
console.log(chalk.gray(` Total prompt length: ${actualPrompt.length} chars`));
|
||||
}
|
||||
}
|
||||
|
||||
// Parse resume IDs for merge scenario
|
||||
const resumeIds = resume && typeof resume === 'string' ? resume.split(',').map(s => s.trim()).filter(Boolean) : [];
|
||||
const isMerge = resumeIds.length > 1;
|
||||
@@ -835,6 +857,7 @@ async function execAction(positionalPrompt: string | undefined, options: CliExec
|
||||
switch (unit.type) {
|
||||
case 'stdout':
|
||||
case 'code':
|
||||
case 'streaming_content': // Show streaming delta content in real-time
|
||||
process.stdout.write(typeof unit.content === 'string' ? unit.content : JSON.stringify(unit.content));
|
||||
break;
|
||||
case 'stderr':
|
||||
@@ -879,9 +902,8 @@ async function execAction(positionalPrompt: string | undefined, options: CliExec
|
||||
uncommitted,
|
||||
base,
|
||||
commit,
|
||||
title,
|
||||
// Rules env vars (PROTO, TMPL)
|
||||
rulesEnv: Object.keys(rulesEnv).length > 0 ? rulesEnv : undefined
|
||||
title
|
||||
// Rules are now concatenated directly into prompt (no env vars)
|
||||
}, onOutput); // Always pass onOutput for real-time dashboard streaming
|
||||
|
||||
if (elapsedInterval) clearInterval(elapsedInterval);
|
||||
@@ -1228,7 +1250,15 @@ export async function cliCommand(
|
||||
|
||||
if (hasPromptOption || hasFileOption || hasResume || subcommandIsPrompt) {
|
||||
// Treat as exec: use subcommand as positional prompt if no -p/-f option
|
||||
const positionalPrompt = subcommandIsPrompt ? subcommand : undefined;
|
||||
let positionalPrompt = subcommandIsPrompt ? subcommand : undefined;
|
||||
|
||||
// On Windows, quoted arguments like -p "a b c" may be split across argsArray
|
||||
// Merge them back together to reconstruct the full prompt
|
||||
if (argsArray.length > 0 && hasPromptOption) {
|
||||
const extraArgs = argsArray.join(' ');
|
||||
positionalPrompt = positionalPrompt ? `${positionalPrompt} ${extraArgs}` : extraArgs;
|
||||
}
|
||||
|
||||
await execAction(positionalPrompt, execOptions);
|
||||
} else {
|
||||
// Show help
|
||||
|
||||
@@ -1149,7 +1149,7 @@ async function executeCliTool(
|
||||
duration_ms: duration,
|
||||
output: newTurnOutput,
|
||||
parsedOutput: flattenOutputUnits(allOutputUnits, {
|
||||
excludeTypes: ['stderr', 'progress', 'metadata', 'system', 'tool_call']
|
||||
excludeTypes: ['stderr', 'progress', 'metadata', 'system', 'tool_call', 'thought']
|
||||
})
|
||||
};
|
||||
|
||||
|
||||
@@ -20,7 +20,8 @@ export type CliOutputUnitType =
|
||||
| 'progress' // Progress updates
|
||||
| 'metadata' // Session/execution metadata
|
||||
| 'system' // System events/messages
|
||||
| 'tool_call'; // Tool invocation/result (Gemini tool_use/tool_result)
|
||||
| 'tool_call' // Tool invocation/result (Gemini tool_use/tool_result)
|
||||
| 'streaming_content'; // Streaming delta content (only last one used in final output)
|
||||
|
||||
/**
|
||||
* Intermediate Representation unit
|
||||
@@ -292,9 +293,9 @@ export class JsonLinesParser implements IOutputParser {
|
||||
if (json.type === 'message' && json.role) {
|
||||
// Gemini assistant/user message
|
||||
if (json.role === 'assistant') {
|
||||
// Delta messages are incremental streaming chunks - treat as progress (filtered from final output)
|
||||
// Only non-delta messages are final content
|
||||
const outputType = json.delta === true ? 'progress' : 'stdout';
|
||||
// Delta messages use 'streaming_content' type - only last one is used in final output
|
||||
// Non-delta (final) messages use 'stdout' type
|
||||
const outputType = json.delta === true ? 'streaming_content' : 'stdout';
|
||||
return {
|
||||
type: outputType,
|
||||
content: json.content || '',
|
||||
@@ -1125,8 +1126,26 @@ export function flattenOutputUnits(
|
||||
separator = '\n'
|
||||
} = options || {};
|
||||
|
||||
// Special handling for streaming_content: concatenate all into a single stdout unit
|
||||
// Gemini delta messages are incremental (each contains partial content to append)
|
||||
let processedUnits = units;
|
||||
const streamingUnits = units.filter(u => u.type === 'streaming_content');
|
||||
if (streamingUnits.length > 0) {
|
||||
// Concatenate all streaming_content into one
|
||||
const concatenatedContent = streamingUnits
|
||||
.map(u => typeof u.content === 'string' ? u.content : '')
|
||||
.join('');
|
||||
processedUnits = units.filter(u => u.type !== 'streaming_content');
|
||||
// Add concatenated content as stdout type for inclusion
|
||||
processedUnits.push({
|
||||
type: 'stdout',
|
||||
content: concatenatedContent,
|
||||
timestamp: streamingUnits[streamingUnits.length - 1].timestamp
|
||||
});
|
||||
}
|
||||
|
||||
// Filter units by type
|
||||
let filtered = units;
|
||||
let filtered = processedUnits;
|
||||
if (includeTypes && includeTypes.length > 0) {
|
||||
filtered = filtered.filter(u => includeTypes.includes(u.type));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user