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 +5 -5
- package/package.json +1 -1
- package/skills/project-requirements/SKILL.md +4 -2
- package/src/commands/chat.js +68 -5
- package/src/core/chat-runtime.js +351 -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 +178 -0
package/deployment.md
CHANGED
|
@@ -13,13 +13,13 @@ npm pack
|
|
|
13
13
|
Expected output:
|
|
14
14
|
|
|
15
15
|
```text
|
|
16
|
-
codemini-cli-0.4.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
69
|
+
npm install C:\temp\codemini-cli-0.4.5.tgz
|
|
70
70
|
```
|
|
71
71
|
|
|
72
72
|
## 5. Confirm Installation
|
package/package.json
CHANGED
|
@@ -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/
|
|
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/
|
|
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.
|
package/src/commands/chat.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
84
|
-
if (result.text) console.log(result.text);
|
|
147
|
+
await submitAndPrint(runtime, parsed.prompt, { output: process.stdout });
|
|
85
148
|
return;
|
|
86
149
|
}
|
|
87
150
|
|
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,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
|
|
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);
|
|
@@ -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>
|