codemini-cli 0.4.3 → 0.4.5

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 CHANGED
@@ -13,13 +13,13 @@ npm pack
13
13
  Expected output:
14
14
 
15
15
  ```text
16
- codemini-cli-0.4.3.tgz
16
+ codemini-cli-0.4.5.tgz
17
17
  ```
18
18
 
19
19
  If you want to verify the package contents:
20
20
 
21
21
  ```bash
22
- tar -tf codemini-cli-0.4.3.tgz
22
+ tar -tf codemini-cli-0.4.5.tgz
23
23
  ```
24
24
 
25
25
  ## 2. Copy To The Target Machine
@@ -34,7 +34,7 @@ Copy the generated `.tgz` file to the Win10 machine by one of these methods:
34
34
  Recommended target path:
35
35
 
36
36
  ```powershell
37
- C:\temp\codemini-cli-0.4.3.tgz
37
+ C:\temp\codemini-cli-0.4.5.tgz
38
38
  ```
39
39
 
40
40
  ## 3. Environment Requirements
@@ -58,7 +58,7 @@ npm -v
58
58
  Global install:
59
59
 
60
60
  ```powershell
61
- npm install -g C:\temp\codemini-cli-0.4.3.tgz
61
+ npm install -g C:\temp\codemini-cli-0.4.5.tgz
62
62
  ```
63
63
 
64
64
  If global install is blocked by company policy, install in a working directory instead:
@@ -66,7 +66,7 @@ If global install is blocked by company policy, install in a working directory i
66
66
  ```powershell
67
67
  mkdir C:\temp\coder-test
68
68
  cd C:\temp\coder-test
69
- npm install C:\temp\codemini-cli-0.4.3.tgz
69
+ npm install C:\temp\codemini-cli-0.4.5.tgz
70
70
  ```
71
71
 
72
72
  ## 5. Confirm Installation
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codemini-cli",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
4
4
  "description": "Coding CLI optimized for small-model workflows and Windows PowerShell",
5
5
  "keywords": [
6
6
  "cli",
@@ -21,17 +21,19 @@ Honor any concrete user request above, such as output format, report path, focus
21
21
  Create the primary report at:
22
22
 
23
23
  ```text
24
- docs/requirements/YYYY-MM-DD-project-requirements.html
24
+ docs/requirements/{{date}}-project-requirements.html
25
25
  ```
26
26
 
27
27
  If a companion Markdown file is useful, create:
28
28
 
29
29
  ```text
30
- docs/requirements/YYYY-MM-DD-project-requirements.md
30
+ docs/requirements/{{date}}-project-requirements.md
31
31
  ```
32
32
 
33
33
  The HTML should be self-contained: inline CSS, inline JavaScript, no build step, no required external assets.
34
34
 
35
+ When the target HTML file already exists and contains `REQUIREMENTS_*` marker sections, treat it as the canonical report shell. Edit those marker sections in place instead of replacing the whole file. Preserve the existing CSS, JavaScript, navigation, metadata, and surrounding structure unless the user explicitly asks to redesign the shell.
36
+
35
37
  Diagrams must be visible when the HTML is opened directly from disk:
36
38
 
37
39
  - Prefer inline SVG for architecture maps, dependency graphs, sequence summaries, and state diagrams.
@@ -42,6 +42,72 @@ function parseChatArgs(args) {
42
42
  return parsed;
43
43
  }
44
44
 
45
+ export async function submitAndPrint(runtime, line, { output: out = process.stdout } = {}) {
46
+ let streamed = false;
47
+ let atLineStart = true;
48
+ const write = (text) => {
49
+ const value = String(text || '');
50
+ if (!value) return;
51
+ out.write(value);
52
+ atLineStart = value.endsWith('\n');
53
+ };
54
+ const writeActivity = (event, label) => {
55
+ const name = String(event?.name || '').trim();
56
+ if (!name) return;
57
+ const summary = String(event?.summary || '').trim();
58
+ if (!atLineStart) write('\n');
59
+ write(`[${label}] ${name}${summary ? ` - ${summary}` : ''}\n`);
60
+ };
61
+ const result = await runtime.submit(line, (event) => {
62
+ if (event?.type === 'assistant:delta' && event.text) {
63
+ streamed = true;
64
+ write(event.text);
65
+ return;
66
+ }
67
+ if (event?.type === 'tool:start') {
68
+ streamed = true;
69
+ writeActivity(event, 'tool:start');
70
+ return;
71
+ }
72
+ if (event?.type === 'tool:end') {
73
+ streamed = true;
74
+ writeActivity(event, 'tool:end');
75
+ return;
76
+ }
77
+ if (event?.type === 'tool:blocked') {
78
+ streamed = true;
79
+ writeActivity(event, 'tool:blocked');
80
+ return;
81
+ }
82
+ if (event?.type === 'tool:error') {
83
+ streamed = true;
84
+ writeActivity(event, 'tool:error');
85
+ return;
86
+ }
87
+ if (event?.type === 'system_tool:start') {
88
+ streamed = true;
89
+ writeActivity(event, 'system:start');
90
+ return;
91
+ }
92
+ if (event?.type === 'system_tool:end') {
93
+ streamed = true;
94
+ writeActivity(event, 'system:end');
95
+ return;
96
+ }
97
+ if (event?.type === 'system_tool:error') {
98
+ streamed = true;
99
+ writeActivity(event, 'system:error');
100
+ }
101
+ });
102
+ if (result.type === 'exit' || result.type === 'noop') return result;
103
+ if (streamed) {
104
+ if (!atLineStart) write('\n');
105
+ return result;
106
+ }
107
+ if (result.text) write(`${result.text}\n`);
108
+ return result;
109
+ }
110
+
45
111
  async function runPlainLoop(runtime) {
46
112
  console.log('CodeMini CLI plain mode. Use /help and /exit.');
47
113
  const rl = readline.createInterface({ input, output });
@@ -53,10 +119,8 @@ async function runPlainLoop(runtime) {
53
119
  } catch {
54
120
  break;
55
121
  }
56
- const result = await runtime.submit(line);
122
+ const result = await submitAndPrint(runtime, line, { output });
57
123
  if (result.type === 'exit') break;
58
- if (result.type === 'noop') continue;
59
- if (result.text) console.log(result.text);
60
124
  }
61
125
  } finally {
62
126
  rl.close();
@@ -80,8 +144,7 @@ export async function handleChat(args) {
80
144
 
81
145
  try {
82
146
  if (parsed.prompt) {
83
- const result = await runtime.submit(parsed.prompt);
84
- if (result.text) console.log(result.text);
147
+ await submitAndPrint(runtime, parsed.prompt, { output: process.stdout });
85
148
  return;
86
149
  }
87
150
 
@@ -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,7 +315,7 @@ 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;
@@ -395,6 +398,7 @@ export function getSubAgentRolePrompt(role) {
395
398
  'You are the summarizer in a multi-step agent pipeline.',
396
399
  'Your job is to synthesize the results of all prior steps into a concise, actionable final summary.',
397
400
  'Do NOT re-analyze the codebase or make new tool calls unless the handed-off evidence is clearly insufficient.',
401
+ 'You may read handed-off artifact files, such as generated reports, when needed to summarize or verify their existence.',
398
402
  'Instead, read the accumulated step results in the plan file context provided to you.',
399
403
  'Output format — keep it short and direct:',
400
404
  'Summary:',
@@ -2708,6 +2712,16 @@ async function executePlanWithSubAgents({
2708
2712
  const step = steps[i];
2709
2713
  if (signal?.aborted) break;
2710
2714
 
2715
+ emitPlanEvent({
2716
+ type: 'plan:progress',
2717
+ planFile: planFilePath,
2718
+ step: i + 1,
2719
+ total: steps.length,
2720
+ role: step.role,
2721
+ title: step.title,
2722
+ status: 'running'
2723
+ });
2724
+
2711
2725
  emitPlanEvent({
2712
2726
  type: 'assistant:delta',
2713
2727
  text: `\n[plan] Step ${i + 1}/${steps.length} -> ${step.role}: ${step.title}\n`
@@ -2771,6 +2785,17 @@ async function executePlanWithSubAgents({
2771
2785
  );
2772
2786
  }
2773
2787
 
2788
+ emitPlanEvent({
2789
+ type: 'plan:progress',
2790
+ planFile: planFilePath,
2791
+ step: i + 1,
2792
+ total: steps.length,
2793
+ role: step.role,
2794
+ title: step.title,
2795
+ status: stepRecord.failed ? 'failed' : 'done',
2796
+ summary: stepRecord.failed ? stepRecord.failureReason : trimInline(stepRecord.output, 160)
2797
+ });
2798
+
2774
2799
  if (stepRecord.failed && i < steps.length - 1) {
2775
2800
  const summarizerIndex = steps.findIndex((candidate, index) => index > i && candidate.role === 'summarizer');
2776
2801
  if (summarizerIndex > i) {
@@ -2968,6 +2993,313 @@ function renderAutoPlanMarkdown({
2968
2993
  return lines.join('\n');
2969
2994
  }
2970
2995
 
2996
+ function buildProjectRequirementsSteps(renderedSkillPrompt, args = []) {
2997
+ const userArgs = args.join(' ').trim();
2998
+ const requestedFocus = userArgs ? `User request/focus: ${userArgs}` : 'User request/focus: full workspace requirements report.';
2999
+ const reportDate = formatLocalDate();
3000
+ const reportPath = `docs/requirements/${reportDate}-project-requirements.html`;
3001
+ const companionPath = `docs/requirements/${reportDate}-project-requirements.md`;
3002
+ const reportContract = [
3003
+ requestedFocus,
3004
+ `Primary report path: ${reportPath}`,
3005
+ `Optional companion Markdown path: ${companionPath}`,
3006
+ 'A pre-created HTML shell already exists at the primary report path.',
3007
+ 'Fill or replace only the named marker sections in that shell instead of rewriting the whole document.',
3008
+ 'Required marker sections: REQUIREMENTS_SUMMARY, REQUIREMENTS_ARCHITECTURE, REQUIREMENTS_INTERFACE_INVENTORY, REQUIREMENTS_API_CARDS, REQUIREMENTS_FLOWS, REQUIREMENTS_SECURITY, REQUIREMENTS_NONFUNCTIONAL, REQUIREMENTS_OPEN_QUESTIONS, REQUIREMENTS_EVIDENCE_INDEX.',
3009
+ 'Use EXTRACTED, INFERRED, and UNKNOWN labels. Preserve source evidence paths.',
3010
+ 'Do not invent dates; use the report paths above.'
3011
+ ].join('\n');
3012
+
3013
+ return [
3014
+ {
3015
+ title: 'Map project interfaces and evidence',
3016
+ role: 'planner',
3017
+ task: [
3018
+ 'Map project interfaces and evidence before any report writing.',
3019
+ reportContract,
3020
+ 'Inspect top-level docs, package manifests, route/command entry points, tests, and obvious interface files.',
3021
+ 'Produce a concise interface inventory grouped by CLI commands, HTTP/API/RPC surfaces, tools, storage/config, UI flows, and operations.',
3022
+ 'Include evidence paths and open questions. Do not write the final report.'
3023
+ ].join('\n')
3024
+ },
3025
+ {
3026
+ title: 'Analyze runtime, tools, and providers',
3027
+ role: 'advisor',
3028
+ task: [
3029
+ 'Analyze the core execution layer and tool/provider surfaces using the prior planner inventory.',
3030
+ reportContract,
3031
+ 'Focus on runtime flow, agent loop, built-in/deferred tools, provider streaming/tool-call behavior, sessions, memory, and plan state.',
3032
+ 'Return requirement-ready findings with evidence paths, inferred requirements, edge cases, and unknowns. Do not write the final report.'
3033
+ ].join('\n')
3034
+ },
3035
+ {
3036
+ title: 'Analyze product flows, storage, security, and operations',
3037
+ role: 'advisor',
3038
+ task: [
3039
+ 'Analyze user-facing workflows and non-functional requirements using the accumulated plan context.',
3040
+ reportContract,
3041
+ 'Cover core product journeys, persistence paths, configuration, security/policy behavior, deployment/operations notes, error handling, and acceptance criteria.',
3042
+ 'Return requirement-ready findings with evidence paths, inferred requirements, edge cases, and unknowns. Do not write the final report.'
3043
+ ].join('\n')
3044
+ },
3045
+ {
3046
+ title: 'Write requirements HTML report',
3047
+ role: 'coder',
3048
+ task: [
3049
+ 'Create the final project requirements report from the accumulated plan context.',
3050
+ reportContract,
3051
+ 'Follow the project-requirements skill instructions below exactly, including chunked HTML writing for medium/large reports.',
3052
+ 'The final HTML must be self-contained and directly openable from disk.',
3053
+ 'Write the primary report to the exact primary report path above. Create the companion Markdown only if useful.',
3054
+ 'Skill instructions:',
3055
+ renderedSkillPrompt
3056
+ ].join('\n\n')
3057
+ },
3058
+ {
3059
+ title: 'Review report coverage and traceability',
3060
+ role: 'reviewer',
3061
+ task: [
3062
+ 'Review the generated requirements report against the project-requirements contract and accumulated evidence.',
3063
+ reportContract,
3064
+ 'Check that major interfaces are represented, evidence paths are present, inferred/unknown content is labeled, diagrams are visible without Mermaid as the only renderer, and the report path matches the required local date.',
3065
+ 'Report concrete gaps and risks only. Do not rewrite the whole report.'
3066
+ ].join('\n')
3067
+ },
3068
+ {
3069
+ title: 'Summarize final requirements report',
3070
+ role: 'summarizer',
3071
+ task: [
3072
+ 'Synthesize the project requirements pipeline results into a concise final status for the user.',
3073
+ reportContract,
3074
+ 'Mention the generated report path, what was covered, what was not verified, and the best next action.',
3075
+ 'Do not re-analyze the codebase unless the accumulated evidence is clearly insufficient.'
3076
+ ].join('\n')
3077
+ }
3078
+ ];
3079
+ }
3080
+
3081
+ function renderProjectRequirementsPlanMarkdown({ goal, steps, reportPath, companionPath }) {
3082
+ const autoPlan = {
3083
+ summary: 'Dedicated sub-agent pipeline for project requirements discovery and HTML report generation.',
3084
+ steps
3085
+ };
3086
+ const progressLines = steps
3087
+ .map((step, index) => `- [ ] Step ${index + 1} [${step.role}] ${step.title}`)
3088
+ .join('\n');
3089
+ return [
3090
+ `# Project Requirements Pipeline: ${goal}`,
3091
+ '',
3092
+ `Primary Report: ${reportPath}`,
3093
+ `Optional Companion: ${companionPath}`,
3094
+ '',
3095
+ renderAutoPlanMarkdown({
3096
+ goal,
3097
+ autoPlan,
3098
+ finalSummary: 'Project requirements pipeline created and will execute immediately.',
3099
+ approvalText: 'No approval required. Triggered explicitly by /project-requirements.',
3100
+ progressLine: progressLines
3101
+ })
3102
+ ].join('\n');
3103
+ }
3104
+
3105
+ function replaceTemplateVariables(template, variables) {
3106
+ let out = String(template || '');
3107
+ for (const [key, value] of Object.entries(variables || {})) {
3108
+ out = out.replaceAll(`{{${key}}}`, String(value ?? ''));
3109
+ }
3110
+ return out;
3111
+ }
3112
+
3113
+ async function createProjectRequirementsShell({
3114
+ reportPath,
3115
+ companionPath,
3116
+ manifestPath,
3117
+ planFile,
3118
+ goal,
3119
+ steps
3120
+ }) {
3121
+ const workspaceRoot = process.cwd();
3122
+ const absoluteReportPath = path.resolve(workspaceRoot, reportPath);
3123
+ const absoluteManifestPath = path.resolve(workspaceRoot, manifestPath);
3124
+ await fs.mkdir(path.dirname(absoluteReportPath), { recursive: true });
3125
+ const template = await fs.readFile(PROJECT_REQUIREMENTS_TEMPLATE, 'utf8');
3126
+ const now = new Date().toISOString();
3127
+ const html = replaceTemplateVariables(template, {
3128
+ title: 'Project Requirements Report',
3129
+ workspace_name: path.basename(workspaceRoot) || workspaceRoot,
3130
+ date: formatLocalDate(),
3131
+ generated_at: now
3132
+ });
3133
+ await fs.writeFile(absoluteReportPath, html, 'utf8');
3134
+
3135
+ const sectionNames = [
3136
+ 'summary',
3137
+ 'architecture',
3138
+ 'interfaces',
3139
+ 'requirements',
3140
+ 'flows',
3141
+ 'security',
3142
+ 'nonfunctional',
3143
+ 'questions',
3144
+ 'evidence'
3145
+ ];
3146
+ const manifest = {
3147
+ status: 'running',
3148
+ goal,
3149
+ html: reportPath,
3150
+ markdown: companionPath,
3151
+ manifest: manifestPath,
3152
+ plan: planFile,
3153
+ createdAt: now,
3154
+ updatedAt: now,
3155
+ sections: Object.fromEntries(sectionNames.map((name) => [name, 'pending'])),
3156
+ steps: steps.map((step, index) => ({
3157
+ step: index + 1,
3158
+ role: step.role,
3159
+ title: step.title,
3160
+ status: 'pending'
3161
+ }))
3162
+ };
3163
+ await fs.writeFile(absoluteManifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
3164
+ return manifest;
3165
+ }
3166
+
3167
+ async function updateProjectRequirementsManifest(manifestPath, updates = {}) {
3168
+ if (!manifestPath) return;
3169
+ try {
3170
+ const absoluteManifestPath = path.resolve(process.cwd(), manifestPath);
3171
+ const current = JSON.parse(await fs.readFile(absoluteManifestPath, 'utf8'));
3172
+ const next = {
3173
+ ...current,
3174
+ ...updates,
3175
+ updatedAt: new Date().toISOString()
3176
+ };
3177
+ await fs.writeFile(absoluteManifestPath, `${JSON.stringify(next, null, 2)}\n`, 'utf8');
3178
+ } catch {
3179
+ // Manifest is best-effort; plan file and events remain the source of truth.
3180
+ }
3181
+ }
3182
+
3183
+ async function runProjectRequirementsPipeline({
3184
+ custom,
3185
+ parsedInput,
3186
+ currentSession,
3187
+ config,
3188
+ model,
3189
+ systemPrompt,
3190
+ onAgentEvent,
3191
+ signal,
3192
+ onSubSessionActive
3193
+ }) {
3194
+ const renderedSkillPrompt = await expandFileMentions(renderCommandPrompt(custom, parsedInput.args), process.cwd());
3195
+ const userFocus = parsedInput.args.join(' ').trim();
3196
+ const goal = userFocus ? `project requirements report: ${userFocus}` : 'project requirements report';
3197
+ const reportDate = formatLocalDate();
3198
+ const reportPath = `docs/requirements/${reportDate}-project-requirements.html`;
3199
+ const companionPath = `docs/requirements/${reportDate}-project-requirements.md`;
3200
+ const manifestPath = `docs/requirements/${reportDate}-project-requirements.manifest.json`;
3201
+ const steps = buildProjectRequirementsSteps(renderedSkillPrompt, parsedInput.args);
3202
+ const planFile = await writeMarkdownInProjectDir(
3203
+ 'plans',
3204
+ 'project-requirements-pipeline',
3205
+ renderProjectRequirementsPlanMarkdown({ goal, steps, reportPath, companionPath }),
3206
+ 'project-requirements',
3207
+ currentSession.id
3208
+ );
3209
+ await createProjectRequirementsShell({
3210
+ reportPath,
3211
+ companionPath,
3212
+ manifestPath,
3213
+ planFile,
3214
+ goal,
3215
+ steps
3216
+ });
3217
+ const planState = {
3218
+ status: 'approved',
3219
+ source: 'project-requirements',
3220
+ goal,
3221
+ filePath: planFile,
3222
+ summary: 'Dedicated sub-agent pipeline for project requirements report generation.',
3223
+ finalSummary: 'Executing project requirements pipeline.',
3224
+ steps
3225
+ };
3226
+ if (onAgentEvent) {
3227
+ onAgentEvent({ type: 'skill:start', name: custom.name });
3228
+ onAgentEvent({
3229
+ type: 'plan:progress',
3230
+ planFile,
3231
+ reportPath,
3232
+ manifestPath,
3233
+ step: 0,
3234
+ total: steps.length,
3235
+ status: 'created',
3236
+ summary: 'Project requirements pipeline created'
3237
+ });
3238
+ }
3239
+ let execution;
3240
+ try {
3241
+ execution = await executePlanWithSubAgents({
3242
+ planState,
3243
+ parentSession: currentSession,
3244
+ config,
3245
+ model,
3246
+ systemPrompt,
3247
+ onAgentEvent,
3248
+ signal,
3249
+ onSubSessionActive
3250
+ });
3251
+ } catch (error) {
3252
+ if (onAgentEvent) {
3253
+ onAgentEvent({
3254
+ type: 'skill:error',
3255
+ name: custom.name,
3256
+ summary: error instanceof Error ? error.message : String(error)
3257
+ });
3258
+ }
3259
+ throw error;
3260
+ }
3261
+ if (onAgentEvent) {
3262
+ onAgentEvent({
3263
+ type: 'plan:progress',
3264
+ planFile,
3265
+ reportPath,
3266
+ manifestPath,
3267
+ step: steps.length,
3268
+ total: steps.length,
3269
+ status: execution.aborted ? 'aborted' : 'done',
3270
+ summary: 'Project requirements pipeline finished'
3271
+ });
3272
+ onAgentEvent({ type: 'skill:end', name: custom.name });
3273
+ }
3274
+ const failedCount = Array.isArray(execution.results)
3275
+ ? execution.results.filter((item) => item.failed).length
3276
+ : 0;
3277
+ await updateProjectRequirementsManifest(manifestPath, {
3278
+ status: execution.aborted ? 'aborted' : failedCount > 0 ? 'failed' : 'completed',
3279
+ failedCount
3280
+ });
3281
+ const text = [
3282
+ execution.text || '',
3283
+ '',
3284
+ 'Project requirements pipeline completed.',
3285
+ `Plan File: ${planFile}`,
3286
+ `Report Path: ${reportPath}`,
3287
+ `Manifest: ${manifestPath}`,
3288
+ `Steps: ${steps.length} total`,
3289
+ `Failed: ${failedCount}`
3290
+ ]
3291
+ .filter(Boolean)
3292
+ .join('\n');
3293
+ return {
3294
+ type: 'assistant',
3295
+ text,
3296
+ planFile,
3297
+ reportPath,
3298
+ manifestPath,
3299
+ aborted: !!execution.aborted
3300
+ };
3301
+ }
3302
+
2971
3303
  async function revisePendingPlanWithModel({
2972
3304
  planState,
2973
3305
  feedback,
@@ -4541,6 +4873,23 @@ export async function createChatRuntime({
4541
4873
  if (custom.metadata.type === 'skill' && !isSkillEnabled(config, custom.name, custom)) {
4542
4874
  return { type: 'system', text: `Skill is disabled: ${custom.name}` };
4543
4875
  }
4876
+ if (custom.metadata.type === 'skill' && custom.name === 'project-requirements') {
4877
+ try {
4878
+ return await runProjectRequirementsPipeline({
4879
+ custom,
4880
+ parsedInput,
4881
+ currentSession,
4882
+ config,
4883
+ model,
4884
+ systemPrompt: activeReplySystemPrompt,
4885
+ onAgentEvent,
4886
+ signal,
4887
+ onSubSessionActive: (sub) => { activeSubSession = sub; }
4888
+ });
4889
+ } finally {
4890
+ activeSubSession = null;
4891
+ }
4892
+ }
4544
4893
 
4545
4894
  const customPrompt =
4546
4895
  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.3'
114
+ version: '0.4.5'
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);
@@ -0,0 +1,178 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>{{title}}</title>
7
+ <style>
8
+ :root {
9
+ color-scheme: light dark;
10
+ --bg: #f7f8fb;
11
+ --panel: #ffffff;
12
+ --text: #182033;
13
+ --muted: #657086;
14
+ --line: #d9deea;
15
+ --accent: #2563eb;
16
+ --ok: #147a4a;
17
+ --warn: #a16207;
18
+ --unknown: #7c3aed;
19
+ }
20
+ @media (prefers-color-scheme: dark) {
21
+ :root {
22
+ --bg: #111521;
23
+ --panel: #181d2b;
24
+ --text: #edf2ff;
25
+ --muted: #a6afc3;
26
+ --line: #30384f;
27
+ --accent: #7aa2ff;
28
+ }
29
+ }
30
+ * { box-sizing: border-box; }
31
+ body {
32
+ margin: 0;
33
+ background: var(--bg);
34
+ color: var(--text);
35
+ font: 15px/1.55 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
36
+ }
37
+ header {
38
+ padding: 32px min(5vw, 56px) 22px;
39
+ border-bottom: 1px solid var(--line);
40
+ background: var(--panel);
41
+ }
42
+ h1, h2, h3 { line-height: 1.2; }
43
+ h1 { margin: 0 0 10px; font-size: clamp(28px, 4vw, 44px); }
44
+ h2 { margin: 36px 0 14px; font-size: 24px; }
45
+ h3 { margin: 24px 0 10px; font-size: 18px; }
46
+ a { color: var(--accent); }
47
+ .meta { color: var(--muted); display: flex; gap: 16px; flex-wrap: wrap; }
48
+ .layout { display: grid; grid-template-columns: minmax(180px, 260px) minmax(0, 1fr); gap: 28px; padding: 24px min(5vw, 56px) 56px; }
49
+ nav { position: sticky; top: 16px; align-self: start; }
50
+ nav a { display: block; padding: 8px 0; text-decoration: none; border-bottom: 1px solid color-mix(in srgb, var(--line), transparent 45%); }
51
+ main { max-width: 1180px; }
52
+ section { padding: 2px 0 18px; border-bottom: 1px solid var(--line); }
53
+ .controls { display: flex; gap: 10px; flex-wrap: wrap; margin: 12px 0 22px; }
54
+ input[type="search"] {
55
+ width: min(520px, 100%);
56
+ padding: 10px 12px;
57
+ border: 1px solid var(--line);
58
+ border-radius: 8px;
59
+ background: var(--panel);
60
+ color: var(--text);
61
+ }
62
+ .card {
63
+ border: 1px solid var(--line);
64
+ border-radius: 8px;
65
+ background: var(--panel);
66
+ padding: 16px;
67
+ margin: 12px 0;
68
+ }
69
+ .tag { display: inline-flex; align-items: center; border-radius: 999px; padding: 2px 8px; font-size: 12px; font-weight: 700; }
70
+ .tag.extracted { color: var(--ok); border: 1px solid color-mix(in srgb, var(--ok), transparent 45%); }
71
+ .tag.inferred { color: var(--warn); border: 1px solid color-mix(in srgb, var(--warn), transparent 45%); }
72
+ .tag.unknown { color: var(--unknown); border: 1px solid color-mix(in srgb, var(--unknown), transparent 45%); }
73
+ table { width: 100%; border-collapse: collapse; margin: 12px 0; }
74
+ th, td { text-align: left; vertical-align: top; padding: 9px 10px; border-bottom: 1px solid var(--line); }
75
+ th { color: var(--muted); font-size: 13px; }
76
+ code, pre { font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace; }
77
+ pre { overflow: auto; padding: 14px; border-radius: 8px; background: color-mix(in srgb, var(--panel), var(--line) 18%); }
78
+ .diagram { overflow: auto; }
79
+ .placeholder { color: var(--muted); font-style: italic; }
80
+ @media (max-width: 820px) {
81
+ .layout { grid-template-columns: 1fr; }
82
+ nav { position: static; }
83
+ }
84
+ </style>
85
+ </head>
86
+ <body>
87
+ <header>
88
+ <h1>{{title}}</h1>
89
+ <div class="meta">
90
+ <span>Workspace: {{workspace_name}}</span>
91
+ <span>Date: {{date}}</span>
92
+ <span>Generated: {{generated_at}}</span>
93
+ </div>
94
+ </header>
95
+ <div class="layout">
96
+ <nav aria-label="Report sections">
97
+ <a href="#summary">Summary</a>
98
+ <a href="#architecture">Architecture</a>
99
+ <a href="#interfaces">Interfaces</a>
100
+ <a href="#requirements">Requirements</a>
101
+ <a href="#flows">Flows</a>
102
+ <a href="#security">Security</a>
103
+ <a href="#nonfunctional">Non-functional</a>
104
+ <a href="#questions">Open Questions</a>
105
+ <a href="#evidence">Evidence</a>
106
+ </nav>
107
+ <main>
108
+ <div class="controls">
109
+ <input id="report-search" type="search" placeholder="Filter cards, APIs, modules, and evidence">
110
+ </div>
111
+ <section id="summary">
112
+ <h2>Executive Summary</h2>
113
+ <!-- REQUIREMENTS_SUMMARY -->
114
+ <p class="placeholder">Pending summary.</p>
115
+ <!-- /REQUIREMENTS_SUMMARY -->
116
+ </section>
117
+ <section id="architecture">
118
+ <h2>System Architecture</h2>
119
+ <!-- REQUIREMENTS_ARCHITECTURE -->
120
+ <p class="placeholder">Pending architecture map.</p>
121
+ <!-- /REQUIREMENTS_ARCHITECTURE -->
122
+ </section>
123
+ <section id="interfaces">
124
+ <h2>Interface Inventory</h2>
125
+ <!-- REQUIREMENTS_INTERFACE_INVENTORY -->
126
+ <p class="placeholder">Pending interface inventory.</p>
127
+ <!-- /REQUIREMENTS_INTERFACE_INVENTORY -->
128
+ </section>
129
+ <section id="requirements">
130
+ <h2>Requirement Cards</h2>
131
+ <!-- REQUIREMENTS_API_CARDS -->
132
+ <p class="placeholder">Pending requirement cards.</p>
133
+ <!-- /REQUIREMENTS_API_CARDS -->
134
+ </section>
135
+ <section id="flows">
136
+ <h2>Core Flows</h2>
137
+ <!-- REQUIREMENTS_FLOWS -->
138
+ <p class="placeholder">Pending flow diagrams.</p>
139
+ <!-- /REQUIREMENTS_FLOWS -->
140
+ </section>
141
+ <section id="security">
142
+ <h2>Security, Permissions, And Errors</h2>
143
+ <!-- REQUIREMENTS_SECURITY -->
144
+ <p class="placeholder">Pending security and error notes.</p>
145
+ <!-- /REQUIREMENTS_SECURITY -->
146
+ </section>
147
+ <section id="nonfunctional">
148
+ <h2>Non-functional Requirements</h2>
149
+ <!-- REQUIREMENTS_NONFUNCTIONAL -->
150
+ <p class="placeholder">Pending non-functional requirements.</p>
151
+ <!-- /REQUIREMENTS_NONFUNCTIONAL -->
152
+ </section>
153
+ <section id="questions">
154
+ <h2>Open Questions</h2>
155
+ <!-- REQUIREMENTS_OPEN_QUESTIONS -->
156
+ <p class="placeholder">Pending open questions.</p>
157
+ <!-- /REQUIREMENTS_OPEN_QUESTIONS -->
158
+ </section>
159
+ <section id="evidence">
160
+ <h2>Source Evidence Index</h2>
161
+ <!-- REQUIREMENTS_EVIDENCE_INDEX -->
162
+ <p class="placeholder">Pending evidence index.</p>
163
+ <!-- /REQUIREMENTS_EVIDENCE_INDEX -->
164
+ </section>
165
+ </main>
166
+ </div>
167
+ <script>
168
+ const search = document.querySelector('#report-search');
169
+ search?.addEventListener('input', () => {
170
+ const query = search.value.trim().toLowerCase();
171
+ for (const card of document.querySelectorAll('.card, details, table')) {
172
+ const matched = !query || card.textContent.toLowerCase().includes(query);
173
+ card.style.display = matched ? '' : 'none';
174
+ }
175
+ });
176
+ </script>
177
+ </body>
178
+ </html>