codemini-cli 0.4.4 → 0.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/deployment.md +5 -5
- package/package.json +1 -1
- package/skills/project-requirements/SKILL.md +98 -52
- package/src/commands/chat.js +59 -6
- package/src/core/chat-runtime.js +437 -2
- package/src/core/command-loader.js +9 -0
- package/src/core/fff-adapter.js +1 -1
- package/src/tui/chat-app.js +47 -14
- package/templates/project-requirements/report-shell.html +580 -0
package/src/core/chat-runtime.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { parseInput } from './input-parser.js';
|
|
2
|
-
import { loadCommandsAndSkills, renderCommandPrompt } from './command-loader.js';
|
|
2
|
+
import { formatLocalDate, loadCommandsAndSkills, renderCommandPrompt } from './command-loader.js';
|
|
3
3
|
import { runAgentLoop } from './agent-loop.js';
|
|
4
4
|
import { setResultDir, clearResultStore } from './tool-result-store.js';
|
|
5
5
|
import { trimInline, normalizePath } from './string-utils.js';
|
|
6
6
|
import fs from 'node:fs/promises';
|
|
7
7
|
import path from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
8
9
|
import {
|
|
9
10
|
createChatCompletion,
|
|
10
11
|
createChatCompletionStream
|
|
@@ -38,6 +39,8 @@ import {
|
|
|
38
39
|
} from './reflect-skill.js';
|
|
39
40
|
|
|
40
41
|
const STREAM_SAVE_DEBOUNCE_MS = 120;
|
|
42
|
+
const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
43
|
+
const PROJECT_REQUIREMENTS_TEMPLATE = path.resolve(MODULE_DIR, '..', '..', 'templates', 'project-requirements', 'report-shell.html');
|
|
41
44
|
|
|
42
45
|
function toOpenAIMessages(sessionMessages) {
|
|
43
46
|
const mapped = [];
|
|
@@ -312,12 +315,25 @@ export const ROLE_TOOL_POLICY = {
|
|
|
312
315
|
coder: ['read', 'grep', 'list', 'edit', 'write', 'delete', 'run', 'ast_query', 'read_ast_node', 'glob', 'tool_search', 'web_fetch', 'web_search', 'update_todos', 'read_plan', 'update_plan'],
|
|
313
316
|
reviewer: ['read', 'grep', 'list', 'glob', 'tool_search', 'ast_query', 'read_ast_node', 'read_plan'],
|
|
314
317
|
tester: ['read', 'grep', 'list', 'run', 'glob', 'tool_search', 'read_plan'],
|
|
315
|
-
summarizer: ['read_plan']
|
|
318
|
+
summarizer: ['read', 'read_plan']
|
|
316
319
|
};
|
|
317
320
|
const SUB_AGENT_CONTEXT_MAX_MESSAGES = 4;
|
|
318
321
|
const SUB_AGENT_CONTEXT_MAX_CHARS = 1200;
|
|
319
322
|
const SUB_AGENT_EVIDENCE_MAX_ITEMS = 3;
|
|
320
323
|
const SUB_AGENT_HANDOFF_MAX_ITEMS = 6;
|
|
324
|
+
const PROJECT_REQUIREMENTS_SECTION_MARKERS = [
|
|
325
|
+
{ key: 'summary', marker: 'REQUIREMENTS_SUMMARY', labels: ['1', 'summary', 'overview', 'project overview', 'executive summary', '项目概述', '项目总览', '概述'] },
|
|
326
|
+
{ key: 'architecture', marker: 'REQUIREMENTS_ARCHITECTURE', labels: ['2', 'architecture', 'system architecture', 'system map', '架构', '系统架构图', '系统架构', '架构图'] },
|
|
327
|
+
{ key: 'interfaces', marker: 'REQUIREMENTS_INTERFACE_INVENTORY', labels: ['3', 'interface inventory', 'interfaces', 'api inventory', '接口清单', '接口', 'api清单'] },
|
|
328
|
+
{ key: 'requirements', marker: 'REQUIREMENTS_API_CARDS', labels: ['4', 'requirement cards', 'api cards', 'interface requirements', '接口需求卡片', '需求卡片'] },
|
|
329
|
+
{ key: 'flows', marker: 'REQUIREMENTS_FLOWS', labels: ['5', 'flows', 'user flows', 'core flows', '核心用户流程', '用户流程', '流程'] },
|
|
330
|
+
{ key: 'domain', marker: 'REQUIREMENTS_DOMAIN_MODEL', labels: ['6', 'domain model', 'data ownership', 'domain', '领域模型', '数据归属', '领域模型与数据归属'] },
|
|
331
|
+
{ key: 'security', marker: 'REQUIREMENTS_SECURITY', labels: ['7', 'security', 'permissions', 'compliance', '权限', '安全', '合规', '权限、安全与合规'] },
|
|
332
|
+
{ key: 'errors', marker: 'REQUIREMENTS_ERROR_HANDLING', labels: ['8', 'errors', 'edge cases', 'error handling', '异常处理', '边界情况', '异常处理与边界情况'] },
|
|
333
|
+
{ key: 'nonfunctional', marker: 'REQUIREMENTS_NONFUNCTIONAL', labels: ['9', 'non-functional', 'nonfunctional', 'nfr', '非功能性需求', '非功能'] },
|
|
334
|
+
{ key: 'questions', marker: 'REQUIREMENTS_OPEN_QUESTIONS', labels: ['10', 'open questions', 'unknowns', '待确认问题', '待确认', '问题'] },
|
|
335
|
+
{ key: 'evidence', marker: 'REQUIREMENTS_EVIDENCE_INDEX', labels: ['11', 'evidence', 'source evidence', 'source evidence index', '源码证据索引', '证据索引', '源码证据'] }
|
|
336
|
+
];
|
|
321
337
|
const PLAN_MEMORY_MARKERS = {
|
|
322
338
|
findings: ['<!-- plan-findings-start -->', '<!-- plan-findings-end -->'],
|
|
323
339
|
progress: ['<!-- plan-progress-start -->', '<!-- plan-progress-end -->']
|
|
@@ -395,6 +411,7 @@ export function getSubAgentRolePrompt(role) {
|
|
|
395
411
|
'You are the summarizer in a multi-step agent pipeline.',
|
|
396
412
|
'Your job is to synthesize the results of all prior steps into a concise, actionable final summary.',
|
|
397
413
|
'Do NOT re-analyze the codebase or make new tool calls unless the handed-off evidence is clearly insufficient.',
|
|
414
|
+
'You may read handed-off artifact files, such as generated reports, when needed to summarize or verify their existence.',
|
|
398
415
|
'Instead, read the accumulated step results in the plan file context provided to you.',
|
|
399
416
|
'Output format — keep it short and direct:',
|
|
400
417
|
'Summary:',
|
|
@@ -2708,6 +2725,16 @@ async function executePlanWithSubAgents({
|
|
|
2708
2725
|
const step = steps[i];
|
|
2709
2726
|
if (signal?.aborted) break;
|
|
2710
2727
|
|
|
2728
|
+
emitPlanEvent({
|
|
2729
|
+
type: 'plan:progress',
|
|
2730
|
+
planFile: planFilePath,
|
|
2731
|
+
step: i + 1,
|
|
2732
|
+
total: steps.length,
|
|
2733
|
+
role: step.role,
|
|
2734
|
+
title: step.title,
|
|
2735
|
+
status: 'running'
|
|
2736
|
+
});
|
|
2737
|
+
|
|
2711
2738
|
emitPlanEvent({
|
|
2712
2739
|
type: 'assistant:delta',
|
|
2713
2740
|
text: `\n[plan] Step ${i + 1}/${steps.length} -> ${step.role}: ${step.title}\n`
|
|
@@ -2771,6 +2798,17 @@ async function executePlanWithSubAgents({
|
|
|
2771
2798
|
);
|
|
2772
2799
|
}
|
|
2773
2800
|
|
|
2801
|
+
emitPlanEvent({
|
|
2802
|
+
type: 'plan:progress',
|
|
2803
|
+
planFile: planFilePath,
|
|
2804
|
+
step: i + 1,
|
|
2805
|
+
total: steps.length,
|
|
2806
|
+
role: step.role,
|
|
2807
|
+
title: step.title,
|
|
2808
|
+
status: stepRecord.failed ? 'failed' : 'done',
|
|
2809
|
+
summary: stepRecord.failed ? stepRecord.failureReason : trimInline(stepRecord.output, 160)
|
|
2810
|
+
});
|
|
2811
|
+
|
|
2774
2812
|
if (stepRecord.failed && i < steps.length - 1) {
|
|
2775
2813
|
const summarizerIndex = steps.findIndex((candidate, index) => index > i && candidate.role === 'summarizer');
|
|
2776
2814
|
if (summarizerIndex > i) {
|
|
@@ -2968,6 +3006,386 @@ function renderAutoPlanMarkdown({
|
|
|
2968
3006
|
return lines.join('\n');
|
|
2969
3007
|
}
|
|
2970
3008
|
|
|
3009
|
+
function parseProjectRequirementsOptions(args = []) {
|
|
3010
|
+
const raw = args.join(' ').trim();
|
|
3011
|
+
const normalized = raw.toLowerCase();
|
|
3012
|
+
const hasIgnoreIntent = /(忽略|跳过|不生成|不要|无需|排除|exclude|skip|omit|without|no\s+)/i.test(raw);
|
|
3013
|
+
if (!hasIgnoreIntent) return { raw, ignoredSections: [] };
|
|
3014
|
+
|
|
3015
|
+
const ignored = [];
|
|
3016
|
+
for (const section of PROJECT_REQUIREMENTS_SECTION_MARKERS) {
|
|
3017
|
+
const matched = section.labels.some((label) => {
|
|
3018
|
+
const value = String(label).toLowerCase();
|
|
3019
|
+
if (/^\d+$/.test(value)) {
|
|
3020
|
+
return new RegExp(`(^|[^0-9])${value}([^0-9]|$)`).test(normalized);
|
|
3021
|
+
}
|
|
3022
|
+
return normalized.includes(value);
|
|
3023
|
+
});
|
|
3024
|
+
if (matched) ignored.push(section);
|
|
3025
|
+
}
|
|
3026
|
+
return { raw, ignoredSections: ignored };
|
|
3027
|
+
}
|
|
3028
|
+
|
|
3029
|
+
function renderProjectRequirementsSectionContract(ignoredSections = []) {
|
|
3030
|
+
const ignored = new Set(ignoredSections.map((section) => section.marker));
|
|
3031
|
+
const required = PROJECT_REQUIREMENTS_SECTION_MARKERS
|
|
3032
|
+
.filter((section) => !ignored.has(section.marker))
|
|
3033
|
+
.map((section) => section.marker);
|
|
3034
|
+
const lines = [`Required marker sections: ${required.join(', ')}.`];
|
|
3035
|
+
if (ignoredSections.length > 0) {
|
|
3036
|
+
lines.push(`User-requested omitted sections: ${ignoredSections.map((section) => `${section.key} (${section.marker})`).join(', ')}.`);
|
|
3037
|
+
lines.push('For omitted sections, leave the shell section visibly marked as omitted and do not spend analysis or writing budget filling it.');
|
|
3038
|
+
}
|
|
3039
|
+
return lines.join('\n');
|
|
3040
|
+
}
|
|
3041
|
+
|
|
3042
|
+
function buildProjectRequirementsSteps(renderedSkillPrompt, args = []) {
|
|
3043
|
+
const options = parseProjectRequirementsOptions(args);
|
|
3044
|
+
const userArgs = args.join(' ').trim();
|
|
3045
|
+
const requestedFocus = userArgs ? `User request/focus: ${userArgs}` : 'User request/focus: full workspace requirements report.';
|
|
3046
|
+
const reportDate = formatLocalDate();
|
|
3047
|
+
const reportPath = `docs/requirements/${reportDate}-project-requirements.html`;
|
|
3048
|
+
const companionPath = `docs/requirements/${reportDate}-project-requirements.md`;
|
|
3049
|
+
const reportContract = [
|
|
3050
|
+
requestedFocus,
|
|
3051
|
+
`Primary report path: ${reportPath}`,
|
|
3052
|
+
`Optional companion Markdown path: ${companionPath}`,
|
|
3053
|
+
'A pre-created HTML shell already exists at the primary report path.',
|
|
3054
|
+
'Fill or replace only the named marker sections in that shell instead of rewriting the whole document.',
|
|
3055
|
+
renderProjectRequirementsSectionContract(options.ignoredSections),
|
|
3056
|
+
'For diagrams, write polished inline HTML/CSS or SVG directly in the report. Do not use Mermaid unless the user explicitly asks for Mermaid source.',
|
|
3057
|
+
'Use a light blue, white, and cool gray banking/financial visual style: conservative, dense, readable, and enterprise-grade.',
|
|
3058
|
+
'Prioritize API/interface-level business requirements. Every major interface should map to business capability, actor, trigger, inputs, outputs, rules, permissions, data reads/writes, errors, acceptance criteria, and evidence.',
|
|
3059
|
+
'Use EXTRACTED, INFERRED, and UNKNOWN labels. Preserve source evidence paths.',
|
|
3060
|
+
'Do not invent dates; use the report paths above.'
|
|
3061
|
+
].join('\n');
|
|
3062
|
+
|
|
3063
|
+
return [
|
|
3064
|
+
{
|
|
3065
|
+
title: '🧭 Map entry points and evidence sources',
|
|
3066
|
+
role: 'planner',
|
|
3067
|
+
task: [
|
|
3068
|
+
'Map project entry points and evidence sources before any report writing.',
|
|
3069
|
+
reportContract,
|
|
3070
|
+
'Inspect top-level docs, package manifests, route/command entry points, tests, and obvious interface files.',
|
|
3071
|
+
'Produce a concise evidence map grouped by docs, routes/commands, handlers, schemas, tests, configuration, storage, and operations.',
|
|
3072
|
+
'Include evidence paths and open questions. Do not write the final report.'
|
|
3073
|
+
].join('\n')
|
|
3074
|
+
},
|
|
3075
|
+
{
|
|
3076
|
+
title: '📚 Build API and interface inventory',
|
|
3077
|
+
role: 'planner',
|
|
3078
|
+
task: [
|
|
3079
|
+
'Build the canonical API/interface inventory using the evidence map.',
|
|
3080
|
+
reportContract,
|
|
3081
|
+
'Enumerate every major HTTP endpoint, CLI command, tool call, MCP/RPC handler, queue/scheduled job, exported SDK function, and user-facing workflow entry point.',
|
|
3082
|
+
'For each item include type, route/command/function, owner module, evidence path, likely actor, and whether it is EXTRACTED, INFERRED, or UNKNOWN.',
|
|
3083
|
+
'Do not write the final report.'
|
|
3084
|
+
].join('\n')
|
|
3085
|
+
},
|
|
3086
|
+
{
|
|
3087
|
+
title: '🧩 Decompose business requirements per API',
|
|
3088
|
+
role: 'advisor',
|
|
3089
|
+
task: [
|
|
3090
|
+
'Decompose business requirements for each major API/interface from the inventory.',
|
|
3091
|
+
reportContract,
|
|
3092
|
+
'For each interface capture business capability, actor, user goal, trigger, inputs, outputs, preconditions, main flow, alternate flows, business rules, acceptance criteria, and open questions.',
|
|
3093
|
+
'Keep findings API-centered rather than module-centered. Do not write the final report.'
|
|
3094
|
+
].join('\n')
|
|
3095
|
+
},
|
|
3096
|
+
{
|
|
3097
|
+
title: '🔐 Analyze validation, permissions, and compliance',
|
|
3098
|
+
role: 'advisor',
|
|
3099
|
+
task: [
|
|
3100
|
+
'Analyze validation, authorization, security, audit, and compliance implications per API/interface.',
|
|
3101
|
+
reportContract,
|
|
3102
|
+
'For each relevant interface identify validation rules, permission checks, sensitive data, audit/traceability needs, policy constraints, retry/rollback behavior, and UNKNOWN compliance gaps.',
|
|
3103
|
+
'Return requirement-ready findings with evidence paths. Do not write the final report.'
|
|
3104
|
+
].join('\n')
|
|
3105
|
+
},
|
|
3106
|
+
{
|
|
3107
|
+
title: '💾 Map data ownership and state changes',
|
|
3108
|
+
role: 'advisor',
|
|
3109
|
+
task: [
|
|
3110
|
+
'Map data ownership, storage paths, state transitions, and side effects per API/interface.',
|
|
3111
|
+
reportContract,
|
|
3112
|
+
'Identify data reads, data writes, config/session/memory/file/database ownership, lifecycle states, cache/index behavior, external dependencies, and operational side effects.',
|
|
3113
|
+
'Return requirement-ready findings with evidence paths. Do not write the final report.'
|
|
3114
|
+
].join('\n')
|
|
3115
|
+
},
|
|
3116
|
+
{
|
|
3117
|
+
title: '🔄 Connect user flows to API dependencies',
|
|
3118
|
+
role: 'advisor',
|
|
3119
|
+
task: [
|
|
3120
|
+
'Connect user-facing flows to the API/interface inventory and implementation dependencies.',
|
|
3121
|
+
reportContract,
|
|
3122
|
+
'Create flow-ready findings for core journeys, API dependency maps, sequence summaries, error paths, and cross-interface handoffs.',
|
|
3123
|
+
'Favor clear business process decomposition over broad architecture prose. Do not write the final report.'
|
|
3124
|
+
].join('\n')
|
|
3125
|
+
},
|
|
3126
|
+
{
|
|
3127
|
+
title: '🎨 Write banking-style requirements HTML report',
|
|
3128
|
+
role: 'coder',
|
|
3129
|
+
task: [
|
|
3130
|
+
'Create the final project requirements report from the accumulated plan context.',
|
|
3131
|
+
reportContract,
|
|
3132
|
+
'Follow the project-requirements skill instructions below exactly, including chunked HTML writing for medium/large reports.',
|
|
3133
|
+
'Use the blue/white/gray banking-style shell and produce polished inline HTML/CSS/SVG diagrams. Keep the report professional, light, and conservative.',
|
|
3134
|
+
'Organize the main requirements section primarily by API/interface business requirement cards.',
|
|
3135
|
+
'The final HTML must be self-contained and directly openable from disk.',
|
|
3136
|
+
'Write the primary report to the exact primary report path above. Create the companion Markdown only if useful.',
|
|
3137
|
+
'Skill instructions:',
|
|
3138
|
+
renderedSkillPrompt
|
|
3139
|
+
].join('\n\n')
|
|
3140
|
+
},
|
|
3141
|
+
{
|
|
3142
|
+
title: '🔎 Review API coverage and traceability',
|
|
3143
|
+
role: 'reviewer',
|
|
3144
|
+
task: [
|
|
3145
|
+
'Review the generated requirements report against the project-requirements contract and accumulated evidence.',
|
|
3146
|
+
reportContract,
|
|
3147
|
+
'Check that major APIs/interfaces are represented, business requirements are decomposed per API, evidence paths are present, inferred/unknown content is labeled, diagrams are visible as inline HTML/CSS/SVG without external rendering libraries, and the report path matches the required local date.',
|
|
3148
|
+
'Check that the visual style is light blue/white/gray and suitable for banking/financial review.',
|
|
3149
|
+
'Report concrete gaps and risks only. Do not rewrite the whole report.'
|
|
3150
|
+
].join('\n')
|
|
3151
|
+
},
|
|
3152
|
+
{
|
|
3153
|
+
title: '🧾 Summarize final report and unresolved questions',
|
|
3154
|
+
role: 'summarizer',
|
|
3155
|
+
task: [
|
|
3156
|
+
'Synthesize the project requirements pipeline results into a concise final status for the user.',
|
|
3157
|
+
reportContract,
|
|
3158
|
+
'Mention the generated report path, API/interface coverage, strongest business requirement findings, unresolved questions, what was not verified, and the best next action.',
|
|
3159
|
+
'Do not re-analyze the codebase unless the accumulated evidence is clearly insufficient.'
|
|
3160
|
+
].join('\n')
|
|
3161
|
+
}
|
|
3162
|
+
];
|
|
3163
|
+
}
|
|
3164
|
+
|
|
3165
|
+
function renderProjectRequirementsPlanMarkdown({ goal, steps, reportPath, companionPath }) {
|
|
3166
|
+
const autoPlan = {
|
|
3167
|
+
summary: 'Dedicated sub-agent pipeline for project requirements discovery and HTML report generation.',
|
|
3168
|
+
steps
|
|
3169
|
+
};
|
|
3170
|
+
const progressLines = steps
|
|
3171
|
+
.map((step, index) => `- [ ] Step ${index + 1} [${step.role}] ${step.title}`)
|
|
3172
|
+
.join('\n');
|
|
3173
|
+
return [
|
|
3174
|
+
`# Project Requirements Pipeline: ${goal}`,
|
|
3175
|
+
'',
|
|
3176
|
+
`Primary Report: ${reportPath}`,
|
|
3177
|
+
`Optional Companion: ${companionPath}`,
|
|
3178
|
+
'',
|
|
3179
|
+
renderAutoPlanMarkdown({
|
|
3180
|
+
goal,
|
|
3181
|
+
autoPlan,
|
|
3182
|
+
finalSummary: 'Project requirements pipeline created and will execute immediately.',
|
|
3183
|
+
approvalText: 'No approval required. Triggered explicitly by /project-requirements.',
|
|
3184
|
+
progressLine: progressLines
|
|
3185
|
+
})
|
|
3186
|
+
].join('\n');
|
|
3187
|
+
}
|
|
3188
|
+
|
|
3189
|
+
function replaceTemplateVariables(template, variables) {
|
|
3190
|
+
let out = String(template || '');
|
|
3191
|
+
for (const [key, value] of Object.entries(variables || {})) {
|
|
3192
|
+
out = out.replaceAll(`{{${key}}}`, String(value ?? ''));
|
|
3193
|
+
}
|
|
3194
|
+
return out;
|
|
3195
|
+
}
|
|
3196
|
+
|
|
3197
|
+
async function createProjectRequirementsShell({
|
|
3198
|
+
reportPath,
|
|
3199
|
+
companionPath,
|
|
3200
|
+
manifestPath,
|
|
3201
|
+
planFile,
|
|
3202
|
+
goal,
|
|
3203
|
+
steps
|
|
3204
|
+
}) {
|
|
3205
|
+
const workspaceRoot = process.cwd();
|
|
3206
|
+
const absoluteReportPath = path.resolve(workspaceRoot, reportPath);
|
|
3207
|
+
const absoluteManifestPath = path.resolve(workspaceRoot, manifestPath);
|
|
3208
|
+
await fs.mkdir(path.dirname(absoluteReportPath), { recursive: true });
|
|
3209
|
+
const template = await fs.readFile(PROJECT_REQUIREMENTS_TEMPLATE, 'utf8');
|
|
3210
|
+
const now = new Date().toISOString();
|
|
3211
|
+
const html = replaceTemplateVariables(template, {
|
|
3212
|
+
title: 'Project Requirements Report',
|
|
3213
|
+
workspace_name: path.basename(workspaceRoot) || workspaceRoot,
|
|
3214
|
+
date: formatLocalDate(),
|
|
3215
|
+
generated_at: now
|
|
3216
|
+
});
|
|
3217
|
+
await fs.writeFile(absoluteReportPath, html, 'utf8');
|
|
3218
|
+
|
|
3219
|
+
const sectionNames = [
|
|
3220
|
+
'summary',
|
|
3221
|
+
'architecture',
|
|
3222
|
+
'interfaces',
|
|
3223
|
+
'requirements',
|
|
3224
|
+
'flows',
|
|
3225
|
+
'domain',
|
|
3226
|
+
'security',
|
|
3227
|
+
'errors',
|
|
3228
|
+
'nonfunctional',
|
|
3229
|
+
'questions',
|
|
3230
|
+
'evidence'
|
|
3231
|
+
];
|
|
3232
|
+
const manifest = {
|
|
3233
|
+
status: 'running',
|
|
3234
|
+
goal,
|
|
3235
|
+
html: reportPath,
|
|
3236
|
+
markdown: companionPath,
|
|
3237
|
+
manifest: manifestPath,
|
|
3238
|
+
plan: planFile,
|
|
3239
|
+
createdAt: now,
|
|
3240
|
+
updatedAt: now,
|
|
3241
|
+
sections: Object.fromEntries(sectionNames.map((name) => [name, 'pending'])),
|
|
3242
|
+
steps: steps.map((step, index) => ({
|
|
3243
|
+
step: index + 1,
|
|
3244
|
+
role: step.role,
|
|
3245
|
+
title: step.title,
|
|
3246
|
+
status: 'pending'
|
|
3247
|
+
}))
|
|
3248
|
+
};
|
|
3249
|
+
await fs.writeFile(absoluteManifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
|
|
3250
|
+
return manifest;
|
|
3251
|
+
}
|
|
3252
|
+
|
|
3253
|
+
async function updateProjectRequirementsManifest(manifestPath, updates = {}) {
|
|
3254
|
+
if (!manifestPath) return;
|
|
3255
|
+
try {
|
|
3256
|
+
const absoluteManifestPath = path.resolve(process.cwd(), manifestPath);
|
|
3257
|
+
const current = JSON.parse(await fs.readFile(absoluteManifestPath, 'utf8'));
|
|
3258
|
+
const next = {
|
|
3259
|
+
...current,
|
|
3260
|
+
...updates,
|
|
3261
|
+
updatedAt: new Date().toISOString()
|
|
3262
|
+
};
|
|
3263
|
+
await fs.writeFile(absoluteManifestPath, `${JSON.stringify(next, null, 2)}\n`, 'utf8');
|
|
3264
|
+
} catch {
|
|
3265
|
+
// Manifest is best-effort; plan file and events remain the source of truth.
|
|
3266
|
+
}
|
|
3267
|
+
}
|
|
3268
|
+
|
|
3269
|
+
async function runProjectRequirementsPipeline({
|
|
3270
|
+
custom,
|
|
3271
|
+
parsedInput,
|
|
3272
|
+
currentSession,
|
|
3273
|
+
config,
|
|
3274
|
+
model,
|
|
3275
|
+
systemPrompt,
|
|
3276
|
+
onAgentEvent,
|
|
3277
|
+
signal,
|
|
3278
|
+
onSubSessionActive
|
|
3279
|
+
}) {
|
|
3280
|
+
const renderedSkillPrompt = await expandFileMentions(renderCommandPrompt(custom, parsedInput.args), process.cwd());
|
|
3281
|
+
const userFocus = parsedInput.args.join(' ').trim();
|
|
3282
|
+
const goal = userFocus ? `project requirements report: ${userFocus}` : 'project requirements report';
|
|
3283
|
+
const reportDate = formatLocalDate();
|
|
3284
|
+
const reportPath = `docs/requirements/${reportDate}-project-requirements.html`;
|
|
3285
|
+
const companionPath = `docs/requirements/${reportDate}-project-requirements.md`;
|
|
3286
|
+
const manifestPath = `docs/requirements/${reportDate}-project-requirements.manifest.json`;
|
|
3287
|
+
const steps = buildProjectRequirementsSteps(renderedSkillPrompt, parsedInput.args);
|
|
3288
|
+
const planFile = await writeMarkdownInProjectDir(
|
|
3289
|
+
'plans',
|
|
3290
|
+
'project-requirements-pipeline',
|
|
3291
|
+
renderProjectRequirementsPlanMarkdown({ goal, steps, reportPath, companionPath }),
|
|
3292
|
+
'project-requirements',
|
|
3293
|
+
currentSession.id
|
|
3294
|
+
);
|
|
3295
|
+
await createProjectRequirementsShell({
|
|
3296
|
+
reportPath,
|
|
3297
|
+
companionPath,
|
|
3298
|
+
manifestPath,
|
|
3299
|
+
planFile,
|
|
3300
|
+
goal,
|
|
3301
|
+
steps
|
|
3302
|
+
});
|
|
3303
|
+
const planState = {
|
|
3304
|
+
status: 'approved',
|
|
3305
|
+
source: 'project-requirements',
|
|
3306
|
+
goal,
|
|
3307
|
+
filePath: planFile,
|
|
3308
|
+
summary: 'Dedicated sub-agent pipeline for project requirements report generation.',
|
|
3309
|
+
finalSummary: 'Executing project requirements pipeline.',
|
|
3310
|
+
steps
|
|
3311
|
+
};
|
|
3312
|
+
if (onAgentEvent) {
|
|
3313
|
+
onAgentEvent({ type: 'skill:start', name: custom.name });
|
|
3314
|
+
onAgentEvent({
|
|
3315
|
+
type: 'plan:progress',
|
|
3316
|
+
planFile,
|
|
3317
|
+
reportPath,
|
|
3318
|
+
manifestPath,
|
|
3319
|
+
step: 0,
|
|
3320
|
+
total: steps.length,
|
|
3321
|
+
status: 'created',
|
|
3322
|
+
summary: 'Project requirements pipeline created'
|
|
3323
|
+
});
|
|
3324
|
+
}
|
|
3325
|
+
let execution;
|
|
3326
|
+
try {
|
|
3327
|
+
execution = await executePlanWithSubAgents({
|
|
3328
|
+
planState,
|
|
3329
|
+
parentSession: currentSession,
|
|
3330
|
+
config,
|
|
3331
|
+
model,
|
|
3332
|
+
systemPrompt,
|
|
3333
|
+
onAgentEvent,
|
|
3334
|
+
signal,
|
|
3335
|
+
onSubSessionActive
|
|
3336
|
+
});
|
|
3337
|
+
} catch (error) {
|
|
3338
|
+
if (onAgentEvent) {
|
|
3339
|
+
onAgentEvent({
|
|
3340
|
+
type: 'skill:error',
|
|
3341
|
+
name: custom.name,
|
|
3342
|
+
summary: error instanceof Error ? error.message : String(error)
|
|
3343
|
+
});
|
|
3344
|
+
}
|
|
3345
|
+
throw error;
|
|
3346
|
+
}
|
|
3347
|
+
if (onAgentEvent) {
|
|
3348
|
+
onAgentEvent({
|
|
3349
|
+
type: 'plan:progress',
|
|
3350
|
+
planFile,
|
|
3351
|
+
reportPath,
|
|
3352
|
+
manifestPath,
|
|
3353
|
+
step: steps.length,
|
|
3354
|
+
total: steps.length,
|
|
3355
|
+
status: execution.aborted ? 'aborted' : 'done',
|
|
3356
|
+
summary: 'Project requirements pipeline finished'
|
|
3357
|
+
});
|
|
3358
|
+
onAgentEvent({ type: 'skill:end', name: custom.name });
|
|
3359
|
+
}
|
|
3360
|
+
const failedCount = Array.isArray(execution.results)
|
|
3361
|
+
? execution.results.filter((item) => item.failed).length
|
|
3362
|
+
: 0;
|
|
3363
|
+
await updateProjectRequirementsManifest(manifestPath, {
|
|
3364
|
+
status: execution.aborted ? 'aborted' : failedCount > 0 ? 'failed' : 'completed',
|
|
3365
|
+
failedCount
|
|
3366
|
+
});
|
|
3367
|
+
const text = [
|
|
3368
|
+
execution.text || '',
|
|
3369
|
+
'',
|
|
3370
|
+
'Project requirements pipeline completed.',
|
|
3371
|
+
`Plan File: ${planFile}`,
|
|
3372
|
+
`Report Path: ${reportPath}`,
|
|
3373
|
+
`Manifest: ${manifestPath}`,
|
|
3374
|
+
`Steps: ${steps.length} total`,
|
|
3375
|
+
`Failed: ${failedCount}`
|
|
3376
|
+
]
|
|
3377
|
+
.filter(Boolean)
|
|
3378
|
+
.join('\n');
|
|
3379
|
+
return {
|
|
3380
|
+
type: 'assistant',
|
|
3381
|
+
text,
|
|
3382
|
+
planFile,
|
|
3383
|
+
reportPath,
|
|
3384
|
+
manifestPath,
|
|
3385
|
+
aborted: !!execution.aborted
|
|
3386
|
+
};
|
|
3387
|
+
}
|
|
3388
|
+
|
|
2971
3389
|
async function revisePendingPlanWithModel({
|
|
2972
3390
|
planState,
|
|
2973
3391
|
feedback,
|
|
@@ -4541,6 +4959,23 @@ export async function createChatRuntime({
|
|
|
4541
4959
|
if (custom.metadata.type === 'skill' && !isSkillEnabled(config, custom.name, custom)) {
|
|
4542
4960
|
return { type: 'system', text: `Skill is disabled: ${custom.name}` };
|
|
4543
4961
|
}
|
|
4962
|
+
if (custom.metadata.type === 'skill' && custom.name === 'project-requirements') {
|
|
4963
|
+
try {
|
|
4964
|
+
return await runProjectRequirementsPipeline({
|
|
4965
|
+
custom,
|
|
4966
|
+
parsedInput,
|
|
4967
|
+
currentSession,
|
|
4968
|
+
config,
|
|
4969
|
+
model,
|
|
4970
|
+
systemPrompt: activeReplySystemPrompt,
|
|
4971
|
+
onAgentEvent,
|
|
4972
|
+
signal,
|
|
4973
|
+
onSubSessionActive: (sub) => { activeSubSession = sub; }
|
|
4974
|
+
});
|
|
4975
|
+
} finally {
|
|
4976
|
+
activeSubSession = null;
|
|
4977
|
+
}
|
|
4978
|
+
}
|
|
4544
4979
|
|
|
4545
4980
|
const customPrompt =
|
|
4546
4981
|
custom.name === 'brainstorm'
|
|
@@ -178,6 +178,14 @@ function loadInstalledSkillsFromRegistry(baseDir, registry, out) {
|
|
|
178
178
|
}
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
+
export function formatLocalDate(date = new Date()) {
|
|
182
|
+
const value = date instanceof Date ? date : new Date(date);
|
|
183
|
+
const year = value.getFullYear();
|
|
184
|
+
const month = String(value.getMonth() + 1).padStart(2, '0');
|
|
185
|
+
const day = String(value.getDate()).padStart(2, '0');
|
|
186
|
+
return `${year}-${month}-${day}`;
|
|
187
|
+
}
|
|
188
|
+
|
|
181
189
|
function substituteVariables(text, args = []) {
|
|
182
190
|
let out = text;
|
|
183
191
|
args.forEach((arg, index) => {
|
|
@@ -185,6 +193,7 @@ function substituteVariables(text, args = []) {
|
|
|
185
193
|
});
|
|
186
194
|
out = out.replaceAll('{{args}}', args.join(' '));
|
|
187
195
|
out = out.replaceAll('{{cwd}}', process.cwd());
|
|
196
|
+
out = out.replaceAll('{{date}}', formatLocalDate());
|
|
188
197
|
return out;
|
|
189
198
|
}
|
|
190
199
|
|
package/src/core/fff-adapter.js
CHANGED
package/src/tui/chat-app.js
CHANGED
|
@@ -1386,6 +1386,7 @@ export function shouldRefreshRuntimeStateForEvent(event) {
|
|
|
1386
1386
|
type === 'assistant:delta' ||
|
|
1387
1387
|
type === 'assistant:response' ||
|
|
1388
1388
|
type === 'tool:result' ||
|
|
1389
|
+
type === 'plan:progress' ||
|
|
1389
1390
|
type === 'compact:auto' ||
|
|
1390
1391
|
type === 'dream:auto' ||
|
|
1391
1392
|
type === 'dream:complete'
|
|
@@ -4182,13 +4183,6 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
|
|
|
4182
4183
|
setRuntimeStatus(makeStatus(copy.runtime.toolBlocked, detail, 'redBright'));
|
|
4183
4184
|
setInputStage('thinking');
|
|
4184
4185
|
setActiveAssistantMeta({ loading: true, phase: 'thinking', liveStatus: copy.toolActivity.waitingModelAdjust(detail) });
|
|
4185
|
-
setPlanState((prev) => ({
|
|
4186
|
-
...prev,
|
|
4187
|
-
failed: prev.total > 0,
|
|
4188
|
-
steps: (prev.steps || []).map((step) =>
|
|
4189
|
-
step.index === prev.current ? { ...step, status: 'failed' } : step
|
|
4190
|
-
)
|
|
4191
|
-
}));
|
|
4192
4186
|
updateActivityStatusOnActiveAssistant({
|
|
4193
4187
|
type: 'tool',
|
|
4194
4188
|
id: event.id,
|
|
@@ -4202,13 +4196,6 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
|
|
|
4202
4196
|
setRuntimeStatus(makeStatus(copy.runtime.toolFailed, event.summary || detail, 'redBright'));
|
|
4203
4197
|
setInputStage('thinking');
|
|
4204
4198
|
setActiveAssistantMeta({ loading: true, phase: 'thinking', liveStatus: copy.toolActivity.waitingModelAdjust(detail) });
|
|
4205
|
-
setPlanState((prev) => ({
|
|
4206
|
-
...prev,
|
|
4207
|
-
failed: prev.total > 0,
|
|
4208
|
-
steps: (prev.steps || []).map((step) =>
|
|
4209
|
-
step.index === prev.current ? { ...step, status: 'failed' } : step
|
|
4210
|
-
)
|
|
4211
|
-
}));
|
|
4212
4199
|
updateActivityStatusOnActiveAssistant({
|
|
4213
4200
|
type: 'tool',
|
|
4214
4201
|
id: event.id,
|
|
@@ -4286,6 +4273,52 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
|
|
|
4286
4273
|
}));
|
|
4287
4274
|
}
|
|
4288
4275
|
}
|
|
4276
|
+
if (event?.type === 'plan:progress') {
|
|
4277
|
+
const current = Number(event.step || 0);
|
|
4278
|
+
const total = Number(event.total || 0);
|
|
4279
|
+
const status = String(event.status || '').trim().toLowerCase();
|
|
4280
|
+
if (current > 0 && total > 0) {
|
|
4281
|
+
const role = String(event.role || '').trim().toLowerCase();
|
|
4282
|
+
const normalizedRole = PLAN_AGENT_ROLES.has(role) ? role : 'coder';
|
|
4283
|
+
const title = String(event.title || '').trim();
|
|
4284
|
+
setPlanState((prev) => {
|
|
4285
|
+
const existingSteps = Array.isArray(prev.steps) ? prev.steps : [];
|
|
4286
|
+
const merged = existingSteps.some((step) => step.index === current)
|
|
4287
|
+
? existingSteps.map((step) =>
|
|
4288
|
+
step.index === current
|
|
4289
|
+
? {
|
|
4290
|
+
...step,
|
|
4291
|
+
total,
|
|
4292
|
+
role: event.role || step.role || normalizedRole,
|
|
4293
|
+
title: title || step.title || '',
|
|
4294
|
+
status: status === 'failed' ? 'failed' : status === 'done' ? 'done' : status === 'running' ? 'active' : step.status
|
|
4295
|
+
}
|
|
4296
|
+
: step
|
|
4297
|
+
)
|
|
4298
|
+
: [
|
|
4299
|
+
...existingSteps,
|
|
4300
|
+
{
|
|
4301
|
+
index: current,
|
|
4302
|
+
total,
|
|
4303
|
+
role: event.role || normalizedRole,
|
|
4304
|
+
title,
|
|
4305
|
+
status: status === 'failed' ? 'failed' : status === 'done' ? 'done' : 'active'
|
|
4306
|
+
}
|
|
4307
|
+
];
|
|
4308
|
+
return {
|
|
4309
|
+
...prev,
|
|
4310
|
+
current,
|
|
4311
|
+
total,
|
|
4312
|
+
role: event.role || prev.role || normalizedRole,
|
|
4313
|
+
title: title || prev.title || '',
|
|
4314
|
+
failed: status === 'failed' ? true : prev.failed,
|
|
4315
|
+
completed: status === 'done' && current === total && !prev.failed,
|
|
4316
|
+
pendingApproval: false,
|
|
4317
|
+
steps: merged.sort((a, b) => a.index - b.index)
|
|
4318
|
+
};
|
|
4319
|
+
});
|
|
4320
|
+
}
|
|
4321
|
+
}
|
|
4289
4322
|
if (event?.type === 'skill:start') {
|
|
4290
4323
|
ensureActiveAssistant();
|
|
4291
4324
|
const detail = describeSkillActivity(event.name, copy);
|