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.
@@ -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
 
@@ -111,7 +111,7 @@ class FffMcpClient {
111
111
  capabilities: {},
112
112
  clientInfo: {
113
113
  name: 'codemini-cli',
114
- version: '0.4.4'
114
+ version: '0.4.6'
115
115
  }
116
116
  });
117
117
  this.sendNotification('notifications/initialized', {});
@@ -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);