Compare commits

...

128 Commits

Author SHA1 Message Date
catlog22
2c675ee4db chore: bump version to 6.3.14
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 22:57:02 +08:00
catlog22
f6dfe28e08 refactor(issue): rename 'labels' to 'tags' in issue schemas and related code 2025-12-29 22:55:33 +08:00
catlog22
e8e8746cc6 refactor(issue): update brief mode to use 'labels' instead of 'tags' in issue listing 2025-12-29 22:47:55 +08:00
catlog22
603bc00bca refactor(issue): enhance documentation and add core guidelines for data access 2025-12-29 22:41:10 +08:00
catlog22
ae76926d5a feat(queue): support solution-based queues and update metadata handling 2025-12-29 22:14:06 +08:00
catlog22
fd48045fe3 Refactor issue management and history tracking
- Removed the interactive issue management command and its associated documentation.
- Enhanced the issue planning command to streamline project context reading and solution creation.
- Improved queue management with conflict clarification and status syncing from queues.
- Added functionality to track completed issues, moving them to a history file upon completion.
- Updated CLI options to support syncing issue statuses from queues.
- Introduced new API endpoint for retrieving completed issues from history.
- Enhanced error handling and validation for issue updates and queue management.
2025-12-29 21:42:06 +08:00
catlog22
6ec6643448 refactor(issue-plan-agent): enhance conflict detection and execution flow 2025-12-29 20:36:56 +08:00
catlog22
945fda2d14 docs(issue): update agent/prompts to use CLI endpoints
- issue-plan-agent.md: Use `ccw issue solution` instead of Bash echo
- issue-plan.md (codex): Use `ccw issue solution` endpoint
- issue-queue.md (codex): Remove assigned_executor from schema

All solution creation now uses the dedicated CLI endpoint for:
- Auto-increment ID: SOL-{issue-id}-{seq}
- Proper JSONL format with trailing newline
- Multi-solution support per issue

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 20:28:39 +08:00
catlog22
7d71f603fe feat(issue): add solution endpoint with auto-increment ID
- Add `ccw issue solution <id> --data '{...}'` for solution creation
- Add createSolution() with proper JSONL handling (trailing newline)
- Fix writeSolutions() to always add trailing newline
- Update plan.md to use CLI endpoint

Supports multiple solutions per issue with sequential IDs.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 20:24:31 +08:00
catlog22
bd11a538a7 docs(issue/new): update Phase 5 to use ccw issue create endpoint
Replace manual Bash echo with CLI endpoint documentation:
- Auto-increment ID: ISS-YYYYMMDD-NNN
- Proper JSONL format with trailing newline
- JSON output with created issue

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 20:16:24 +08:00
catlog22
b9b4da6d8c feat(issue): add create endpoint with auto-increment ID
- Add `ccw issue create --data '{...}'` for programmatic issue creation
- Add generateIssueId() for auto-increment: ISS-YYYYMMDD-NNN
- Add createIssue() with proper JSONL handling (trailing newline)
- Fix writeIssues() to always add trailing newline

Fixes JSONL format corruption when appending via shell echo.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 20:12:57 +08:00
catlog22
70f8b14eaa refactor(vector_store): use safer SQL query construction pattern
Replaces f-string interpolation with safer string formatting.
Adds documentation on SQL injection prevention.
No functional changes - parameterized queries still used.

Fixes: ISS-1766921318981-9

Solution-ID: SOL-1735386000-9
Issue-ID: ISS-1766921318981-9
Task-ID: T1
2025-12-29 20:09:49 +08:00
catlog22
0c8b2f2ec9 fix(vector_store): add bounds checking for chunk ID generation
Prevents potential integer overflow when start_id is near sys.maxsize.
Adds validation before range() calculation in batch insert methods.

Fixes: ISS-1766921318981-6

Solution-ID: SOL-1735386000-6
Issue-ID: ISS-1766921318981-6
Task-ID: T1
2025-12-29 20:02:19 +08:00
catlog22
d532b3fd02 refactor(queue): streamline output requirements and storage structure documentation 2025-12-29 19:54:10 +08:00
catlog22
c56104c082 fix(vector_store): add null check for ANN search results before filtering
Prevents errors when HNSW search returns null/empty results due to race conditions.
Adds validation for ids and distances before zip operation.

Fixes: ISS-1766921318981-5

Solution-ID: SOL-1735386000-5
Issue-ID: ISS-1766921318981-5
Task-ID: T1
2025-12-29 19:53:32 +08:00
catlog22
66ae1972ae refactor(issue): remove assigned_executor from QueueItem interface
Queue is an execution plan without executor assignment.
Executor is determined at runtime by /issue:execute command.

Changes:
- Remove assigned_executor from QueueItem interface
- Remove from dag nodes, list display, execution_hints
- Simplify queue list output (remove Executor column)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 19:49:25 +08:00
catlog22
7f4433e449 fix(vector_store): add parameter validation for min_score range
Validates min_score is within [0.0, 1.0] for cosine similarity.
Raises ValueError for out-of-range values to prevent unexpected filtering.

Fixes: ISS-1766921318981-14

Solution-ID: SOL-1735386000-14
Issue-ID: ISS-1766921318981-14
Task-ID: T1
2025-12-29 19:46:26 +08:00
catlog22
e1f2fc72d9 refactor(schema): remove assigned_executor from queue-schema
Queue produces execution plan without executor assignment.
Executor is determined at runtime by /issue:execute command.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 19:45:43 +08:00
catlog22
aa093f9468 refactor(issue/queue): remove assigned_executor field from schema
Executor assignment is not needed in solution schema - execution
context is determined at runtime.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 19:42:27 +08:00
catlog22
a27f76abcb refactor(issue/queue): simplify Phase 1 and Phase 5 with semantic descriptions
- Replace complex JavaScript code in Phase 1 with semantic bullet points
- Simplify Phase 5 validation code to semantic description
- Add Quality Checklist for completion verification
- Consistent style with plan.md optimizations

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 19:41:32 +08:00
catlog22
df34ef38d9 refactor(issue/plan): update semantic grouping limit to 4 issues per group and enhance conflict handling process 2025-12-29 19:38:45 +08:00
catlog22
60fbb4177c fix(config): add specific exception handling for path operations
Replaces generic Exception handling with specific PermissionError and OSError
handling in __post_init__ and ensure_runtime_dirs(). Provides clear diagnostic
messages to distinguish permission issues from other filesystem errors.

Solution-ID: SOL-1735385400008
Issue-ID: ISS-1766921318981-8
Task-ID: T1
2025-12-29 19:34:27 +08:00
catlog22
3289562be7 docs(issue/plan): add quality checklist for completion verification
Checklist items:
- Solution files exist for all issues
- Auto-binding for single solutions
- User selection for multi-solutions
- Tasks have modification_points
- Acceptance criteria quantified
- Conflicts detected
- Status updated to planned

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 19:32:19 +08:00
catlog22
73fc68a187 refactor(issue/plan): remove redundant file specs from command doc
Agent (issue-plan-agent.md) handles file generation.
Command doc should only describe input/output behavior.

Removed: Generate Files, Storage Structure, Completion Criteria
Kept: Command Output JSON format, Behavior summary

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 19:31:00 +08:00
catlog22
bce6fa7a91 refactor(issue/new): simplify clarification to open-ended prompt
Remove prescriptive options (Bug fix/New feature/Improvement)
that may interfere with natural issue description.

Now: single open-ended prompt for user to describe freely.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 19:28:03 +08:00
catlog22
88724a4df9 refactor(issue): simplify Issue schema and remove lifecycle_requirements
Schema changes:
- Remove lifecycle_requirements (deferred to plan agent)
- Remove solution_count (computed dynamically from solutions/*.jsonl)
- Keep context as single source of truth (remove problem_statement)
- Add feedback[] for failure history + human clarifications
- Add source, source_url, labels, expected/actual_behavior

Fields removed (unused downstream):
- lifecycle_requirements.test_strategy
- lifecycle_requirements.regression_scope
- lifecycle_requirements.acceptance_type
- lifecycle_requirements.commit_strategy
- solution_count (denormalized)

Fields added:
- feedback[]: { type, stage, content, created_at }
- source, source_url, labels
- expected_behavior, actual_behavior, affected_components

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 19:25:58 +08:00
catlog22
5914b1c5fc fix(vector-store): protect bulk insert mode transitions with lock
Ensure begin_bulk_insert() and end_bulk_insert() are fully
lock-protected to prevent TOCTOU race conditions.

Solution-ID: SOL-1735392000003
Issue-ID: ISS-1766921318981-12
Task-ID: T2
2025-12-29 19:20:02 +08:00
catlog22
d8be23fa83 fix(vector-store): add lock protection for bulk insert mode flag
Protect _bulk_insert_mode flag and accumulation lists with
_ann_write_lock to prevent corruption during concurrent access.

Solution-ID: SOL-1735392000003
Issue-ID: ISS-1766921318981-12
Task-ID: T1
2025-12-29 19:16:30 +08:00
catlog22
ffbc4a4b76 style(issue/new): use hybrid Phase + table flow diagram
Combines sequential Phase structure with inline table for branching:
- Phase 1-4 for overall flow clarity
- Table for Score 0/1-2/3 decision branching
- More compact and scannable than pure ASCII boxes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 19:16:25 +08:00
catlog22
dd62a7ac13 refactor(issue/new): make ACE search conditional on clarity
- ACE only for medium clarity (score 1-2), skip for:
  - GitHub URLs (score 3): already has context
  - Vague inputs (score 0): needs clarification first
- Limit to quick search: max 3 keywords, max 3 files
- Non-blocking: ACE failure continues without hints
- Note: Deep exploration deferred to /issue:plan

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 19:14:13 +08:00
catlog22
3f29dfd4cf refactor(issue/new): simplify command with clarity-based flow
Key changes:
- Add Clarity Detection (score 0-3) to determine question needs
- Remove mandatory Lifecycle Configuration questions (auto-detect)
- Integrate ACE tool for smart context discovery
- Ask only 1 question for vague inputs (clarityScore < 2)
- Direct creation for clear inputs (GitHub URL, structured text)
- Reduce from 6 phases to streamlined flow

Decision flow:
- Score 3: GitHub URL → parse → create directly
- Score 2: Structured text → parse + ACE → create directly
- Score 0-1: Vague → ask clarification → confirm → create

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 19:10:49 +08:00
catlog22
3fdd52742b fix(storage): handle rollback failures in batch operations
Adds nested exception handling in add_files() and _migrate_fts_to_external()
to catch and log rollback failures. Uses exception chaining to preserve both
transaction and rollback errors, preventing silent database inconsistency.

Solution-ID: SOL-1735385400010
Issue-ID: ISS-1766921318981-10
Task-ID: T1
2025-12-29 19:08:49 +08:00
catlog22
76ab4d67fe test(entities): add zero vector validation tests
Add comprehensive test coverage for zero and near-zero vector
detection in SemanticChunk embedding validation.

Solution-ID: SOL-20251228113612
Issue-ID: ISS-1766921318981-7
Task-ID: T2
2025-12-29 19:03:20 +08:00
catlog22
c859af1abf fix(entities): validate embeddings are non-zero vectors
Add L2 norm check to SemanticChunk.validate_embedding to reject
zero vectors. Prevents division by zero in cosine similarity
calculations downstream in vector search.

Solution-ID: SOL-20251228113612
Issue-ID: ISS-1766921318981-7
Task-ID: T1
2025-12-29 19:01:27 +08:00
catlog22
6a73d3c379 fix(search): handle path operation failures in symbol filtering
Adds robust exception handling for os.path.commonpath() in search_symbols()
to prevent crashes on malformed paths and Windows cross-drive scenarios.
Invalid symbols are skipped with debug logging, search continues.

Solution-ID: SOL-1735385400004
Issue-ID: ISS-1766921318981-4
Task-ID: T1
2025-12-29 18:59:10 +08:00
catlog22
5d5652c2c5 fix(sqlite-store): improve thread tracking in connection cleanup
Add fallback validation to detect dead threads missed by
threading.enumerate(), ensuring all stale connections are cleaned.

Solution-ID: SOL-1735392000002
Issue-ID: ISS-1766921318981-3
Task-ID: T2
2025-12-29 18:50:22 +08:00
catlog22
b958a1ea96 fix(sqlite-store): add periodic cleanup timer for connection pool
Implement background timer to proactively clean stale connections
every 5 minutes, preventing indefinite accumulation.

Solution-ID: SOL-1735392000002
Issue-ID: ISS-1766921318981-3
Task-ID: T1
2025-12-29 18:43:55 +08:00
catlog22
bc385a32fd test(storage): add TypeScript storage manager concurrency tests
Solution-ID: SOL-1735410004
Issue-ID: ISS-1766921318981-24
Task-ID: T4
2025-12-29 18:38:46 +08:00
catlog22
9a45732a39 test(codex-lens): add connection pool stress tests
Solution-ID: SOL-1735410004
Issue-ID: ISS-1766921318981-24
Task-ID: T3
2025-12-29 18:16:03 +08:00
catlog22
015b46e58b test(codex-lens): add concurrent write operation tests
Solution-ID: SOL-1735410004
Issue-ID: ISS-1766921318981-24
Task-ID: T2
2025-12-29 18:12:09 +08:00
catlog22
042a99dbe3 test(codex-lens): add concurrent read operation tests
Solution-ID: SOL-1735410004

Issue-ID: ISS-1766921318981-24

Task-ID: T1
2025-12-29 17:59:08 +08:00
catlog22
99291053f5 test(cli-executor): add end-to-end orchestration validation tests
Solution-ID: SOL-1735410003

Issue-ID: ISS-1766921318981-23

Task-ID: T4
2025-12-29 17:50:33 +08:00
catlog22
99eeeff6f7 test(cli-executor): add qwen/codex and multi-tool workflow tests
Solution-ID: SOL-1735410003

Issue-ID: ISS-1766921318981-23

Task-ID: T3
2025-12-29 17:36:50 +08:00
catlog22
883b9f0672 refactor(issue): unify Solution ID format to SOL-{issue-id}-{seq}
- Replace timestamp-based ID generation with issue-id-based format
- generateSolutionId() now accepts issueId and existing solutions
- Automatically calculates next sequence number (SOL-GH-123-1, -2, -3)
- Update taskAction and bindAction to use new format
- Ensures consistency with agent-generated IDs

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 17:33:12 +08:00
catlog22
3c07e743e1 fix(issue-plan-agent): ensure shell safety by escaping single quotes in solution JSON 2025-12-29 17:30:19 +08:00
catlog22
823e1dc487 test(cli-executor): add gemini workflow integration tests
Solution-ID: SOL-1735410003

Issue-ID: ISS-1766921318981-23

Task-ID: T2
2025-12-29 17:29:48 +08:00
catlog22
3537c0fc74 test(cli-executor): enhance solution registration and binding process 2025-12-29 17:21:00 +08:00
catlog22
f3e23f0a57 test(cli-executor): add integration test infrastructure
Solution-ID: SOL-1735410003

Issue-ID: ISS-1766921318981-23

Task-ID: T1
2025-12-29 17:15:15 +08:00
catlog22
3df1eac2fc test(ci): add visual regression testing workflow
Solution-ID: SOL-1735410002

Issue-ID: ISS-1766921318981-22

Task-ID: T4
2025-12-29 16:42:31 +08:00
catlog22
141472117d test(ui-tools): add visual regression tests for prototype instantiation
Solution-ID: SOL-1735410002

Issue-ID: ISS-1766921318981-22

Task-ID: T3
2025-12-29 16:37:10 +08:00
catlog22
e2dbeca080 test(ui-tools): add visual regression tests for preview generation
Solution-ID: SOL-1735410002

Issue-ID: ISS-1766921318981-22

Task-ID: T2
2025-12-29 16:15:12 +08:00
catlog22
70063f4045 test(ui-tools): add visual regression testing infrastructure
Solution-ID: SOL-1735410002
Issue-ID: ISS-1766921318981-22
Task-ID: T1
2025-12-29 15:47:08 +08:00
catlog22
8578d2d426 test(litellm): add integration tests for retry and rate limiting
Solution-ID: SOL-1735410001

Issue-ID: ISS-1766921318981-21

Task-ID: T3
2025-12-29 15:14:03 +08:00
catlog22
5d31bfd9fa test(litellm): add executor error scenario and validation tests
Solution-ID: SOL-1735410001

Issue-ID: ISS-1766921318981-21

Task-ID: T2
2025-12-29 13:28:11 +08:00
catlog22
c7291ba532 test(litellm): add comprehensive error scenario tests for client
Solution-ID: SOL-1735410001

Issue-ID: ISS-1766921318981-21

Task-ID: T1
2025-12-29 13:13:33 +08:00
catlog22
1396010437 fix(embedder): add lock protection for cache read operations
Protect fast path cache read in get_embedder() to prevent KeyError

during concurrent access and cache clearing operations.

Solution-ID: SOL-1735392000001

Issue-ID: ISS-1766921318981-2

Task-ID: T1
2025-12-29 12:33:23 +08:00
catlog22
1654b121bc test(routes): add integration tests for CodexLens and discovery routes
Solution-ID: SOL-1735386000005

Issue-ID: ISS-1766921318981-19

Task-ID: T4
2025-12-29 12:22:09 +08:00
catlog22
d5130fc4da test(routes): add integration tests for session and core-memory routes
Solution-ID: SOL-1735386000005

Issue-ID: ISS-1766921318981-19

Task-ID: T3
2025-12-29 11:58:00 +08:00
catlog22
c4f3afd8eb chore: bump version to 6.3.12
- Refactor issue workflow with unified solution ID format (SOL-{issue-id}-{seq})
- Add follow-up questions to issue creation flow
- Fix JSONL append newline handling
- Align conflicts format in plan.md with agent output

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 11:53:12 +08:00
catlog22
fb2f80ee3a fix(issue/plan): align conflicts format and add missing helper function
- Update Return Summary conflicts format to match Phase 3 processing requirements
- Add extractSelectedSolutionId helper function for solution selection

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 11:48:47 +08:00
catlog22
dda6af130c refactor(issue): unify solution ID format to SOL-{issue-id}-{seq}
- Update solution-schema.json pattern to support new format
- Add Solution ID Format specification to plan.md
- Fix JSON parsing with extractJsonFromMarkdown + try-catch
- Update all examples in agent and prompt files:
  - issue-plan-agent.md
  - issue-queue-agent.md
  - issue-execute.md
  - issue-queue.md
  - queue.md

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 11:45:31 +08:00
catlog22
0d82c9fa03 refactor(issue): simplify extended_context and add follow-up questions
- Replace discovery_context with minimal extended_context (3 fields: location, suggested_fix, notes)
- Add Phase 4.5: intelligent follow-up questions for missing issue fields
- Fix JSONL append to ensure proper newline handling
- Update schema and plan.md to use new extended_context structure

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 11:33:59 +08:00
catlog22
7c389d5028 test(routes): add integration tests for CLI and memory routes
Solution-ID: SOL-1735386000005

Issue-ID: ISS-1766921318981-19

Task-ID: T2
2025-12-29 10:51:57 +08:00
catlog22
d5704f8344 test(routes): add integration tests for status and system routes
Solution-ID: SOL-1735386000005

Issue-ID: ISS-1766921318981-19

Task-ID: T1
2025-12-29 10:27:20 +08:00
catlog22
ec4018a930 test(embedding-batch): add tests for batch embedding operations
Solution-ID: SOL-1735386000004

Issue-ID: ISS-1766921318981-18

Task-ID: T4
2025-12-29 10:11:27 +08:00
catlog22
673cb03a2e test(semantic-search): add integration tests for semantic search workflow
Solution-ID: SOL-1735386000004

Issue-ID: ISS-1766921318981-18

Task-ID: T3
2025-12-29 09:59:26 +08:00
catlog22
4d7bf5b245 test(embedding-client): add unit tests for embedding generation
Solution-ID: SOL-1735386000004

Issue-ID: ISS-1766921318981-18

Task-ID: T2
2025-12-29 09:50:30 +08:00
catlog22
267426e332 test(core-memory-store): add unit tests for memory store operations
Solution-ID: SOL-1735386000004

Issue-ID: ISS-1766921318981-18

Task-ID: T1
2025-12-29 09:42:01 +08:00
catlog22
d68401fa1a test(core-memory-store): add assertion for missing memory retrieval 2025-12-29 09:35:52 +08:00
catlog22
eb4ba89693 feat(issue-plan): enhance conflict detection and resolution process with semantic grouping and user clarifications 2025-12-29 09:26:57 +08:00
catlog22
ef3b6b9f6e test(session-clustering): add integration tests for session clustering
Solution-ID: SOL-1735386000003

Issue-ID: ISS-1766921318981-17

Task-ID: T4
2025-12-29 09:03:50 +08:00
catlog22
2d1be7cd4f test(session-archive): add integration tests for archiving and cleanup
Solution-ID: SOL-1735386000003

Issue-ID: ISS-1766921318981-17

Task-ID: T3
2025-12-29 08:50:32 +08:00
catlog22
bf0a2bde34 test(session-content): add integration tests for session CRUD operations
Solution-ID: SOL-1735386000003

Issue-ID: ISS-1766921318981-17

Task-ID: T2
2025-12-29 08:44:09 +08:00
catlog22
d85ab2a12c test(session-lifecycle): add integration tests for session creation
Solution-ID: SOL-1735386000003

Issue-ID: ISS-1766921318981-17

Task-ID: T1
2025-12-29 08:39:32 +08:00
catlog22
33a2bdb9f0 test(browser-launcher): add cross-platform browser launch tests
Solution-ID: SOL-1735386000002

Issue-ID: ISS-1766921318981-16

Task-ID: T4
2025-12-29 00:19:37 +08:00
catlog22
11a7dcb6c8 test(file-utils): add unit tests for file operations
Solution-ID: SOL-1735386000002

Issue-ID: ISS-1766921318981-16

Task-ID: T3
2025-12-29 00:11:57 +08:00
catlog22
d8e389df00 test(path-validator): add unit tests for path security validation
Solution-ID: SOL-1735386000002

Issue-ID: ISS-1766921318981-16

Task-ID: T2
2025-12-29 00:09:33 +08:00
catlog22
203b51527b test(path-resolver): add unit tests for path resolution and validation
Solution-ID: SOL-1735386000002

Issue-ID: ISS-1766921318981-16

Task-ID: T1
2025-12-29 00:05:52 +08:00
catlog22
bf05886770 fix(queue): streamline validation and status update logic in queue processing 2025-12-28 23:47:26 +08:00
catlog22
079ecdad3e test(commands): add unit tests for session and server commands
Solution-ID: SOL-1735386000001

Issue-ID: ISS-1766921318981-15

Task-ID: T4
2025-12-28 23:47:13 +08:00
catlog22
075a8357cd test(core-memory-command): add unit tests for core memory module
Solution-ID: SOL-1735386000001

Issue-ID: ISS-1766921318981-15

Task-ID: T3
2025-12-28 23:08:54 +08:00
catlog22
c99ad377c6 test(memory-command): add unit tests for memory module
Solution-ID: SOL-1735386000001

Issue-ID: ISS-1766921318981-15

Task-ID: T2
2025-12-28 23:01:40 +08:00
catlog22
382d330525 fix(issue): improve queue archiving logic to handle empty tasks and solutions 2025-12-28 22:56:51 +08:00
catlog22
e2f4241b2e feat(cli-stream): add search functionality and improve validation in CLI Stream Viewer 2025-12-28 22:52:32 +08:00
catlog22
32cea006b9 Add initial implementation of the Docsify shell template for interactive software manuals 2025-12-28 22:46:43 +08:00
catlog22
6ffac8810b test(cli-command): add unit tests for CLI command executor
Solution-ID: SOL-1735386000001

Issue-ID: ISS-1766921318981-15

Task-ID: T1
2025-12-28 22:44:47 +08:00
catlog22
84d06f4273 fix(registry): normalize path case for comparison on Windows
Adds case normalization for path comparison on Windows to handle
case-insensitive filesystem behavior. Preserves case-sensitivity on Unix.

Fixes: ISS-1766921318981-13

Solution-ID: SOL-1735386000-13
Issue-ID: ISS-1766921318981-13
Task-ID: T1
2025-12-28 21:51:23 +08:00
catlog22
18cc536f65 refactor(vector-store): use consistent EPSILON constant
Define module-level EPSILON constant and use it in both
_cosine_similarity and _refresh_cache for consistent
floating point precision handling.

Solution-ID: SOL-20251228113619
Issue-ID: ISS-1766921318981-11
Task-ID: T3
2025-12-28 21:40:46 +08:00
catlog22
af2ff54cb7 test(vector-store): add epsilon tolerance edge case tests
Add comprehensive test coverage for near-zero norms, product
underflow, and floating point precision edge cases in
_cosine_similarity function.

Solution-ID: SOL-20251228113619
Issue-ID: ISS-1766921318981-11
Task-ID: T2
2025-12-28 21:37:59 +08:00
catlog22
6486c56850 fix(vector-store): add epsilon tolerance for norm checks
Replace exact zero comparison with epsilon-based check (< 1e-10)
in _cosine_similarity to handle floating point precision issues.
Also check for product underflow to prevent inf/nan from division
by very small numbers.

Solution-ID: SOL-20251228113619
Issue-ID: ISS-1766921318981-11
Task-ID: T1
2025-12-28 21:11:30 +08:00
catlog22
93dcdd2293 fix(config): log configuration loading errors instead of silently ignoring
Replaces bare exception handler in load_settings() with logging.warning()
to help users debug configuration file issues (syntax errors, permissions).
Maintains backward compatibility - errors do not break initialization.

Solution-ID: SOL-1735385400001
Issue-ID: ISS-1766921318981-1
Task-ID: T1
2025-12-28 21:06:23 +08:00
catlog22
58caccb250 test(ranking): add edge case tests for normalize_weights
Add comprehensive test coverage for NaN, infinity, and all-None
edge cases in weight normalization to prevent regression.

Solution-ID: SOL-20251228113631
Issue-ID: ISS-1766921318981-0
Task-ID: T2
2025-12-28 20:59:08 +08:00
catlog22
598eed92cb fix(ranking): add explicit NaN check in normalize_weights
Add math.isnan() check before math.isfinite() to properly catch
NaN values in weight totals. Prevents division by NaN which could
produce unexpected results in RRF fusion calculations.

Solution-ID: SOL-20251228113631
Issue-ID: ISS-1766921318981-0
Task-ID: T1
2025-12-28 20:55:03 +08:00
catlog22
d3e7ecca21 feat: 添加任务跟踪功能,初始化待办事项列表并更新任务状态 2025-12-28 20:50:24 +08:00
catlog22
847abcefce feat: 添加选项以标记任务为失败 2025-12-28 20:45:05 +08:00
catlog22
c24ad501b5 feat: 更新问题执行和队列生成逻辑,支持解决方案格式并增强元数据管理 2025-12-28 20:35:29 +08:00
catlog22
35c7fe28bb feat: 更新队列生成逻辑,使用提供的队列ID并严格限制输出文件 2025-12-28 20:20:13 +08:00
catlog22
a33cacfd75 feat: 添加更新命令以修改问题字段(状态、优先级、标题等) 2025-12-28 20:14:31 +08:00
catlog22
338c3d612c feat: 更新问题规划和队列命令,增强解决方案注册和用户选择逻辑 2025-12-28 19:58:44 +08:00
catlog22
8b17fad723 feat(discovery): enhance discovery progress reading with new schema support 2025-12-28 19:33:17 +08:00
catlog22
169f218f7a feat(discovery): enhance discovery index reading and issue exporting
- Improved the reading of the discovery index by adding a fallback mechanism to scan directories for discovery folders if the index.json is invalid or missing.
- Added sorting of discoveries by creation time in descending order.
- Enhanced the `appendToIssuesJsonl` function to include deduplication logic for issues based on ID and source finding ID.
- Updated the discovery route handler to reflect the number of issues added and skipped during export.
- Introduced UI elements for selecting and deselecting findings in the dashboard.
- Added CSS styles for exported findings and action buttons.
- Implemented search functionality for filtering findings based on title, file, and description.
- Added internationalization support for new UI elements.
- Created scripts for automated API extraction from various project types, including FastAPI and TypeScript.
- Documented the API extraction process and library bundling instructions.
2025-12-28 19:27:34 +08:00
catlog22
3ef1e54412 feat: 更新软件手册,优化截图捕获流程和规则说明 2025-12-28 18:19:39 +08:00
catlog22
4419c50942 feat: enhance internationalization support and improve GPU mode selector with Python environment checks 2025-12-28 17:49:40 +08:00
catlog22
7aa1cda367 feat: add issue discovery view for managing discovery sessions and findings
- Implemented main render function for the issue discovery view.
- Added data loading functions to fetch discoveries, details, findings, and progress.
- Created rendering functions for discovery list and detail sections.
- Introduced filtering and searching capabilities for findings.
- Implemented actions for exporting and dismissing findings.
- Added polling mechanism to track discovery progress.
- Included utility functions for HTML escaping and cleanup.
2025-12-28 17:21:07 +08:00
catlog22
a2c88ba885 feat: Add project guidelines support and enhance project overview rendering 2025-12-28 14:50:50 +08:00
catlog22
e16950ef1e refactor: Remove unused context-tools-ace.md and clean up CLI status management 2025-12-28 14:09:51 +08:00
catlog22
5b973b00ea feat(issue): Enhance queue management with solution-level granularity and improved item identification 2025-12-28 13:58:43 +08:00
catlog22
3a1ebf8684 refactor(issue): Simplify issue and task structures by removing unused fields 2025-12-28 13:39:52 +08:00
catlog22
2eaefb61ab feat: Enhance issue management to support solution-level queues
- Added support for solution-level queues in the issue management system.
- Updated interfaces to include solution-specific properties such as `approach`, `task_count`, and `files_touched`.
- Modified queue handling to differentiate between task-level and solution-level items.
- Adjusted rendering logic in the dashboard to display solutions and their associated tasks correctly.
- Enhanced queue statistics and conflict resolution to accommodate the new solution structure.
- Updated actions (next, done, retry) to handle both tasks and solutions seamlessly.
2025-12-28 13:21:34 +08:00
catlog22
4c6b28030f Enhance project management workflow by introducing dual file system for project guidelines and tech analysis
- Updated workflow initialization to create `.workflow/project-tech.json` and `.workflow/project-guidelines.json` for comprehensive project understanding.
- Added mandatory context reading steps in various commands to ensure compliance with user-defined constraints and technology stack.
- Implemented a new command `/workflow:session:solidify` to capture session learnings and solidify them into project guidelines.
- Introduced a detail action in issue management to retrieve task details without altering status.
- Enhanced documentation across multiple workflow commands to reflect changes in project structure and guidelines.
2025-12-28 12:47:39 +08:00
catlog22
2c42cefa5a feat(issue): add DAG support for parallel execution planning and enhance task fetching 2025-12-28 12:04:10 +08:00
catlog22
35ffd3419e chore(release): v6.3.9 - Issue System Consistency
- Unified four-layer architecture (Schema/Agent/Command/Implementation)
- Upgraded to Rich Plan model with lifecycle fields
- Added multi-solution generation support
- Consolidated schemas (deleted redundant issue-task-jsonl-schema, solutions-jsonl-schema)
- Fixed field naming consistency (acceptance, lifecycle_status, priority mapping)
- Added search tool fallback chain to issue-plan-agent

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-27 23:57:30 +08:00
catlog22
e3223edbb1 fix(plan): Update delivery criteria terminology to acceptance.criteria 2025-12-27 23:54:00 +08:00
catlog22
a061fc1428 fix(issue-plan-agent): Update acceptance criteria terminology and enhance issue loading process with metadata 2025-12-27 23:51:30 +08:00
catlog22
0992d27523 refactor(issue-manager): Enhance queue detail item styling and update modal content 2025-12-27 23:43:40 +08:00
catlog22
5aa0c9610d fix(issue-manager): Add History button to empty queue state
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-27 23:00:17 +08:00
catlog22
7620ff703d feat(issue-manager): Add queue history modal for viewing and switching queues
- Add GET /api/queue/history endpoint to fetch all queues from index
- Add GET /api/queue/:id endpoint to fetch specific queue details
- Add POST /api/queue/switch endpoint to switch active queue
- Add History button in queue toolbar
- Add queue history modal with list view and detail view
- Add switch functionality to change active queue
- Add CSS styles for queue history components

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-27 22:56:32 +08:00
catlog22
d705a3e7d9 fix: Add active_queue_id to QueueIndex interface
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-27 22:48:38 +08:00
catlog22
726151bfea Refactor issue management commands and introduce lifecycle requirements
- Updated lifecycle requirements in issue creation to include new fields for testing, regression, acceptance, and commit strategies.
- Enhanced the planning command to generate structured output and handle multi-solution scenarios.
- Improved queue formation logic to ensure valid DAG and conflict resolution.
- Introduced a new interactive issue management skill for CRUD operations, allowing users to manage issues through a menu-driven interface.
- Updated documentation across commands to reflect changes in task structure and output requirements.
2025-12-27 22:44:49 +08:00
catlog22
b58589ddad refactor: Update issue queue structure and commands
- Changed queue structure from 'queue' to 'tasks' in various files for clarity.
- Updated CLI commands to reflect new task ID usage instead of queue ID.
- Enhanced queue management with new delete functionality for historical queues.
- Improved metadata handling and task execution tracking.
- Updated dashboard and issue manager views to accommodate new task structure.
- Bumped version to 6.3.8 in package.json and package-lock.json.
2025-12-27 22:04:15 +08:00
catlog22
2e493277a1 feat(issue-queue): Enhance task execution flow with priority handling and stuck task reset option 2025-12-27 14:43:18 +08:00
catlog22
8b19edd2de chore: bump version to 6.3.6
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-27 11:50:35 +08:00
catlog22
3e54b5f7d8 feat(issue-execute): Implement sequential task execution with detailed lifecycle and commit process 2025-12-27 11:46:18 +08:00
catlog22
4da06864f8 feat: Enhance issue and solution management with new UI components and functionality
- Added internationalization support for new issue and solution-related strings in i18n.js.
- Implemented a solution detail modal in issue-manager.js to display solution information and bind/unbind actions.
- Enhanced the skill loading function to combine project and user skills in hook-manager.js.
- Improved queue rendering logic to handle empty states and display queue statistics in issue-manager.js.
- Introduced command modals for queue operations, allowing users to generate execution queues via CLI commands.
- Added functionality to auto-generate issue IDs and regenerate them in the create issue modal.
- Implemented detailed rendering of solution tasks, including acceptance criteria and modification points.
2025-12-27 11:27:45 +08:00
catlog22
8f310339df feat(issue-management): Implement interactive issue management command with CRUD operations
- Added `/issue:manage` command for interactive issue management via CLI.
- Implemented features for listing, viewing, editing, deleting, and bulk operations on issues.
- Integrated GitHub issue fetching and text description parsing for issue creation.
- Enhanced user experience with menu-driven interface and structured output.
- Created helper functions for parsing user input and managing issue data.
- Added error handling and related command references for better usability.

feat(issue-creation): Introduce structured issue creation from GitHub URL or text description

- Added `/issue:new` command to create structured issues from GitHub issues or text descriptions.
- Implemented parsing logic for extracting key elements from issue descriptions.
- Integrated user confirmation for issue creation with options to edit title and priority.
- Ensured proper writing of issues to `.workflow/issues/issues.jsonl` with metadata.
- Included examples and error handling for various input scenarios.
2025-12-27 10:20:34 +08:00
catlog22
0157e36344 feat: add CLI Stream Viewer component for real-time output monitoring
- Implemented a new CLI Stream Viewer to display real-time output from CLI executions.
- Added state management for CLI executions, including handling of start, output, completion, and errors.
- Introduced UI rendering for stream tabs and content, with auto-scroll functionality.
- Integrated keyboard shortcuts for toggling the viewer and handling user interactions.

feat: create Issue Manager view for managing issues and execution queue

- Developed the Issue Manager view to manage issues, solutions, and execution queue.
- Implemented data loading functions for fetching issues and queue data from the API.
- Added filtering and rendering logic for issues and queue items, including drag-and-drop functionality.
- Created detail panel for viewing and editing issue details, including tasks and solutions.
2025-12-27 09:46:12 +08:00
catlog22
cdf4833977 feat(issue): implement JSONL task generation and management for issue resolution
- Added `/issue:plan` command to generate a structured task plan from GitHub issues or descriptions, including delivery and pause criteria, and a dependency graph.
- Introduced JSONL schema for task entries to enforce structure and validation.
- Developed comprehensive issue command with functionalities for initializing, listing, adding, updating, and exporting tasks.
- Implemented error handling and user feedback for various operations within the issue management workflow.
- Enhanced task management with priority levels, phase results, and executor preferences.
2025-12-26 17:21:32 +08:00
catlog22
c8a914aeca Refactor agent configurations and context gathering processes across multiple workflow tools
- Removed agent references in context-gather and test-context-gather commands for clarity.
- Updated test-task-generate to streamline agent configuration references.
- Enhanced action-planning-agent documentation by removing redundant examples.
- Adjusted execute command to eliminate direct agent path references.
- Improved context-gather documentation to clarify agent autonomy and project.json integration.
- Revised test-context-gather to focus on agent delegation and coverage analysis.
- Cleaned up test-task-generate to focus on task generation rules without direct agent references.
- Implemented automatic agent assignment based on exploration file names in deep analysis phase.
- Expanded project exploration phase to include intelligent angle selection based on software type.
- Enhanced output schemas and success criteria for exploration tasks to ensure clarity and completeness.
2025-12-26 16:20:46 +08:00
195 changed files with 37183 additions and 616 deletions

View File

@@ -2,7 +2,7 @@
- **CLI Tools Usage**: @~/.claude/workflows/cli-tools-usage.md
- **Coding Philosophy**: @~/.claude/workflows/coding-philosophy.md
- **Context Requirements**: @~/.claude/workflows/context-tools-ace.md
- **Context Requirements**: @~/.claude/workflows/context-tools.md
- **File Modification**: @~/.claude/workflows/file-modification.md
- **CLI Endpoints Config**: @.claude/cli-tools.json

View File

@@ -0,0 +1,302 @@
---
name: issue-plan-agent
description: |
Closed-loop issue planning agent combining ACE exploration and solution generation.
Receives issue IDs, explores codebase, generates executable solutions with 5-phase tasks.
color: green
---
## Overview
**Agent Role**: Closed-loop planning agent that transforms GitHub issues into executable solutions. Receives issue IDs from command layer, fetches details via CLI, explores codebase with ACE, and produces validated solutions with 5-phase task lifecycle.
**Core Capabilities**:
- ACE semantic search for intelligent code discovery
- Batch processing (1-3 issues per invocation)
- 5-phase task lifecycle (analyze → implement → test → optimize → commit)
- Conflict-aware planning (isolate file modifications across issues)
- Dependency DAG validation
- Auto-bind for single solution, return for selection on multiple
**Key Principle**: Generate tasks conforming to schema with quantified acceptance criteria.
---
## 1. Input & Execution
### 1.1 Input Context
```javascript
{
issue_ids: string[], // Issue IDs only (e.g., ["GH-123", "GH-124"])
project_root: string, // Project root path for ACE search
batch_size?: number, // Max issues per batch (default: 3)
}
```
**Note**: Agent receives IDs only. Fetch details via `ccw issue status <id> --json`.
### 1.2 Execution Flow
```
Phase 1: Issue Understanding (10%)
↓ Fetch details, extract requirements, determine complexity
Phase 2: ACE Exploration (30%)
↓ Semantic search, pattern discovery, dependency mapping
Phase 3: Solution Planning (45%)
↓ Task decomposition, 5-phase lifecycle, acceptance criteria
Phase 4: Validation & Output (15%)
↓ DAG validation, solution registration, binding
```
#### Phase 1: Issue Understanding
**Step 1**: Fetch issue details via CLI
```bash
ccw issue status <issue-id> --json
```
**Step 2**: Analyze and classify
```javascript
function analyzeIssue(issue) {
return {
issue_id: issue.id,
requirements: extractRequirements(issue.context),
scope: inferScope(issue.title, issue.context),
complexity: determineComplexity(issue) // Low | Medium | High
}
}
```
**Complexity Rules**:
| Complexity | Files | Tasks |
|------------|-------|-------|
| Low | 1-2 | 1-3 |
| Medium | 3-5 | 3-6 |
| High | 6+ | 5-10 |
#### Phase 2: ACE Exploration
**Primary**: ACE semantic search
```javascript
mcp__ace-tool__search_context({
project_root_path: project_root,
query: `Find code related to: ${issue.title}. Keywords: ${extractKeywords(issue)}`
})
```
**Exploration Checklist**:
- [ ] Identify relevant files (direct matches)
- [ ] Find related patterns (similar implementations)
- [ ] Map integration points
- [ ] Discover dependencies
- [ ] Locate test patterns
**Fallback Chain**: ACE → smart_search → Grep → rg → Glob
| Tool | When to Use |
|------|-------------|
| `mcp__ace-tool__search_context` | Semantic search (primary) |
| `mcp__ccw-tools__smart_search` | Symbol/pattern search |
| `Grep` | Exact regex matching |
| `rg` / `grep` | CLI fallback |
| `Glob` | File path discovery |
#### Phase 3: Solution Planning
**Multi-Solution Generation**:
Generate multiple candidate solutions when:
- Issue complexity is HIGH
- Multiple valid implementation approaches exist
- Trade-offs between approaches (performance vs simplicity, etc.)
| Condition | Solutions |
|-----------|-----------|
| Low complexity, single approach | 1 solution, auto-bind |
| Medium complexity, clear path | 1-2 solutions |
| High complexity, multiple approaches | 2-3 solutions, user selection |
**Solution Evaluation** (for each candidate):
```javascript
{
analysis: {
risk: "low|medium|high", // Implementation risk
impact: "low|medium|high", // Scope of changes
complexity: "low|medium|high" // Technical complexity
},
score: 0.0-1.0 // Overall quality score (higher = recommended)
}
```
**Selection Flow**:
1. Generate all candidate solutions
2. Evaluate and score each
3. Single solution → auto-bind
4. Multiple solutions → return `pending_selection` for user choice
**Task Decomposition** following schema:
```javascript
function decomposeTasks(issue, exploration) {
return groups.map(group => ({
id: `T${taskId++}`, // Pattern: ^T[0-9]+$
title: group.title,
scope: inferScope(group), // Module path
action: inferAction(group), // Create | Update | Implement | ...
description: group.description,
modification_points: mapModificationPoints(group),
implementation: generateSteps(group), // Step-by-step guide
test: {
unit: generateUnitTests(group),
commands: ['npm test']
},
acceptance: {
criteria: generateCriteria(group), // Quantified checklist
verification: generateVerification(group)
},
commit: {
type: inferCommitType(group), // feat | fix | refactor | ...
scope: inferScope(group),
message_template: generateCommitMsg(group)
},
depends_on: inferDependencies(group, tasks),
priority: calculatePriority(group) // 1-5 (1=highest)
}))
}
```
#### Phase 4: Validation & Output
**Validation**:
- DAG validation (no circular dependencies)
- Task validation (all 5 phases present)
- File isolation check (ensure minimal overlap across issues in batch)
**Solution Registration** (via file write):
**Step 1: Create solution files**
Write solution JSON to JSONL file (one line per solution):
```
.workflow/issues/solutions/{issue-id}.jsonl
```
**File Format** (JSONL - each line is a complete solution):
```
{"id":"SOL-GH-123-1","description":"...","approach":"...","analysis":{...},"score":0.85,"tasks":[...]}
{"id":"SOL-GH-123-2","description":"...","approach":"...","analysis":{...},"score":0.75,"tasks":[...]}
```
**Solution Schema** (must match CLI `Solution` interface):
```typescript
{
id: string; // Format: SOL-{issue-id}-{N}
description?: string;
approach?: string;
tasks: SolutionTask[];
analysis?: { risk, impact, complexity };
score?: number;
// Note: is_bound, created_at are added by CLI on read
}
```
**Write Operation**:
```javascript
// Append solution to JSONL file (one line per solution)
const solutionId = `SOL-${issueId}-${seq}`;
const solutionLine = JSON.stringify({ id: solutionId, ...solution });
// Read existing, append new line, write back
const filePath = `.workflow/issues/solutions/${issueId}.jsonl`;
const existing = existsSync(filePath) ? readFileSync(filePath) : '';
const newContent = existing.trimEnd() + (existing ? '\n' : '') + solutionLine + '\n';
Write({ file_path: filePath, content: newContent })
```
**Step 2: Bind decision**
- **Single solution** → Auto-bind: `ccw issue bind <issue-id> <solution-id>`
- **Multiple solutions** → Return for user selection (no bind)
---
## 2. Output Requirements
### 2.1 Generate Files (Primary)
**Solution file per issue**:
```
.workflow/issues/solutions/{issue-id}.jsonl
```
Each line is a solution JSON containing tasks. Schema: `cat .claude/workflows/cli-templates/schemas/solution-schema.json`
### 2.2 Binding
| Scenario | Action |
|----------|--------|
| Single solution | `ccw issue bind <issue-id> <solution-id>` (auto) |
| Multiple solutions | Register only, return for selection |
### 2.3 Return Summary
```json
{
"bound": [{ "issue_id": "...", "solution_id": "...", "task_count": N }],
"pending_selection": [{ "issue_id": "GH-123", "solutions": [{ "id": "SOL-GH-123-1", "description": "...", "task_count": N }] }]
}
```
---
## 3. Quality Standards
### 3.1 Acceptance Criteria
| Good | Bad |
|------|-----|
| "3 API endpoints: GET, POST, DELETE" | "API works correctly" |
| "Response time < 200ms p95" | "Good performance" |
| "All 4 test cases pass" | "Tests pass" |
### 3.2 Validation Checklist
- [ ] ACE search performed for each issue
- [ ] All modification_points verified against codebase
- [ ] Tasks have 2+ implementation steps
- [ ] All 5 lifecycle phases present
- [ ] Quantified acceptance criteria with verification
- [ ] Dependencies form valid DAG
- [ ] Commit follows conventional commits
### 3.3 Guidelines
**ALWAYS**:
1. Read schema first: `cat .claude/workflows/cli-templates/schemas/solution-schema.json`
2. Use ACE semantic search as PRIMARY exploration tool
3. Fetch issue details via `ccw issue status <id> --json`
4. Quantify acceptance.criteria with testable conditions
5. Validate DAG before output
6. Evaluate each solution with `analysis` and `score`
7. Write solutions to `.workflow/issues/solutions/{issue-id}.jsonl` (append mode)
8. For HIGH complexity: generate 2-3 candidate solutions
9. **Solution ID format**: `SOL-{issue-id}-{N}` (e.g., `SOL-GH-123-1`, `SOL-GH-123-2`)
**CONFLICT AVOIDANCE** (for batch processing of similar issues):
1. **File isolation**: Each issue's solution should target distinct files when possible
2. **Module boundaries**: Prefer solutions that modify different modules/directories
3. **Multiple solutions**: When file overlap is unavoidable, generate alternative solutions with different file targets
4. **Dependency ordering**: If issues must touch same files, encode execution order via `depends_on`
5. **Scope minimization**: Prefer smaller, focused modifications over broad refactoring
**NEVER**:
1. Execute implementation (return plan only)
2. Use vague criteria ("works correctly", "good performance")
3. Create circular dependencies
4. Generate more than 10 tasks per issue
5. **Bind when multiple solutions exist** - MUST check `solutions.length === 1` before calling `ccw issue bind`
**OUTPUT**:
1. Write solutions to `.workflow/issues/solutions/{issue-id}.jsonl` (JSONL format)
2. Single solution → `ccw issue bind <issue-id> <solution-id>`; Multiple → return only
3. Return JSON with `bound`, `pending_selection`

View File

@@ -0,0 +1,307 @@
---
name: issue-queue-agent
description: |
Solution ordering agent for queue formation with Gemini CLI conflict analysis.
Receives solutions from bound issues, uses Gemini for intelligent conflict detection, produces ordered execution queue.
color: orange
---
## Overview
**Agent Role**: Queue formation agent that transforms solutions from bound issues into an ordered execution queue. Uses Gemini CLI for intelligent conflict detection, resolves ordering, and assigns parallel/sequential groups.
**Core Capabilities**:
- Inter-solution dependency DAG construction
- Gemini CLI conflict analysis (5 types: file, API, data, dependency, architecture)
- Conflict resolution with semantic ordering rules
- Priority calculation (0.0-1.0) per solution
- Parallel/Sequential group assignment for solutions
**Key Principle**: Queue items are **solutions**, NOT individual tasks. Each executor receives a complete solution with all its tasks.
---
## 1. Input & Execution
### 1.1 Input Context
```javascript
{
solutions: [{
issue_id: string, // e.g., "ISS-20251227-001"
solution_id: string, // e.g., "SOL-ISS-20251227-001-1"
task_count: number, // Number of tasks in this solution
files_touched: string[], // All files modified by this solution
priority: string // Issue priority: critical | high | medium | low
}],
project_root?: string,
rebuild?: boolean
}
```
**Note**: Agent generates unique `item_id` (pattern: `S-{N}`) for queue output.
### 1.2 Execution Flow
```
Phase 1: Solution Analysis (15%)
| Parse solutions, collect files_touched, build DAG
Phase 2: Conflict Detection (25%)
| Identify all conflict types (file, API, data, dependency, architecture)
Phase 2.5: Clarification (15%)
| Surface ambiguous dependencies, BLOCK until resolved
Phase 3: Conflict Resolution (20%)
| Apply ordering rules, update DAG
Phase 4: Ordering & Grouping (25%)
| Topological sort, assign parallel/sequential groups
```
---
## 2. Processing Logic
### 2.1 Dependency Graph
**Build DAG from solutions**:
1. Create node for each solution with `inDegree: 0` and `outEdges: []`
2. Build file→solutions mapping from `files_touched`
3. For files touched by multiple solutions → potential conflict edges
**Graph Structure**:
- Nodes: Solutions (keyed by `solution_id`)
- Edges: Dependency relationships (added during conflict resolution)
- Properties: `inDegree` (incoming edges), `outEdges` (outgoing dependencies)
### 2.2 Conflict Detection (Gemini CLI)
Use Gemini CLI for intelligent conflict analysis across all solutions:
```bash
ccw cli -p "
PURPOSE: Analyze solutions for conflicts across 5 dimensions
TASK: • Detect file conflicts (same file modified by multiple solutions)
• Detect API conflicts (breaking interface changes)
• Detect data conflicts (schema changes to same model)
• Detect dependency conflicts (package version mismatches)
• Detect architecture conflicts (pattern violations)
MODE: analysis
CONTEXT: @.workflow/issues/solutions/**/*.jsonl | Solution data: \${SOLUTIONS_JSON}
EXPECTED: JSON array of conflicts with type, severity, solutions, recommended_order
RULES: $(cat ~/.claude/workflows/cli-templates/protocols/analysis-protocol.md) | Severity: high (API/data) > medium (file/dependency) > low (architecture)
" --tool gemini --mode analysis --cd .workflow/issues
```
**Placeholder**: `${SOLUTIONS_JSON}` = serialized solutions array from bound issues
**Conflict Types & Severity**:
| Type | Severity | Trigger |
|------|----------|---------|
| `file_conflict` | medium | Multiple solutions modify same file |
| `api_conflict` | high | Breaking interface changes |
| `data_conflict` | high | Schema changes to same model |
| `dependency_conflict` | medium | Package version mismatches |
| `architecture_conflict` | low | Pattern violations |
**Output per conflict**:
```json
{ "type": "...", "severity": "...", "solutions": [...], "recommended_order": [...], "rationale": "..." }
```
### 2.2.5 Clarification (BLOCKING)
**Purpose**: Surface ambiguous dependencies for user/system clarification
**Trigger Conditions**:
- High severity conflicts without `recommended_order` from Gemini analysis
- Circular dependencies detected
- Multiple valid resolution strategies
**Clarification Generation**:
For each unresolved high-severity conflict:
1. Generate conflict ID: `CFT-{N}`
2. Build question: `"{type}: Which solution should execute first?"`
3. List options with solution summaries (issue title + task count)
4. Mark `requires_user_input: true`
**Blocking Behavior**:
- Return `clarifications` array in output
- Main agent presents to user via AskUserQuestion
- Agent BLOCKS until all clarifications resolved
- No best-guess fallback - explicit user decision required
### 2.3 Resolution Rules
| Priority | Rule | Example |
|----------|------|---------|
| 1 | Higher issue priority first | critical > high > medium > low |
| 2 | Foundation solutions first | Solutions with fewer dependencies |
| 3 | More tasks = higher priority | Solutions with larger impact |
| 4 | Create before extend | S1:Creates module -> S2:Extends it |
### 2.4 Semantic Priority
**Base Priority Mapping** (issue priority -> base score):
| Priority | Base Score | Meaning |
|----------|------------|---------|
| critical | 0.9 | Highest |
| high | 0.7 | High |
| medium | 0.5 | Medium |
| low | 0.3 | Low |
**Task-count Boost** (applied to base score):
| Factor | Boost |
|--------|-------|
| task_count >= 5 | +0.1 |
| task_count >= 3 | +0.05 |
| Foundation scope | +0.1 |
| Fewer dependencies | +0.05 |
**Formula**: `semantic_priority = clamp(baseScore + sum(boosts), 0.0, 1.0)`
### 2.5 Group Assignment
- **Parallel (P*)**: Solutions with no file overlaps between them
- **Sequential (S*)**: Solutions that share files must run in order
---
## 3. Output Requirements
### 3.1 Generate Files (Primary)
**Queue files**:
```
.workflow/issues/queues/{queue-id}.json # Full queue with solutions, conflicts, groups
.workflow/issues/queues/index.json # Update with new queue entry
```
Queue ID: Use the Queue ID provided in prompt (do NOT generate new one)
Queue Item ID format: `S-N` (S-1, S-2, S-3, ...)
### 3.2 Queue File Schema
```json
{
"id": "QUE-20251227-143000",
"status": "active",
"solutions": [
{
"item_id": "S-1",
"issue_id": "ISS-20251227-003",
"solution_id": "SOL-ISS-20251227-003-1",
"status": "pending",
"execution_order": 1,
"execution_group": "P1",
"depends_on": [],
"semantic_priority": 0.8,
"files_touched": ["src/auth.ts", "src/utils.ts"],
"task_count": 3
}
],
"conflicts": [
{
"type": "file_conflict",
"file": "src/auth.ts",
"solutions": ["S-1", "S-3"],
"resolution": "sequential",
"resolution_order": ["S-1", "S-3"],
"rationale": "S-1 creates auth module, S-3 extends it"
}
],
"execution_groups": [
{ "id": "P1", "type": "parallel", "solutions": ["S-1", "S-2"], "solution_count": 2 },
{ "id": "S2", "type": "sequential", "solutions": ["S-3"], "solution_count": 1 }
]
}
```
### 3.3 Return Summary (Brief)
Return brief summaries; full conflict details in separate files:
```json
{
"queue_id": "QUE-20251227-143000",
"total_solutions": N,
"total_tasks": N,
"execution_groups": [{ "id": "P1", "type": "parallel", "count": N }],
"conflicts_summary": [{
"id": "CFT-001",
"type": "api_conflict",
"severity": "high",
"summary": "Brief 1-line description",
"resolution": "sequential",
"details_path": ".workflow/issues/conflicts/CFT-001.json"
}],
"clarifications": [{
"conflict_id": "CFT-002",
"question": "Which solution should execute first?",
"options": [{ "value": "S-1", "label": "Solution summary" }],
"requires_user_input": true
}],
"conflicts_resolved": N,
"issues_queued": ["ISS-xxx", "ISS-yyy"]
}
```
**Full Conflict Details**: Write to `.workflow/issues/conflicts/{conflict-id}.json`
---
## 4. Quality Standards
### 4.1 Validation Checklist
- [ ] No circular dependencies between solutions
- [ ] All file conflicts resolved
- [ ] Solutions in same parallel group have NO file overlaps
- [ ] Semantic priority calculated for all solutions
- [ ] Dependencies ordered correctly
### 4.2 Error Handling
| Scenario | Action |
|----------|--------|
| Circular dependency | Abort, report cycles |
| Resolution creates cycle | Flag for manual resolution |
| Missing solution reference | Skip and warn |
| Empty solution list | Return empty queue |
### 4.3 Guidelines
**ALWAYS**:
1. Build dependency graph before ordering
2. Detect file overlaps between solutions
3. Apply resolution rules consistently
4. Calculate semantic priority for all solutions
5. Include rationale for conflict resolutions
6. Validate ordering before output
**NEVER**:
1. Execute solutions (ordering only)
2. Ignore circular dependencies
3. Skip conflict detection
4. Output invalid DAG
5. Merge conflicting solutions in parallel group
6. Split tasks from their solution
**WRITE** (exactly 2 files):
- `.workflow/issues/queues/{Queue ID}.json` - Full queue with solutions, groups
- `.workflow/issues/queues/index.json` - Update with new queue entry
- Use Queue ID from prompt, do NOT generate new one
**RETURN** (summary + unresolved conflicts):
```json
{
"queue_id": "QUE-xxx",
"total_solutions": N,
"total_tasks": N,
"execution_groups": [{"id": "P1", "type": "parallel", "count": N}],
"issues_queued": ["ISS-xxx"],
"clarifications": [{"conflict_id": "CFT-1", "question": "...", "options": [...]}]
}
```
- `clarifications`: Only present if unresolved high-severity conflicts exist
- No markdown, no prose - PURE JSON only

View File

@@ -0,0 +1,427 @@
---
name: issue:discover
description: Discover potential issues from multiple perspectives (bug, UX, test, quality, security, performance, maintainability, best-practices) using CLI explore. Supports Exa external research for security and best-practices perspectives.
argument-hint: "<path-pattern> [--perspectives=bug,ux,...] [--external]"
allowed-tools: SlashCommand(*), TodoWrite(*), Read(*), Bash(*), Task(*), AskUserQuestion(*), Glob(*), Grep(*)
---
# Issue Discovery Command
## Quick Start
```bash
# Discover issues in specific module (interactive perspective selection)
/issue:discover src/auth/**
# Discover with specific perspectives
/issue:discover src/payment/** --perspectives=bug,security,test
# Discover with external research for all perspectives
/issue:discover src/api/** --external
# Discover in multiple modules
/issue:discover src/auth/**,src/payment/**
```
**Discovery Scope**: Specified modules/files only
**Output Directory**: `.workflow/issues/discoveries/{discovery-id}/`
**Available Perspectives**: bug, ux, test, quality, security, performance, maintainability, best-practices
**Exa Integration**: Auto-enabled for security and best-practices perspectives
**CLI Tools**: Gemini → Qwen → Codex (fallback chain)
## What & Why
### Core Concept
Multi-perspective issue discovery orchestrator that explores code from different angles to identify potential bugs, UX improvements, test gaps, and other actionable items. Unlike code review (which assesses existing code quality), discovery focuses on **finding opportunities for improvement and potential problems**.
**vs Code Review**:
- **Code Review** (`review-module-cycle`): Evaluates code quality against standards
- **Issue Discovery** (`issue:discover`): Finds actionable issues, bugs, and improvement opportunities
### Value Proposition
1. **Proactive Issue Detection**: Find problems before they become bugs
2. **Multi-Perspective Analysis**: Each perspective surfaces different types of issues
3. **External Benchmarking**: Compare against industry best practices via Exa
4. **Direct Issue Integration**: Discoveries can be exported to issue tracker
5. **Dashboard Management**: View, filter, and export discoveries via CCW dashboard
## How It Works
### Execution Flow
```
Phase 1: Discovery & Initialization
└─ Parse target pattern, create session, initialize output structure
Phase 2: Interactive Perspective Selection
└─ AskUserQuestion for perspective selection (or use --perspectives)
Phase 3: Parallel Perspective Analysis
├─ Launch N @cli-explore-agent instances (one per perspective)
├─ Security & Best-Practices auto-trigger Exa research
├─ Agent writes perspective JSON, returns summary
└─ Update discovery-progress.json
Phase 4: Aggregation & Prioritization
├─ Collect agent return summaries
├─ Load perspective JSON files
├─ Merge findings, deduplicate by file+line
└─ Calculate priority scores
Phase 5: Issue Generation & Summary
├─ Convert high-priority discoveries to issue format
├─ Write to discovery-issues.jsonl
├─ Generate single summary.md from agent returns
└─ Update discovery-state.json to complete
```
## Perspectives
### Available Perspectives
| Perspective | Focus | Categories | Exa |
|-------------|-------|------------|-----|
| **bug** | Potential Bugs | edge-case, null-check, resource-leak, race-condition, boundary, exception-handling | - |
| **ux** | User Experience | error-message, loading-state, feedback, accessibility, interaction, consistency | - |
| **test** | Test Coverage | missing-test, edge-case-test, integration-gap, coverage-hole, assertion-quality | - |
| **quality** | Code Quality | complexity, duplication, naming, documentation, code-smell, readability | - |
| **security** | Security Issues | injection, auth, encryption, input-validation, data-exposure, access-control | ✓ |
| **performance** | Performance | n-plus-one, memory-usage, caching, algorithm, blocking-operation, resource | - |
| **maintainability** | Maintainability | coupling, cohesion, tech-debt, extensibility, module-boundary, interface-design | - |
| **best-practices** | Best Practices | convention, pattern, framework-usage, anti-pattern, industry-standard | ✓ |
### Interactive Perspective Selection
When no `--perspectives` flag is provided, the command uses AskUserQuestion:
```javascript
AskUserQuestion({
questions: [{
question: "Select primary discovery focus:",
header: "Focus",
multiSelect: false,
options: [
{ label: "Bug + Test + Quality", description: "Quick scan: potential bugs, test gaps, code quality (Recommended)" },
{ label: "Security + Performance", description: "System audit: security issues, performance bottlenecks" },
{ label: "Maintainability + Best-practices", description: "Long-term health: coupling, tech debt, conventions" },
{ label: "Full analysis", description: "All 7 perspectives (comprehensive, takes longer)" }
]
}]
})
```
**Recommended Combinations**:
- Quick scan: bug, test, quality
- Full analysis: all perspectives
- Security audit: security, bug, quality
## Core Responsibilities
### Orchestrator
**Phase 1: Discovery & Initialization**
```javascript
// Step 1: Parse target pattern and resolve files
const resolvedFiles = await expandGlobPattern(targetPattern);
if (resolvedFiles.length === 0) {
throw new Error(`No files matched pattern: ${targetPattern}`);
}
// Step 2: Generate discovery ID
const discoveryId = `DSC-${formatDate(new Date(), 'YYYYMMDD-HHmmss')}`;
// Step 3: Create output directory
const outputDir = `.workflow/issues/discoveries/${discoveryId}`;
await mkdir(outputDir, { recursive: true });
await mkdir(`${outputDir}/perspectives`, { recursive: true });
// Step 4: Initialize unified discovery state (merged state+progress)
await writeJson(`${outputDir}/discovery-state.json`, {
discovery_id: discoveryId,
target_pattern: targetPattern,
phase: "initialization",
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
target: { files_count: { total: resolvedFiles.length }, project: {} },
perspectives: [], // filled after selection: [{name, status, findings}]
external_research: { enabled: false, completed: false },
results: { total_findings: 0, issues_generated: 0, priority_distribution: {} }
});
```
**Phase 2: Perspective Selection**
```javascript
// Check for --perspectives flag
let selectedPerspectives = [];
if (args.perspectives) {
selectedPerspectives = args.perspectives.split(',').map(p => p.trim());
} else {
// Interactive selection via AskUserQuestion
const response = await AskUserQuestion({...});
selectedPerspectives = parseSelectedPerspectives(response);
}
// Validate and update state
await updateDiscoveryState(outputDir, {
'metadata.perspectives': selectedPerspectives,
phase: 'parallel'
});
```
**Phase 3: Parallel Perspective Analysis**
Launch N agents in parallel (one per selected perspective):
```javascript
// Launch agents in parallel - agents write JSON and return summary
const agentPromises = selectedPerspectives.map(perspective =>
Task({
subagent_type: "cli-explore-agent",
run_in_background: false,
description: `Discover ${perspective} issues`,
prompt: buildPerspectivePrompt(perspective, discoveryId, resolvedFiles, outputDir)
})
);
// Wait for all agents - collect their return summaries
const results = await Promise.all(agentPromises);
// results contain agent summaries for final report
```
**Phase 4: Aggregation & Prioritization**
```javascript
// Load all perspective JSON files written by agents
const allFindings = [];
for (const perspective of selectedPerspectives) {
const jsonPath = `${outputDir}/perspectives/${perspective}.json`;
if (await fileExists(jsonPath)) {
const data = await readJson(jsonPath);
allFindings.push(...data.findings.map(f => ({ ...f, perspective })));
}
}
// Deduplicate and prioritize
const prioritizedFindings = deduplicateAndPrioritize(allFindings);
// Update unified state
await updateDiscoveryState(outputDir, {
phase: 'aggregation',
'results.total_findings': prioritizedFindings.length,
'results.priority_distribution': countByPriority(prioritizedFindings)
});
```
**Phase 5: Issue Generation & Summary**
```javascript
// Convert high-priority findings to issues
const issueWorthy = prioritizedFindings.filter(f =>
f.priority === 'critical' || f.priority === 'high' || f.priority_score >= 0.7
);
// Write discovery-issues.jsonl
await writeJsonl(`${outputDir}/discovery-issues.jsonl`, issues);
// Generate single summary.md from agent return summaries
// Orchestrator briefly summarizes what agents returned (NO detailed reports)
await writeSummaryFromAgentReturns(outputDir, results, prioritizedFindings, issues);
// Update final state
await updateDiscoveryState(outputDir, {
phase: 'complete',
updated_at: new Date().toISOString(),
'results.issues_generated': issues.length
});
```
### Output File Structure
```
.workflow/issues/discoveries/
├── index.json # Discovery session index
└── {discovery-id}/
├── discovery-state.json # Unified state (merged state+progress)
├── perspectives/
│ └── {perspective}.json # Per-perspective findings
├── external-research.json # Exa research results (if enabled)
├── discovery-issues.jsonl # Generated candidate issues
└── summary.md # Single summary (from agent returns)
```
### Schema References
**External Schema Files** (agent MUST read and follow exactly):
| Schema | Path | Purpose |
|--------|------|---------|
| **Discovery State** | `~/.claude/workflows/cli-templates/schemas/discovery-state-schema.json` | Session state machine |
| **Discovery Finding** | `~/.claude/workflows/cli-templates/schemas/discovery-finding-schema.json` | Perspective analysis results |
### Agent Invocation Template
**Perspective Analysis Agent**:
```javascript
Task({
subagent_type: "cli-explore-agent",
run_in_background: false,
description: `Discover ${perspective} issues`,
prompt: `
## Task Objective
Discover potential ${perspective} issues in specified module files.
## Discovery Context
- Discovery ID: ${discoveryId}
- Perspective: ${perspective}
- Target Pattern: ${targetPattern}
- Resolved Files: ${resolvedFiles.length} files
- Output Directory: ${outputDir}
## MANDATORY FIRST STEPS
1. Read discovery state: ${outputDir}/discovery-state.json
2. Read schema: ~/.claude/workflows/cli-templates/schemas/discovery-finding-schema.json
3. Analyze target files for ${perspective} concerns
## Output Requirements
**1. Write JSON file**: ${outputDir}/perspectives/${perspective}.json
- Follow discovery-finding-schema.json exactly
- Each finding: id, title, priority, category, description, file, line, snippet, suggested_issue, confidence
**2. Return summary** (DO NOT write report file):
- Return a brief text summary of findings
- Include: total findings, priority breakdown, key issues
- This summary will be used by orchestrator for final report
## Perspective-Specific Guidance
${getPerspectiveGuidance(perspective)}
## Success Criteria
- [ ] JSON written to ${outputDir}/perspectives/${perspective}.json
- [ ] Summary returned with findings count and key issues
- [ ] Each finding includes actionable suggested_issue
- [ ] Priority uses lowercase enum: critical/high/medium/low
`
})
```
**Exa Research Agent** (for security and best-practices):
```javascript
Task({
subagent_type: "cli-explore-agent",
run_in_background: false,
description: `External research for ${perspective} via Exa`,
prompt: `
## Task Objective
Research industry best practices for ${perspective} using Exa search
## Research Steps
1. Read project tech stack: .workflow/project-tech.json
2. Use Exa to search for best practices
3. Synthesize findings relevant to this project
## Output Requirements
**1. Write JSON file**: ${outputDir}/external-research.json
- Include sources, key_findings, gap_analysis, recommendations
**2. Return summary** (DO NOT write report file):
- Brief summary of external research findings
- Key recommendations for the project
## Success Criteria
- [ ] JSON written to ${outputDir}/external-research.json
- [ ] Summary returned with key recommendations
- [ ] Findings are relevant to project's tech stack
`
})
```
### Perspective Guidance Reference
```javascript
function getPerspectiveGuidance(perspective) {
const guidance = {
bug: `
Focus: Null checks, edge cases, resource leaks, race conditions, boundary conditions, exception handling
Priority: Critical=data corruption/crash, High=malfunction, Medium=edge case issues, Low=minor
`,
ux: `
Focus: Error messages, loading states, feedback, accessibility, interaction patterns, form validation
Priority: Critical=inaccessible, High=confusing, Medium=inconsistent, Low=cosmetic
`,
test: `
Focus: Missing unit tests, edge case coverage, integration gaps, assertion quality, test isolation
Priority: Critical=no security tests, High=no core logic tests, Medium=weak coverage, Low=minor gaps
`,
quality: `
Focus: Complexity, duplication, naming, documentation, code smells, readability
Priority: Critical=unmaintainable, High=significant issues, Medium=naming/docs, Low=minor refactoring
`,
security: `
Focus: Input validation, auth/authz, injection, XSS/CSRF, data exposure, access control
Priority: Critical=auth bypass/injection, High=missing authz, Medium=weak validation, Low=headers
`,
performance: `
Focus: N+1 queries, memory leaks, caching, algorithm efficiency, blocking operations
Priority: Critical=memory leaks, High=N+1/inefficient, Medium=missing cache, Low=minor optimization
`,
maintainability: `
Focus: Coupling, interface design, tech debt, extensibility, module boundaries, configuration
Priority: Critical=unrelated code changes, High=unclear boundaries, Medium=coupling, Low=refactoring
`,
'best-practices': `
Focus: Framework conventions, language patterns, anti-patterns, deprecated APIs, coding standards
Priority: Critical=anti-patterns causing bugs, High=convention violations, Medium=style, Low=cosmetic
`
};
return guidance[perspective] || 'General code discovery analysis';
}
```
## Dashboard Integration
### Viewing Discoveries
Open CCW dashboard to manage discoveries:
```bash
ccw view
```
Navigate to **Issues > Discovery** to:
- View all discovery sessions
- Filter findings by perspective and priority
- Preview finding details
- Select and export findings as issues
### Exporting to Issues
From the dashboard, select findings and click "Export as Issues" to:
1. Convert discoveries to standard issue format
2. Append to `.workflow/issues/issues.jsonl`
3. Set status to `registered`
4. Continue with `/issue:plan` workflow
## Related Commands
```bash
# After discovery, plan solutions for exported issues
/issue:plan DSC-001,DSC-002,DSC-003
# Or use interactive management
/issue:manage
```
## Best Practices
1. **Start Focused**: Begin with specific modules rather than entire codebase
2. **Use Quick Scan First**: Start with bug, test, quality for fast results
3. **Review Before Export**: Not all discoveries warrant issues - use dashboard to filter
4. **Combine Perspectives**: Run related perspectives together (e.g., security + bug)
5. **Enable Exa for New Tech**: When using unfamiliar frameworks, enable external research

View File

@@ -0,0 +1,307 @@
---
name: execute
description: Execute queue with codex using DAG-based parallel orchestration (solution-level)
argument-hint: ""
allowed-tools: TodoWrite(*), Bash(*), Read(*), AskUserQuestion(*)
---
# Issue Execute Command (/issue:execute)
## Overview
Minimal orchestrator that dispatches **solution IDs** to executors. Each executor receives a complete solution with all its tasks.
**Design Principles:**
- `queue dag` → returns parallel batches with solution IDs (S-1, S-2, ...)
- `detail <id>` → READ-ONLY solution fetch (returns full solution with all tasks)
- `done <id>` → update solution completion status
- No race conditions: status changes only via `done`
- **Executor handles all tasks within a solution sequentially**
## Usage
```bash
/issue:execute
```
**Parallelism**: Determined automatically by task dependency DAG (no manual control)
**Executor & Dry-run**: Selected via interactive prompt (AskUserQuestion)
## Execution Flow
```
Phase 1: Get DAG & User Selection
├─ ccw issue queue dag → { parallel_batches: [["S-1","S-2"], ["S-3"]] }
└─ AskUserQuestion → executor type (codex|gemini|agent), dry-run mode
Phase 2: Dispatch Parallel Batch (DAG-driven)
├─ Parallelism determined by DAG (no manual limit)
├─ For each solution ID in batch (parallel - all at once):
│ ├─ Executor calls: ccw issue detail <id> (READ-ONLY)
│ ├─ Executor gets FULL SOLUTION with all tasks
│ ├─ Executor implements all tasks sequentially (T1 → T2 → T3)
│ ├─ Executor tests + commits per task
│ └─ Executor calls: ccw issue done <id>
└─ Wait for batch completion
Phase 3: Next Batch
└─ ccw issue queue dag → check for newly-ready solutions
```
## Implementation
### Phase 1: Get DAG & User Selection
```javascript
// Get dependency graph and parallel batches
const dagJson = Bash(`ccw issue queue dag`).trim();
const dag = JSON.parse(dagJson);
if (dag.error || dag.ready_count === 0) {
console.log(dag.error || 'No solutions ready for execution');
console.log('Use /issue:queue to form a queue first');
return;
}
console.log(`
## Queue DAG (Solution-Level)
- Total Solutions: ${dag.total}
- Ready: ${dag.ready_count}
- Completed: ${dag.completed_count}
- Parallel in batch 1: ${dag.parallel_batches[0]?.length || 0}
`);
// Interactive selection via AskUserQuestion
const answer = AskUserQuestion({
questions: [
{
question: 'Select executor type:',
header: 'Executor',
multiSelect: false,
options: [
{ label: 'Codex (Recommended)', description: 'Autonomous coding with full write access' },
{ label: 'Gemini', description: 'Large context analysis and implementation' },
{ label: 'Agent', description: 'Claude Code sub-agent for complex tasks' }
]
},
{
question: 'Execution mode:',
header: 'Mode',
multiSelect: false,
options: [
{ label: 'Execute (Recommended)', description: 'Run all ready solutions' },
{ label: 'Dry-run', description: 'Show DAG and batches without executing' }
]
}
]
});
const executor = answer['Executor'].toLowerCase().split(' ')[0]; // codex|gemini|agent
const isDryRun = answer['Mode'].includes('Dry-run');
// Dry run mode
if (isDryRun) {
console.log('### Parallel Batches (Dry-run):\n');
dag.parallel_batches.forEach((batch, i) => {
console.log(`Batch ${i + 1}: ${batch.join(', ')}`);
});
return;
}
```
### Phase 2: Dispatch Parallel Batch (DAG-driven)
```javascript
// Parallelism determined by DAG - no manual limit
// All solutions in same batch have NO file conflicts and can run in parallel
const batch = dag.parallel_batches[0] || [];
// Initialize TodoWrite
TodoWrite({
todos: batch.map(id => ({
content: `Execute solution ${id}`,
status: 'pending',
activeForm: `Executing solution ${id}`
}))
});
console.log(`\n### Executing Solutions (DAG batch 1): ${batch.join(', ')}`);
// Launch ALL solutions in batch in parallel (DAG guarantees no conflicts)
const executions = batch.map(solutionId => {
updateTodo(solutionId, 'in_progress');
return dispatchExecutor(solutionId, executor);
});
await Promise.all(executions);
batch.forEach(id => updateTodo(id, 'completed'));
```
### Executor Dispatch
```javascript
function dispatchExecutor(solutionId, executorType) {
// Executor fetches FULL SOLUTION via READ-ONLY detail command
// Executor handles all tasks within solution sequentially
// Then reports completion via done command
const prompt = `
## Execute Solution ${solutionId}
### Step 1: Get Solution (read-only)
\`\`\`bash
ccw issue detail ${solutionId}
\`\`\`
### Step 2: Execute All Tasks Sequentially
The detail command returns a FULL SOLUTION with all tasks.
Execute each task in order (T1 → T2 → T3 → ...):
For each task:
1. Follow task.implementation steps
2. Run task.test commands
3. Verify task.acceptance criteria
4. Commit using task.commit specification
### Step 3: Report Completion
When ALL tasks in solution are done:
\`\`\`bash
ccw issue done ${solutionId} --result '{"summary": "...", "files_modified": [...], "tasks_completed": N}'
\`\`\`
If any task failed:
\`\`\`bash
ccw issue done ${solutionId} --fail --reason "Task TX failed: ..."
\`\`\`
`;
if (executorType === 'codex') {
return Bash(
`ccw cli -p "${escapePrompt(prompt)}" --tool codex --mode write --id exec-${solutionId}`,
{ timeout: 7200000, run_in_background: true } // 2hr for full solution
);
} else if (executorType === 'gemini') {
return Bash(
`ccw cli -p "${escapePrompt(prompt)}" --tool gemini --mode write --id exec-${solutionId}`,
{ timeout: 3600000, run_in_background: true }
);
} else {
return Task({
subagent_type: 'code-developer',
run_in_background: false,
description: `Execute solution ${solutionId}`,
prompt: prompt
});
}
}
```
### Phase 3: Check Next Batch
```javascript
// Refresh DAG after batch completes
const refreshedDag = JSON.parse(Bash(`ccw issue queue dag`).trim());
console.log(`
## Batch Complete
- Solutions Completed: ${refreshedDag.completed_count}/${refreshedDag.total}
- Next ready: ${refreshedDag.ready_count}
`);
if (refreshedDag.ready_count > 0) {
console.log('Run `/issue:execute` again for next batch.');
}
```
## Parallel Execution Model
```
┌─────────────────────────────────────────────────────────────┐
│ Orchestrator │
├─────────────────────────────────────────────────────────────┤
│ 1. ccw issue queue dag │
│ → { parallel_batches: [["S-1","S-2"], ["S-3"]] } │
│ │
│ 2. Dispatch batch 1 (parallel): │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ Executor 1 │ │ Executor 2 │ │
│ │ detail S-1 │ │ detail S-2 │ │
│ │ → gets full solution │ │ → gets full solution │ │
│ │ [T1→T2→T3 sequential]│ │ [T1→T2 sequential] │ │
│ │ done S-1 │ │ done S-2 │ │
│ └──────────────────────┘ └──────────────────────┘ │
│ │
│ 3. ccw issue queue dag (refresh) │
│ → S-3 now ready (S-1 completed, file conflict resolved) │
└─────────────────────────────────────────────────────────────┘
```
**Why this works for parallel:**
- `detail <id>` is READ-ONLY → no race conditions
- Each executor handles **all tasks within a solution** sequentially
- `done <id>` updates only its own solution status
- `queue dag` recalculates ready solutions after each batch
- Solutions in same batch have NO file conflicts
## CLI Endpoint Contract
### `ccw issue queue dag`
Returns dependency graph with parallel batches (solution-level):
```json
{
"queue_id": "QUE-...",
"total": 3,
"ready_count": 2,
"completed_count": 0,
"nodes": [
{ "id": "S-1", "issue_id": "ISS-xxx", "status": "pending", "ready": true, "task_count": 3 },
{ "id": "S-2", "issue_id": "ISS-yyy", "status": "pending", "ready": true, "task_count": 2 },
{ "id": "S-3", "issue_id": "ISS-zzz", "status": "pending", "ready": false, "depends_on": ["S-1"] }
],
"parallel_batches": [["S-1", "S-2"], ["S-3"]]
}
```
### `ccw issue detail <item_id>`
Returns FULL SOLUTION with all tasks (READ-ONLY):
```json
{
"item_id": "S-1",
"issue_id": "ISS-xxx",
"solution_id": "SOL-xxx",
"status": "pending",
"solution": {
"id": "SOL-xxx",
"approach": "...",
"tasks": [
{ "id": "T1", "title": "...", "implementation": [...], "test": {...} },
{ "id": "T2", "title": "...", "implementation": [...], "test": {...} },
{ "id": "T3", "title": "...", "implementation": [...], "test": {...} }
],
"exploration_context": { "relevant_files": [...] }
},
"execution_hints": { "executor": "codex", "estimated_minutes": 180 }
}
```
### `ccw issue done <item_id>`
Marks solution completed/failed, updates queue state, checks for queue completion.
## Error Handling
| Error | Resolution |
|-------|------------|
| No queue | Run /issue:queue first |
| No ready solutions | Dependencies blocked, check DAG |
| Executor timeout | Solution not marked done, can retry |
| Solution failure | Use `ccw issue retry` to reset |
| Partial task failure | Executor reports which task failed via `done --fail` |
## Related Commands
- `/issue:plan` - Plan issues with solutions
- `/issue:queue` - Form execution queue
- `ccw issue queue dag` - View dependency graph
- `ccw issue detail <id>` - View task details
- `ccw issue retry` - Reset failed tasks

View File

@@ -0,0 +1,309 @@
---
name: new
description: Create structured issue from GitHub URL or text description
argument-hint: "<github-url | text-description> [--priority 1-5]"
allowed-tools: TodoWrite(*), Bash(*), Read(*), AskUserQuestion(*), mcp__ace-tool__search_context(*)
---
# Issue New Command (/issue:new)
## Core Principle
**Requirement Clarity Detection** → Ask only when needed
```
Clear Input (GitHub URL, structured text) → Direct creation
Unclear Input (vague description) → Minimal clarifying questions
```
## Issue Structure
```typescript
interface Issue {
id: string; // GH-123 or ISS-YYYYMMDD-HHMMSS
title: string;
status: 'registered' | 'planned' | 'queued' | 'in_progress' | 'completed' | 'failed';
priority: number; // 1 (critical) to 5 (low)
context: string; // Problem description (single source of truth)
source: 'github' | 'text' | 'discovery';
source_url?: string;
labels?: string[];
// Optional structured fields
expected_behavior?: string;
actual_behavior?: string;
affected_components?: string[];
// Feedback history (failures + human clarifications)
feedback?: {
type: 'failure' | 'clarification' | 'rejection';
stage: string; // new/plan/execute
content: string;
created_at: string;
}[];
// Solution binding
bound_solution_id: string | null;
// Timestamps
created_at: string;
updated_at: string;
}
```
## Quick Reference
```bash
# Clear inputs - direct creation
/issue:new https://github.com/owner/repo/issues/123
/issue:new "Login fails with special chars. Expected: success. Actual: 500 error"
# Vague input - will ask clarifying questions
/issue:new "something wrong with auth"
```
## Implementation
### Phase 1: Input Analysis & Clarity Detection
```javascript
const input = userInput.trim();
const flags = parseFlags(userInput); // --priority
// Detect input type and clarity
const isGitHubUrl = input.match(/github\.com\/[\w-]+\/[\w-]+\/issues\/\d+/);
const isGitHubShort = input.match(/^#(\d+)$/);
const hasStructure = input.match(/(expected|actual|affects|steps):/i);
// Clarity score: 0-3
let clarityScore = 0;
if (isGitHubUrl || isGitHubShort) clarityScore = 3; // GitHub = fully clear
else if (hasStructure) clarityScore = 2; // Structured text = clear
else if (input.length > 50) clarityScore = 1; // Long text = somewhat clear
else clarityScore = 0; // Vague
let issueData = {};
```
### Phase 2: Data Extraction (GitHub or Text)
```javascript
if (isGitHubUrl || isGitHubShort) {
// GitHub - fetch via gh CLI
const result = Bash(`gh issue view ${extractIssueRef(input)} --json number,title,body,labels,url`);
const gh = JSON.parse(result);
issueData = {
id: `GH-${gh.number}`,
title: gh.title,
source: 'github',
source_url: gh.url,
labels: gh.labels.map(l => l.name),
context: gh.body?.substring(0, 500) || gh.title,
...parseMarkdownBody(gh.body)
};
} else {
// Text description
issueData = {
id: `ISS-${new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14)}`,
source: 'text',
...parseTextDescription(input)
};
}
```
### Phase 3: Lightweight Context Hint (Conditional)
```javascript
// ACE search ONLY for medium clarity (1-2) AND missing components
// Skip for: GitHub (has context), vague (needs clarification first)
// Note: Deep exploration happens in /issue:plan, this is just a quick hint
if (clarityScore >= 1 && clarityScore <= 2 && !issueData.affected_components?.length) {
const keywords = extractKeywords(issueData.context);
if (keywords.length >= 2) {
try {
const aceResult = mcp__ace-tool__search_context({
project_root_path: process.cwd(),
query: keywords.slice(0, 3).join(' ')
});
issueData.affected_components = aceResult.files?.slice(0, 3) || [];
} catch {
// ACE failure is non-blocking
}
}
}
```
### Phase 4: Conditional Clarification (Only if Unclear)
```javascript
// ONLY ask questions if clarity is low - simple open-ended prompt
if (clarityScore < 2 && (!issueData.context || issueData.context.length < 20)) {
const answer = AskUserQuestion({
questions: [{
question: 'Please describe the issue in more detail:',
header: 'Clarify',
multiSelect: false,
options: [
{ label: 'Provide details', description: 'Describe what, where, and expected behavior' }
]
}]
});
// Use custom text input (via "Other")
if (answer.customText) {
issueData.context = answer.customText;
issueData.title = answer.customText.split(/[.\n]/)[0].substring(0, 60);
issueData.feedback = [{
type: 'clarification',
stage: 'new',
content: answer.customText,
created_at: new Date().toISOString()
}];
}
}
```
### Phase 5: Create Issue
**Summary Display:**
- Show ID, title, source, affected files (if any)
**Confirmation** (only for vague inputs, clarityScore < 2):
- Use `AskUserQuestion` to confirm before creation
**Issue Creation** (via CLI endpoint):
```bash
ccw issue create --data '{"title":"...", "context":"...", "priority":3, ...}'
```
**CLI Endpoint Features:**
| Feature | Description |
|---------|-------------|
| Auto-increment ID | `ISS-YYYYMMDD-NNN` (e.g., `ISS-20251229-001`) |
| Trailing newline | Proper JSONL format, no corruption |
| JSON output | Returns created issue with all fields |
**Example:**
```bash
# Create issue via CLI
ccw issue create --data '{
"title": "Login fails with special chars",
"context": "500 error when password contains quotes",
"priority": 2,
"source": "text",
"expected_behavior": "Login succeeds",
"actual_behavior": "500 Internal Server Error"
}'
# Output (JSON)
{
"id": "ISS-20251229-001",
"title": "Login fails with special chars",
"status": "registered",
...
}
```
**Completion:**
- Display created issue ID
- Show next step: `/issue:plan <id>`
## Execution Flow
```
Phase 1: Input Analysis
└─ Detect clarity score (GitHub URL? Structured text? Keywords?)
Phase 2: Data Extraction (branched by clarity)
┌────────────┬─────────────────┬──────────────┐
│ Score 3 │ Score 1-2 │ Score 0 │
│ GitHub │ Text + ACE │ Vague │
├────────────┼─────────────────┼──────────────┤
│ gh CLI │ Parse struct │ AskQuestion │
│ → parse │ + quick hint │ (1 question) │
│ │ (3 files max) │ → feedback │
└────────────┴─────────────────┴──────────────┘
Phase 3: Create Issue
├─ Score ≥ 2: Direct creation
└─ Score < 2: Confirm first → Create
Note: Deep exploration & lifecycle deferred to /issue:plan
```
## Helper Functions
```javascript
function extractKeywords(text) {
const stopWords = new Set(['the', 'a', 'an', 'is', 'are', 'was', 'were', 'not', 'with']);
return text
.toLowerCase()
.split(/\W+/)
.filter(w => w.length > 3 && !stopWords.has(w))
.slice(0, 5);
}
function parseTextDescription(text) {
const result = { title: '', context: '' };
const sentences = text.split(/\.(?=\s|$)/);
result.title = sentences[0]?.trim().substring(0, 60) || 'Untitled';
result.context = text.substring(0, 500);
// Extract structured fields if present
const expected = text.match(/expected:?\s*([^.]+)/i);
const actual = text.match(/actual:?\s*([^.]+)/i);
const affects = text.match(/affects?:?\s*([^.]+)/i);
if (expected) result.expected_behavior = expected[1].trim();
if (actual) result.actual_behavior = actual[1].trim();
if (affects) {
result.affected_components = affects[1].split(/[,\s]+/).filter(c => c.includes('/') || c.includes('.'));
}
return result;
}
function parseMarkdownBody(body) {
if (!body) return {};
const result = {};
const problem = body.match(/##?\s*(problem|description)[:\s]*([\s\S]*?)(?=##|$)/i);
const expected = body.match(/##?\s*expected[:\s]*([\s\S]*?)(?=##|$)/i);
const actual = body.match(/##?\s*actual[:\s]*([\s\S]*?)(?=##|$)/i);
if (problem) result.context = problem[2].trim().substring(0, 500);
if (expected) result.expected_behavior = expected[2].trim();
if (actual) result.actual_behavior = actual[2].trim();
return result;
}
```
## Examples
### Clear Input (No Questions)
```bash
/issue:new https://github.com/org/repo/issues/42
# → Fetches, parses, creates immediately
/issue:new "Login fails with special chars. Expected: success. Actual: 500"
# → Parses structure, creates immediately
```
### Vague Input (1 Question)
```bash
/issue:new "auth broken"
# → Asks: "Input unclear. What is the issue about?"
# → User provides details → saved to feedback[]
# → Creates issue
```
## Related Commands
- `/issue:plan` - Plan solution for issue
- `/issue:manage` - Interactive issue management

View File

@@ -0,0 +1,324 @@
---
name: plan
description: Batch plan issue resolution using issue-plan-agent (explore + plan closed-loop)
argument-hint: "--all-pending <issue-id>[,<issue-id>,...] [--batch-size 3] "
allowed-tools: TodoWrite(*), Task(*), SlashCommand(*), AskUserQuestion(*), Bash(*), Read(*), Write(*)
---
# Issue Plan Command (/issue:plan)
## Overview
Unified planning command using **issue-plan-agent** that combines exploration and planning into a single closed-loop workflow.
**Behavior:**
- Single solution per issue → auto-bind
- Multiple solutions → return for user selection
- Agent handles file generation
## Core Guidelines
**⚠️ Data Access Principle**: Issues and solutions files can grow very large. To avoid context overflow:
| Operation | Correct | Incorrect |
|-----------|---------|-----------|
| List issues (brief) | `ccw issue list --status pending --brief` | `Read('issues.jsonl')` |
| Read issue details | `ccw issue status <id> --json` | `Read('issues.jsonl')` |
| Update status | `ccw issue update <id> --status ...` | Direct file edit |
| Bind solution | `ccw issue bind <id> <sol-id>` | Direct file edit |
**Output Options**:
- `--brief`: JSON with minimal fields (id, title, status, priority, tags)
- `--json`: Full JSON (agent use only)
**Orchestration vs Execution**:
- **Command (orchestrator)**: Use `--brief` for minimal context
- **Agent (executor)**: Fetch full details → `ccw issue status <id> --json`
**ALWAYS** use CLI commands for CRUD operations. **NEVER** read entire `issues.jsonl` or `solutions/*.jsonl` directly.
## Usage
```bash
/issue:plan [<issue-id>[,<issue-id>,...]] [FLAGS]
# Examples
/issue:plan # Default: --all-pending
/issue:plan GH-123 # Single issue
/issue:plan GH-123,GH-124,GH-125 # Batch (up to 3)
/issue:plan --all-pending # All pending issues (explicit)
# Flags
--batch-size <n> Max issues per agent batch (default: 3)
```
## Execution Process
```
Phase 1: Issue Loading
├─ Parse input (single, comma-separated, or --all-pending)
├─ Fetch issue metadata (ID, title, tags)
├─ Validate issues exist (create if needed)
└─ Group by similarity (shared tags or title keywords, max 3 per batch)
Phase 2: Unified Explore + Plan (issue-plan-agent)
├─ Launch issue-plan-agent per batch
├─ Agent performs:
│ ├─ ACE semantic search for each issue
│ ├─ Codebase exploration (files, patterns, dependencies)
│ ├─ Solution generation with task breakdown
│ └─ Conflict detection across issues
└─ Output: solution JSON per issue
Phase 3: Solution Registration & Binding
├─ Append solutions to solutions/{issue-id}.jsonl
├─ Single solution per issue → auto-bind
├─ Multiple candidates → AskUserQuestion to select
└─ Update issues.jsonl with bound_solution_id
Phase 4: Summary
├─ Display bound solutions
├─ Show task counts per issue
└─ Display next steps (/issue:queue)
```
## Implementation
### Phase 1: Issue Loading (Brief Info Only)
```javascript
const batchSize = flags.batchSize || 3;
let issues = []; // {id, title, tags} - brief info for grouping only
// Default to --all-pending if no input provided
const useAllPending = flags.allPending || !userInput || userInput.trim() === '';
if (useAllPending) {
// Get pending issues with brief metadata via CLI
const result = Bash(`ccw issue list --status pending,registered --json`).trim();
const parsed = result ? JSON.parse(result) : [];
issues = parsed.map(i => ({ id: i.id, title: i.title || '', tags: i.tags || [] }));
if (issues.length === 0) {
console.log('No pending issues found.');
return;
}
console.log(`Found ${issues.length} pending issues`);
} else {
// Parse comma-separated issue IDs, fetch brief metadata
const ids = userInput.includes(',')
? userInput.split(',').map(s => s.trim())
: [userInput.trim()];
for (const id of ids) {
Bash(`ccw issue init ${id} --title "Issue ${id}" 2>/dev/null || true`);
const info = Bash(`ccw issue status ${id} --json`).trim();
const parsed = info ? JSON.parse(info) : {};
issues.push({ id, title: parsed.title || '', tags: parsed.tags || [] });
}
}
// Note: Agent fetches full issue content via `ccw issue status <id> --json`
// Semantic grouping via Gemini CLI (max 4 issues per group)
async function groupBySimilarityGemini(issues) {
const issueSummaries = issues.map(i => ({
id: i.id, title: i.title, tags: i.tags
}));
const prompt = `
PURPOSE: Group similar issues by semantic similarity for batch processing; maximize within-group coherence; max 4 issues per group
TASK: • Analyze issue titles/tags semantically • Identify functional/architectural clusters • Assign each issue to one group
MODE: analysis
CONTEXT: Issue metadata only
EXPECTED: JSON with groups array, each containing max 4 issue_ids, theme, rationale
RULES: $(cat ~/.claude/workflows/cli-templates/protocols/analysis-protocol.md) | Each issue in exactly one group | Max 4 issues per group | Balance group sizes
INPUT:
${JSON.stringify(issueSummaries, null, 2)}
OUTPUT FORMAT:
{"groups":[{"group_id":1,"theme":"...","issue_ids":["..."],"rationale":"..."}],"ungrouped":[]}
`;
const taskId = Bash({
command: `ccw cli -p "${prompt}" --tool gemini --mode analysis`,
run_in_background: true, timeout: 600000
});
const output = TaskOutput({ task_id: taskId, block: true });
// Extract JSON from potential markdown code blocks
function extractJsonFromMarkdown(text) {
const jsonMatch = text.match(/```json\s*\n([\s\S]*?)\n```/) ||
text.match(/```\s*\n([\s\S]*?)\n```/);
return jsonMatch ? jsonMatch[1] : text;
}
const result = JSON.parse(extractJsonFromMarkdown(output));
return result.groups.map(g => g.issue_ids.map(id => issues.find(i => i.id === id)));
}
const batches = await groupBySimilarityGemini(issues);
console.log(`Processing ${issues.length} issues in ${batches.length} batch(es) (max 4 issues/agent)`);
TodoWrite({
todos: batches.map((_, i) => ({
content: `Plan batch ${i+1}`,
status: 'pending',
activeForm: `Planning batch ${i+1}`
}))
});
```
### Phase 2: Unified Explore + Plan (issue-plan-agent) - PARALLEL
```javascript
Bash(`mkdir -p .workflow/issues/solutions`);
const pendingSelections = []; // Collect multi-solution issues for user selection
const agentResults = []; // Collect all agent results for conflict aggregation
// Build prompts for all batches
const agentTasks = batches.map((batch, batchIndex) => {
const issueList = batch.map(i => `- ${i.id}: ${i.title}${i.tags.length ? ` [${i.tags.join(', ')}]` : ''}`).join('\n');
const batchIds = batch.map(i => i.id);
const issuePrompt = `
## Plan Issues
**Issues** (grouped by similarity):
${issueList}
**Project Root**: ${process.cwd()}
### Project Context (MANDATORY)
1. Read: .workflow/project-tech.json (technology stack, architecture)
2. Read: .workflow/project-guidelines.json (constraints and conventions)
### Workflow
1. Fetch issue details: ccw issue status <id> --json
2. Load project context files
3. Explore codebase (ACE semantic search)
4. Plan solution with tasks (schema: solution-schema.json)
5. Write solution to: .workflow/issues/solutions/{issue-id}.jsonl
6. Single solution → auto-bind; Multiple → return for selection
### Rules
- Solution ID format: SOL-{issue-id}-{seq}
- Single solution per issue → auto-bind via ccw issue bind
- Multiple solutions → register only, return pending_selection
- Tasks must have quantified acceptance.criteria
### Return Summary
{"bound":[{"issue_id":"...","solution_id":"...","task_count":N}],"pending_selection":[{"issue_id":"...","solutions":[{"id":"...","description":"...","task_count":N}]}]}
`;
return { batchIndex, batchIds, issuePrompt, batch };
});
// Launch agents in parallel (max 10 concurrent)
const MAX_PARALLEL = 10;
for (let i = 0; i < agentTasks.length; i += MAX_PARALLEL) {
const chunk = agentTasks.slice(i, i + MAX_PARALLEL);
const taskIds = [];
// Launch chunk in parallel
for (const { batchIndex, batchIds, issuePrompt, batch } of chunk) {
updateTodo(`Plan batch ${batchIndex + 1}`, 'in_progress');
const taskId = Task(
subagent_type="issue-plan-agent",
run_in_background=true,
description=`Explore & plan ${batch.length} issues: ${batchIds.join(', ')}`,
prompt=issuePrompt
);
taskIds.push({ taskId, batchIndex });
}
console.log(`Launched ${taskIds.length} agents (batch ${i/MAX_PARALLEL + 1}/${Math.ceil(agentTasks.length/MAX_PARALLEL)})...`);
// Collect results from this chunk
for (const { taskId, batchIndex } of taskIds) {
const result = TaskOutput(task_id=taskId, block=true);
// Extract JSON from potential markdown code blocks (agent may wrap in ```json...```)
const jsonText = extractJsonFromMarkdown(result);
let summary;
try {
summary = JSON.parse(jsonText);
} catch (e) {
console.log(`⚠ Batch ${batchIndex + 1}: Failed to parse agent result, skipping`);
updateTodo(`Plan batch ${batchIndex + 1}`, 'completed');
continue;
}
agentResults.push(summary); // Store for Phase 3 conflict aggregation
for (const item of summary.bound || []) {
console.log(`${item.issue_id}: ${item.solution_id} (${item.task_count} tasks)`);
}
// Collect and notify pending selections
for (const pending of summary.pending_selection || []) {
console.log(`${pending.issue_id}: ${pending.solutions.length} solutions → awaiting selection`);
pendingSelections.push(pending);
}
if (summary.conflicts?.length > 0) {
console.log(`⚠ Conflicts: ${summary.conflicts.length} detected (will resolve in Phase 3)`);
}
updateTodo(`Plan batch ${batchIndex + 1}`, 'completed');
}
}
```
### Phase 3: Conflict Resolution & Solution Selection
**Conflict Handling:**
- Collect `conflicts` from all agent results
- Low/Medium severity → auto-resolve with `recommended_resolution`
- High severity → use `AskUserQuestion` to let user choose resolution
**Multi-Solution Selection:**
- If `pending_selection` contains issues with multiple solutions:
- Use `AskUserQuestion` to present options (solution ID + task count + description)
- Extract selected solution ID from user response
- Verify solution file exists, recover from payload if missing
- Bind selected solution via `ccw issue bind <issue-id> <solution-id>`
### Phase 4: Summary
```javascript
// Count planned issues via CLI
const planned = JSON.parse(Bash(`ccw issue list --status planned --brief`) || '[]');
const plannedCount = planned.length;
console.log(`
## Done: ${issues.length} issues → ${plannedCount} planned
Next: \`/issue:queue\`\`/issue:execute\`
`);
```
## Error Handling
| Error | Resolution |
|-------|------------|
| Issue not found | Auto-create in issues.jsonl |
| ACE search fails | Agent falls back to ripgrep |
| No solutions generated | Display error, suggest manual planning |
| User cancels selection | Skip issue, continue with others |
| File conflicts | Agent detects and suggests resolution order |
## Quality Checklist
Before completing, verify:
- [ ] All input issues have solutions in `solutions/{issue-id}.jsonl`
- [ ] Single solution issues are auto-bound (`bound_solution_id` set)
- [ ] Multi-solution issues returned in `pending_selection` for user choice
- [ ] Each solution has executable tasks with `modification_points`
- [ ] Task acceptance criteria are quantified (not vague)
- [ ] Conflicts detected and reported (if multiple issues touch same files)
- [ ] Issue status updated to `planned` after binding
## Related Commands
- `/issue:queue` - Form execution queue from bound solutions
- `/issue:execute` - Execute queue with codex
- `ccw issue list` - List all issues
- `ccw issue status` - View issue and solution details

View File

@@ -0,0 +1,327 @@
---
name: queue
description: Form execution queue from bound solutions using issue-queue-agent (solution-level)
argument-hint: "[--rebuild] [--issue <id>]"
allowed-tools: TodoWrite(*), Task(*), Bash(*), Read(*), Write(*)
---
# Issue Queue Command (/issue:queue)
## Overview
Queue formation command using **issue-queue-agent** that analyzes all bound solutions, resolves **inter-solution** conflicts, and creates an ordered execution queue at **solution level**.
**Design Principle**: Queue items are **solutions**, not individual tasks. Each executor receives a complete solution with all its tasks.
## Core Capabilities
- **Agent-driven**: issue-queue-agent handles all ordering logic
- **Solution-level granularity**: Queue items are solutions, not tasks
- **Conflict clarification**: High-severity conflicts prompt user decision
- Semantic priority calculation per solution (0.0-1.0)
- Parallel/Sequential group assignment for solutions
## Core Guidelines
**⚠️ Data Access Principle**: Issues and queue files can grow very large. To avoid context overflow:
| Operation | Correct | Incorrect |
|-----------|---------|-----------|
| List issues (brief) | `ccw issue list --status planned --brief` | `Read('issues.jsonl')` |
| List queue (brief) | `ccw issue queue --brief` | `Read('queues/*.json')` |
| Read issue details | `ccw issue status <id> --json` | `Read('issues.jsonl')` |
| Get next item | `ccw issue next --json` | `Read('queues/*.json')` |
| Update status | `ccw issue update <id> --status ...` | Direct file edit |
| Sync from queue | `ccw issue update --from-queue` | Direct file edit |
**Output Options**:
- `--brief`: JSON with minimal fields (id, status, counts)
- `--json`: Full JSON (agent use only)
**Orchestration vs Execution**:
- **Command (orchestrator)**: Use `--brief` for minimal context
- **Agent (executor)**: Fetch full details → `ccw issue status <id> --json`
**ALWAYS** use CLI commands for CRUD operations. **NEVER** read entire `issues.jsonl` or `queues/*.json` directly.
## Usage
```bash
/issue:queue [FLAGS]
# Examples
/issue:queue # Form NEW queue from all bound solutions
/issue:queue --issue GH-123 # Form queue for specific issue only
/issue:queue --append GH-124 # Append to active queue
/issue:queue --list # List all queues (history)
/issue:queue --switch QUE-xxx # Switch active queue
/issue:queue --archive # Archive completed active queue
# Flags
--issue <id> Form queue for specific issue only
--append <id> Append issue to active queue (don't create new)
# CLI subcommands (ccw issue queue ...)
ccw issue queue list List all queues with status
ccw issue queue switch <queue-id> Switch active queue
ccw issue queue archive Archive current queue
ccw issue queue delete <queue-id> Delete queue from history
```
## Execution Process
```
Phase 1: Solution Loading
├─ Load issues.jsonl, filter by status='planned' + bound_solution_id
├─ Read solutions/{issue-id}.jsonl, find bound solution
├─ Extract files_touched from task modification_points
└─ Build solution objects array
Phase 2-4: Agent-Driven Queue Formation (issue-queue-agent)
├─ Launch issue-queue-agent with solutions array
├─ Agent performs:
│ ├─ Conflict analysis (5 types via Gemini CLI)
│ ├─ Build dependency DAG from conflicts
│ ├─ Calculate semantic priority per solution
│ └─ Assign execution groups (parallel/sequential)
└─ Agent writes: queue JSON + index update
Phase 5: Conflict Clarification (if needed)
├─ Check agent return for `clarifications` array
├─ If clarifications exist → AskUserQuestion
├─ Pass user decisions back to agent (resume)
└─ Agent updates queue with resolved conflicts
Phase 6: Status Update & Summary
├─ Update issue statuses to 'queued'
└─ Display queue summary, next step: /issue:execute
```
## Implementation
### Phase 1: Solution Loading
**Data Loading:**
- Load `issues.jsonl` and filter issues with `status === 'planned'` and `bound_solution_id`
- If no planned issues found → display message, suggest `/issue:plan`
**Solution Collection** (for each planned issue):
- Read `solutions/{issue-id}.jsonl`
- Find bound solution by `bound_solution_id`
- If bound solution not found → warn and skip issue
- Extract `files_touched` from all task `modification_points`
**Build Solution Objects:**
```json
{
"issue_id": "ISS-xxx",
"solution_id": "SOL-ISS-xxx-1",
"task_count": 3,
"files_touched": ["src/auth.ts", "src/utils.ts"],
"priority": "medium"
}
```
**Output:** Array of solution objects ready for agent processing
### Phase 2-4: Agent-Driven Queue Formation
**Generate Queue ID** (command layer, pass to agent):
```javascript
const queueId = `QUE-${new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14)}`;
```
**Agent Prompt**:
```
## Order Solutions into Execution Queue
**Queue ID**: ${queueId}
**Solutions**: ${solutions.length} from ${issues.length} issues
**Project Root**: ${cwd}
### Input
${JSON.stringify(solutions)}
### Workflow
Step 1: Build dependency graph from solutions (nodes=solutions, edges=file conflicts)
Step 2: Use Gemini CLI for conflict analysis (5 types: file, API, data, dependency, architecture)
Step 3: For high-severity conflicts without clear resolution → add to `clarifications`
Step 4: Calculate semantic priority (base from issue priority + task_count boost)
Step 5: Assign execution groups: P* (parallel, no overlaps) / S* (sequential, shared files)
Step 6: Write queue JSON + update index
### Output Requirements
**Write files** (exactly 2):
- `.workflow/issues/queues/${queueId}.json` - Full queue with solutions, conflicts, groups
- `.workflow/issues/queues/index.json` - Update with new queue entry
**Return JSON**:
\`\`\`json
{
"queue_id": "${queueId}",
"total_solutions": N,
"total_tasks": N,
"execution_groups": [{"id": "P1", "type": "parallel", "count": N}],
"issues_queued": ["ISS-xxx"],
"clarifications": [{"conflict_id": "CFT-1", "question": "...", "options": [...]}]
}
\`\`\`
### Rules
- Solution granularity (NOT individual tasks)
- Queue Item ID format: S-1, S-2, S-3, ...
- Use provided Queue ID (do NOT generate new)
- `clarifications` only present if high-severity unresolved conflicts exist
### Done Criteria
- [ ] Queue JSON written with all solutions ordered
- [ ] Index updated with active_queue_id
- [ ] No circular dependencies
- [ ] Parallel groups have no file overlaps
- [ ] Return JSON matches required shape
```
**Launch Agent**:
```javascript
const result = Task(
subagent_type="issue-queue-agent",
prompt=agentPrompt,
description=`Order ${solutions.length} solutions`
);
```
### Phase 5: Conflict Clarification
**Check Agent Return:**
- Parse agent result JSON
- If `clarifications` array exists and non-empty → user decision required
**Clarification Flow:**
```javascript
if (result.clarifications?.length > 0) {
for (const clarification of result.clarifications) {
// Present to user via AskUserQuestion
const answer = AskUserQuestion({
questions: [{
question: clarification.question,
header: clarification.conflict_id,
options: clarification.options,
multiSelect: false
}]
});
// Resume agent with user decision
Task(
subagent_type="issue-queue-agent",
resume=agentId,
prompt=`Conflict ${clarification.conflict_id} resolved: ${answer.selected}`
);
}
}
```
### Phase 6: Status Update & Summary
**Status Update** (MUST use CLI command, NOT direct file operations):
```bash
# Option 1: Batch update from queue (recommended)
ccw issue update --from-queue [queue-id] --json
ccw issue update --from-queue --json # Use active queue
ccw issue update --from-queue QUE-xxx --json # Use specific queue
# Option 2: Individual issue update
ccw issue update <issue-id> --status queued
```
**⚠️ IMPORTANT**: Do NOT directly modify `issues.jsonl`. Always use CLI command to ensure proper validation and history tracking.
**Output** (JSON):
```json
{
"success": true,
"queue_id": "QUE-xxx",
"queued": ["ISS-001", "ISS-002"],
"queued_count": 2,
"unplanned": ["ISS-003"],
"unplanned_count": 1
}
```
**Behavior:**
- Updates issues in queue to `status: 'queued'` (skips already queued/executing/completed)
- Identifies planned issues with `bound_solution_id` NOT in queue → `unplanned` array
- Optional `queue-id`: defaults to active queue if omitted
**Summary Output:**
- Display queue ID, solution count, task count
- Show unplanned issues (planned but NOT in queue)
- Show next step: `/issue:execute`
## Storage Structure (Queue History)
```
.workflow/issues/
├── issues.jsonl # All issues (one per line)
├── queues/ # Queue history directory
│ ├── index.json # Queue index (active + history)
│ ├── {queue-id}.json # Individual queue files
│ └── ...
└── solutions/
├── {issue-id}.jsonl # Solutions for issue
└── ...
```
### Queue Index Schema
```json
{
"active_queue_id": "QUE-20251227-143000",
"queues": [
{
"id": "QUE-20251227-143000",
"status": "active",
"issue_ids": ["ISS-xxx", "ISS-yyy"],
"total_solutions": 3,
"completed_solutions": 1,
"created_at": "2025-12-27T14:30:00Z"
}
]
}
```
**Note**: Queue file schema is produced by `issue-queue-agent`. See agent documentation for details.
## Error Handling
| Error | Resolution |
|-------|------------|
| No bound solutions | Display message, suggest /issue:plan |
| Circular dependency | List cycles, abort queue formation |
| High-severity conflict | Return `clarifications`, prompt user decision |
| User cancels clarification | Abort queue formation |
| **index.json not updated** | Auto-fix: Set active_queue_id to new queue |
| **Queue file missing solutions** | Abort with error, agent must regenerate |
## Quality Checklist
Before completing, verify:
- [ ] All planned issues with `bound_solution_id` are included
- [ ] Queue JSON written to `queues/{queue-id}.json`
- [ ] Index updated in `queues/index.json` with `active_queue_id`
- [ ] No circular dependencies in solution DAG
- [ ] All conflicts resolved (auto or via user clarification)
- [ ] Parallel groups have no file overlaps
- [ ] Issue statuses updated to `queued`
## Related Commands
- `/issue:plan` - Plan issues and bind solutions
- `/issue:execute` - Execute queue with codex
- `ccw issue queue list` - View current queue
- `ccw issue update --from-queue [queue-id]` - Sync issue statuses from queue

View File

@@ -410,7 +410,6 @@ Task(subagent_type="{meta.agent}",
1. Read complete task JSON: {session.task_json_path}
2. Load context package: {session.context_package_path}
Follow complete execution guidelines in @.claude/agents/{meta.agent}.md
**Session Paths**:
- Workflow Dir: {session.workflow_dir}

View File

@@ -10,7 +10,11 @@ examples:
# Workflow Init Command (/workflow:init)
## Overview
Initialize `.workflow/project.json` with comprehensive project understanding by delegating analysis to **cli-explore-agent**.
Initialize `.workflow/project-tech.json` and `.workflow/project-guidelines.json` with comprehensive project understanding by delegating analysis to **cli-explore-agent**.
**Dual File System**:
- `project-tech.json`: Auto-generated technical analysis (stack, architecture, components)
- `project-guidelines.json`: User-maintained rules and constraints (created as scaffold)
**Note**: This command may be called by other workflow commands. Upon completion, return immediately to continue the calling workflow without interrupting the task flow.
@@ -27,7 +31,7 @@ Input Parsing:
└─ Parse --regenerate flag → regenerate = true | false
Decision:
├─ EXISTS + no --regenerate → Exit: "Already initialized"
├─ BOTH_EXIST + no --regenerate → Exit: "Already initialized"
├─ EXISTS + --regenerate → Backup existing → Continue analysis
└─ NOT_FOUND → Continue analysis
@@ -37,11 +41,14 @@ Analysis Flow:
│ ├─ Structural scan (get_modules_by_depth.sh, find, wc)
│ ├─ Semantic analysis (Gemini CLI)
│ ├─ Synthesis and merge
│ └─ Write .workflow/project.json
│ └─ Write .workflow/project-tech.json
├─ Create guidelines scaffold (if not exists)
│ └─ Write .workflow/project-guidelines.json (empty structure)
└─ Display summary
Output:
─ .workflow/project.json (+ .backup if regenerate)
─ .workflow/project-tech.json (+ .backup if regenerate)
└─ .workflow/project-guidelines.json (scaffold if new)
```
## Implementation
@@ -56,13 +63,18 @@ const regenerate = $ARGUMENTS.includes('--regenerate')
**Check existing state**:
```bash
bash(test -f .workflow/project.json && echo "EXISTS" || echo "NOT_FOUND")
bash(test -f .workflow/project-tech.json && echo "TECH_EXISTS" || echo "TECH_NOT_FOUND")
bash(test -f .workflow/project-guidelines.json && echo "GUIDELINES_EXISTS" || echo "GUIDELINES_NOT_FOUND")
```
**If EXISTS and no --regenerate**: Exit early
**If BOTH_EXIST and no --regenerate**: Exit early
```
Project already initialized at .workflow/project.json
Use /workflow:init --regenerate to rebuild
Project already initialized:
- Tech analysis: .workflow/project-tech.json
- Guidelines: .workflow/project-guidelines.json
Use /workflow:init --regenerate to rebuild tech analysis
Use /workflow:session:solidify to add guidelines
Use /workflow:status --project to view state
```
@@ -78,7 +90,7 @@ bash(mkdir -p .workflow)
**For --regenerate**: Backup and preserve existing data
```bash
bash(cp .workflow/project.json .workflow/project.json.backup)
bash(cp .workflow/project-tech.json .workflow/project-tech.json.backup)
```
**Delegate analysis to agent**:
@@ -89,20 +101,17 @@ Task(
run_in_background=false,
description="Deep project analysis",
prompt=`
Analyze project for workflow initialization and generate .workflow/project.json.
Analyze project for workflow initialization and generate .workflow/project-tech.json.
## MANDATORY FIRST STEPS
1. Execute: cat ~/.claude/workflows/cli-templates/schemas/project-json-schema.json (get schema reference)
1. Execute: cat ~/.claude/workflows/cli-templates/schemas/project-tech-schema.json (get schema reference)
2. Execute: ccw tool exec get_modules_by_depth '{}' (get project structure)
## Task
Generate complete project.json with:
- project_name: ${projectName}
- initialized_at: current ISO timestamp
- overview: {description, technology_stack, architecture, key_components}
- features: ${regenerate ? 'preserve from backup' : '[] (empty)'}
- development_index: ${regenerate ? 'preserve from backup' : '{feature: [], enhancement: [], bugfix: [], refactor: [], docs: []}'}
- statistics: ${regenerate ? 'preserve from backup' : '{total_features: 0, total_sessions: 0, last_updated}'}
Generate complete project-tech.json with:
- project_metadata: {name: ${projectName}, root_path: ${projectRoot}, initialized_at, updated_at}
- technology_analysis: {description, languages, frameworks, build_tools, test_frameworks, architecture, key_components, dependencies}
- development_status: ${regenerate ? 'preserve from backup' : '{completed_features: [], development_index: {feature: [], enhancement: [], bugfix: [], refactor: [], docs: []}, statistics: {total_features: 0, total_sessions: 0, last_updated}}'}
- _metadata: {initialized_by: "cli-explore-agent", analysis_timestamp, analysis_mode}
## Analysis Requirements
@@ -123,8 +132,8 @@ Generate complete project.json with:
1. Structural scan: get_modules_by_depth.sh, find, wc -l
2. Semantic analysis: Gemini for patterns/architecture
3. Synthesis: Merge findings
4. ${regenerate ? 'Merge with preserved features/development_index/statistics from .workflow/project.json.backup' : ''}
5. Write JSON: Write('.workflow/project.json', jsonContent)
4. ${regenerate ? 'Merge with preserved development_status from .workflow/project-tech.json.backup' : ''}
5. Write JSON: Write('.workflow/project-tech.json', jsonContent)
6. Report: Return brief completion summary
Project root: ${projectRoot}
@@ -132,29 +141,66 @@ Project root: ${projectRoot}
)
```
### Step 3.5: Create Guidelines Scaffold (if not exists)
```javascript
// Only create if not exists (never overwrite user guidelines)
if (!file_exists('.workflow/project-guidelines.json')) {
const guidelinesScaffold = {
conventions: {
coding_style: [],
naming_patterns: [],
file_structure: [],
documentation: []
},
constraints: {
architecture: [],
tech_stack: [],
performance: [],
security: []
},
quality_rules: [],
learnings: [],
_metadata: {
created_at: new Date().toISOString(),
version: "1.0.0"
}
};
Write('.workflow/project-guidelines.json', JSON.stringify(guidelinesScaffold, null, 2));
}
```
### Step 4: Display Summary
```javascript
const projectJson = JSON.parse(Read('.workflow/project.json'));
const projectTech = JSON.parse(Read('.workflow/project-tech.json'));
const guidelinesExists = file_exists('.workflow/project-guidelines.json');
console.log(`
✓ Project initialized successfully
## Project Overview
Name: ${projectJson.project_name}
Description: ${projectJson.overview.description}
Name: ${projectTech.project_metadata.name}
Description: ${projectTech.technology_analysis.description}
### Technology Stack
Languages: ${projectJson.overview.technology_stack.languages.map(l => l.name).join(', ')}
Frameworks: ${projectJson.overview.technology_stack.frameworks.join(', ')}
Languages: ${projectTech.technology_analysis.languages.map(l => l.name).join(', ')}
Frameworks: ${projectTech.technology_analysis.frameworks.join(', ')}
### Architecture
Style: ${projectJson.overview.architecture.style}
Components: ${projectJson.overview.key_components.length} core modules
Style: ${projectTech.technology_analysis.architecture.style}
Components: ${projectTech.technology_analysis.key_components.length} core modules
---
Project state: .workflow/project.json
${regenerate ? 'Backup: .workflow/project.json.backup' : ''}
Files created:
- Tech analysis: .workflow/project-tech.json
- Guidelines: .workflow/project-guidelines.json ${guidelinesExists ? '(scaffold)' : ''}
${regenerate ? '- Backup: .workflow/project-tech.json.backup' : ''}
Next steps:
- Use /workflow:session:solidify to add project guidelines
- Use /workflow:plan to start planning
`);
```

View File

@@ -181,6 +181,8 @@ Execute **${angle}** diagnosis for bug root cause analysis. Analyze codebase fro
1. Run: ccw tool exec get_modules_by_depth '{}' (project structure)
2. Run: rg -l "{error_keyword_from_bug}" --type ts (locate relevant files)
3. Execute: cat ~/.claude/workflows/cli-templates/schemas/diagnosis-json-schema.json (get output schema reference)
4. Read: .workflow/project-tech.json (technology stack and architecture context)
5. Read: .workflow/project-guidelines.json (user-defined constraints and conventions)
## Diagnosis Strategy (${angle} focus)
@@ -409,6 +411,12 @@ Generate fix plan and write fix-plan.json.
## Output Schema Reference
Execute: cat ~/.claude/workflows/cli-templates/schemas/fix-plan-json-schema.json (get schema reference before generating plan)
## Project Context (MANDATORY - Read Both Files)
1. Read: .workflow/project-tech.json (technology stack, architecture, key components)
2. Read: .workflow/project-guidelines.json (user-defined constraints and conventions)
**CRITICAL**: All fix tasks MUST comply with constraints in project-guidelines.json
## Bug Description
${bug_description}

View File

@@ -184,6 +184,8 @@ Execute **${angle}** exploration for task planning context. Analyze codebase fro
1. Run: ccw tool exec get_modules_by_depth '{}' (project structure)
2. Run: rg -l "{keyword_from_task}" --type ts (locate relevant files)
3. Execute: cat ~/.claude/workflows/cli-templates/schemas/explore-json-schema.json (get output schema reference)
4. Read: .workflow/project-tech.json (technology stack and architecture context)
5. Read: .workflow/project-guidelines.json (user-defined constraints and conventions)
## Exploration Strategy (${angle} focus)
@@ -416,6 +418,12 @@ Generate implementation plan and write plan.json.
## Output Schema Reference
Execute: cat ~/.claude/workflows/cli-templates/schemas/plan-json-schema.json (get schema reference before generating plan)
## Project Context (MANDATORY - Read Both Files)
1. Read: .workflow/project-tech.json (technology stack, architecture, key components)
2. Read: .workflow/project-guidelines.json (user-defined constraints and conventions)
**CRITICAL**: All generated tasks MUST comply with constraints in project-guidelines.json
## Task Description
${task_description}

View File

@@ -409,6 +409,8 @@ Task(
2. Get target files: Read resolved_files from review-state.json
3. Validate file access: bash(ls -la ${targetFiles.join(' ')})
4. Execute: cat ~/.claude/workflows/cli-templates/schemas/review-dimension-results-schema.json (get output schema reference)
5. Read: .workflow/project-tech.json (technology stack and architecture context)
6. Read: .workflow/project-guidelines.json (user-defined constraints and conventions to validate against)
## Review Context
- Review Type: module (independent)
@@ -511,6 +513,8 @@ Task(
3. Identify related code: bash(grep -r "import.*${basename(file)}" ${projectDir}/src --include="*.ts")
4. Read test files: bash(find ${projectDir}/tests -name "*${basename(file, '.ts')}*" -type f)
5. Execute: cat ~/.claude/workflows/cli-templates/schemas/review-deep-dive-results-schema.json (get output schema reference)
6. Read: .workflow/project-tech.json (technology stack and architecture context)
7. Read: .workflow/project-guidelines.json (user-defined constraints for remediation compliance)
## CLI Configuration
- Tool Priority: gemini → qwen → codex

View File

@@ -420,6 +420,8 @@ Task(
3. Get changed files: bash(cd ${workflowDir} && git log --since="${sessionCreatedAt}" --name-only --pretty=format: | sort -u)
4. Read review state: ${reviewStateJsonPath}
5. Execute: cat ~/.claude/workflows/cli-templates/schemas/review-dimension-results-schema.json (get output schema reference)
6. Read: .workflow/project-tech.json (technology stack and architecture context)
7. Read: .workflow/project-guidelines.json (user-defined constraints and conventions to validate against)
## Session Context
- Session ID: ${sessionId}
@@ -522,6 +524,8 @@ Task(
3. Identify related code: bash(grep -r "import.*${basename(file)}" ${workflowDir}/src --include="*.ts")
4. Read test files: bash(find ${workflowDir}/tests -name "*${basename(file, '.ts')}*" -type f)
5. Execute: cat ~/.claude/workflows/cli-templates/schemas/review-deep-dive-results-schema.json (get output schema reference)
6. Read: .workflow/project-tech.json (technology stack and architecture context)
7. Read: .workflow/project-guidelines.json (user-defined constraints for remediation compliance)
## CLI Configuration
- Tool Priority: gemini → qwen → codex

View File

@@ -139,7 +139,7 @@ After bash validation, the model takes control to:
ccw cli -p "
PURPOSE: Security audit of completed implementation
TASK: Review code for security vulnerabilities, insecure patterns, auth/authz issues
CONTEXT: @.summaries/IMPL-*.md,../.. @../../CLAUDE.md
CONTEXT: @.summaries/IMPL-*.md,../.. @../../project-tech.json @../../project-guidelines.json
EXPECTED: Security findings report with severity levels
RULES: Focus on OWASP Top 10, authentication, authorization, data validation, injection risks
" --tool gemini --mode write --cd .workflow/active/${sessionId}
@@ -151,7 +151,7 @@ After bash validation, the model takes control to:
ccw cli -p "
PURPOSE: Architecture compliance review
TASK: Evaluate adherence to architectural patterns, identify technical debt, review design decisions
CONTEXT: @.summaries/IMPL-*.md,../.. @../../CLAUDE.md
CONTEXT: @.summaries/IMPL-*.md,../.. @../../project-tech.json @../../project-guidelines.json
EXPECTED: Architecture assessment with recommendations
RULES: Check for patterns, separation of concerns, modularity, scalability
" --tool qwen --mode write --cd .workflow/active/${sessionId}
@@ -163,7 +163,7 @@ After bash validation, the model takes control to:
ccw cli -p "
PURPOSE: Code quality and best practices review
TASK: Assess code readability, maintainability, adherence to best practices
CONTEXT: @.summaries/IMPL-*.md,../.. @../../CLAUDE.md
CONTEXT: @.summaries/IMPL-*.md,../.. @../../project-tech.json @../../project-guidelines.json
EXPECTED: Quality assessment with improvement suggestions
RULES: Check for code smells, duplication, complexity, naming conventions
" --tool gemini --mode write --cd .workflow/active/${sessionId}
@@ -185,7 +185,7 @@ After bash validation, the model takes control to:
ccw cli -p "
PURPOSE: Verify all requirements and acceptance criteria are met
TASK: Cross-check implementation summaries against original requirements
CONTEXT: @.task/IMPL-*.json,.summaries/IMPL-*.md,../.. @../../CLAUDE.md
CONTEXT: @.task/IMPL-*.json,.summaries/IMPL-*.md,../.. @../../project-tech.json @../../project-guidelines.json
EXPECTED:
- Requirements coverage matrix
- Acceptance criteria verification

View File

@@ -0,0 +1,299 @@
---
name: solidify
description: Crystallize session learnings and user-defined constraints into permanent project guidelines
argument-hint: "[--type <convention|constraint|learning>] [--category <category>] \"rule or insight\""
examples:
- /workflow:session:solidify "Use functional components for all React code" --type convention
- /workflow:session:solidify "No direct DB access from controllers" --type constraint --category architecture
- /workflow:session:solidify "Cache invalidation requires event sourcing" --type learning --category architecture
- /workflow:session:solidify --interactive
---
# Session Solidify Command (/workflow:session:solidify)
## Overview
Crystallizes ephemeral session context (insights, decisions, constraints) into permanent project guidelines stored in `.workflow/project-guidelines.json`. This ensures valuable learnings persist across sessions and inform future planning.
## Use Cases
1. **During Session**: Capture important decisions as they're made
2. **After Session**: Reflect on lessons learned before archiving
3. **Proactive**: Add team conventions or architectural rules
## Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `rule` | string | ✅ (unless --interactive) | The rule, convention, or insight to solidify |
| `--type` | enum | ❌ | Type: `convention`, `constraint`, `learning` (default: auto-detect) |
| `--category` | string | ❌ | Category for organization (see categories below) |
| `--interactive` | flag | ❌ | Launch guided wizard for adding rules |
### Type Categories
**convention** → Coding style preferences (goes to `conventions` section)
- Subcategories: `coding_style`, `naming_patterns`, `file_structure`, `documentation`
**constraint** → Hard rules that must not be violated (goes to `constraints` section)
- Subcategories: `architecture`, `tech_stack`, `performance`, `security`
**learning** → Session-specific insights (goes to `learnings` array)
- Subcategories: `architecture`, `performance`, `security`, `testing`, `process`, `other`
## Execution Process
```
Input Parsing:
├─ Parse: rule text (required unless --interactive)
├─ Parse: --type (convention|constraint|learning)
├─ Parse: --category (subcategory)
└─ Parse: --interactive (flag)
Step 1: Ensure Guidelines File Exists
└─ If not exists → Create with empty structure
Step 2: Auto-detect Type (if not specified)
└─ Analyze rule text for keywords
Step 3: Validate and Format Entry
└─ Build entry object based on type
Step 4: Update Guidelines File
└─ Add entry to appropriate section
Step 5: Display Confirmation
└─ Show what was added and where
```
## Implementation
### Step 1: Ensure Guidelines File Exists
```bash
bash(test -f .workflow/project-guidelines.json && echo "EXISTS" || echo "NOT_FOUND")
```
**If NOT_FOUND**, create scaffold:
```javascript
const scaffold = {
conventions: {
coding_style: [],
naming_patterns: [],
file_structure: [],
documentation: []
},
constraints: {
architecture: [],
tech_stack: [],
performance: [],
security: []
},
quality_rules: [],
learnings: [],
_metadata: {
created_at: new Date().toISOString(),
version: "1.0.0"
}
};
Write('.workflow/project-guidelines.json', JSON.stringify(scaffold, null, 2));
```
### Step 2: Auto-detect Type (if not specified)
```javascript
function detectType(ruleText) {
const text = ruleText.toLowerCase();
// Constraint indicators
if (/\b(no|never|must not|forbidden|prohibited|always must)\b/.test(text)) {
return 'constraint';
}
// Learning indicators
if (/\b(learned|discovered|realized|found that|turns out)\b/.test(text)) {
return 'learning';
}
// Default to convention
return 'convention';
}
function detectCategory(ruleText, type) {
const text = ruleText.toLowerCase();
if (type === 'constraint' || type === 'learning') {
if (/\b(architecture|layer|module|dependency|circular)\b/.test(text)) return 'architecture';
if (/\b(security|auth|permission|sanitize|xss|sql)\b/.test(text)) return 'security';
if (/\b(performance|cache|lazy|async|sync|slow)\b/.test(text)) return 'performance';
if (/\b(test|coverage|mock|stub)\b/.test(text)) return 'testing';
}
if (type === 'convention') {
if (/\b(name|naming|prefix|suffix|camel|pascal)\b/.test(text)) return 'naming_patterns';
if (/\b(file|folder|directory|structure|organize)\b/.test(text)) return 'file_structure';
if (/\b(doc|comment|jsdoc|readme)\b/.test(text)) return 'documentation';
return 'coding_style';
}
return type === 'constraint' ? 'tech_stack' : 'other';
}
```
### Step 3: Build Entry
```javascript
function buildEntry(rule, type, category, sessionId) {
if (type === 'learning') {
return {
date: new Date().toISOString().split('T')[0],
session_id: sessionId || null,
insight: rule,
category: category,
context: null
};
}
// For conventions and constraints, just return the rule string
return rule;
}
```
### Step 4: Update Guidelines File
```javascript
const guidelines = JSON.parse(Read('.workflow/project-guidelines.json'));
if (type === 'convention') {
if (!guidelines.conventions[category]) {
guidelines.conventions[category] = [];
}
if (!guidelines.conventions[category].includes(rule)) {
guidelines.conventions[category].push(rule);
}
} else if (type === 'constraint') {
if (!guidelines.constraints[category]) {
guidelines.constraints[category] = [];
}
if (!guidelines.constraints[category].includes(rule)) {
guidelines.constraints[category].push(rule);
}
} else if (type === 'learning') {
guidelines.learnings.push(buildEntry(rule, type, category, sessionId));
}
guidelines._metadata.updated_at = new Date().toISOString();
guidelines._metadata.last_solidified_by = sessionId;
Write('.workflow/project-guidelines.json', JSON.stringify(guidelines, null, 2));
```
### Step 5: Display Confirmation
```
✓ Guideline solidified
Type: ${type}
Category: ${category}
Rule: "${rule}"
Location: .workflow/project-guidelines.json → ${type}s.${category}
Total ${type}s in ${category}: ${count}
```
## Interactive Mode
When `--interactive` flag is provided:
```javascript
AskUserQuestion({
questions: [
{
question: "What type of guideline are you adding?",
header: "Type",
multiSelect: false,
options: [
{ label: "Convention", description: "Coding style preference (e.g., use functional components)" },
{ label: "Constraint", description: "Hard rule that must not be violated (e.g., no direct DB access)" },
{ label: "Learning", description: "Insight from this session (e.g., cache invalidation needs events)" }
]
}
]
});
// Follow-up based on type selection...
```
## Examples
### Add a Convention
```bash
/workflow:session:solidify "Use async/await instead of callbacks" --type convention --category coding_style
```
Result in `project-guidelines.json`:
```json
{
"conventions": {
"coding_style": ["Use async/await instead of callbacks"]
}
}
```
### Add an Architectural Constraint
```bash
/workflow:session:solidify "No direct DB access from controllers" --type constraint --category architecture
```
Result:
```json
{
"constraints": {
"architecture": ["No direct DB access from controllers"]
}
}
```
### Capture a Session Learning
```bash
/workflow:session:solidify "Cache invalidation requires event sourcing for consistency" --type learning
```
Result:
```json
{
"learnings": [
{
"date": "2024-12-28",
"session_id": "WFS-auth-feature",
"insight": "Cache invalidation requires event sourcing for consistency",
"category": "architecture"
}
]
}
```
## Integration with Planning
The `project-guidelines.json` is consumed by:
1. **`/workflow:tools:context-gather`**: Loads guidelines into context-package.json
2. **`/workflow:plan`**: Passes guidelines to task generation agent
3. **`task-generate-agent`**: Includes guidelines as "CRITICAL CONSTRAINTS" in system prompt
This ensures all future planning respects solidified rules without users needing to re-state them.
## Error Handling
- **Duplicate Rule**: Warn and skip if exact rule already exists
- **Invalid Category**: Suggest valid categories for the type
- **File Corruption**: Backup existing file before modification
## Related Commands
- `/workflow:session:start` - Start a session (may prompt for solidify at end)
- `/workflow:session:complete` - Complete session (prompts for learnings to solidify)
- `/workflow:init` - Creates project-guidelines.json scaffold if missing

View File

@@ -38,26 +38,29 @@ ERROR: Invalid session type. Valid types: workflow, review, tdd, test, docs
## Step 0: Initialize Project State (First-time Only)
**Executed before all modes** - Ensures project-level state file exists by calling `/workflow:init`.
**Executed before all modes** - Ensures project-level state files exist by calling `/workflow:init`.
### Check and Initialize
```bash
# Check if project state exists
bash(test -f .workflow/project.json && echo "EXISTS" || echo "NOT_FOUND")
# Check if project state exists (both files required)
bash(test -f .workflow/project-tech.json && echo "TECH_EXISTS" || echo "TECH_NOT_FOUND")
bash(test -f .workflow/project-guidelines.json && echo "GUIDELINES_EXISTS" || echo "GUIDELINES_NOT_FOUND")
```
**If NOT_FOUND**, delegate to `/workflow:init`:
**If either NOT_FOUND**, delegate to `/workflow:init`:
```javascript
// Call workflow:init for intelligent project analysis
SlashCommand({command: "/workflow:init"});
// Wait for init completion
// project.json will be created with comprehensive project overview
// project-tech.json and project-guidelines.json will be created
```
**Output**:
- If EXISTS: `PROJECT_STATE: initialized`
- If NOT_FOUND: Calls `/workflow:init` → creates `.workflow/project.json` with full project analysis
- If BOTH_EXIST: `PROJECT_STATE: initialized`
- If NOT_FOUND: Calls `/workflow:init` → creates:
- `.workflow/project-tech.json` with full technical analysis
- `.workflow/project-guidelines.json` with empty scaffold
**Note**: `/workflow:init` uses cli-explore-agent to build comprehensive project understanding (technology stack, architecture, key components). This step runs once per project. Subsequent executions skip initialization.

View File

@@ -15,7 +15,6 @@ allowed-tools: Task(*), Read(*), Glob(*)
Orchestrator command that invokes `context-search-agent` to gather comprehensive project context for implementation planning. Generates standardized `context-package.json` with codebase analysis, dependencies, and conflict detection.
**Agent**: `context-search-agent` (`.claude/agents/context-search-agent.md`)
## Core Philosophy
@@ -237,7 +236,10 @@ Task(
Execute complete context-search-agent workflow for implementation planning:
### Phase 1: Initialization & Pre-Analysis
1. **Project State Loading**: Read and parse `.workflow/project.json`. Use its `overview` section as the foundational `project_context`. This is your primary source for architecture, tech stack, and key components. If file doesn't exist, proceed with fresh analysis.
1. **Project State Loading**:
- Read and parse `.workflow/project-tech.json`. Use its `technology_analysis` section as the foundational `project_context`. This is your primary source for architecture, tech stack, and key components.
- Read and parse `.workflow/project-guidelines.json`. Load `conventions`, `constraints`, and `learnings` into a `project_guidelines` section.
- If files don't exist, proceed with fresh analysis.
2. **Detection**: Check for existing context-package (early exit if valid)
3. **Foundation**: Initialize CodexLens, get project structure, load docs
4. **Analysis**: Extract keywords, determine scope, classify complexity based on task description and project state
@@ -252,17 +254,19 @@ Execute all discovery tracks:
### Phase 3: Synthesis, Assessment & Packaging
1. Apply relevance scoring and build dependency graph
2. **Synthesize 4-source data**: Merge findings from all sources (archive > docs > code > web). **Prioritize the context from `project.json`** for architecture and tech stack unless code analysis reveals it's outdated.
3. **Populate `project_context`**: Directly use the `overview` from `project.json` to fill the `project_context` section of the output `context-package.json`. Include description, technology_stack, architecture, and key_components.
4. Integrate brainstorm artifacts (if .brainstorming/ exists, read content)
5. Perform conflict detection with risk assessment
6. **Inject historical conflicts** from archive analysis into conflict_detection
7. Generate and validate context-package.json
2. **Synthesize 4-source data**: Merge findings from all sources (archive > docs > code > web). **Prioritize the context from `project-tech.json`** for architecture and tech stack unless code analysis reveals it's outdated.
3. **Populate `project_context`**: Directly use the `technology_analysis` from `project-tech.json` to fill the `project_context` section. Include description, technology_stack, architecture, and key_components.
4. **Populate `project_guidelines`**: Load conventions, constraints, and learnings from `project-guidelines.json` into a dedicated section.
5. Integrate brainstorm artifacts (if .brainstorming/ exists, read content)
6. Perform conflict detection with risk assessment
7. **Inject historical conflicts** from archive analysis into conflict_detection
8. Generate and validate context-package.json
## Output Requirements
Complete context-package.json with:
- **metadata**: task_description, keywords, complexity, tech_stack, session_id
- **project_context**: description, technology_stack, architecture, key_components (sourced from `project.json` overview)
- **project_context**: description, technology_stack, architecture, key_components (sourced from `project-tech.json`)
- **project_guidelines**: {conventions, constraints, quality_rules, learnings} (sourced from `project-guidelines.json`)
- **assets**: {documentation[], source_code[], config[], tests[]} with relevance scores
- **dependencies**: {internal[], external[]} with dependency graph
- **brainstorm_artifacts**: {guidance_specification, role_analyses[], synthesis_output} with content
@@ -315,7 +319,8 @@ Refer to `context-search-agent.md` Phase 3.7 for complete `context-package.json`
**Key Sections**:
- **metadata**: Session info, keywords, complexity, tech stack
- **project_context**: Architecture patterns, conventions, tech stack (populated from `project.json` overview)
- **project_context**: Architecture patterns, conventions, tech stack (populated from `project-tech.json`)
- **project_guidelines**: Conventions, constraints, quality rules, learnings (populated from `project-guidelines.json`)
- **assets**: Categorized files with relevance scores (documentation, source_code, config, tests)
- **dependencies**: Internal and external dependency graphs
- **brainstorm_artifacts**: Brainstorm documents with full content (if exists)
@@ -430,7 +435,7 @@ if (historicalConflicts.length > 0 && currentRisk === "low") {
## Notes
- **Detection-first**: Always check for existing package before invoking agent
- **Project.json integration**: Agent reads `.workflow/project.json` as primary source for project context, avoiding redundant analysis
- **Agent autonomy**: Agent handles all discovery logic per `.claude/agents/context-search-agent.md`
- **Dual project file integration**: Agent reads both `.workflow/project-tech.json` (tech analysis) and `.workflow/project-guidelines.json` (user constraints) as primary sources
- **Guidelines injection**: Project guidelines are included in context-package to ensure task generation respects user-defined constraints
- **No redundancy**: This command is a thin orchestrator, all logic in agent
- **Plan-specific**: Use this for implementation planning; brainstorm mode uses direct agent call

View File

@@ -239,15 +239,6 @@ If conflict_risk was medium/high, modifications have been applied to:
**Agent Configuration Reference**: All TDD task generation rules, quantification requirements, Red-Green-Refactor cycle structure, quality standards, and execution details are defined in action-planning-agent.
Refer to: @.claude/agents/action-planning-agent.md for:
- TDD Task Decomposition Standards
- Red-Green-Refactor Cycle Requirements
- Quantification Requirements (MANDATORY)
- 5-Field Task JSON Schema
- IMPL_PLAN.md Structure (TDD variant)
- TODO_LIST.md Format
- TDD Execution Flow & Quality Validation
### TDD-Specific Requirements Summary
#### Task Structure Philosophy

View File

@@ -14,7 +14,7 @@ allowed-tools: Task(*), Read(*), Glob(*)
Orchestrator command that invokes `test-context-search-agent` to gather comprehensive test coverage context for test generation workflows. Generates standardized `test-context-package.json` with coverage analysis, framework detection, and source implementation context.
**Agent**: `test-context-search-agent` (`.claude/agents/test-context-search-agent.md`)
## Core Philosophy
@@ -89,7 +89,6 @@ Task(
run_in_background=false,
description="Gather test coverage context",
prompt=`
You are executing as test-context-search-agent (.claude/agents/test-context-search-agent.md).
## Execution Mode
**PLAN MODE** (Comprehensive) - Full Phase 1-3 execution
@@ -229,7 +228,7 @@ Refer to `test-context-search-agent.md` Phase 3.2 for complete `test-context-pac
## Notes
- **Detection-first**: Always check for existing test-context-package before invoking agent
- **Agent autonomy**: Agent handles all coverage analysis logic per `.claude/agents/test-context-search-agent.md`
- **No redundancy**: This command is a thin orchestrator, all logic in agent
- **Framework agnostic**: Supports Jest, Mocha, pytest, RSpec, Go testing, etc.
- **Coverage focus**: Primary goal is identifying implementation files without tests

View File

@@ -107,8 +107,6 @@ CRITICAL:
- Follow the progressive loading strategy defined in your agent specification (load context incrementally from memory-first approach)
## AGENT CONFIGURATION REFERENCE
All test task generation rules, schemas, and quality standards are defined in your agent specification:
@.claude/agents/action-planning-agent.md
Refer to your specification for:
- Test Task JSON Schema (6-field structure with test-specific metadata)

View File

@@ -806,8 +806,6 @@ Use `analysis_results.complexity` or task count to determine structure:
**Examples**:
- GOOD: `"Implement 5 commands: [cmd1, cmd2, cmd3, cmd4, cmd5]"`
- BAD: `"Implement new commands"`
- GOOD: `"5 files created: verify by ls .claude/commands/*.md | wc -l = 5"`
- BAD: `"All commands implemented successfully"`
### 3.2 Planning & Organization Standards

View File

@@ -400,7 +400,7 @@ Task(subagent_type="{meta.agent}",
1. Read complete task JSON: {session.task_json_path}
2. Load context package: {session.context_package_path}
Follow complete execution guidelines in @.claude/agents/{meta.agent}.md
**Session Paths**:
- Workflow Dir: {session.workflow_dir}

View File

@@ -15,7 +15,6 @@ allowed-tools: Task(*), Read(*), Glob(*)
Orchestrator command that invokes `context-search-agent` to gather comprehensive project context for implementation planning. Generates standardized `context-package.json` with codebase analysis, dependencies, and conflict detection.
**Agent**: `context-search-agent` (`.claude/agents/context-search-agent.md`)
## Core Philosophy
@@ -429,6 +428,6 @@ if (historicalConflicts.length > 0 && currentRisk === "low") {
- **Detection-first**: Always check for existing package before invoking agent
- **Project.json integration**: Agent reads `.workflow/project.json` as primary source for project context, avoiding redundant analysis
- **Agent autonomy**: Agent handles all discovery logic per `.claude/agents/context-search-agent.md`
- **No redundancy**: This command is a thin orchestrator, all logic in agent
- **Plan-specific**: Use this for implementation planning; brainstorm mode uses direct agent call

View File

@@ -238,14 +238,7 @@ If conflict_risk was medium/high, modifications have been applied to:
**Agent Configuration Reference**: All TDD task generation rules, quantification requirements, Red-Green-Refactor cycle structure, quality standards, and execution details are defined in action-planning-agent.
Refer to: @.claude/agents/action-planning-agent.md for:
- TDD Task Decomposition Standards
- Red-Green-Refactor Cycle Requirements
- Quantification Requirements (MANDATORY)
- 5-Field Task JSON Schema
- IMPL_PLAN.md Structure (TDD variant)
- TODO_LIST.md Format
- TDD Execution Flow & Quality Validation
### TDD-Specific Requirements Summary

View File

@@ -14,8 +14,6 @@ allowed-tools: Task(*), Read(*), Glob(*)
Orchestrator command that invokes `test-context-search-agent` to gather comprehensive test coverage context for test generation workflows. Generates standardized `test-context-package.json` with coverage analysis, framework detection, and source implementation context.
**Agent**: `test-context-search-agent` (`.claude/agents/test-context-search-agent.md`)
## Core Philosophy
- **Agent Delegation**: Delegate all test coverage analysis to `test-context-search-agent` for autonomous execution
@@ -88,7 +86,6 @@ Task(
subagent_type="test-context-search-agent",
description="Gather test coverage context",
prompt=`
You are executing as test-context-search-agent (.claude/agents/test-context-search-agent.md).
## Execution Mode
**PLAN MODE** (Comprehensive) - Full Phase 1-3 execution
@@ -228,7 +225,7 @@ Refer to `test-context-search-agent.md` Phase 3.2 for complete `test-context-pac
## Notes
- **Detection-first**: Always check for existing test-context-package before invoking agent
- **Agent autonomy**: Agent handles all coverage analysis logic per `.claude/agents/test-context-search-agent.md`
- **No redundancy**: This command is a thin orchestrator, all logic in agent
- **Framework agnostic**: Supports Jest, Mocha, pytest, RSpec, Go testing, etc.
- **Coverage focus**: Primary goal is identifying implementation files without tests

View File

@@ -106,8 +106,6 @@ CRITICAL:
- Follow the progressive loading strategy defined in your agent specification (load context incrementally from memory-first approach)
## AGENT CONFIGURATION REFERENCE
All test task generation rules, schemas, and quality standards are defined in your agent specification:
@.claude/agents/action-planning-agent.md
Refer to your specification for:
- Test Task JSON Schema (6-field structure with test-specific metadata)

View File

@@ -0,0 +1,150 @@
# Phase 1.5: Project Exploration
基于元数据,启动并行探索 Agent 收集代码信息。
## Execution
### Step 1: Intelligent Angle Selection
```javascript
// 根据软件类型选择探索角度
const ANGLE_PRESETS = {
'CLI': ['architecture', 'commands', 'algorithms', 'exceptions'],
'API': ['architecture', 'endpoints', 'data-structures', 'interfaces'],
'SDK': ['architecture', 'interfaces', 'data-structures', 'algorithms'],
'DataProcessing': ['architecture', 'algorithms', 'data-structures', 'dataflow'],
'Automation': ['architecture', 'algorithms', 'exceptions', 'dataflow']
};
// 从 metadata.category 映射到预设
function getCategoryKey(category) {
if (category.includes('CLI') || category.includes('命令行')) return 'CLI';
if (category.includes('API') || category.includes('后端')) return 'API';
if (category.includes('SDK') || category.includes('库')) return 'SDK';
if (category.includes('数据处理')) return 'DataProcessing';
if (category.includes('自动化')) return 'Automation';
return 'API'; // default
}
const categoryKey = getCategoryKey(metadata.category);
const selectedAngles = ANGLE_PRESETS[categoryKey];
console.log(`
## Exploration Plan
Software: ${metadata.software_name}
Category: ${metadata.category}${categoryKey}
Selected Angles: ${selectedAngles.join(', ')}
Launching ${selectedAngles.length} parallel explorations...
`);
```
### Step 2: Launch Parallel Agents (Direct Output)
**⚠️ CRITICAL**: Agents write output files directly.
```javascript
const explorationTasks = selectedAngles.map((angle, index) =>
Task({
subagent_type: "cli-explore-agent",
run_in_background: false,
description: `Explore: ${angle}`,
prompt: `
## Exploration Objective
为 CPCC 软著申请文档执行 **${angle}** 探索。
## Assigned Context
- **Exploration Angle**: ${angle}
- **Software Name**: ${metadata.software_name}
- **Scope Path**: ${metadata.scope_path}
- **Category**: ${metadata.category}
- **Exploration Index**: ${index + 1} of ${selectedAngles.length}
- **Output File**: ${sessionFolder}/exploration-${angle}.json
## MANDATORY FIRST STEPS
1. Run: ccw tool exec get_modules_by_depth '{}' (project structure)
2. Run: rg -l "{relevant_keyword}" --type ts (locate relevant files)
3. Analyze from ${angle} perspective
## Exploration Strategy (${angle} focus)
**Step 1: Structural Scan**
- 识别与 ${angle} 相关的模块和文件
- 分析导入/导出关系
**Step 2: Pattern Recognition**
- ${angle} 相关的设计模式
- 代码组织方式
**Step 3: Write Output**
- 输出 JSON 到指定路径
## Expected Output Schema
**File**: ${sessionFolder}/exploration-${angle}.json
\`\`\`json
{
"angle": "${angle}",
"findings": {
"structure": [
{ "component": "...", "type": "module|layer|service", "path": "...", "description": "..." }
],
"patterns": [
{ "name": "...", "usage": "...", "files": ["path1", "path2"] }
],
"key_files": [
{ "path": "src/file.ts", "relevance": 0.85, "rationale": "Core ${angle} logic" }
]
},
"insights": [
{ "observation": "...", "cpcc_section": "2|3|4|5|6|7", "recommendation": "..." }
],
"_metadata": {
"exploration_angle": "${angle}",
"exploration_index": ${index + 1},
"software_name": "${metadata.software_name}",
"timestamp": "ISO8601"
}
}
\`\`\`
## Success Criteria
- [ ] get_modules_by_depth 执行完成
- [ ] 至少识别 3 个相关文件
- [ ] patterns 包含具体代码示例
- [ ] insights 关联到 CPCC 章节 (2-7)
- [ ] JSON 输出到指定路径
- [ ] Return: 2-3 句话总结 ${angle} 发现
`
})
);
// Execute all exploration tasks in parallel
```
## Output
Session folder structure after exploration:
```
${sessionFolder}/
├── exploration-architecture.json
├── exploration-{angle2}.json
├── exploration-{angle3}.json
└── exploration-{angle4}.json
```
## Downstream Usage (Phase 2 Analysis Input)
Phase 2 agents read exploration files as context:
```javascript
// Discover exploration files by known angle pattern
const explorationData = {};
selectedAngles.forEach(angle => {
const filePath = `${sessionFolder}/exploration-${angle}.json`;
explorationData[angle] = JSON.parse(Read(filePath));
});
```

View File

@@ -5,15 +5,161 @@
> **模板参考**: [../templates/agent-base.md](../templates/agent-base.md)
> **规范参考**: [../specs/cpcc-requirements.md](../specs/cpcc-requirements.md)
## Agent 执行前置条件
## Exploration → Agent 自动分配
**每个 Agent 必须首先读取以下规范文件**
根据 Phase 1.5 生成的 exploration 文件名自动分配对应的 analysis agent。
### 映射规则
```javascript
// Agent 启动时的第一步操作
const specs = {
cpcc: Read(`${skillRoot}/specs/cpcc-requirements.md`)
// Exploration 角度 → Agent 映射(基于文件名识别,不读取内容)
const EXPLORATION_TO_AGENT = {
'architecture': 'architecture',
'commands': 'functions', // CLI 命令 → 功能模块
'endpoints': 'interfaces', // API 端点 → 接口设计
'algorithms': 'algorithms',
'data-structures': 'data_structures',
'dataflow': 'data_structures', // 数据流 → 数据结构
'interfaces': 'interfaces',
'exceptions': 'exceptions'
};
// 从文件名提取角度
function extractAngle(filename) {
// exploration-architecture.json → architecture
const match = filename.match(/exploration-(.+)\.json$/);
return match ? match[1] : null;
}
// 分配 agent
function assignAgent(explorationFile) {
const angle = extractAngle(path.basename(explorationFile));
return EXPLORATION_TO_AGENT[angle] || null;
}
// Agent 配置(用于 buildAgentPrompt
const AGENT_CONFIGS = {
architecture: {
role: '系统架构师,专注于分层设计和模块依赖',
section: '2',
output: 'section-2-architecture.md',
focus: '分层结构、模块依赖、数据流向'
},
functions: {
role: '功能分析师,专注于功能点识别和交互',
section: '3',
output: 'section-3-functions.md',
focus: '功能点枚举、模块分组、入口文件、功能交互'
},
algorithms: {
role: '算法工程师,专注于核心逻辑和复杂度分析',
section: '4',
output: 'section-4-algorithms.md',
focus: '核心算法、流程步骤、复杂度、输入输出'
},
data_structures: {
role: '数据建模师,专注于实体关系和类型定义',
section: '5',
output: 'section-5-data-structures.md',
focus: '实体定义、属性类型、关系映射、枚举'
},
interfaces: {
role: 'API设计师专注于接口契约和协议',
section: '6',
output: 'section-6-interfaces.md',
focus: 'API端点、参数校验、响应格式、时序'
},
exceptions: {
role: '可靠性工程师,专注于异常处理和恢复策略',
section: '7',
output: 'section-7-exceptions.md',
focus: '异常类型、错误码、处理模式、恢复策略'
}
};
```
### 自动发现与分配流程
```javascript
// 1. 发现所有 exploration 文件(仅看文件名)
const explorationFiles = bash(`find ${sessionFolder} -name "exploration-*.json" -type f`)
.split('\n')
.filter(f => f.trim());
// 2. 按文件名自动分配 agent
const agentAssignments = explorationFiles.map(file => {
const angle = extractAngle(path.basename(file));
const agentName = EXPLORATION_TO_AGENT[angle];
return {
exploration_file: file,
angle: angle,
agent: agentName,
output_file: AGENT_CONFIGS[agentName]?.output
};
}).filter(a => a.agent);
// 3. 补充未被 exploration 覆盖的必需 agent分配相关 exploration
const coveredAgents = new Set(agentAssignments.map(a => a.agent));
const requiredAgents = ['architecture', 'functions', 'algorithms', 'data_structures', 'interfaces', 'exceptions'];
const missingAgents = requiredAgents.filter(a => !coveredAgents.has(a));
// 相关性映射:为缺失 agent 分配最相关的 exploration
const RELATED_EXPLORATIONS = {
architecture: ['architecture', 'dataflow', 'interfaces'],
functions: ['commands', 'endpoints', 'architecture'],
algorithms: ['algorithms', 'dataflow', 'architecture'],
data_structures: ['data-structures', 'dataflow', 'architecture'],
interfaces: ['interfaces', 'endpoints', 'architecture'],
exceptions: ['exceptions', 'algorithms', 'architecture']
};
function findRelatedExploration(agent, availableFiles) {
const preferences = RELATED_EXPLORATIONS[agent] || ['architecture'];
for (const pref of preferences) {
const match = availableFiles.find(f => f.includes(`exploration-${pref}.json`));
if (match) return { file: match, angle: pref, isRelated: true };
}
// 最后兜底:任意 exploration 都比没有强
return availableFiles.length > 0
? { file: availableFiles[0], angle: extractAngle(path.basename(availableFiles[0])), isRelated: true }
: { file: null, angle: null, isRelated: false };
}
missingAgents.forEach(agent => {
const related = findRelatedExploration(agent, explorationFiles);
agentAssignments.push({
exploration_file: related.file,
angle: related.angle,
agent: agent,
output_file: AGENT_CONFIGS[agent].output,
is_related: related.isRelated // 标记为相关而非直接匹配
});
});
console.log(`
## Agent Auto-Assignment
Found ${explorationFiles.length} exploration files:
${agentAssignments.map(a => {
if (!a.exploration_file) return `- ${a.agent} agent (no exploration)`;
if (a.is_related) return `- ${a.agent} agent ← ${a.angle} (related)`;
return `- ${a.agent} agent ← ${a.angle} (direct)`;
}).join('\n')}
`);
```
---
## Agent 执行前置条件
**每个 Agent 接收 exploration 文件路径,自行读取内容**
```javascript
// Agent prompt 中包含文件路径
// Agent 启动后的操作顺序:
// 1. Read exploration 文件(如有)
// 2. Read CPCC 规范文件
// 3. 执行分析任务
```
规范文件路径(相对于 skill 根目录):
@@ -47,26 +193,90 @@ const specs = {
## 执行流程
```javascript
// 1. 准备目录
// 1. 发现 exploration 文件并自动分配 agent
const explorationFiles = bash(`find ${sessionFolder} -name "exploration-*.json" -type f`)
.split('\n')
.filter(f => f.trim());
const agentAssignments = explorationFiles.map(file => {
const angle = extractAngle(path.basename(file));
const agentName = EXPLORATION_TO_AGENT[angle];
return { exploration_file: file, angle, agent: agentName };
}).filter(a => a.agent);
// 补充必需 agent
const coveredAgents = new Set(agentAssignments.map(a => a.agent));
const requiredAgents = ['architecture', 'functions', 'algorithms', 'data_structures', 'interfaces', 'exceptions'];
requiredAgents.filter(a => !coveredAgents.has(a)).forEach(agent => {
agentAssignments.push({ exploration_file: null, angle: null, agent });
});
// 2. 准备目录
Bash(`mkdir -p ${outputDir}/sections`);
// 2. 并行启动 6 个 Agent
const results = await Promise.all([
launchAgent('architecture', metadata, outputDir),
launchAgent('functions', metadata, outputDir),
launchAgent('algorithms', metadata, outputDir),
launchAgent('data_structures', metadata, outputDir),
launchAgent('interfaces', metadata, outputDir),
launchAgent('exceptions', metadata, outputDir)
]);
// 3. 并行启动所有 Agent(传递 exploration 文件路径)
const results = await Promise.all(
agentAssignments.map(assignment =>
Task({
subagent_type: "cli-explore-agent",
run_in_background: false,
description: `Analyze: ${assignment.agent}`,
prompt: buildAgentPrompt(assignment, metadata, outputDir)
})
)
);
// 3. 收集返回信息
// 4. 收集返回信息
const summaries = results.map(r => JSON.parse(r));
// 4. 传递给 Phase 2.5
// 5. 传递给 Phase 2.5
return { summaries, cross_notes: summaries.flatMap(s => s.cross_module_notes) };
```
### Agent Prompt 构建
```javascript
function buildAgentPrompt(assignment, metadata, outputDir) {
const config = AGENT_CONFIGS[assignment.agent];
let contextSection = '';
if (assignment.exploration_file) {
const matchType = assignment.is_related ? '相关' : '直接匹配';
contextSection = `[CONTEXT]
**Exploration 文件**: ${assignment.exploration_file}
**匹配类型**: ${matchType}
首先读取此文件获取 ${assignment.angle} 探索结果作为分析上下文。
${assignment.is_related ? `注意:这是相关探索结果(非直接匹配),请提取与 ${config.focus} 相关的信息。` : ''}
`;
}
return `
${contextSection}
[SPEC]
读取规范文件:
- Read: ${skillRoot}/specs/cpcc-requirements.md
[ROLE] ${config.role}
[TASK]
分析 ${metadata.scope_path},生成 Section ${config.section}
输出: ${outputDir}/sections/${config.output}
[CPCC_SPEC]
- 内容基于代码分析,无臆测
- 图表编号: 图${config.section}-1, 图${config.section}-2...
- 每个子章节 ≥100字
- 包含文件路径引用
[FOCUS]
${config.focus}
[RETURN JSON]
{"status":"completed","output_file":"${config.output}","summary":"<50字>","cross_module_notes":[],"stats":{}}
`;
}
```
---
## Agent 提示词

View File

@@ -0,0 +1,285 @@
---
name: issue-manage
description: Interactive issue management with menu-driven CRUD operations. Use when managing issues, viewing issue status, editing issue fields, performing bulk operations, or viewing issue history. Triggers on "manage issue", "list issues", "edit issue", "delete issue", "bulk update", "issue dashboard", "issue history", "completed issues".
allowed-tools: Bash, Read, Write, AskUserQuestion, Task, Glob
---
# Issue Management Skill
Interactive menu-driven interface for issue CRUD operations via `ccw issue` CLI.
## Quick Start
Ask me:
- "Show all issues" → List with filters
- "View issue GH-123" → Detailed inspection
- "Edit issue priority" → Modify fields
- "Delete old issues" → Remove with confirmation
- "Bulk update status" → Batch operations
- "Show completed issues" → View issue history
- "Archive old issues" → Move to history
## CLI Endpoints
```bash
# Core operations
ccw issue list # List active issues
ccw issue list <id> --json # Get issue details
ccw issue history # List completed issues (from history)
ccw issue history --json # Completed issues as JSON
ccw issue status <id> # Detailed status
ccw issue init <id> --title "..." # Create issue
ccw issue task <id> --title "..." # Add task
ccw issue bind <id> <solution-id> # Bind solution
ccw issue update <id> --status completed # Complete & auto-archive
# Queue management
ccw issue queue # List current queue
ccw issue queue add <id> # Add to queue
ccw issue queue list # Queue history
ccw issue queue switch <queue-id> # Switch queue
ccw issue queue archive # Archive queue
ccw issue queue delete <queue-id> # Delete queue
ccw issue next # Get next task
ccw issue done <queue-id> # Mark completed
ccw issue update --from-queue # Sync statuses from queue
```
## Operations
### 1. LIST 📋
Filter and browse issues:
```
┌─ Filter by Status ─────────────────┐
│ □ All □ Registered │
│ □ Planned □ Queued │
│ □ Executing □ Completed │
└────────────────────────────────────┘
```
**Flow**:
1. Ask filter preferences → `ccw issue list --json`
2. Display table: ID | Status | Priority | Title
3. Select issue for detail view
### 2. VIEW 🔍
Detailed issue inspection:
```
┌─ Issue: GH-123 ─────────────────────┐
│ Title: Fix authentication bug │
│ Status: planned | Priority: P2 │
│ Solutions: 2 (1 bound) │
│ Tasks: 5 pending │
└─────────────────────────────────────┘
```
**Flow**:
1. Fetch `ccw issue status <id> --json`
2. Display issue + solutions + tasks
3. Offer actions: Edit | Plan | Queue | Delete
### 3. EDIT ✏️
Modify issue fields:
| Field | Options |
|-------|---------|
| Title | Free text |
| Priority | P1-P5 |
| Status | registered → completed |
| Context | Problem description |
| Labels | Comma-separated |
**Flow**:
1. Select field to edit
2. Show current value
3. Collect new value via AskUserQuestion
4. Update `.workflow/issues/issues.jsonl`
### 4. DELETE 🗑️
Remove with confirmation:
```
⚠️ Delete issue GH-123?
This will also remove:
- Associated solutions
- Queued tasks
[Delete] [Cancel]
```
**Flow**:
1. Confirm deletion via AskUserQuestion
2. Remove from `issues.jsonl`
3. Clean up `solutions/<id>.jsonl`
4. Remove from `queue.json`
### 5. HISTORY 📚
View and manage completed issues:
```
┌─ Issue History ─────────────────────┐
│ ID Completed Title │
│ ISS-001 2025-12-28 12:00 Fix bug │
│ ISS-002 2025-12-27 15:30 Feature │
└──────────────────────────────────────┘
```
**Flow**:
1. Fetch `ccw issue history --json`
2. Display table: ID | Completed At | Title
3. Optional: Filter by date range
**Auto-Archive**: When issue status → `completed`:
- Issue moves from `issues.jsonl``issue-history.jsonl`
- Solutions remain in `solutions/<id>.jsonl`
- Queue items marked completed
### 6. BULK 📦
Batch operations:
| Operation | Description |
|-----------|-------------|
| Update Status | Change multiple issues |
| Update Priority | Batch priority change |
| Add Labels | Tag multiple issues |
| Delete Multiple | Bulk removal |
| Queue All Planned | Add all planned to queue |
| Retry All Failed | Reset failed tasks |
| Sync from Queue | Update statuses from active queue |
## Workflow
```
┌────────────────────────────────────────────────┐
│ Main Menu │
│ ┌────┐ ┌────┐ ┌────┐ ┌─────┐ ┌────┐ │
│ │List│ │View│ │Edit│ │Hist.│ │Bulk│ │
│ └──┬─┘ └──┬─┘ └──┬─┘ └──┬──┘ └──┬─┘ │
└─────┼──────┼──────┼──────┼───────┼─────────────┘
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
Filter Detail Fields History Multi
Select Actions Update Browse Select
│ │ │ │ │
└──────┴──────┴──────┴───────┘
Back to Menu
```
**Issue Lifecycle**:
```
registered → planned → queued → executing → completed
issue-history.jsonl
```
## Implementation Guide
### Entry Point
```javascript
// Parse input for issue ID
const issueId = input.match(/^([A-Z]+-\d+|ISS-\d+)/i)?.[1];
// Show main menu
await showMainMenu(issueId);
```
### Main Menu Pattern
```javascript
// 1. Fetch dashboard data
const issues = JSON.parse(Bash('ccw issue list --json') || '[]');
const history = JSON.parse(Bash('ccw issue history --json 2>/dev/null') || '[]');
const queue = JSON.parse(Bash('ccw issue queue --json 2>/dev/null') || '{}');
// 2. Display summary
console.log(`Active: ${issues.length} | Completed: ${history.length} | Queue: ${queue.pending_count || 0} pending`);
// 3. Ask action via AskUserQuestion
const action = AskUserQuestion({
questions: [{
question: 'What would you like to do?',
header: 'Action',
options: [
{ label: 'List Issues', description: 'Browse active issues' },
{ label: 'View Issue', description: 'Detail view' },
{ label: 'Edit Issue', description: 'Modify fields' },
{ label: 'View History', description: 'Completed issues' },
{ label: 'Bulk Operations', description: 'Batch actions' }
]
}]
});
// 4. Route to handler
```
### Filter Pattern
```javascript
const filter = AskUserQuestion({
questions: [{
question: 'Filter by status?',
header: 'Filter',
multiSelect: true,
options: [
{ label: 'All', description: 'Show all' },
{ label: 'Registered', description: 'Unplanned' },
{ label: 'Planned', description: 'Has solution' },
{ label: 'Executing', description: 'In progress' }
]
}]
});
```
### Edit Pattern
```javascript
// Select field
const field = AskUserQuestion({...});
// Get new value based on field type
// For Priority: show P1-P5 options
// For Status: show status options
// For Title: accept free text via "Other"
// Update file
const issuesPath = '.workflow/issues/issues.jsonl';
// Read → Parse → Update → Write
```
## Data Files
| File | Purpose |
|------|---------|
| `.workflow/issues/issues.jsonl` | Active issue records |
| `.workflow/issues/issue-history.jsonl` | Completed issues (archived) |
| `.workflow/issues/solutions/<id>.jsonl` | Solutions per issue |
| `.workflow/issues/queues/index.json` | Queue index (multi-queue) |
| `.workflow/issues/queues/<queue-id>.json` | Individual queue files |
## Error Handling
| Error | Resolution |
|-------|------------|
| No issues found | Suggest `/issue:new` to create |
| Issue not found | Show available issues, re-prompt |
| Write failure | Check file permissions |
| Queue error | Display ccw error message |
## Related Commands
- `/issue:new` - Create structured issue
- `/issue:plan` - Generate solution
- `/issue:queue` - Form execution queue
- `/issue:execute` - Execute tasks

View File

@@ -1,75 +1,176 @@
# Phase 2: Project Exploration
Launch parallel exploration agents based on report type.
Launch parallel exploration agents based on report type and task context.
## Execution
### Step 1: Map Exploration Angles
### Step 1: Intelligent Angle Selection
```javascript
const angleMapping = {
architecture: ["Layer Structure", "Module Dependencies", "Entry Points", "Data Flow"],
design: ["Design Patterns", "Class Relationships", "Interface Contracts", "State Management"],
methods: ["Core Algorithms", "Critical Paths", "Public APIs", "Complex Logic"],
comprehensive: ["Layer Structure", "Design Patterns", "Core Algorithms", "Data Flow"]
// Angle presets based on report type (adapted from lite-plan.md)
const ANGLE_PRESETS = {
architecture: ['layer-structure', 'module-dependencies', 'entry-points', 'data-flow'],
design: ['design-patterns', 'class-relationships', 'interface-contracts', 'state-management'],
methods: ['core-algorithms', 'critical-paths', 'public-apis', 'complex-logic'],
comprehensive: ['architecture', 'patterns', 'dependencies', 'integration-points']
};
const angles = angleMapping[config.type];
// Depth-based angle count
const angleCount = {
shallow: 2,
standard: 3,
deep: 4
};
function selectAngles(reportType, depth) {
const preset = ANGLE_PRESETS[reportType] || ANGLE_PRESETS.comprehensive;
const count = angleCount[depth] || 3;
return preset.slice(0, count);
}
const selectedAngles = selectAngles(config.type, config.depth);
console.log(`
## Exploration Plan
Report Type: ${config.type}
Depth: ${config.depth}
Selected Angles: ${selectedAngles.join(', ')}
Launching ${selectedAngles.length} parallel explorations...
`);
```
### Step 2: Launch Parallel Agents
### Step 2: Launch Parallel Agents (Direct Output)
For each angle, launch an exploration agent:
**⚠️ CRITICAL**: Agents write output files directly. No aggregation needed.
```javascript
Task({
subagent_type: "cli-explore-agent",
run_in_background: false,
description: `Explore: ${angle}`,
prompt: `
// Launch agents with pre-assigned angles
const explorationTasks = selectedAngles.map((angle, index) =>
Task({
subagent_type: "cli-explore-agent",
run_in_background: false, // ⚠️ MANDATORY: Must wait for results
description: `Explore: ${angle}`,
prompt: `
## Exploration Objective
Execute **${angle}** exploration for project analysis report.
Execute **${angle}** exploration for ${config.type} project analysis report.
## Context
- **Angle**: ${angle}
## Assigned Context
- **Exploration Angle**: ${angle}
- **Report Type**: ${config.type}
- **Depth**: ${config.depth}
- **Scope**: ${config.scope}
- **Exploration Index**: ${index + 1} of ${selectedAngles.length}
- **Output File**: ${sessionFolder}/exploration-${angle}.json
## Exploration Protocol
1. Structural Discovery (get_modules_by_depth, rg, glob)
2. Pattern Recognition (conventions, naming, organization)
3. Relationship Mapping (dependencies, integration points)
## MANDATORY FIRST STEPS (Execute by Agent)
**You (cli-explore-agent) MUST execute these steps in order:**
1. Run: ccw tool exec get_modules_by_depth '{}' (project structure)
2. Run: rg -l "{relevant_keyword}" --type ts (locate relevant files)
3. Analyze project from ${angle} perspective
## Output Format
## Exploration Strategy (${angle} focus)
**Step 1: Structural Scan** (Bash)
- get_modules_by_depth.sh → identify modules related to ${angle}
- find/rg → locate files relevant to ${angle} aspect
- Analyze imports/dependencies from ${angle} perspective
**Step 2: Semantic Analysis** (Gemini/Qwen CLI)
- How does existing code handle ${angle} concerns?
- What patterns are used for ${angle}?
- Identify key architectural decisions related to ${angle}
**Step 3: Write Output Directly**
- Consolidate ${angle} findings into JSON
- Write to output file path specified above
## Expected Output Schema
**File**: ${sessionFolder}/exploration-${angle}.json
\`\`\`json
{
"angle": "${angle}",
"findings": {
"structure": [...],
"patterns": [...],
"relationships": [...],
"key_files": [{path, relevance, rationale}]
"structure": [
{ "component": "...", "type": "module|layer|service", "description": "..." }
],
"patterns": [
{ "name": "...", "usage": "...", "files": ["path1", "path2"] }
],
"relationships": [
{ "from": "...", "to": "...", "type": "depends|imports|calls", "strength": "high|medium|low" }
],
"key_files": [
{ "path": "src/file.ts", "relevance": 0.85, "rationale": "Core ${angle} logic" }
]
},
"insights": [...]
"insights": [
{ "observation": "...", "impact": "high|medium|low", "recommendation": "..." }
],
"_metadata": {
"exploration_angle": "${angle}",
"exploration_index": ${index + 1},
"report_type": "${config.type}",
"timestamp": "ISO8601"
}
}
\`\`\`
## Success Criteria
- [ ] get_modules_by_depth.sh executed
- [ ] At least 3 relevant files identified with ${angle} rationale
- [ ] Patterns are actionable (code examples, not generic advice)
- [ ] Relationships include concrete file references
- [ ] JSON output written to ${sessionFolder}/exploration-${angle}.json
- [ ] Return: 2-3 sentence summary of ${angle} findings
`
})
```
})
);
### Step 3: Aggregate Results
Merge all exploration results into unified findings:
```javascript
const aggregatedFindings = {
structure: [], // from all angles
patterns: [], // from all angles
relationships: [], // from all angles
key_files: [], // deduplicated
insights: [] // prioritized
};
// Execute all exploration tasks in parallel
```
## Output
Save exploration results to `exploration-{angle}.json` files.
Session folder structure after exploration:
```
${sessionFolder}/
├── exploration-{angle1}.json # Agent 1 direct output
├── exploration-{angle2}.json # Agent 2 direct output
├── exploration-{angle3}.json # Agent 3 direct output (if applicable)
└── exploration-{angle4}.json # Agent 4 direct output (if applicable)
```
## Downstream Usage (Phase 3 Analysis Input)
Subsequent analysis phases MUST read exploration outputs as input:
```javascript
// Discover exploration files by known angle pattern
const explorationData = {};
selectedAngles.forEach(angle => {
const filePath = `${sessionFolder}/exploration-${angle}.json`;
explorationData[angle] = JSON.parse(Read(filePath));
});
// Pass to analysis agent
Task({
subagent_type: "analysis-agent",
prompt: `
## Analysis Input
### Exploration Data by Angle
${Object.entries(explorationData).map(([angle, data]) => `
#### ${angle}
${JSON.stringify(data, null, 2)}
`).join('\n')}
## Analysis Task
Synthesize findings from all exploration angles...
`
});
```

View File

@@ -5,16 +5,176 @@
> **规范参考**: [../specs/quality-standards.md](../specs/quality-standards.md)
> **写作风格**: [../specs/writing-style.md](../specs/writing-style.md)
## Agent 执行前置条件
## Exploration → Agent 自动分配
**每个 Agent 必须首先读取以下规范文件**
根据 Phase 2 生成的 exploration 文件名自动分配对应的 analysis agent。
### 映射规则
```javascript
// Agent 启动时的第一步操作
const specs = {
quality: Read(`${skillRoot}/specs/quality-standards.md`),
style: Read(`${skillRoot}/specs/writing-style.md`)
// Exploration 角度 → Agent 映射(基于文件名识别,不读取内容)
const EXPLORATION_TO_AGENT = {
// Architecture Report 角度
'layer-structure': 'layers',
'module-dependencies': 'dependencies',
'entry-points': 'entrypoints',
'data-flow': 'dataflow',
// Design Report 角度
'design-patterns': 'patterns',
'class-relationships': 'classes',
'interface-contracts': 'interfaces',
'state-management': 'state',
// Methods Report 角度
'core-algorithms': 'algorithms',
'critical-paths': 'paths',
'public-apis': 'apis',
'complex-logic': 'logic',
// Comprehensive 角度
'architecture': 'overview',
'patterns': 'patterns',
'dependencies': 'dependencies',
'integration-points': 'entrypoints'
};
// 从文件名提取角度
function extractAngle(filename) {
// exploration-layer-structure.json → layer-structure
const match = filename.match(/exploration-(.+)\.json$/);
return match ? match[1] : null;
}
// 分配 agent
function assignAgent(explorationFile) {
const angle = extractAngle(path.basename(explorationFile));
return EXPLORATION_TO_AGENT[angle] || null;
}
// Agent 配置(用于 buildAgentPrompt
const AGENT_CONFIGS = {
overview: {
role: '首席系统架构师',
task: '基于代码库全貌,撰写"总体架构"章节,洞察核心价值主张和顶层技术决策',
focus: '领域边界与定位、架构范式、核心技术决策、顶层模块划分',
constraint: '避免罗列目录结构重点阐述设计意图包含至少1个 Mermaid 架构图'
},
layers: {
role: '资深软件设计师',
task: '分析系统逻辑分层结构,撰写"逻辑视点与分层架构"章节',
focus: '职责分配体系、数据流向与约束、边界隔离策略、异常处理流',
constraint: '不要列举具体文件名,关注层级间契约和隔离艺术'
},
dependencies: {
role: '集成架构专家',
task: '审视系统外部连接与内部耦合,撰写"依赖管理与生态集成"章节',
focus: '外部集成拓扑、核心依赖分析、依赖注入与控制反转、供应链安全',
constraint: '禁止简单列出依赖配置,必须分析集成策略和风险控制模型'
},
dataflow: {
role: '数据架构师',
task: '追踪系统数据流转机制,撰写"数据流与状态管理"章节',
focus: '数据入口与出口、数据转换管道、持久化策略、一致性保障',
constraint: '关注数据生命周期和形态演变,不要罗列数据库表结构'
},
entrypoints: {
role: '系统边界分析师',
task: '识别系统入口设计和关键路径,撰写"系统入口与调用链"章节',
focus: '入口类型与职责、请求处理管道、关键业务路径、异常与边界处理',
constraint: '关注入口设计哲学,不要逐个列举所有端点'
},
patterns: {
role: '核心开发规范制定者',
task: '挖掘代码中的复用机制和标准化实践,撰写"设计模式与工程规范"章节',
focus: '架构级模式、通信与并发模式、横切关注点实现、抽象与复用策略',
constraint: '避免教科书式解释,必须结合项目上下文说明应用场景'
},
classes: {
role: '领域模型设计师',
task: '分析系统类型体系和领域模型,撰写"类型体系与领域建模"章节',
focus: '领域模型设计、继承与组合策略、职责分配原则、类型安全与约束',
constraint: '关注建模思想,用 UML 类图辅助说明核心关系'
},
interfaces: {
role: '契约设计专家',
task: '分析系统接口设计和抽象层次,撰写"接口契约与抽象设计"章节',
focus: '抽象层次设计、契约与实现分离、扩展点设计、版本演进策略',
constraint: '关注接口设计哲学,不要逐个列举接口方法签名'
},
state: {
role: '状态管理架构师',
task: '分析系统状态管理机制,撰写"状态管理与生命周期"章节',
focus: '状态模型设计、状态生命周期、并发与一致性、状态恢复与容错',
constraint: '关注状态管理设计决策,不要列举具体变量名'
},
algorithms: {
role: '算法架构师',
task: '分析系统核心算法设计,撰写"核心算法与计算模型"章节',
focus: '算法选型与权衡、计算模型设计、性能与可扩展性、正确性保障',
constraint: '关注算法思想,用流程图辅助说明复杂逻辑'
},
paths: {
role: '性能架构师',
task: '分析系统关键执行路径,撰写"关键路径与性能设计"章节',
focus: '关键业务路径、性能敏感区域、瓶颈识别与缓解、降级与熔断',
constraint: '关注路径设计战略考量,不要罗列所有代码执行步骤'
},
apis: {
role: 'API 设计规范专家',
task: '分析系统对外接口设计规范,撰写"API 设计与规范"章节',
focus: 'API 设计风格、命名与结构规范、版本管理策略、错误处理规范',
constraint: '关注设计规范和一致性,不要逐个列举所有 API 端点'
},
logic: {
role: '业务逻辑架构师',
task: '分析系统业务逻辑建模,撰写"业务逻辑与规则引擎"章节',
focus: '业务规则建模、决策点设计、边界条件处理、业务流程编排',
constraint: '关注业务逻辑组织方式,不要逐行解释代码逻辑'
}
};
```
### 自动发现与分配流程
```javascript
// 1. 发现所有 exploration 文件(仅看文件名)
const explorationFiles = bash(`find ${sessionFolder} -name "exploration-*.json" -type f`)
.split('\n')
.filter(f => f.trim());
// 2. 按文件名自动分配 agent
const agentAssignments = explorationFiles.map(file => {
const angle = extractAngle(path.basename(file));
const agentName = EXPLORATION_TO_AGENT[angle];
return {
exploration_file: file,
angle: angle,
agent: agentName,
output_file: `section-${agentName}.md`
};
}).filter(a => a.agent); // 过滤未映射的角度
console.log(`
## Agent Auto-Assignment
Found ${explorationFiles.length} exploration files:
${agentAssignments.map(a => `- ${a.angle}${a.agent} agent`).join('\n')}
`);
```
---
## Agent 执行前置条件
**每个 Agent 接收 exploration 文件路径,自行读取内容**
```javascript
// Agent prompt 中包含文件路径
// Agent 启动后的操作顺序:
// 1. Read exploration 文件(上下文输入)
// 2. Read 规范文件
// 3. 执行分析任务
```
规范文件路径(相对于 skill 根目录):
@@ -617,15 +777,30 @@ Task({
## 执行流程
```javascript
// 1. 根据报告类型选择 Agent 配置
const agentConfigs = getAgentConfigs(config.type);
// 1. 发现 exploration 文件并自动分配 agent
const explorationFiles = bash(`find ${sessionFolder} -name "exploration-*.json" -type f`)
.split('\n')
.filter(f => f.trim());
const agentAssignments = explorationFiles.map(file => {
const angle = extractAngle(path.basename(file));
const agentName = EXPLORATION_TO_AGENT[angle];
return { exploration_file: file, angle, agent: agentName };
}).filter(a => a.agent);
// 2. 准备目录
Bash(`mkdir "${outputDir}\\sections"`);
// 3. 并行启动所有 Agent
// 3. 并行启动所有 Agent(传递 exploration 文件路径)
const results = await Promise.all(
agentConfigs.map(agent => launchAgent(agent, config, outputDir))
agentAssignments.map(assignment =>
Task({
subagent_type: "cli-explore-agent",
run_in_background: false,
description: `Analyze: ${assignment.agent}`,
prompt: buildAgentPrompt(assignment, config, outputDir)
})
)
);
// 4. 收集简要返回信息
@@ -635,6 +810,45 @@ const summaries = results.map(r => JSON.parse(r));
return { summaries, cross_notes: summaries.flatMap(s => s.cross_module_notes) };
```
### Agent Prompt 构建
```javascript
function buildAgentPrompt(assignment, config, outputDir) {
const agentConfig = AGENT_CONFIGS[assignment.agent];
return `
[CONTEXT]
**Exploration 文件**: ${assignment.exploration_file}
首先读取此文件获取 ${assignment.angle} 探索结果作为分析上下文。
[SPEC]
读取规范文件:
- Read: ${skillRoot}/specs/quality-standards.md
- Read: ${skillRoot}/specs/writing-style.md
[ROLE] ${agentConfig.role}
[TASK]
${agentConfig.task}
输出: ${outputDir}/sections/section-${assignment.agent}.md
[STYLE]
- 严谨专业的中文技术写作,专业术语保留英文
- 完全客观的第三人称视角,严禁"我们"、"开发者"
- 段落式叙述,采用"论点-论据-结论"结构
- 善用逻辑连接词体现设计推演过程
[FOCUS]
${agentConfig.focus}
[CONSTRAINT]
${agentConfig.constraint}
[RETURN JSON]
{"status":"completed","output_file":"section-${assignment.agent}.md","summary":"<50字>","cross_module_notes":[],"stats":{}}
`;
}
```
## Output
各 Agent 写入 `sections/section-xxx.md`,返回简要 JSON 供 Phase 3.5 汇总。

View File

@@ -4,6 +4,29 @@
> **写作规范**: [../specs/writing-style.md](../specs/writing-style.md)
## 执行要求
**必须执行**Phase 3 所有 Analysis Agents 完成后,主编排器**必须**调用此 Consolidation Agent。
**触发条件**
- Phase 3 所有 agent 已返回结果status: completed/partial/failed
- `sections/section-*.md` 文件已生成
**输入来源**
- `agent_summaries`: Phase 3 各 agent 返回的 JSON包含 status, output_file, summary, cross_module_notes
- `cross_module_notes`: 从各 agent 返回中提取的跨模块备注数组
**调用时机**
```javascript
// Phase 3 完成后,主编排器执行:
const phase3Results = await runPhase3Agents(); // 并行执行所有 analysis agents
const agentSummaries = phase3Results.map(r => JSON.parse(r));
const crossNotes = agentSummaries.flatMap(s => s.cross_module_notes || []);
// 必须调用 Phase 3.5 Consolidation Agent
await runPhase35Consolidation(agentSummaries, crossNotes);
```
## 核心职责
1. **跨章节综合分析**:生成 synthesis报告综述
@@ -22,7 +45,9 @@ interface ConsolidationInput {
}
```
## 执行
## Agent 调用代码
主编排器使用以下代码调用 Consolidation Agent
```javascript
Task({

View File

@@ -0,0 +1,184 @@
---
name: software-manual
description: Generate interactive TiddlyWiki-style HTML software manuals with screenshots, API docs, and multi-level code examples. Use when creating user guides, software documentation, or API references. Triggers on "software manual", "user guide", "generate manual", "create docs".
allowed-tools: Task, AskUserQuestion, Read, Bash, Glob, Grep, Write, mcp__chrome__*
---
# Software Manual Skill
Generate comprehensive, interactive software manuals in TiddlyWiki-style single-file HTML format.
## Architecture Overview
```
┌─────────────────────────────────────────────────────────────────┐
│ Context-Optimized Architecture │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Phase 1: Requirements → manual-config.json │
│ ↓ │
│ Phase 2: Exploration → exploration-*.json │
│ ↓ │
│ Phase 3: Parallel Agents → sections/section-*.md │
│ ↓ (6 Agents) │
│ Phase 3.5: Consolidation → consolidation-summary.md │
│ ↓ │
│ Phase 4: Screenshot → screenshots/*.png │
│ Capture (via Chrome MCP) │
│ ↓ │
│ Phase 5: HTML Assembly → {name}-使用手册.html │
│ ↓ │
│ Phase 6: Refinement → iterations/ │
│ │
└─────────────────────────────────────────────────────────────────┘
```
## Key Design Principles
1. **主 Agent 编排,子 Agent 执行**: 所有繁重计算委托给 `universal-executor` 子 Agent
2. **Brief Returns**: Agents return path + summary, not full content (avoid context overflow)
3. **System Agents**: 使用 `cli-explore-agent` (探索) 和 `universal-executor` (执行)
4. **成熟库内嵌**: marked.js (MD 解析) + highlight.js (语法高亮),无 CDN 依赖
5. **Single-File HTML**: TiddlyWiki-style interactive document with embedded resources
6. **动态标签**: 根据实际章节自动生成导航标签
## Execution Flow
```
┌─────────────────────────────────────────────────────────────────┐
│ Phase 1: Requirements Discovery (主 Agent) │
│ → AskUserQuestion: 收集软件类型、目标用户、文档范围 │
│ → Output: manual-config.json │
├─────────────────────────────────────────────────────────────────┤
│ Phase 2: Project Exploration (cli-explore-agent × N) │
│ → 并行探索: architecture, ui-routes, api-endpoints, config │
│ → Output: exploration-*.json │
├─────────────────────────────────────────────────────────────────┤
│ Phase 2.5: API Extraction (extract_apis.py) │
│ → 自动提取: FastAPI/TypeDoc/pdoc │
│ → Output: api-docs/{backend,frontend,modules}/*.md │
├─────────────────────────────────────────────────────────────────┤
│ Phase 3: Parallel Analysis (universal-executor × 6) │
│ → 6 个子 Agent 并行: overview, ui-guide, api-docs, config, │
│ troubleshooting, code-examples │
│ → Output: sections/section-*.md │
├─────────────────────────────────────────────────────────────────┤
│ Phase 3.5: Consolidation (universal-executor) │
│ → 质量检查: 一致性、交叉引用、截图标记 │
│ → Output: consolidation-summary.md, screenshots-list.json │
├─────────────────────────────────────────────────────────────────┤
│ Phase 4: Screenshot Capture (universal-executor + Chrome MCP) │
│ → 批量截图: 调用 mcp__chrome__screenshot │
│ → Output: screenshots/*.png + manifest.json │
├─────────────────────────────────────────────────────────────────┤
│ Phase 5: HTML Assembly (universal-executor) │
│ → 组装 HTML: MD→tiddlers, 嵌入 CSS/JS/图片 │
│ → Output: {name}-使用手册.html │
├─────────────────────────────────────────────────────────────────┤
│ Phase 6: Iterative Refinement (主 Agent) │
│ → 预览 + 用户反馈 + 迭代修复 │
│ → Output: iterations/v*.html │
└─────────────────────────────────────────────────────────────────┘
```
## Agent Configuration
| Agent | Role | Output File | Focus Areas |
|-------|------|-------------|-------------|
| overview | Product Manager | section-overview.md | Product intro, features, quick start |
| ui-guide | UX Expert | section-ui-guide.md | UI operations, step-by-step guides |
| api-docs | API Architect | section-api-reference.md | REST API, Frontend API |
| config | DevOps Engineer | section-configuration.md | Env vars, deployment, settings |
| troubleshooting | Support Engineer | section-troubleshooting.md | FAQs, error codes, solutions |
| code-examples | Developer Advocate | section-examples.md | Beginner/Intermediate/Advanced examples |
## Agent Return Format
```typescript
interface ManualAgentReturn {
status: "completed" | "partial" | "failed";
output_file: string;
summary: string; // Max 50 chars
screenshots_needed: Array<{
id: string; // e.g., "ss-login-form"
url: string; // Relative or absolute URL
description: string; // "Login form interface"
selector?: string; // CSS selector for partial screenshot
wait_for?: string; // Element to wait for
}>;
cross_references: string[]; // Other sections referenced
difficulty_level: "beginner" | "intermediate" | "advanced";
}
```
## HTML Features (TiddlyWiki-style)
1. **Search**: Full-text search with result highlighting
2. **Collapse/Expand**: Per-section collapsible content
3. **Tag Navigation**: Filter by category tags
4. **Theme Toggle**: Light/Dark mode with localStorage persistence
5. **Single File**: All CSS/JS/images embedded as Base64
6. **Offline**: Works without internet connection
7. **Print-friendly**: Optimized print stylesheet
## Directory Setup
```javascript
// Generate timestamp directory name
const timestamp = new Date().toISOString().slice(0,19).replace(/[-:T]/g, '');
const dir = `.workflow/.scratchpad/manual-${timestamp}`;
// Windows
Bash(`mkdir "${dir}\\sections" && mkdir "${dir}\\screenshots" && mkdir "${dir}\\api-docs" && mkdir "${dir}\\iterations"`);
```
## Output Structure
```
.workflow/.scratchpad/manual-{timestamp}/
├── manual-config.json # Phase 1
├── exploration/ # Phase 2
│ ├── exploration-architecture.json
│ ├── exploration-ui-routes.json
│ └── exploration-api-endpoints.json
├── sections/ # Phase 3
│ ├── section-overview.md
│ ├── section-ui-guide.md
│ ├── section-api-reference.md
│ ├── section-configuration.md
│ ├── section-troubleshooting.md
│ └── section-examples.md
├── consolidation-summary.md # Phase 3.5
├── api-docs/ # API documentation
│ ├── frontend/ # TypeDoc output
│ └── backend/ # Swagger/OpenAPI output
├── screenshots/ # Phase 4
│ ├── ss-*.png
│ └── screenshots-manifest.json
├── iterations/ # Phase 6
│ ├── v1.html
│ └── v2.html
└── {软件名}-使用手册.html # Final Output
```
## Reference Documents
| Document | Purpose |
|----------|---------|
| [phases/01-requirements-discovery.md](phases/01-requirements-discovery.md) | 用户配置收集 |
| [phases/02-project-exploration.md](phases/02-project-exploration.md) | 项目类型检测 |
| [phases/02.5-api-extraction.md](phases/02.5-api-extraction.md) | API 自动提取 |
| [phases/03-parallel-analysis.md](phases/03-parallel-analysis.md) | 6 Agent 并行分析 |
| [phases/03.5-consolidation.md](phases/03.5-consolidation.md) | 整合与质量检查 |
| [phases/04-screenshot-capture.md](phases/04-screenshot-capture.md) | Chrome MCP 截图 |
| [phases/05-html-assembly.md](phases/05-html-assembly.md) | HTML 组装 |
| [phases/06-iterative-refinement.md](phases/06-iterative-refinement.md) | 迭代优化 |
| [specs/quality-standards.md](specs/quality-standards.md) | 质量标准 |
| [specs/writing-style.md](specs/writing-style.md) | 写作风格 |
| [templates/tiddlywiki-shell.html](templates/tiddlywiki-shell.html) | HTML 模板 |
| [templates/css/wiki-base.css](templates/css/wiki-base.css) | 基础样式 |
| [templates/css/wiki-dark.css](templates/css/wiki-dark.css) | 暗色主题 |
| [scripts/bundle-libraries.md](scripts/bundle-libraries.md) | 库文件打包 |
| [scripts/api-extractor.md](scripts/api-extractor.md) | API 提取说明 |
| [scripts/extract_apis.py](scripts/extract_apis.py) | API 提取脚本 |
| [scripts/screenshot-helper.md](scripts/screenshot-helper.md) | 截图辅助 |

View File

@@ -0,0 +1,162 @@
# Phase 1: Requirements Discovery
Collect user requirements and generate configuration for the manual generation process.
## Objective
Gather essential information about the software project to customize the manual generation:
- Software type and characteristics
- Target user audience
- Documentation scope and depth
- Special requirements
## Execution Steps
### Step 1: Software Information Collection
Use `AskUserQuestion` to collect:
```javascript
AskUserQuestion({
questions: [
{
question: "What type of software is this project?",
header: "Software Type",
options: [
{ label: "Web Application", description: "Frontend + Backend web app with UI" },
{ label: "CLI Tool", description: "Command-line interface tool" },
{ label: "SDK/Library", description: "Developer library or SDK" },
{ label: "Desktop App", description: "Desktop application (Electron, etc.)" }
],
multiSelect: false
},
{
question: "Who is the target audience for this manual?",
header: "Target Users",
options: [
{ label: "End Users", description: "Non-technical users who use the product" },
{ label: "Developers", description: "Developers integrating or extending the product" },
{ label: "Administrators", description: "System admins deploying and maintaining" },
{ label: "All Audiences", description: "Mixed audience with different sections" }
],
multiSelect: false
},
{
question: "What documentation scope do you need?",
header: "Doc Scope",
options: [
{ label: "Quick Start", description: "Essential getting started guide only" },
{ label: "User Guide", description: "Complete user-facing documentation" },
{ label: "API Reference", description: "Focus on API documentation" },
{ label: "Comprehensive", description: "Full documentation including all sections" }
],
multiSelect: false
},
{
question: "What difficulty levels should code examples cover?",
header: "Example Levels",
options: [
{ label: "Beginner Only", description: "Simple, basic examples" },
{ label: "Beginner + Intermediate", description: "Basic to moderate complexity" },
{ label: "All Levels", description: "Beginner, Intermediate, and Advanced" }
],
multiSelect: false
}
]
});
```
### Step 2: Auto-Detection (Supplement)
Automatically detect project characteristics:
```javascript
// Detect from package.json
const packageJson = Read('package.json');
const softwareName = packageJson.name;
const version = packageJson.version;
const description = packageJson.description;
// Detect tech stack
const hasReact = packageJson.dependencies?.react;
const hasVue = packageJson.dependencies?.vue;
const hasExpress = packageJson.dependencies?.express;
const hasNestJS = packageJson.dependencies?.['@nestjs/core'];
// Detect CLI
const hasBin = !!packageJson.bin;
// Detect UI
const hasPages = Glob('src/pages/**/*').length > 0 || Glob('pages/**/*').length > 0;
const hasRoutes = Glob('**/routes.*').length > 0;
```
### Step 3: Generate Configuration
Create `manual-config.json`:
```json
{
"software": {
"name": "{{detected or user input}}",
"version": "{{from package.json}}",
"description": "{{from package.json}}",
"type": "{{web|cli|sdk|desktop}}"
},
"target_audience": "{{end_users|developers|admins|all}}",
"doc_scope": "{{quick_start|user_guide|api_reference|comprehensive}}",
"example_levels": ["beginner", "intermediate", "advanced"],
"tech_stack": {
"frontend": "{{react|vue|angular|vanilla}}",
"backend": "{{express|nestjs|fastify|none}}",
"language": "{{typescript|javascript}}",
"ui_framework": "{{tailwind|mui|antd|none}}"
},
"features": {
"has_ui": true,
"has_api": true,
"has_cli": false,
"has_config": true
},
"agents_to_run": [
"overview",
"ui-guide",
"api-docs",
"config",
"troubleshooting",
"code-examples"
],
"screenshot_config": {
"enabled": true,
"dev_command": "npm run dev",
"dev_url": "http://localhost:3000",
"wait_timeout": 5000
},
"output": {
"filename": "{{name}}-使用手册.html",
"theme": "light",
"language": "zh-CN"
},
"timestamp": "{{ISO8601}}"
}
```
## Agent Selection Logic
Based on `doc_scope`, select agents to run:
| Scope | Agents |
|-------|--------|
| quick_start | overview |
| user_guide | overview, ui-guide, config, troubleshooting |
| api_reference | overview, api-docs, code-examples |
| comprehensive | ALL 6 agents |
## Output
- **File**: `manual-config.json`
- **Location**: `.workflow/.scratchpad/manual-{timestamp}/`
## Next Phase
Proceed to [Phase 2: Project Exploration](02-project-exploration.md) with the generated configuration.

View File

@@ -0,0 +1,101 @@
# Phase 2: Project Exploration
使用 `cli-explore-agent` 探索项目结构,生成文档所需的结构化数据。
## 探索角度
```javascript
const EXPLORATION_ANGLES = {
web: ['architecture', 'ui-routes', 'api-endpoints', 'config'],
cli: ['architecture', 'commands', 'config'],
sdk: ['architecture', 'public-api', 'types', 'config'],
desktop: ['architecture', 'ui-screens', 'config']
};
```
## 执行流程
```javascript
const config = JSON.parse(Read(`${workDir}/manual-config.json`));
const angles = EXPLORATION_ANGLES[config.software.type];
// 并行探索
const tasks = angles.map(angle => Task({
subagent_type: 'cli-explore-agent',
run_in_background: false,
prompt: buildExplorationPrompt(angle, config, workDir)
}));
const results = await Promise.all(tasks);
```
## 探索配置
```javascript
const EXPLORATION_CONFIGS = {
architecture: {
task: '分析项目模块结构、入口点、依赖关系',
patterns: ['src/*/', 'package.json', 'tsconfig.json'],
output: 'exploration-architecture.json'
},
'ui-routes': {
task: '提取 UI 路由、页面组件、导航结构',
patterns: ['src/pages/**', 'src/views/**', 'app/**/page.*', 'src/router/**'],
output: 'exploration-ui-routes.json'
},
'api-endpoints': {
task: '提取 REST API 端点、请求/响应类型',
patterns: ['src/**/*.controller.*', 'src/routes/**', 'openapi.*', 'swagger.*'],
output: 'exploration-api-endpoints.json'
},
config: {
task: '提取环境变量、配置文件选项',
patterns: ['.env.example', 'config/**', 'docker-compose.yml'],
output: 'exploration-config.json'
},
commands: {
task: '提取 CLI 命令、选项、示例',
patterns: ['src/cli*', 'bin/*', 'src/commands/**'],
output: 'exploration-commands.json'
}
};
```
## Prompt 构建
```javascript
function buildExplorationPrompt(angle, config, workDir) {
const cfg = EXPLORATION_CONFIGS[angle];
return `
[TASK]
${cfg.task}
[SCOPE]
项目类型: ${config.software.type}
扫描模式: deep-scan
文件模式: ${cfg.patterns.join(', ')}
[OUTPUT]
文件: ${workDir}/exploration/${cfg.output}
格式: JSON (schema-compliant)
[RETURN]
简要说明发现的内容数量和关键发现
`;
}
```
## 输出结构
```
exploration/
├── exploration-architecture.json # 模块结构
├── exploration-ui-routes.json # UI 路由
├── exploration-api-endpoints.json # API 端点
├── exploration-config.json # 配置选项
└── exploration-commands.json # CLI 命令 (if CLI)
```
## 下一阶段
→ [Phase 3: Parallel Analysis](03-parallel-analysis.md)

View File

@@ -0,0 +1,161 @@
# Phase 2.5: API Extraction
在项目探索后、并行分析前,自动提取 API 文档。
## 核心原则
**使用成熟工具提取,确保输出格式与 wiki 模板兼容。**
## 执行流程
```javascript
const config = JSON.parse(Read(`${workDir}/manual-config.json`));
// 检查项目路径配置
const apiSources = config.api_sources || detectApiSources(config.project_path);
// 执行 API 提取
Bash({
command: `python .claude/skills/software-manual/scripts/extract_apis.py -o "${workDir}" -p ${apiSources.join(' ')}`
});
// 验证输出
const apiDocsDir = `${workDir}/api-docs`;
const extractedFiles = Glob(`${apiDocsDir}/**/*.{json,md}`);
console.log(`Extracted ${extractedFiles.length} API documentation files`);
```
## 支持的项目类型
| 类型 | 检测方式 | 提取工具 | 输出格式 |
|------|----------|----------|----------|
| FastAPI | `app/main.py` + FastAPI import | OpenAPI JSON | `openapi.json` + `API_SUMMARY.md` |
| Next.js | `package.json` + next | TypeDoc | `*.md` (Markdown) |
| Python Module | `__init__.py` + setup.py/pyproject.toml | pdoc | `*.md` (Markdown) |
| Express | `package.json` + express | swagger-jsdoc | `openapi.json` |
| NestJS | `package.json` + @nestjs | @nestjs/swagger | `openapi.json` |
## 输出格式规范
### Markdown 兼容性要求
确保输出 Markdown 与 wiki CSS 样式兼容:
```markdown
# API Reference → <h1> (wiki-base.css)
## Endpoints → <h2>
| Method | Path | Summary | → <table> 蓝色表头
|--------|------|---------|
| `GET` | `/api/...` | ... | → <code> 红色高亮
### GET /api/users → <h3>
\`\`\`json → <pre><code> 深色背景
{
"id": 1,
"name": "example"
}
\`\`\`
- Parameter: `id` (required) → <ul><li> + <code>
```
### 格式验证检查
```javascript
function validateApiDocsFormat(apiDocsDir) {
const issues = [];
const mdFiles = Glob(`${apiDocsDir}/**/*.md`);
for (const file of mdFiles) {
const content = Read(file);
// 检查表格格式
if (content.includes('|') && !content.match(/\|.*\|.*\|/)) {
issues.push(`${file}: 表格格式不完整`);
}
// 检查代码块语言标注
const codeBlocks = content.match(/```(\w*)\n/g) || [];
const unlabeled = codeBlocks.filter(b => b === '```\n');
if (unlabeled.length > 0) {
issues.push(`${file}: ${unlabeled.length} 个代码块缺少语言标注`);
}
// 检查标题层级
if (!content.match(/^# /m)) {
issues.push(`${file}: 缺少一级标题`);
}
}
return issues;
}
```
## 项目配置示例
`manual-config.json` 中配置 API 源:
```json
{
"software": {
"name": "Hydro Generator Workbench",
"type": "web"
},
"api_sources": {
"backend": {
"path": "D:/dongdiankaifa9/backend",
"type": "fastapi",
"entry": "app.main:app"
},
"frontend": {
"path": "D:/dongdiankaifa9/frontend",
"type": "typescript",
"entries": ["lib", "hooks", "components"]
},
"hydro_generator_module": {
"path": "D:/dongdiankaifa9/hydro_generator_module",
"type": "python"
},
"multiphysics_network": {
"path": "D:/dongdiankaifa9/multiphysics_network",
"type": "python"
}
}
}
```
## 输出结构
```
{workDir}/api-docs/
├── backend/
│ ├── openapi.json # OpenAPI 3.0 规范
│ └── API_SUMMARY.md # Markdown 摘要wiki 兼容)
├── frontend/
│ ├── modules.md # TypeDoc 模块文档
│ ├── classes/ # 类文档
│ └── functions/ # 函数文档
├── hydro_generator/
│ ├── assembler.md # pdoc 模块文档
│ ├── blueprint.md
│ └── builders/
└── multiphysics/
├── analysis_domain.md
├── builders.md
└── compilers.md
```
## 质量门禁
- [ ] 所有配置的 API 源已提取
- [ ] Markdown 格式与 wiki CSS 兼容
- [ ] 表格正确渲染(蓝色表头)
- [ ] 代码块有语言标注
- [ ] 无空文件或错误文件
## 下一阶段
→ [Phase 3: Parallel Analysis](03-parallel-analysis.md)

View File

@@ -0,0 +1,183 @@
# Phase 3: Parallel Analysis
使用 `universal-executor` 并行生成 6 个文档章节。
## Agent 配置
```javascript
const AGENT_CONFIGS = {
overview: {
role: 'Product Manager',
output: 'section-overview.md',
task: '撰写产品概览、核心功能、快速入门指南',
focus: '产品定位、目标用户、5步快速入门、系统要求',
input: ['exploration-architecture.json', 'README.md', 'package.json'],
tag: 'getting-started'
},
'interface-guide': {
role: 'Product Designer',
output: 'section-interface.md',
task: '撰写界面或交互指南Web 截图、CLI 命令交互、桌面应用操作)',
focus: '视觉布局、交互流程、命令行参数、输入/输出示例',
input: ['exploration-ui-routes.json', 'src/**', 'pages/**', 'views/**', 'components/**', 'src/commands/**'],
tag: 'interface',
screenshot_rules: `
根据项目类型标注交互点:
[Web] <!-- SCREENSHOT: id="ss-{功能}" url="{路由}" selector="{CSS选择器}" description="{描述}" -->
[CLI] 使用代码块展示命令交互:
\`\`\`bash
$ command --flag value
Expected output here
\`\`\`
[Desktop] <!-- SCREENSHOT: id="ss-{功能}" description="{描述}" -->
`
},
'api-reference': {
role: 'Technical Architect',
output: 'section-reference.md',
task: '撰写接口参考文档REST API / 函数库 / CLI 命令)',
focus: '函数签名、端点定义、参数说明、返回值、错误代码',
pre_extract: 'python .claude/skills/software-manual/scripts/extract_apis.py -o ${workDir}',
input: [
'${workDir}/api-docs/backend/openapi.json', // FastAPI OpenAPI
'${workDir}/api-docs/backend/API_SUMMARY.md', // Backend summary
'${workDir}/api-docs/frontend/**/*.md', // TypeDoc output
'${workDir}/api-docs/hydro_generator/**/*.md', // Python module
'${workDir}/api-docs/multiphysics/**/*.md' // Python module
],
tag: 'api'
},
config: {
role: 'DevOps Engineer',
output: 'section-configuration.md',
task: '撰写配置指南,涵盖环境变量、配置文件、部署设置',
focus: '环境变量表格、配置文件格式、部署选项、安全设置',
input: ['exploration-config.json', '.env.example', 'config/**', '*.config.*'],
tag: 'config'
},
troubleshooting: {
role: 'Support Engineer',
output: 'section-troubleshooting.md',
task: '撰写故障排查指南涵盖常见问题、错误码、FAQ',
focus: '常见问题与解决方案、错误码参考、FAQ、获取帮助',
input: ['docs/troubleshooting.md', 'src/**/errors.*', 'src/**/exceptions.*', 'TROUBLESHOOTING.md'],
tag: 'troubleshooting'
},
'code-examples': {
role: 'Developer Advocate',
output: 'section-examples.md',
task: '撰写多难度级别代码示例入门40%/进阶40%/高级20%',
focus: '完整可运行代码、分步解释、预期输出、最佳实践',
input: ['examples/**', 'tests/**', 'demo/**', 'samples/**'],
tag: 'examples'
}
};
```
## 执行流程
```javascript
const config = JSON.parse(Read(`${workDir}/manual-config.json`));
// 1. 预提取 API 文档(如有 pre_extract 配置)
for (const [name, cfg] of Object.entries(AGENT_CONFIGS)) {
if (cfg.pre_extract) {
const cmd = cfg.pre_extract.replace(/\$\{workDir\}/g, workDir);
console.log(`[Pre-extract] ${name}: ${cmd}`);
Bash({ command: cmd });
}
}
// 2. 并行启动 6 个 universal-executor
const tasks = Object.entries(AGENT_CONFIGS).map(([name, cfg]) =>
Task({
subagent_type: 'universal-executor',
run_in_background: false,
prompt: buildAgentPrompt(name, cfg, config, workDir)
})
);
const results = await Promise.all(tasks);
```
## Prompt 构建
```javascript
function buildAgentPrompt(name, cfg, config, workDir) {
const screenshotSection = cfg.screenshot_rules
? `\n[SCREENSHOT RULES]\n${cfg.screenshot_rules}`
: '';
return `
[ROLE] ${cfg.role}
[PROJECT CONTEXT]
项目类型: ${config.software.type} (web/cli/sdk/desktop)
语言: ${config.software.language || 'auto-detect'}
名称: ${config.software.name}
[TASK]
${cfg.task}
输出: ${workDir}/sections/${cfg.output}
[INPUT]
- 配置: ${workDir}/manual-config.json
- 探索结果: ${workDir}/exploration/
- 扫描路径: ${cfg.input.join(', ')}
[CONTENT REQUIREMENTS]
- 标题层级: # ## ### (最多3级)
- 代码块: \`\`\`language ... \`\`\` (必须标注语言)
- 表格: | col1 | col2 | 格式
- 列表: 有序 1. 2. 3. / 无序 - - -
- 内联代码: \`code\`
- 链接: [text](url)
${screenshotSection}
[FOCUS]
${cfg.focus}
[OUTPUT FORMAT]
Markdown 文件,包含:
- 清晰的章节结构
- 具体的代码示例
- 参数/配置表格
- 常见用例说明
[RETURN JSON]
{
"status": "completed",
"output_file": "sections/${cfg.output}",
"summary": "<50字>",
"tag": "${cfg.tag}",
"screenshots_needed": []
}
`;
}
```
## 结果收集
```javascript
const agentResults = results.map(r => JSON.parse(r));
const allScreenshots = agentResults.flatMap(r => r.screenshots_needed);
Write(`${workDir}/agent-results.json`, JSON.stringify({
results: agentResults,
screenshots_needed: allScreenshots,
timestamp: new Date().toISOString()
}, null, 2));
```
## 质量检查
- [ ] Markdown 语法有效
- [ ] 无占位符文本
- [ ] 代码块标注语言
- [ ] 截图标记格式正确
- [ ] 交叉引用有效
## 下一阶段
→ [Phase 3.5: Consolidation](03.5-consolidation.md)

View File

@@ -0,0 +1,82 @@
# Phase 3.5: Consolidation
使用 `universal-executor` 子 Agent 执行质量检查,避免主 Agent 内存溢出。
## 核心原则
**主 Agent 负责编排,子 Agent 负责繁重计算。**
## 执行流程
```javascript
const agentResults = JSON.parse(Read(`${workDir}/agent-results.json`));
// 委托给 universal-executor 执行整合检查
const result = Task({
subagent_type: 'universal-executor',
run_in_background: false,
prompt: buildConsolidationPrompt(workDir)
});
const consolidationResult = JSON.parse(result);
```
## Prompt 构建
```javascript
function buildConsolidationPrompt(workDir) {
return `
[ROLE] Quality Analyst
[TASK]
检查所有章节的一致性和完整性
[INPUT]
- 章节文件: ${workDir}/sections/section-*.md
- Agent 结果: ${workDir}/agent-results.json
[CHECKS]
1. Markdown 语法有效性
2. 截图标记格式 (<!-- SCREENSHOT: id="..." -->)
3. 交叉引用有效性
4. 术语一致性
5. 代码块语言标注
[OUTPUT]
1. 写入 ${workDir}/consolidation-summary.md
2. 写入 ${workDir}/screenshots-list.json (截图清单)
[RETURN JSON]
{
"status": "completed",
"sections_checked": <n>,
"screenshots_found": <n>,
"issues": { "errors": <n>, "warnings": <n> },
"quality_score": <0-100>
}
`;
}
```
## Agent 职责
1. **读取章节** → 逐个检查 section-*.md
2. **提取截图** → 收集所有截图标记
3. **验证引用** → 检查交叉引用有效性
4. **评估质量** → 计算综合分数
5. **输出报告** → consolidation-summary.md
## 输出
- `consolidation-summary.md` - 质量报告
- `screenshots-list.json` - 截图清单(供 Phase 4 使用)
## 质量门禁
- [ ] 无错误
- [ ] 总分 >= 60%
- [ ] 交叉引用有效
## 下一阶段
→ [Phase 4: Screenshot Capture](04-screenshot-capture.md)

View File

@@ -0,0 +1,89 @@
# Phase 4: Screenshot Capture
使用 `universal-executor` 子 Agent 调用 Chrome MCP 截图。
## 核心原则
**主 Agent 负责编排,子 Agent 负责截图采集。**
## 执行流程
```javascript
const config = JSON.parse(Read(`${workDir}/manual-config.json`));
const screenshotsList = JSON.parse(Read(`${workDir}/screenshots-list.json`));
// 委托给 universal-executor 执行截图
const result = Task({
subagent_type: 'universal-executor',
run_in_background: false,
prompt: buildScreenshotPrompt(config, screenshotsList, workDir)
});
const captureResult = JSON.parse(result);
```
## Prompt 构建
```javascript
function buildScreenshotPrompt(config, screenshotsList, workDir) {
return `
[ROLE] Screenshot Capturer
[TASK]
使用 Chrome MCP 批量截图
[INPUT]
- 配置: ${workDir}/manual-config.json
- 截图清单: ${workDir}/screenshots-list.json
[STEPS]
1. 检查 Chrome MCP 可用性 (mcp__chrome__*)
2. 启动开发服务器: ${config.screenshot_config?.dev_command || 'npm run dev'}
3. 等待服务器就绪: ${config.screenshot_config?.dev_url || 'http://localhost:3000'}
4. 遍历截图清单,逐个调用 mcp__chrome__screenshot
5. 保存截图到 ${workDir}/screenshots/
6. 生成 manifest: ${workDir}/screenshots/screenshots-manifest.json
7. 停止开发服务器
[MCP CALLS]
- mcp__chrome__screenshot({ url, selector?, viewport })
- 保存为 PNG 文件
[FALLBACK]
若 Chrome MCP 不可用,生成手动截图指南: MANUAL_CAPTURE.md
[RETURN JSON]
{
"status": "completed|skipped",
"captured": <n>,
"failed": <n>,
"manifest_file": "screenshots-manifest.json"
}
`;
}
```
## Agent 职责
1. **检查 MCP** → Chrome MCP 可用性
2. **启动服务** → 开发服务器
3. **批量截图** → 调用 mcp__chrome__screenshot
4. **保存文件** → screenshots/*.png
5. **生成清单** → screenshots-manifest.json
## 输出
- `screenshots/*.png` - 截图文件
- `screenshots/screenshots-manifest.json` - 清单
- `screenshots/MANUAL_CAPTURE.md` - 手动指南fallback
## 质量门禁
- [ ] 高优先级截图完成
- [ ] 尺寸一致 (1280×800)
- [ ] 无空白截图
- [ ] Manifest 完整
## 下一阶段
→ [Phase 5: HTML Assembly](05-html-assembly.md)

View File

@@ -0,0 +1,132 @@
# Phase 5: HTML Assembly
使用 `universal-executor` 子 Agent 生成最终 HTML避免主 Agent 内存溢出。
## 核心原则
**主 Agent 负责编排,子 Agent 负责繁重计算。**
## 执行流程
```javascript
const config = JSON.parse(Read(`${workDir}/manual-config.json`));
// 委托给 universal-executor 执行 HTML 组装
const result = Task({
subagent_type: 'universal-executor',
run_in_background: false,
prompt: buildAssemblyPrompt(config, workDir)
});
const buildResult = JSON.parse(result);
```
## Prompt 构建
```javascript
function buildAssemblyPrompt(config, workDir) {
return `
[ROLE] HTML Assembler
[TASK]
生成 TiddlyWiki 风格的交互式 HTML 手册(使用成熟库,无外部 CDN 依赖)
[INPUT]
- 模板: .claude/skills/software-manual/templates/tiddlywiki-shell.html
- CSS: .claude/skills/software-manual/templates/css/wiki-base.css, wiki-dark.css
- 配置: ${workDir}/manual-config.json
- 章节: ${workDir}/sections/section-*.md
- Agent 结果: ${workDir}/agent-results.json (含 tag 信息)
- 截图: ${workDir}/screenshots/
[LIBRARIES TO EMBED]
1. marked.js (v14+) - Markdown 转 HTML
- 从 https://unpkg.com/marked/marked.min.js 获取内容内嵌
2. highlight.js (v11+) - 代码语法高亮
- 核心 + 常用语言包 (js, ts, python, bash, json, yaml, html, css)
- 使用 github-dark 主题
[STEPS]
1. 读取 HTML 模板和 CSS
2. 内嵌 marked.js 和 highlight.js 代码
3. 读取 agent-results.json 提取各章节 tag
4. 动态生成 {{TAG_BUTTONS_HTML}} (基于实际使用的 tags)
5. 逐个读取 section-*.md使用 marked 转换为 HTML
6. 为代码块添加 data-language 属性和语法高亮
7. 处理 <!-- SCREENSHOT: id="..." --> 标记,嵌入 Base64 图片
8. 生成目录、搜索索引
9. 组装最终 HTML写入 ${workDir}/${config.software.name}-使用手册.html
[CONTENT FORMATTING]
- 代码块: 深色背景 + 语言标签 + 语法高亮
- 表格: 蓝色表头 + 边框 + 悬停效果
- 内联代码: 红色高亮
- 列表: 有序/无序样式增强
- 左侧导航: 固定侧边栏 + TOC
[RETURN JSON]
{
"status": "completed",
"output_file": "${config.software.name}-使用手册.html",
"file_size": "<size>",
"sections_count": <n>,
"tags_generated": [],
"screenshots_embedded": <n>
}
`;
}
```
## Agent 职责
1. **读取模板** → HTML + CSS
2. **转换章节** → Markdown → HTML tiddlers
3. **嵌入截图** → Base64 编码
4. **生成索引** → 搜索数据
5. **组装输出** → 单文件 HTML
## Markdown 转换规则
Agent 内部实现:
```
# H1 → <h1>
## H2 → <h2>
### H3 → <h3>
```code``` → <pre><code>
**bold** → <strong>
*italic* → <em>
[text](url) → <a href>
- item → <li>
<!-- SCREENSHOT: id="xxx" --> → <figure><img src="data:..."></figure>
```
## Tiddler 结构
```html
<article class="tiddler" id="tiddler-{name}" data-tags="..." data-difficulty="...">
<header class="tiddler-header">
<h2><button class="collapse-toggle"></button> {title}</h2>
<div class="tiddler-meta">{badges}</div>
</header>
<div class="tiddler-content">{html}</div>
</article>
```
## 输出
- `{软件名}-使用手册.html` - 最终 HTML
- `build-report.json` - 构建报告
## 质量门禁
- [ ] HTML 渲染正确
- [ ] 搜索功能可用
- [ ] 折叠/展开正常
- [ ] 主题切换持久化
- [ ] 截图显示正确
- [ ] 文件大小 < 10MB
## 下一阶段
→ [Phase 6: Iterative Refinement](06-iterative-refinement.md)

View File

@@ -0,0 +1,259 @@
# Phase 6: Iterative Refinement
Preview, collect feedback, and iterate until quality meets standards.
## Objective
- Preview generated HTML in browser
- Collect user feedback
- Address issues iteratively
- Finalize documentation
## Execution Steps
### Step 1: Preview HTML
```javascript
const buildReport = JSON.parse(Read(`${workDir}/build-report.json`));
const outputFile = `${workDir}/${buildReport.output}`;
// Open in default browser for preview
Bash({ command: `start "${outputFile}"` }); // Windows
// Bash({ command: `open "${outputFile}"` }); // macOS
// Report to user
console.log(`
📖 Manual Preview
File: ${buildReport.output}
Size: ${buildReport.size_human}
Sections: ${buildReport.sections}
Screenshots: ${buildReport.screenshots}
Please review the manual in your browser.
`);
```
### Step 2: Collect Feedback
```javascript
const feedback = await AskUserQuestion({
questions: [
{
question: "How does the manual look overall?",
header: "Overall",
options: [
{ label: "Looks great!", description: "Ready to finalize" },
{ label: "Minor issues", description: "Small tweaks needed" },
{ label: "Major issues", description: "Significant changes required" },
{ label: "Missing content", description: "Need to add more sections" }
],
multiSelect: false
},
{
question: "Which aspects need improvement? (Select all that apply)",
header: "Improvements",
options: [
{ label: "Content accuracy", description: "Fix incorrect information" },
{ label: "More examples", description: "Add more code examples" },
{ label: "Better screenshots", description: "Retake or add screenshots" },
{ label: "Styling/Layout", description: "Improve visual appearance" }
],
multiSelect: true
}
]
});
```
### Step 3: Address Feedback
Based on feedback, take appropriate action:
#### Minor Issues
```javascript
if (feedback.overall === "Minor issues") {
// Prompt for specific changes
const details = await AskUserQuestion({
questions: [{
question: "What specific changes are needed?",
header: "Details",
options: [
{ label: "Typo fixes", description: "Fix spelling/grammar" },
{ label: "Reorder sections", description: "Change section order" },
{ label: "Update content", description: "Modify existing text" },
{ label: "Custom changes", description: "I'll describe the changes" }
],
multiSelect: true
}]
});
// Apply changes based on user input
applyMinorChanges(details);
}
```
#### Major Issues
```javascript
if (feedback.overall === "Major issues") {
// Return to relevant phase
console.log(`
Major issues require returning to an earlier phase:
- Content issues → Phase 3 (Parallel Analysis)
- Screenshot issues → Phase 4 (Screenshot Capture)
- Structure issues → Phase 2 (Project Exploration)
Which phase should we return to?
`);
const phase = await selectPhase();
return { action: 'restart', from_phase: phase };
}
```
#### Missing Content
```javascript
if (feedback.overall === "Missing content") {
// Identify missing sections
const missing = await AskUserQuestion({
questions: [{
question: "What content is missing?",
header: "Missing",
options: [
{ label: "API endpoints", description: "More API documentation" },
{ label: "UI features", description: "Additional UI guides" },
{ label: "Examples", description: "More code examples" },
{ label: "Troubleshooting", description: "More FAQ items" }
],
multiSelect: true
}]
});
// Run additional agent(s) for missing content
await runSupplementaryAgents(missing);
}
```
### Step 4: Save Iteration
```javascript
// Save current version before changes
const iterationNum = getNextIterationNumber(workDir);
const iterationDir = `${workDir}/iterations`;
// Copy current version
Bash({ command: `copy "${outputFile}" "${iterationDir}\\v${iterationNum}.html"` });
// Log iteration
const iterationLog = {
version: iterationNum,
timestamp: new Date().toISOString(),
feedback: feedback,
changes: appliedChanges
};
Write(`${iterationDir}/iteration-${iterationNum}.json`, JSON.stringify(iterationLog, null, 2));
```
### Step 5: Regenerate if Needed
```javascript
if (changesApplied) {
// Re-run HTML assembly with updated sections
await runPhase('05-html-assembly');
// Open updated preview
Bash({ command: `start "${outputFile}"` });
}
```
### Step 6: Finalize
When user approves:
```javascript
if (feedback.overall === "Looks great!") {
// Final quality check
const finalReport = {
...buildReport,
iterations: iterationNum,
finalized_at: new Date().toISOString(),
quality_score: calculateFinalQuality()
};
Write(`${workDir}/final-report.json`, JSON.stringify(finalReport, null, 2));
// Suggest final location
console.log(`
✅ Manual Finalized!
Output: ${buildReport.output}
Size: ${buildReport.size_human}
Quality: ${finalReport.quality_score}%
Iterations: ${iterationNum}
Suggested actions:
1. Copy to project root: copy "${outputFile}" "docs/"
2. Add to version control
3. Publish to documentation site
`);
return { status: 'completed', output: outputFile };
}
```
## Iteration History
Each iteration is logged:
```
iterations/
├── v1.html # First version
├── iteration-1.json # Feedback and changes
├── v2.html # After first iteration
├── iteration-2.json # Feedback and changes
└── ...
```
## Quality Metrics
Track improvement across iterations:
```javascript
const qualityMetrics = {
content_completeness: 0, // All sections present
screenshot_coverage: 0, // Screenshots for all UI
example_diversity: 0, // Different difficulty levels
search_accuracy: 0, // Search returns relevant results
user_satisfaction: 0 // Based on feedback
};
```
## Exit Conditions
The refinement phase ends when:
1. User explicitly approves ("Looks great!")
2. Maximum iterations reached (configurable, default: 5)
3. Quality score exceeds threshold (default: 90%)
## Output
- **Final HTML**: `{软件名}-使用手册.html`
- **Final Report**: `final-report.json`
- **Iteration History**: `iterations/`
## Completion
When finalized, the skill is complete. Final output location:
```
.workflow/.scratchpad/manual-{timestamp}/
├── {软件名}-使用手册.html ← Final deliverable
├── final-report.json
└── iterations/
```
Consider copying to a permanent location like `docs/` or project root.

View File

@@ -0,0 +1,245 @@
# API 文档提取脚本
根据项目类型自动提取 API 文档,支持 FastAPI、Next.js、Python 模块。
## 支持的技术栈
| 类型 | 技术栈 | 工具 | 输出格式 |
|------|--------|------|----------|
| Backend | FastAPI | openapi-to-md | Markdown |
| Frontend | Next.js/TypeScript | TypeDoc | Markdown |
| Python Module | Python | pdoc | Markdown/HTML |
## 使用方法
### 1. FastAPI Backend (OpenAPI)
```bash
# 提取 OpenAPI JSON
cd D:/dongdiankaifa9/backend
python -c "
from app.main import app
import json
print(json.dumps(app.openapi(), indent=2))
" > api-docs/openapi.json
# 转换为 Markdown (使用 widdershins)
npx widdershins api-docs/openapi.json -o api-docs/API_REFERENCE.md --language_tabs 'python:Python' 'javascript:JavaScript' 'bash:cURL'
```
**备选方案 (无需启动服务)**:
```python
# scripts/extract_fastapi_openapi.py
import sys
sys.path.insert(0, 'D:/dongdiankaifa9/backend')
from app.main import app
import json
openapi_schema = app.openapi()
with open('api-docs/openapi.json', 'w', encoding='utf-8') as f:
json.dump(openapi_schema, f, indent=2, ensure_ascii=False)
print(f"Extracted {len(openapi_schema.get('paths', {}))} endpoints")
```
### 2. Next.js Frontend (TypeDoc)
```bash
cd D:/dongdiankaifa9/frontend
# 安装 TypeDoc
npm install --save-dev typedoc typedoc-plugin-markdown
# 生成文档
npx typedoc --plugin typedoc-plugin-markdown \
--out api-docs \
--entryPoints "./lib" "./hooks" "./components" \
--entryPointStrategy expand \
--exclude "**/node_modules/**" \
--exclude "**/*.test.*" \
--readme none
```
**typedoc.json 配置**:
```json
{
"$schema": "https://typedoc.org/schema.json",
"entryPoints": ["./lib", "./hooks", "./components"],
"entryPointStrategy": "expand",
"out": "api-docs",
"plugin": ["typedoc-plugin-markdown"],
"exclude": ["**/node_modules/**", "**/*.test.*", "**/*.spec.*"],
"excludePrivate": true,
"excludeInternal": true,
"readme": "none",
"name": "Frontend API Reference"
}
```
### 3. Python Module (pdoc)
```bash
# 安装 pdoc
pip install pdoc
# hydro_generator_module
cd D:/dongdiankaifa9
pdoc hydro_generator_module \
--output-dir api-docs/hydro_generator \
--format markdown \
--no-show-source
# multiphysics_network
pdoc multiphysics_network \
--output-dir api-docs/multiphysics \
--format markdown \
--no-show-source
```
**备选: Sphinx (更强大)**:
```bash
# 安装 Sphinx
pip install sphinx sphinx-markdown-builder
# 生成 API 文档
sphinx-apidoc -o docs/source hydro_generator_module
cd docs && make markdown
```
## 集成脚本
```python
#!/usr/bin/env python3
# scripts/extract_all_apis.py
import subprocess
import sys
from pathlib import Path
PROJECTS = {
'backend': {
'path': 'D:/dongdiankaifa9/backend',
'type': 'fastapi',
'output': 'api-docs/backend'
},
'frontend': {
'path': 'D:/dongdiankaifa9/frontend',
'type': 'typescript',
'output': 'api-docs/frontend'
},
'hydro_generator_module': {
'path': 'D:/dongdiankaifa9/hydro_generator_module',
'type': 'python',
'output': 'api-docs/hydro_generator'
},
'multiphysics_network': {
'path': 'D:/dongdiankaifa9/multiphysics_network',
'type': 'python',
'output': 'api-docs/multiphysics'
}
}
def extract_fastapi(config):
"""提取 FastAPI OpenAPI 文档"""
path = Path(config['path'])
sys.path.insert(0, str(path))
try:
from app.main import app
import json
output_dir = Path(config['output'])
output_dir.mkdir(parents=True, exist_ok=True)
# 导出 OpenAPI JSON
with open(output_dir / 'openapi.json', 'w', encoding='utf-8') as f:
json.dump(app.openapi(), f, indent=2, ensure_ascii=False)
print(f"✓ FastAPI: {len(app.openapi().get('paths', {}))} endpoints")
return True
except Exception as e:
print(f"✗ FastAPI error: {e}")
return False
def extract_typescript(config):
"""提取 TypeScript 文档"""
try:
subprocess.run([
'npx', 'typedoc',
'--plugin', 'typedoc-plugin-markdown',
'--out', config['output'],
'--entryPoints', './lib', './hooks',
'--entryPointStrategy', 'expand'
], cwd=config['path'], check=True)
print(f"✓ TypeDoc: {config['path']}")
return True
except Exception as e:
print(f"✗ TypeDoc error: {e}")
return False
def extract_python(config):
"""提取 Python 模块文档"""
try:
module_name = Path(config['path']).name
subprocess.run([
'pdoc', module_name,
'--output-dir', config['output'],
'--format', 'markdown'
], cwd=Path(config['path']).parent, check=True)
print(f"✓ pdoc: {module_name}")
return True
except Exception as e:
print(f"✗ pdoc error: {e}")
return False
EXTRACTORS = {
'fastapi': extract_fastapi,
'typescript': extract_typescript,
'python': extract_python
}
if __name__ == '__main__':
for name, config in PROJECTS.items():
print(f"\n[{name}]")
extractor = EXTRACTORS.get(config['type'])
if extractor:
extractor(config)
```
## Phase 3 集成
`api-reference` Agent 提示词中添加:
```
[PRE-EXTRACTION]
运行 API 提取脚本获取结构化文档:
- python scripts/extract_all_apis.py
[INPUT FILES]
- api-docs/backend/openapi.json (FastAPI endpoints)
- api-docs/frontend/*.md (TypeDoc output)
- api-docs/hydro_generator/*.md (pdoc output)
- api-docs/multiphysics/*.md (pdoc output)
```
## 输出结构
```
api-docs/
├── backend/
│ ├── openapi.json # Raw OpenAPI spec
│ └── API_REFERENCE.md # Converted Markdown
├── frontend/
│ ├── modules.md
│ ├── functions.md
│ └── classes/
├── hydro_generator/
│ ├── assembler.md
│ ├── blueprint.md
│ └── builders/
└── multiphysics/
├── analysis_domain.md
├── builders.md
└── compilers.md
```

View File

@@ -0,0 +1,584 @@
#!/usr/bin/env python3
"""
Docsify-Style HTML Manual Assembly Script Template
Generates interactive single-file documentation with hierarchical navigation
Usage:
1. Copy this script to your manual output directory
2. Customize MANUAL_META and NAV_STRUCTURE
3. Run: python assemble_docsify.py
"""
import json
import base64
import re
from pathlib import Path
from typing import Dict, List, Any
# Try to import markdown library
try:
import markdown
from markdown.extensions.codehilite import CodeHiliteExtension
from markdown.extensions.fenced_code import FencedCodeExtension
from markdown.extensions.tables import TableExtension
from markdown.extensions.toc import TocExtension
HAS_MARKDOWN = True
except ImportError:
HAS_MARKDOWN = False
print("Warning: markdown library not found. Install with: pip install markdown pygments")
# ============================================================
# CONFIGURATION - Customize these for your project
# ============================================================
# Paths - Update these paths for your environment
BASE_DIR = Path(__file__).parent
SECTIONS_DIR = BASE_DIR / "sections"
SCREENSHOTS_DIR = BASE_DIR / "screenshots"
# Template paths - Point to skill templates directory
SKILL_DIR = Path(__file__).parent.parent # Adjust based on where script is placed
TEMPLATE_FILE = SKILL_DIR / "templates" / "docsify-shell.html"
CSS_BASE_FILE = SKILL_DIR / "templates" / "css" / "docsify-base.css"
# Manual metadata - Customize for your software
MANUAL_META = {
"title": "Your Software",
"subtitle": "使用手册",
"version": "v1.0.0",
"timestamp": "2025-01-01",
"language": "zh-CN",
"logo_icon": "Y" # First letter or emoji
}
# Output file
OUTPUT_FILE = BASE_DIR / f"{MANUAL_META['title']}{MANUAL_META['subtitle']}.html"
# Hierarchical navigation structure
# Customize groups and items based on your sections
NAV_STRUCTURE = [
{
"type": "group",
"title": "入门指南",
"icon": "📚",
"expanded": True,
"items": [
{"id": "overview", "title": "产品概述", "file": "section-overview.md"},
]
},
{
"type": "group",
"title": "使用教程",
"icon": "🎯",
"expanded": False,
"items": [
{"id": "ui-guide", "title": "UI操作指南", "file": "section-ui-guide.md"},
]
},
{
"type": "group",
"title": "API参考",
"icon": "🔧",
"expanded": False,
"items": [
{"id": "api-reference", "title": "API文档", "file": "section-api-reference.md"},
]
},
{
"type": "group",
"title": "配置与部署",
"icon": "⚙️",
"expanded": False,
"items": [
{"id": "configuration", "title": "配置指南", "file": "section-configuration.md"},
]
},
{
"type": "group",
"title": "帮助与支持",
"icon": "💡",
"expanded": False,
"items": [
{"id": "troubleshooting", "title": "故障排除", "file": "section-troubleshooting.md"},
{"id": "examples", "title": "代码示例", "file": "section-examples.md"},
]
}
]
# Screenshot ID to filename mapping - Customize for your screenshots
SCREENSHOT_MAPPING = {
# "截图ID": "filename.png",
}
# ============================================================
# CORE FUNCTIONS - Generally don't need to modify
# ============================================================
# Global cache for embedded images
_embedded_images = {}
def read_file(filepath: Path) -> str:
"""Read file content with UTF-8 encoding"""
return filepath.read_text(encoding='utf-8')
# ============================================================
# MERMAID VALIDATION
# ============================================================
# Valid Mermaid diagram types
MERMAID_DIAGRAM_TYPES = [
'graph', 'flowchart', 'sequenceDiagram', 'classDiagram',
'stateDiagram', 'stateDiagram-v2', 'erDiagram', 'journey',
'gantt', 'pie', 'quadrantChart', 'requirementDiagram',
'gitGraph', 'mindmap', 'timeline', 'zenuml', 'sankey-beta',
'xychart-beta', 'block-beta'
]
# Common Mermaid syntax patterns
MERMAID_PATTERNS = {
'graph': r'^graph\s+(TB|BT|LR|RL|TD)\s*$',
'flowchart': r'^flowchart\s+(TB|BT|LR|RL|TD)\s*$',
'sequenceDiagram': r'^sequenceDiagram\s*$',
'classDiagram': r'^classDiagram\s*$',
'stateDiagram': r'^stateDiagram(-v2)?\s*$',
'erDiagram': r'^erDiagram\s*$',
'gantt': r'^gantt\s*$',
'pie': r'^pie\s*(showData|title\s+.*)?\s*$',
'journey': r'^journey\s*$',
}
class MermaidBlock:
"""Represents a mermaid code block found in markdown"""
def __init__(self, content: str, file: str, line_num: int, indented: bool = False):
self.content = content
self.file = file
self.line_num = line_num
self.indented = indented
self.errors: List[str] = []
self.warnings: List[str] = []
self.diagram_type: str = None
def __repr__(self):
return f"MermaidBlock({self.diagram_type}, {self.file}:{self.line_num})"
def extract_mermaid_blocks(markdown_text: str, filename: str) -> List[MermaidBlock]:
"""Extract all mermaid code blocks from markdown text"""
blocks = []
# More flexible pattern - matches opening fence with optional indent,
# then captures content until closing fence (with any indent)
pattern = r'^(\s*)(```|~~~)mermaid\s*\n(.*?)\n\s*\2\s*$'
for match in re.finditer(pattern, markdown_text, re.MULTILINE | re.DOTALL):
indent = match.group(1)
content = match.group(3)
# Calculate line number
line_num = markdown_text[:match.start()].count('\n') + 1
indented = len(indent) > 0
block = MermaidBlock(
content=content,
file=filename,
line_num=line_num,
indented=indented
)
blocks.append(block)
return blocks
def validate_mermaid_block(block: MermaidBlock) -> bool:
"""Validate a mermaid block and populate errors/warnings"""
content = block.content.strip()
lines = content.split('\n')
if not lines:
block.errors.append("Empty mermaid block")
return False
first_line = lines[0].strip()
# Detect diagram type
for dtype in MERMAID_DIAGRAM_TYPES:
if first_line.startswith(dtype):
block.diagram_type = dtype
break
if not block.diagram_type:
block.errors.append(f"Unknown diagram type: '{first_line[:30]}...'")
block.errors.append(f"Valid types: {', '.join(MERMAID_DIAGRAM_TYPES[:8])}...")
return False
# Check for balanced brackets/braces
brackets = {'[': ']', '{': '}', '(': ')'}
stack = []
for i, char in enumerate(content):
if char in brackets:
stack.append((char, i))
elif char in brackets.values():
if not stack:
block.errors.append(f"Unmatched closing bracket '{char}' at position {i}")
else:
open_char, _ = stack.pop()
if brackets[open_char] != char:
block.errors.append(f"Mismatched brackets: '{open_char}' and '{char}'")
if stack:
for open_char, pos in stack:
block.warnings.append(f"Unclosed bracket '{open_char}' at position {pos}")
# Check for common graph/flowchart issues
if block.diagram_type in ['graph', 'flowchart']:
# Check direction specifier
if not re.match(r'^(graph|flowchart)\s+(TB|BT|LR|RL|TD)', first_line):
block.warnings.append("Missing or invalid direction (TB/BT/LR/RL/TD)")
# Check for arrow syntax
arrow_count = content.count('-->') + content.count('---') + content.count('-.->') + content.count('==>')
if arrow_count == 0 and len(lines) > 1:
block.warnings.append("No arrows found - graph may be incomplete")
# Check for sequenceDiagram issues
if block.diagram_type == 'sequenceDiagram':
if '->' not in content and '->>' not in content:
block.warnings.append("No message arrows found in sequence diagram")
# Indentation warning
if block.indented:
block.warnings.append("Indented code block - may not render in some markdown parsers")
return len(block.errors) == 0
def validate_all_mermaid(nav_structure: List[Dict], sections_dir: Path) -> Dict[str, Any]:
"""Validate all mermaid blocks in all section files"""
report = {
'total_blocks': 0,
'valid_blocks': 0,
'error_blocks': 0,
'warning_blocks': 0,
'blocks': [],
'by_file': {},
'by_type': {}
}
for group in nav_structure:
for item in group.get("items", []):
section_file = item.get("file")
if not section_file:
continue
filepath = sections_dir / section_file
if not filepath.exists():
continue
content = read_file(filepath)
blocks = extract_mermaid_blocks(content, section_file)
file_report = {'blocks': [], 'errors': 0, 'warnings': 0}
for block in blocks:
report['total_blocks'] += 1
is_valid = validate_mermaid_block(block)
if is_valid:
report['valid_blocks'] += 1
else:
report['error_blocks'] += 1
file_report['errors'] += 1
if block.warnings:
report['warning_blocks'] += 1
file_report['warnings'] += len(block.warnings)
# Track by diagram type
if block.diagram_type:
if block.diagram_type not in report['by_type']:
report['by_type'][block.diagram_type] = 0
report['by_type'][block.diagram_type] += 1
report['blocks'].append(block)
file_report['blocks'].append(block)
if blocks:
report['by_file'][section_file] = file_report
return report
def print_mermaid_report(report: Dict[str, Any]) -> None:
"""Print mermaid validation report"""
print("\n" + "=" * 60)
print("MERMAID DIAGRAM VALIDATION REPORT")
print("=" * 60)
print(f"\nSummary:")
print(f" Total blocks: {report['total_blocks']}")
print(f" Valid: {report['valid_blocks']}")
print(f" With errors: {report['error_blocks']}")
print(f" With warnings: {report['warning_blocks']}")
if report['by_type']:
print(f"\nDiagram Types:")
for dtype, count in sorted(report['by_type'].items()):
print(f" {dtype}: {count}")
# Print errors and warnings
has_issues = False
for block in report['blocks']:
if block.errors or block.warnings:
if not has_issues:
print(f"\nIssues Found:")
has_issues = True
print(f"\n [{block.file}:{block.line_num}] {block.diagram_type or 'unknown'}")
for error in block.errors:
print(f" [ERROR] {error}")
for warning in block.warnings:
print(f" [WARN] {warning}")
if not has_issues:
print(f"\n No issues found!")
print("=" * 60 + "\n")
def convert_md_to_html(markdown_text: str) -> str:
"""Convert Markdown to HTML with syntax highlighting"""
if not HAS_MARKDOWN:
# Fallback: just escape HTML and wrap in pre
escaped = markdown_text.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
return f'<pre>{escaped}</pre>'
md = markdown.Markdown(
extensions=[
FencedCodeExtension(),
TableExtension(),
TocExtension(toc_depth=3),
CodeHiliteExtension(
css_class='highlight',
linenums=False,
guess_lang=True,
use_pygments=True
),
],
output_format='html5'
)
html = md.convert(markdown_text)
md.reset()
return html
def embed_screenshot_base64(screenshot_id: str) -> str:
"""Embed screenshot as base64, using cache to avoid duplicates"""
global _embedded_images
filename = SCREENSHOT_MAPPING.get(screenshot_id)
if not filename:
return f'<div class="screenshot-placeholder">📷 {screenshot_id}</div>'
filepath = SCREENSHOTS_DIR / filename
if not filepath.exists():
return f'<div class="screenshot-placeholder">📷 {screenshot_id}</div>'
# Check cache
if filename not in _embedded_images:
try:
with open(filepath, 'rb') as f:
image_data = base64.b64encode(f.read()).decode('utf-8')
ext = filepath.suffix[1:].lower()
_embedded_images[filename] = f"data:image/{ext};base64,{image_data}"
except Exception as e:
return f'<div class="screenshot-placeholder">📷 {screenshot_id} (加载失败)</div>'
return f'''<figure class="screenshot">
<img src="{_embedded_images[filename]}" alt="{screenshot_id}" loading="lazy" />
<figcaption>{screenshot_id}</figcaption>
</figure>'''
def process_markdown_screenshots(markdown_text: str) -> str:
"""Replace [[screenshot:xxx]] placeholders with embedded images"""
pattern = r'\[\[screenshot:(.*?)\]\]'
def replacer(match):
screenshot_id = match.group(1)
return embed_screenshot_base64(screenshot_id)
return re.sub(pattern, replacer, markdown_text)
def generate_sidebar_nav_html(nav_structure: List[Dict]) -> str:
"""Generate hierarchical sidebar navigation HTML"""
html_parts = []
for group in nav_structure:
if group["type"] == "group":
expanded_class = "expanded" if group.get("expanded", False) else ""
html_parts.append(f'''
<div class="nav-group {expanded_class}">
<div class="nav-group-header">
<button class="nav-group-toggle" aria-expanded="{str(group.get('expanded', False)).lower()}">
<svg viewBox="0 0 24 24"><path d="M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6z" fill="currentColor"/></svg>
</button>
<span class="nav-group-title">{group.get('icon', '')} {group['title']}</span>
</div>
<div class="nav-group-items">''')
for item in group.get("items", []):
html_parts.append(f'''
<a class="nav-item" href="#/{item['id']}" data-section="{item['id']}">{item['title']}</a>''')
html_parts.append('''
</div>
</div>''')
return '\n'.join(html_parts)
def generate_sections_html(nav_structure: List[Dict]) -> str:
"""Generate content sections HTML"""
sections_html = []
for group in nav_structure:
for item in group.get("items", []):
section_id = item["id"]
section_title = item["title"]
section_file = item.get("file")
if not section_file:
continue
filepath = SECTIONS_DIR / section_file
if not filepath.exists():
print(f"Warning: Section file not found: {filepath}")
continue
# Read and convert markdown
markdown_content = read_file(filepath)
markdown_content = process_markdown_screenshots(markdown_content)
html_content = convert_md_to_html(markdown_content)
sections_html.append(f'''
<section class="content-section" id="section-{section_id}" data-title="{section_title}">
{html_content}
</section>''')
return '\n'.join(sections_html)
def generate_search_index(nav_structure: List[Dict]) -> str:
"""Generate search index JSON"""
search_index = {}
for group in nav_structure:
for item in group.get("items", []):
section_id = item["id"]
section_file = item.get("file")
if not section_file:
continue
filepath = SECTIONS_DIR / section_file
if filepath.exists():
content = read_file(filepath)
clean_content = re.sub(r'[#*`\[\]()]', '', content)
clean_content = re.sub(r'\s+', ' ', clean_content)[:1500]
search_index[section_id] = {
"title": item["title"],
"body": clean_content,
"group": group["title"]
}
return json.dumps(search_index, ensure_ascii=False, indent=2)
def generate_nav_structure_json(nav_structure: List[Dict]) -> str:
"""Generate navigation structure JSON for client-side"""
return json.dumps(nav_structure, ensure_ascii=False, indent=2)
def assemble_manual(validate_mermaid: bool = True):
"""Main assembly function
Args:
validate_mermaid: Whether to validate mermaid diagrams (default: True)
"""
global _embedded_images
_embedded_images = {}
full_title = f"{MANUAL_META['title']} {MANUAL_META['subtitle']}"
print(f"Assembling Docsify-style manual: {full_title}")
# Verify template exists
if not TEMPLATE_FILE.exists():
print(f"Error: Template not found at {TEMPLATE_FILE}")
print("Please update TEMPLATE_FILE path in this script.")
return None, 0
if not CSS_BASE_FILE.exists():
print(f"Error: CSS not found at {CSS_BASE_FILE}")
print("Please update CSS_BASE_FILE path in this script.")
return None, 0
# Validate Mermaid diagrams
mermaid_report = None
if validate_mermaid:
print("\nValidating Mermaid diagrams...")
mermaid_report = validate_all_mermaid(NAV_STRUCTURE, SECTIONS_DIR)
print_mermaid_report(mermaid_report)
# Warn if there are errors (but continue)
if mermaid_report['error_blocks'] > 0:
print(f"[WARN] {mermaid_report['error_blocks']} mermaid block(s) have errors!")
print(" These diagrams may not render correctly.")
# Read template and CSS
template_html = read_file(TEMPLATE_FILE)
css_content = read_file(CSS_BASE_FILE)
# Generate components
sidebar_nav_html = generate_sidebar_nav_html(NAV_STRUCTURE)
sections_html = generate_sections_html(NAV_STRUCTURE)
search_index_json = generate_search_index(NAV_STRUCTURE)
nav_structure_json = generate_nav_structure_json(NAV_STRUCTURE)
# Replace placeholders
output_html = template_html
output_html = output_html.replace('{{SOFTWARE_NAME}}', full_title)
output_html = output_html.replace('{{VERSION}}', MANUAL_META['version'])
output_html = output_html.replace('{{TIMESTAMP}}', MANUAL_META['timestamp'])
output_html = output_html.replace('{{LOGO_ICON}}', MANUAL_META['logo_icon'])
output_html = output_html.replace('{{EMBEDDED_CSS}}', css_content)
output_html = output_html.replace('{{SIDEBAR_NAV_HTML}}', sidebar_nav_html)
output_html = output_html.replace('{{SECTIONS_HTML}}', sections_html)
output_html = output_html.replace('{{SEARCH_INDEX_JSON}}', search_index_json)
output_html = output_html.replace('{{NAV_STRUCTURE_JSON}}', nav_structure_json)
# Write output file
OUTPUT_FILE.write_text(output_html, encoding='utf-8')
file_size = OUTPUT_FILE.stat().st_size
file_size_mb = file_size / (1024 * 1024)
section_count = sum(len(g.get("items", [])) for g in NAV_STRUCTURE)
print("[OK] Docsify-style manual generated successfully!")
print(f" Output: {OUTPUT_FILE}")
print(f" Size: {file_size_mb:.2f} MB ({file_size:,} bytes)")
print(f" Navigation Groups: {len(NAV_STRUCTURE)}")
print(f" Sections: {section_count}")
return str(OUTPUT_FILE), file_size
if __name__ == "__main__":
output_path, size = assemble_manual()

View File

@@ -0,0 +1,85 @@
# 库文件打包说明
## 依赖库
HTML 组装阶段需要内嵌以下成熟库(无 CDN 依赖):
### 1. marked.js - Markdown 解析
```bash
# 获取最新版本
curl -o templates/libs/marked.min.js https://unpkg.com/marked/marked.min.js
```
### 2. highlight.js - 代码语法高亮
```bash
# 获取核心 + 常用语言包
curl -o templates/libs/highlight.min.js https://unpkg.com/@highlightjs/cdn-assets/highlight.min.js
# 获取 github-dark 主题
curl -o templates/libs/github-dark.min.css https://unpkg.com/@highlightjs/cdn-assets/styles/github-dark.min.css
```
## 内嵌方式
Phase 5 Agent 应:
1. 读取 `templates/libs/*.js``*.css`
2. 将内容嵌入 HTML 的 `<script>``<style>` 标签
3.`DOMContentLoaded` 后初始化:
```javascript
// 初始化 marked
marked.setOptions({
highlight: function(code, lang) {
if (lang && hljs.getLanguage(lang)) {
return hljs.highlight(code, { language: lang }).value;
}
return hljs.highlightAuto(code).value;
},
breaks: true,
gfm: true
});
// 应用高亮
document.querySelectorAll('pre code').forEach(block => {
hljs.highlightElement(block);
});
```
## 备选方案
如果无法获取外部库,使用内置的简化 Markdown 转换:
```javascript
function simpleMarkdown(md) {
return md
.replace(/^### (.+)$/gm, '<h3>$1</h3>')
.replace(/^## (.+)$/gm, '<h2>$1</h2>')
.replace(/^# (.+)$/gm, '<h1>$1</h1>')
.replace(/```(\w+)?\n([\s\S]*?)```/g, (m, lang, code) =>
`<pre data-language="${lang || ''}"><code class="language-${lang || ''}">${escapeHtml(code)}</code></pre>`)
.replace(/`([^`]+)`/g, '<code>$1</code>')
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.+?)\*/g, '<em>$1</em>')
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>')
.replace(/^\|(.+)\|$/gm, processTableRow)
.replace(/^- (.+)$/gm, '<li>$1</li>')
.replace(/^\d+\. (.+)$/gm, '<li>$1</li>');
}
```
## 文件结构
```
templates/
├── libs/
│ ├── marked.min.js # Markdown parser
│ ├── highlight.min.js # Syntax highlighting
│ └── github-dark.min.css # Code theme
├── tiddlywiki-shell.html
└── css/
├── wiki-base.css
└── wiki-dark.css
```

View File

@@ -0,0 +1,270 @@
#!/usr/bin/env python3
"""
API 文档提取脚本
支持 FastAPI、TypeScript、Python 模块
"""
import subprocess
import sys
import json
from pathlib import Path
from typing import Dict, Any, Optional
# 项目配置
PROJECTS = {
'backend': {
'path': Path('D:/dongdiankaifa9/backend'),
'type': 'fastapi',
'entry': 'app.main:app',
'output': 'api-docs/backend'
},
'frontend': {
'path': Path('D:/dongdiankaifa9/frontend'),
'type': 'typescript',
'entries': ['lib', 'hooks', 'components'],
'output': 'api-docs/frontend'
},
'hydro_generator_module': {
'path': Path('D:/dongdiankaifa9/hydro_generator_module'),
'type': 'python',
'output': 'api-docs/hydro_generator'
},
'multiphysics_network': {
'path': Path('D:/dongdiankaifa9/multiphysics_network'),
'type': 'python',
'output': 'api-docs/multiphysics'
}
}
def extract_fastapi(name: str, config: Dict[str, Any], output_base: Path) -> bool:
"""提取 FastAPI OpenAPI 文档"""
path = config['path']
output_dir = output_base / config['output']
output_dir.mkdir(parents=True, exist_ok=True)
# 添加路径到 sys.path
if str(path) not in sys.path:
sys.path.insert(0, str(path))
try:
# 动态导入 app
from app.main import app
# 获取 OpenAPI schema
openapi_schema = app.openapi()
# 保存 JSON
json_path = output_dir / 'openapi.json'
with open(json_path, 'w', encoding='utf-8') as f:
json.dump(openapi_schema, f, indent=2, ensure_ascii=False)
# 生成 Markdown 摘要
md_path = output_dir / 'API_SUMMARY.md'
generate_api_markdown(openapi_schema, md_path)
endpoints = len(openapi_schema.get('paths', {}))
print(f" ✓ Extracted {endpoints} endpoints → {output_dir}")
return True
except ImportError as e:
print(f" ✗ Import error: {e}")
return False
except Exception as e:
print(f" ✗ Error: {e}")
return False
def generate_api_markdown(schema: Dict, output_path: Path):
"""从 OpenAPI schema 生成 Markdown"""
lines = [
f"# {schema.get('info', {}).get('title', 'API Reference')}",
"",
f"Version: {schema.get('info', {}).get('version', '1.0.0')}",
"",
"## Endpoints",
"",
"| Method | Path | Summary |",
"|--------|------|---------|"
]
for path, methods in schema.get('paths', {}).items():
for method, details in methods.items():
if method in ('get', 'post', 'put', 'delete', 'patch'):
summary = details.get('summary', details.get('operationId', '-'))
lines.append(f"| `{method.upper()}` | `{path}` | {summary} |")
lines.extend([
"",
"## Schemas",
""
])
for name, schema_def in schema.get('components', {}).get('schemas', {}).items():
lines.append(f"### {name}")
lines.append("")
if 'properties' in schema_def:
lines.append("| Property | Type | Required |")
lines.append("|----------|------|----------|")
required = schema_def.get('required', [])
for prop, prop_def in schema_def['properties'].items():
prop_type = prop_def.get('type', prop_def.get('$ref', 'any'))
is_required = '' if prop in required else ''
lines.append(f"| `{prop}` | {prop_type} | {is_required} |")
lines.append("")
with open(output_path, 'w', encoding='utf-8') as f:
f.write('\n'.join(lines))
def extract_typescript(name: str, config: Dict[str, Any], output_base: Path) -> bool:
"""提取 TypeScript 文档 (TypeDoc)"""
path = config['path']
output_dir = output_base / config['output']
# 检查 TypeDoc 是否已安装
try:
result = subprocess.run(
['npx', 'typedoc', '--version'],
cwd=path,
capture_output=True,
text=True
)
if result.returncode != 0:
print(f" ⚠ TypeDoc not installed, installing...")
subprocess.run(
['npm', 'install', '--save-dev', 'typedoc', 'typedoc-plugin-markdown'],
cwd=path,
check=True
)
except FileNotFoundError:
print(f" ✗ npm/npx not found")
return False
# 运行 TypeDoc
try:
entries = config.get('entries', ['lib'])
cmd = [
'npx', 'typedoc',
'--plugin', 'typedoc-plugin-markdown',
'--out', str(output_dir),
'--entryPointStrategy', 'expand',
'--exclude', '**/node_modules/**',
'--exclude', '**/*.test.*',
'--readme', 'none'
]
for entry in entries:
entry_path = path / entry
if entry_path.exists():
cmd.extend(['--entryPoints', str(entry_path)])
result = subprocess.run(cmd, cwd=path, capture_output=True, text=True)
if result.returncode == 0:
print(f" ✓ TypeDoc generated → {output_dir}")
return True
else:
print(f" ✗ TypeDoc error: {result.stderr[:200]}")
return False
except Exception as e:
print(f" ✗ Error: {e}")
return False
def extract_python_module(name: str, config: Dict[str, Any], output_base: Path) -> bool:
"""提取 Python 模块文档 (pdoc)"""
path = config['path']
output_dir = output_base / config['output']
module_name = path.name
# 检查 pdoc
try:
subprocess.run(['pdoc', '--version'], capture_output=True, check=True)
except (FileNotFoundError, subprocess.CalledProcessError):
print(f" ⚠ pdoc not installed, installing...")
subprocess.run([sys.executable, '-m', 'pip', 'install', 'pdoc'], check=True)
# 运行 pdoc
try:
result = subprocess.run(
[
'pdoc', module_name,
'--output-dir', str(output_dir),
'--format', 'markdown'
],
cwd=path.parent,
capture_output=True,
text=True
)
if result.returncode == 0:
# 统计生成的文件
md_files = list(output_dir.glob('**/*.md'))
print(f" ✓ pdoc generated {len(md_files)} files → {output_dir}")
return True
else:
print(f" ✗ pdoc error: {result.stderr[:200]}")
return False
except Exception as e:
print(f" ✗ Error: {e}")
return False
EXTRACTORS = {
'fastapi': extract_fastapi,
'typescript': extract_typescript,
'python': extract_python_module
}
def main(output_base: Optional[str] = None, projects: Optional[list] = None):
"""主入口"""
base = Path(output_base) if output_base else Path.cwd()
print("=" * 50)
print("API Documentation Extraction")
print("=" * 50)
results = {}
for name, config in PROJECTS.items():
if projects and name not in projects:
continue
print(f"\n[{name}] ({config['type']})")
if not config['path'].exists():
print(f" ✗ Path not found: {config['path']}")
results[name] = False
continue
extractor = EXTRACTORS.get(config['type'])
if extractor:
results[name] = extractor(name, config, base)
else:
print(f" ✗ Unknown type: {config['type']}")
results[name] = False
# 汇总
print("\n" + "=" * 50)
print("Summary")
print("=" * 50)
success = sum(1 for v in results.values() if v)
print(f"Success: {success}/{len(results)}")
return all(results.values())
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser(description='Extract API documentation')
parser.add_argument('--output', '-o', default='.', help='Output base directory')
parser.add_argument('--projects', '-p', nargs='+', help='Specific projects to extract')
args = parser.parse_args()
success = main(args.output, args.projects)
sys.exit(0 if success else 1)

View File

@@ -0,0 +1,447 @@
# Screenshot Helper
Guide for capturing screenshots using Chrome MCP.
## Overview
This script helps capture screenshots of web interfaces for the software manual using Chrome MCP or fallback methods.
## Chrome MCP Prerequisites
### Check MCP Availability
```javascript
async function checkChromeMCPAvailability() {
try {
// Attempt to get Chrome version via MCP
const version = await mcp__chrome__getVersion();
return {
available: true,
browser: version.browser,
version: version.version
};
} catch (error) {
return {
available: false,
error: error.message
};
}
}
```
### MCP Configuration
Expected Claude configuration for Chrome MCP:
```json
{
"mcpServers": {
"chrome": {
"command": "npx",
"args": ["@anthropic-ai/mcp-chrome"],
"env": {
"CHROME_PATH": "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"
}
}
}
}
```
## Screenshot Workflow
### Step 1: Prepare Environment
```javascript
async function prepareScreenshotEnvironment(workDir, config) {
const screenshotDir = `${workDir}/screenshots`;
// Create directory
Bash({ command: `mkdir -p "${screenshotDir}"` });
// Check Chrome MCP
const chromeMCP = await checkChromeMCPAvailability();
if (!chromeMCP.available) {
console.log('Chrome MCP not available. Will generate manual guide.');
return { mode: 'manual' };
}
// Start development server if needed
if (config.screenshot_config?.dev_command) {
const server = await startDevServer(config);
return { mode: 'auto', server, screenshotDir };
}
return { mode: 'auto', screenshotDir };
}
```
### Step 2: Start Development Server
```javascript
async function startDevServer(config) {
const devCommand = config.screenshot_config.dev_command;
const devUrl = config.screenshot_config.dev_url;
// Start server in background
const server = Bash({
command: devCommand,
run_in_background: true
});
console.log(`Starting dev server: ${devCommand}`);
// Wait for server to be ready
const ready = await waitForServer(devUrl, 30000);
if (!ready) {
throw new Error(`Server at ${devUrl} did not start in time`);
}
console.log(`Dev server ready at ${devUrl}`);
return server;
}
async function waitForServer(url, timeout = 30000) {
const start = Date.now();
while (Date.now() - start < timeout) {
try {
const response = await fetch(url, { method: 'HEAD' });
if (response.ok) return true;
} catch (e) {
// Server not ready
}
await sleep(1000);
}
return false;
}
```
### Step 3: Capture Screenshots
```javascript
async function captureScreenshots(screenshots, config, workDir) {
const results = {
captured: [],
failed: []
};
const devUrl = config.screenshot_config.dev_url;
const screenshotDir = `${workDir}/screenshots`;
for (const ss of screenshots) {
try {
// Build full URL
const fullUrl = new URL(ss.url, devUrl).href;
console.log(`Capturing: ${ss.id} (${fullUrl})`);
// Configure capture options
const options = {
url: fullUrl,
viewport: { width: 1280, height: 800 },
fullPage: ss.fullPage || false
};
// Wait for specific element if specified
if (ss.wait_for) {
options.waitFor = ss.wait_for;
}
// Capture specific element if selector provided
if (ss.selector) {
options.selector = ss.selector;
}
// Add delay for animations
await sleep(500);
// Capture via Chrome MCP
const result = await mcp__chrome__screenshot(options);
// Save as PNG
const filename = `${ss.id}.png`;
Write(`${screenshotDir}/${filename}`, result.data, { encoding: 'base64' });
results.captured.push({
id: ss.id,
file: filename,
url: ss.url,
description: ss.description,
size: result.data.length
});
} catch (error) {
console.error(`Failed to capture ${ss.id}:`, error.message);
results.failed.push({
id: ss.id,
url: ss.url,
error: error.message
});
}
}
return results;
}
```
### Step 4: Generate Manifest
```javascript
function generateScreenshotManifest(results, workDir) {
const manifest = {
generated: new Date().toISOString(),
total: results.captured.length + results.failed.length,
captured: results.captured.length,
failed: results.failed.length,
screenshots: results.captured,
failures: results.failed
};
Write(`${workDir}/screenshots/screenshots-manifest.json`,
JSON.stringify(manifest, null, 2));
return manifest;
}
```
### Step 5: Cleanup
```javascript
async function cleanupScreenshotEnvironment(env) {
if (env.server) {
console.log('Stopping dev server...');
KillShell({ shell_id: env.server.task_id });
}
}
```
## Main Runner
```javascript
async function runScreenshotCapture(workDir, screenshots) {
const config = JSON.parse(Read(`${workDir}/manual-config.json`));
// Prepare environment
const env = await prepareScreenshotEnvironment(workDir, config);
if (env.mode === 'manual') {
// Generate manual capture guide
generateManualCaptureGuide(screenshots, workDir);
return { success: false, mode: 'manual' };
}
try {
// Capture screenshots
const results = await captureScreenshots(screenshots, config, workDir);
// Generate manifest
const manifest = generateScreenshotManifest(results, workDir);
// Generate manual guide for failed captures
if (results.failed.length > 0) {
generateManualCaptureGuide(results.failed, workDir);
}
return {
success: true,
captured: results.captured.length,
failed: results.failed.length,
manifest
};
} finally {
// Cleanup
await cleanupScreenshotEnvironment(env);
}
}
```
## Manual Capture Fallback
When Chrome MCP is unavailable:
```javascript
function generateManualCaptureGuide(screenshots, workDir) {
const guide = `
# Manual Screenshot Capture Guide
Chrome MCP is not available. Please capture screenshots manually.
## Prerequisites
1. Start your development server
2. Open a browser
3. Use a screenshot tool (Snipping Tool, Screenshot, etc.)
## Screenshots Required
${screenshots.map((ss, i) => `
### ${i + 1}. ${ss.id}
- **URL**: ${ss.url}
- **Description**: ${ss.description}
- **Save as**: \`screenshots/${ss.id}.png\`
${ss.selector ? `- **Capture area**: \`${ss.selector}\` element only` : '- **Type**: Full page or viewport'}
${ss.wait_for ? `- **Wait for**: \`${ss.wait_for}\` to be visible` : ''}
**Steps:**
1. Navigate to ${ss.url}
${ss.wait_for ? `2. Wait for ${ss.wait_for} to appear` : ''}
${ss.selector ? `2. Capture only the ${ss.selector} area` : '2. Capture the full viewport'}
3. Save as \`${ss.id}.png\`
`).join('\n')}
## After Capturing
1. Place all PNG files in the \`screenshots/\` directory
2. Ensure filenames match exactly (case-sensitive)
3. Run Phase 5 (HTML Assembly) to continue
## Screenshot Specifications
- **Format**: PNG
- **Width**: 1280px recommended
- **Quality**: High
- **Annotations**: None (add in post-processing if needed)
`;
Write(`${workDir}/screenshots/MANUAL_CAPTURE.md`, guide);
}
```
## Advanced Options
### Viewport Sizes
```javascript
const viewportPresets = {
desktop: { width: 1280, height: 800 },
tablet: { width: 768, height: 1024 },
mobile: { width: 375, height: 667 },
wide: { width: 1920, height: 1080 }
};
async function captureResponsive(ss, config, workDir) {
const results = [];
for (const [name, viewport] of Object.entries(viewportPresets)) {
const result = await mcp__chrome__screenshot({
url: ss.url,
viewport
});
const filename = `${ss.id}-${name}.png`;
Write(`${workDir}/screenshots/${filename}`, result.data, { encoding: 'base64' });
results.push({ viewport: name, file: filename });
}
return results;
}
```
### Before/After Comparisons
```javascript
async function captureInteraction(ss, config, workDir) {
const baseUrl = config.screenshot_config.dev_url;
const fullUrl = new URL(ss.url, baseUrl).href;
// Capture before state
const before = await mcp__chrome__screenshot({
url: fullUrl,
viewport: { width: 1280, height: 800 }
});
Write(`${workDir}/screenshots/${ss.id}-before.png`, before.data, { encoding: 'base64' });
// Perform interaction (click, type, etc.)
if (ss.interaction) {
await mcp__chrome__click({ selector: ss.interaction.click });
await sleep(500);
}
// Capture after state
const after = await mcp__chrome__screenshot({
url: fullUrl,
viewport: { width: 1280, height: 800 }
});
Write(`${workDir}/screenshots/${ss.id}-after.png`, after.data, { encoding: 'base64' });
return {
before: `${ss.id}-before.png`,
after: `${ss.id}-after.png`
};
}
```
### Screenshot Annotation
```javascript
function generateAnnotationGuide(screenshots, workDir) {
const guide = `
# Screenshot Annotation Guide
For screenshots requiring callouts or highlights:
## Tools
- macOS: Preview, Skitch
- Windows: Snipping Tool, ShareX
- Cross-platform: Greenshot, Lightshot
## Annotation Guidelines
1. **Callouts**: Use numbered circles (1, 2, 3)
2. **Highlights**: Use semi-transparent rectangles
3. **Arrows**: Point from text to element
4. **Text**: Use sans-serif font, 12-14pt
## Color Scheme
- Primary: #0d6efd (blue)
- Secondary: #6c757d (gray)
- Success: #198754 (green)
- Warning: #ffc107 (yellow)
- Danger: #dc3545 (red)
## Screenshots Needing Annotation
${screenshots.filter(s => s.annotate).map(ss => `
- **${ss.id}**: ${ss.description}
- Highlight: ${ss.annotate.highlight || 'N/A'}
- Callouts: ${ss.annotate.callouts?.join(', ') || 'N/A'}
`).join('\n')}
`;
Write(`${workDir}/screenshots/ANNOTATION_GUIDE.md`, guide);
}
```
## Troubleshooting
### Chrome MCP Not Found
1. Check Claude MCP configuration
2. Verify Chrome is installed
3. Check CHROME_PATH environment variable
### Screenshots Are Blank
1. Increase wait time before capture
2. Check if page requires authentication
3. Verify URL is correct
### Elements Not Visible
1. Scroll element into view
2. Expand collapsed sections
3. Wait for animations to complete
### Server Not Starting
1. Check if port is already in use
2. Verify dev command is correct
3. Check for startup errors in logs

View File

@@ -0,0 +1,419 @@
# Swagger/OpenAPI Runner
Guide for generating backend API documentation from OpenAPI/Swagger specifications.
## Overview
This script extracts and converts OpenAPI/Swagger specifications to Markdown format for inclusion in the software manual.
## Detection Strategy
### Check for Existing Specification
```javascript
async function detectOpenAPISpec() {
// Check for existing spec files
const specPatterns = [
'openapi.json',
'openapi.yaml',
'openapi.yml',
'swagger.json',
'swagger.yaml',
'swagger.yml',
'**/openapi*.json',
'**/swagger*.json'
];
for (const pattern of specPatterns) {
const files = Glob(pattern);
if (files.length > 0) {
return { found: true, type: 'file', path: files[0] };
}
}
// Check for swagger-jsdoc in dependencies
const packageJson = JSON.parse(Read('package.json'));
if (packageJson.dependencies?.['swagger-jsdoc'] ||
packageJson.devDependencies?.['swagger-jsdoc']) {
return { found: true, type: 'jsdoc' };
}
// Check for NestJS Swagger
if (packageJson.dependencies?.['@nestjs/swagger']) {
return { found: true, type: 'nestjs' };
}
// Check for runtime endpoint
return { found: false, suggestion: 'runtime' };
}
```
## Extraction Methods
### Method A: From Existing Spec File
```javascript
async function extractFromFile(specPath, workDir) {
const outputDir = `${workDir}/api-docs/backend`;
Bash({ command: `mkdir -p "${outputDir}"` });
// Copy spec to output
Bash({ command: `cp "${specPath}" "${outputDir}/openapi.json"` });
// Convert to Markdown using widdershins
const result = Bash({
command: `npx widdershins "${specPath}" -o "${outputDir}/api-reference.md" --language_tabs 'javascript:JavaScript' 'python:Python' 'bash:cURL'`,
timeout: 60000
});
return { success: result.exitCode === 0, outputDir };
}
```
### Method B: From swagger-jsdoc
```javascript
async function extractFromJsDoc(workDir) {
const outputDir = `${workDir}/api-docs/backend`;
// Look for swagger definition file
const defFiles = Glob('**/swagger*.js').concat(Glob('**/openapi*.js'));
if (defFiles.length === 0) {
return { success: false, error: 'No swagger definition found' };
}
// Generate spec
const result = Bash({
command: `npx swagger-jsdoc -d "${defFiles[0]}" -o "${outputDir}/openapi.json"`,
timeout: 60000
});
if (result.exitCode !== 0) {
return { success: false, error: result.stderr };
}
// Convert to Markdown
Bash({
command: `npx widdershins "${outputDir}/openapi.json" -o "${outputDir}/api-reference.md" --language_tabs 'javascript:JavaScript' 'bash:cURL'`
});
return { success: true, outputDir };
}
```
### Method C: From NestJS Swagger
```javascript
async function extractFromNestJS(workDir) {
const outputDir = `${workDir}/api-docs/backend`;
// NestJS typically exposes /api-docs-json at runtime
// We need to start the server temporarily
// Start server in background
const server = Bash({
command: 'npm run start:dev',
run_in_background: true,
timeout: 30000
});
// Wait for server to be ready
await waitForServer('http://localhost:3000', 30000);
// Fetch OpenAPI spec
const spec = await fetch('http://localhost:3000/api-docs-json');
const specJson = await spec.json();
// Save spec
Write(`${outputDir}/openapi.json`, JSON.stringify(specJson, null, 2));
// Stop server
KillShell({ shell_id: server.task_id });
// Convert to Markdown
Bash({
command: `npx widdershins "${outputDir}/openapi.json" -o "${outputDir}/api-reference.md" --language_tabs 'javascript:JavaScript' 'bash:cURL'`
});
return { success: true, outputDir };
}
```
### Method D: From Runtime Endpoint
```javascript
async function extractFromRuntime(workDir, serverUrl = 'http://localhost:3000') {
const outputDir = `${workDir}/api-docs/backend`;
// Common OpenAPI endpoint paths
const endpointPaths = [
'/api-docs-json',
'/swagger.json',
'/openapi.json',
'/docs/json',
'/api/v1/docs.json'
];
let specJson = null;
for (const path of endpointPaths) {
try {
const response = await fetch(`${serverUrl}${path}`);
if (response.ok) {
specJson = await response.json();
break;
}
} catch (e) {
continue;
}
}
if (!specJson) {
return { success: false, error: 'Could not fetch OpenAPI spec from server' };
}
// Save and convert
Write(`${outputDir}/openapi.json`, JSON.stringify(specJson, null, 2));
Bash({
command: `npx widdershins "${outputDir}/openapi.json" -o "${outputDir}/api-reference.md"`
});
return { success: true, outputDir };
}
```
## Installation
### Required Tools
```bash
# For OpenAPI to Markdown conversion
npm install -g widdershins
# Or as dev dependency
npm install --save-dev widdershins
# For generating from JSDoc comments
npm install --save-dev swagger-jsdoc
```
## Configuration
### widdershins Options
```bash
npx widdershins openapi.json \
-o api-reference.md \
--language_tabs 'javascript:JavaScript' 'python:Python' 'bash:cURL' \
--summary \
--omitHeader \
--resolve \
--expandBody
```
| Option | Description |
|--------|-------------|
| `--language_tabs` | Code example languages |
| `--summary` | Use summary as operation heading |
| `--omitHeader` | Don't include title header |
| `--resolve` | Resolve $ref references |
| `--expandBody` | Show full request body |
### swagger-jsdoc Definition
Example `swagger-def.js`:
```javascript
module.exports = {
definition: {
openapi: '3.0.0',
info: {
title: 'MyApp API',
version: '1.0.0',
description: 'API documentation for MyApp'
},
servers: [
{ url: 'http://localhost:3000/api/v1' }
]
},
apis: ['./src/routes/*.js', './src/controllers/*.js']
};
```
## Output Format
### Generated Markdown Structure
```markdown
# MyApp API
## Overview
Base URL: `http://localhost:3000/api/v1`
## Authentication
This API uses Bearer token authentication.
---
## Projects
### List Projects
`GET /projects`
Returns a list of all projects.
**Parameters**
| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| status | query | string | false | Filter by status |
| page | query | integer | false | Page number |
**Responses**
| Status | Description |
|--------|-------------|
| 200 | Successful response |
| 401 | Unauthorized |
**Example Request**
```javascript
fetch('/api/v1/projects?status=active')
.then(res => res.json())
.then(data => console.log(data));
```
**Example Response**
```json
{
"data": [
{ "id": "1", "name": "Project 1" }
],
"pagination": {
"page": 1,
"total": 10
}
}
```
```
## Integration
### Main Runner
```javascript
async function runSwaggerExtraction(workDir) {
const detection = await detectOpenAPISpec();
if (!detection.found) {
console.log('No OpenAPI spec detected. Skipping backend API docs.');
return { success: false, skipped: true };
}
let result;
switch (detection.type) {
case 'file':
result = await extractFromFile(detection.path, workDir);
break;
case 'jsdoc':
result = await extractFromJsDoc(workDir);
break;
case 'nestjs':
result = await extractFromNestJS(workDir);
break;
default:
result = await extractFromRuntime(workDir);
}
if (result.success) {
// Post-process the Markdown
await postProcessApiDocs(result.outputDir);
}
return result;
}
async function postProcessApiDocs(outputDir) {
const mdFile = `${outputDir}/api-reference.md`;
let content = Read(mdFile);
// Remove widdershins header
content = content.replace(/^---[\s\S]*?---\n/, '');
// Add custom styling hints
content = content.replace(/^(#{1,3} .+)$/gm, '$1\n');
Write(mdFile, content);
}
```
## Troubleshooting
### Common Issues
#### "widdershins: command not found"
```bash
npm install -g widdershins
# Or use npx
npx widdershins openapi.json -o api.md
```
#### "Error parsing OpenAPI spec"
```bash
# Validate spec first
npx @redocly/cli lint openapi.json
# Fix common issues
npx @redocly/cli bundle openapi.json -o fixed.json
```
#### "Server not responding"
Ensure the development server is running and accessible:
```bash
# Check if server is running
curl http://localhost:3000/health
# Check OpenAPI endpoint
curl http://localhost:3000/api-docs-json
```
### Manual Fallback
If automatic extraction fails, document APIs manually:
1. List all route files: `Glob('**/routes/*.js')`
2. Extract route definitions using regex
3. Build documentation structure manually
```javascript
async function manualApiExtraction(workDir) {
const routeFiles = Glob('src/routes/*.js').concat(Glob('src/routes/*.ts'));
const endpoints = [];
for (const file of routeFiles) {
const content = Read(file);
const routes = content.matchAll(/router\.(get|post|put|delete|patch)\(['"]([^'"]+)['"]/g);
for (const match of routes) {
endpoints.push({
method: match[1].toUpperCase(),
path: match[2],
file: file
});
}
}
return endpoints;
}
```

View File

@@ -0,0 +1,357 @@
# TypeDoc Runner
Guide for generating frontend API documentation using TypeDoc.
## Overview
TypeDoc generates API documentation from TypeScript source code by analyzing type annotations and JSDoc comments.
## Prerequisites
### Check TypeScript Project
```javascript
// Verify TypeScript is used
const packageJson = JSON.parse(Read('package.json'));
const hasTypeScript = packageJson.devDependencies?.typescript ||
packageJson.dependencies?.typescript;
if (!hasTypeScript) {
console.log('Not a TypeScript project. Skipping TypeDoc.');
return;
}
// Check for tsconfig.json
const hasTsConfig = Glob('tsconfig.json').length > 0;
```
## Installation
### Install TypeDoc
```bash
npm install --save-dev typedoc typedoc-plugin-markdown
```
### Optional Plugins
```bash
# For better Markdown output
npm install --save-dev typedoc-plugin-markdown
# For README inclusion
npm install --save-dev typedoc-plugin-rename-defaults
```
## Configuration
### typedoc.json
Create `typedoc.json` in project root:
```json
{
"entryPoints": ["./src/index.ts"],
"entryPointStrategy": "expand",
"out": ".workflow/.scratchpad/manual-{timestamp}/api-docs/frontend",
"plugin": ["typedoc-plugin-markdown"],
"exclude": [
"**/node_modules/**",
"**/*.test.ts",
"**/*.spec.ts",
"**/tests/**"
],
"excludePrivate": true,
"excludeProtected": true,
"excludeInternal": true,
"hideGenerator": true,
"readme": "none",
"categorizeByGroup": true,
"navigation": {
"includeCategories": true,
"includeGroups": true
}
}
```
### Alternative: CLI Options
```bash
npx typedoc \
--entryPoints src/index.ts \
--entryPointStrategy expand \
--out api-docs/frontend \
--plugin typedoc-plugin-markdown \
--exclude "**/node_modules/**" \
--exclude "**/*.test.ts" \
--excludePrivate \
--excludeProtected \
--readme none
```
## Execution
### Basic Run
```javascript
async function runTypeDoc(workDir) {
const outputDir = `${workDir}/api-docs/frontend`;
// Create output directory
Bash({ command: `mkdir -p "${outputDir}"` });
// Run TypeDoc
const result = Bash({
command: `npx typedoc --out "${outputDir}" --plugin typedoc-plugin-markdown src/`,
timeout: 120000 // 2 minutes
});
if (result.exitCode !== 0) {
console.error('TypeDoc failed:', result.stderr);
return { success: false, error: result.stderr };
}
// List generated files
const files = Glob(`${outputDir}/**/*.md`);
console.log(`Generated ${files.length} documentation files`);
return { success: true, files };
}
```
### With Custom Entry Points
```javascript
async function runTypeDocCustom(workDir, entryPoints) {
const outputDir = `${workDir}/api-docs/frontend`;
// Build entry points string
const entries = entryPoints.map(e => `--entryPoints "${e}"`).join(' ');
const result = Bash({
command: `npx typedoc ${entries} --out "${outputDir}" --plugin typedoc-plugin-markdown`,
timeout: 120000
});
return { success: result.exitCode === 0 };
}
// Example: Document specific files
await runTypeDocCustom(workDir, [
'src/api/index.ts',
'src/hooks/index.ts',
'src/utils/index.ts'
]);
```
## Output Structure
```
api-docs/frontend/
├── README.md # Index
├── modules.md # Module list
├── modules/
│ ├── api.md # API module
│ ├── hooks.md # Hooks module
│ └── utils.md # Utils module
├── classes/
│ ├── ApiClient.md # Class documentation
│ └── ...
├── interfaces/
│ ├── Config.md # Interface documentation
│ └── ...
└── functions/
├── formatDate.md # Function documentation
└── ...
```
## Integration with Manual
### Reading TypeDoc Output
```javascript
async function integrateTypeDocOutput(workDir) {
const apiDocsDir = `${workDir}/api-docs/frontend`;
const files = Glob(`${apiDocsDir}/**/*.md`);
// Build API reference content
let content = '## Frontend API Reference\n\n';
// Add modules
const modules = Glob(`${apiDocsDir}/modules/*.md`);
for (const mod of modules) {
const modContent = Read(mod);
content += `### ${extractTitle(modContent)}\n\n`;
content += summarizeModule(modContent);
}
// Add functions
const functions = Glob(`${apiDocsDir}/functions/*.md`);
content += '\n### Functions\n\n';
for (const fn of functions) {
const fnContent = Read(fn);
content += formatFunctionDoc(fnContent);
}
// Add hooks
const hooks = Glob(`${apiDocsDir}/functions/*Hook*.md`);
if (hooks.length > 0) {
content += '\n### Hooks\n\n';
for (const hook of hooks) {
const hookContent = Read(hook);
content += formatHookDoc(hookContent);
}
}
return content;
}
```
### Example Output Format
```markdown
## Frontend API Reference
### API Module
Functions for interacting with the backend API.
#### fetchProjects
```typescript
function fetchProjects(options?: FetchOptions): Promise<Project[]>
```
Fetches all projects for the current user.
**Parameters:**
| Name | Type | Description |
|------|------|-------------|
| options | FetchOptions | Optional fetch configuration |
**Returns:** Promise<Project[]>
### Hooks
#### useProjects
```typescript
function useProjects(options?: UseProjectsOptions): UseProjectsResult
```
React hook for managing project data.
**Parameters:**
| Name | Type | Description |
|------|------|-------------|
| options.status | string | Filter by project status |
| options.limit | number | Max projects to fetch |
**Returns:**
| Property | Type | Description |
|----------|------|-------------|
| projects | Project[] | Array of projects |
| loading | boolean | Loading state |
| error | Error \| null | Error if failed |
| refetch | () => void | Refresh data |
```
## Troubleshooting
### Common Issues
#### "Cannot find module 'typescript'"
```bash
npm install --save-dev typescript
```
#### "No entry points found"
Ensure entry points exist:
```bash
# Check entry points
ls src/index.ts
# Or use glob pattern
npx typedoc --entryPoints "src/**/*.ts"
```
#### "Unsupported TypeScript version"
```bash
# Check TypeDoc compatibility
npm info typedoc peerDependencies
# Install compatible version
npm install --save-dev typedoc@0.25.x
```
### Debugging
```bash
# Verbose output
npx typedoc --logLevel Verbose src/
# Show warnings
npx typedoc --treatWarningsAsErrors src/
```
## Best Practices
### Document Exports Only
```typescript
// Good: Public API documented
/**
* Fetches projects from the API.
* @param options - Fetch options
* @returns Promise resolving to projects
*/
export function fetchProjects(options?: FetchOptions): Promise<Project[]> {
// ...
}
// Internal: Not documented
function internalHelper() {
// ...
}
```
### Use JSDoc Comments
```typescript
/**
* User hook for managing authentication state.
*
* @example
* ```tsx
* const { user, login, logout } = useAuth();
* ```
*
* @returns Authentication state and methods
*/
export function useAuth(): AuthResult {
// ...
}
```
### Define Types Properly
```typescript
/**
* Configuration for the API client.
*/
export interface ApiConfig {
/** API base URL */
baseUrl: string;
/** Request timeout in milliseconds */
timeout?: number;
/** Custom headers to include */
headers?: Record<string, string>;
}
```

View File

@@ -0,0 +1,325 @@
# HTML Template Specification
Technical specification for the TiddlyWiki-style HTML output.
## Overview
The output is a single, self-contained HTML file with:
- All CSS embedded inline
- All JavaScript embedded inline
- All images embedded as Base64
- Full offline functionality
## File Structure
```html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{SOFTWARE_NAME}} - User Manual</title>
<style>{{EMBEDDED_CSS}}</style>
</head>
<body class="wiki-container" data-theme="light">
<aside class="wiki-sidebar">...</aside>
<main class="wiki-content">...</main>
<button class="theme-toggle">...</button>
<script id="search-index" type="application/json">{{SEARCH_INDEX}}</script>
<script>{{EMBEDDED_JS}}</script>
</body>
</html>
```
## Placeholders
| Placeholder | Description | Source |
|-------------|-------------|--------|
| `{{SOFTWARE_NAME}}` | Software name | manual-config.json |
| `{{VERSION}}` | Version number | manual-config.json |
| `{{EMBEDDED_CSS}}` | All CSS content | wiki-base.css + wiki-dark.css |
| `{{TOC_HTML}}` | Table of contents | Generated from sections |
| `{{TIDDLERS_HTML}}` | All content blocks | Converted from Markdown |
| `{{SEARCH_INDEX_JSON}}` | Search data | Generated from content |
| `{{EMBEDDED_JS}}` | JavaScript code | Inline in template |
| `{{TIMESTAMP}}` | Generation timestamp | ISO 8601 format |
| `{{LOGO_BASE64}}` | Logo image | Project logo or generated |
## Component Specifications
### Sidebar (`.wiki-sidebar`)
```
Width: 280px (fixed)
Position: Fixed left
Height: 100vh
Components:
- Logo area (.wiki-logo)
- Search box (.wiki-search)
- Tag navigation (.wiki-tags)
- Table of contents (.wiki-toc)
```
### Main Content (`.wiki-content`)
```
Margin-left: 280px (sidebar width)
Max-width: 900px (content)
Components:
- Header bar (.content-header)
- Tiddler container (.tiddler-container)
- Footer (.wiki-footer)
```
### Tiddler (Content Block)
```html
<article class="tiddler"
id="tiddler-{{ID}}"
data-tags="{{TAGS}}"
data-difficulty="{{DIFFICULTY}}">
<header class="tiddler-header">
<h2 class="tiddler-title">
<button class="collapse-toggle"></button>
{{TITLE}}
</h2>
<div class="tiddler-meta">
<span class="difficulty-badge {{DIFFICULTY}}">{{DIFFICULTY_LABEL}}</span>
{{TAG_BADGES}}
</div>
</header>
<div class="tiddler-content">
{{CONTENT_HTML}}
</div>
</article>
```
### Search Index Format
```json
{
"tiddler-overview": {
"title": "Product Overview",
"body": "Plain text content for searching...",
"tags": ["getting-started", "overview"]
},
"tiddler-ui-guide": {
"title": "UI Guide",
"body": "Plain text content...",
"tags": ["ui-guide"]
}
}
```
## Interactive Features
### 1. Search
- Full-text search with result highlighting
- Searches title, body, and tags
- Shows up to 10 results
- Keyboard accessible (Enter to search, Esc to close)
### 2. Collapse/Expand
- Per-section toggle via button
- Expand All / Collapse All buttons
- State indicated by ▼ (expanded) or ▶ (collapsed)
- Smooth transition animation
### 3. Tag Filtering
- Tags: all, getting-started, ui-guide, api, config, troubleshooting, examples
- Single selection (radio behavior)
- "all" shows everything
- Hidden tiddlers via `display: none`
### 4. Theme Toggle
- Light/Dark mode switch
- Persists to localStorage (`wiki-theme`)
- Applies to entire document via `[data-theme="dark"]`
- Toggle button shows sun/moon icon
### 5. Responsive Design
```
Breakpoints:
- Desktop (> 1024px): Sidebar visible
- Tablet (768-1024px): Sidebar collapsible
- Mobile (< 768px): Sidebar hidden, hamburger menu
```
### 6. Print Support
- Hides sidebar, toggles, interactive elements
- Expands all collapsed sections
- Adjusts colors for print
- Page breaks between sections
## Accessibility
### Keyboard Navigation
- Tab through interactive elements
- Enter to activate buttons
- Escape to close search results
- Arrow keys in search results
### ARIA Attributes
```html
<input aria-label="Search">
<nav aria-label="Table of Contents">
<button aria-label="Toggle theme">
<div aria-live="polite"> <!-- For search results -->
```
### Color Contrast
- Text/background ratio ≥ 4.5:1
- Interactive elements clearly visible
- Focus indicators visible
## Performance
### Target Metrics
| Metric | Target |
|--------|--------|
| Total file size | < 10MB |
| Time to interactive | < 2s |
| Search latency | < 100ms |
### Optimization Strategies
1. **Lazy loading for images**: `loading="lazy"`
2. **Efficient search**: In-memory index, no external requests
3. **CSS containment**: Scope styles to components
4. **Minimal JavaScript**: Vanilla JS, no libraries
## CSS Variables
### Light Theme
```css
:root {
--bg-primary: #ffffff;
--bg-secondary: #f8f9fa;
--text-primary: #212529;
--text-secondary: #495057;
--accent-color: #0d6efd;
--border-color: #dee2e6;
}
```
### Dark Theme
```css
[data-theme="dark"] {
--bg-primary: #1a1a2e;
--bg-secondary: #16213e;
--text-primary: #eaeaea;
--text-secondary: #b8b8b8;
--accent-color: #4dabf7;
--border-color: #2d3748;
}
```
## Markdown to HTML Mapping
| Markdown | HTML |
|----------|------|
| `# Heading` | `<h1>` |
| `## Heading` | `<h2>` |
| `**bold**` | `<strong>` |
| `*italic*` | `<em>` |
| `` `code` `` | `<code>` |
| `[link](url)` | `<a href="url">` |
| `- item` | `<ul><li>` |
| `1. item` | `<ol><li>` |
| ``` ```js ``` | `<pre><code class="language-js">` |
| `> quote` | `<blockquote>` |
| `---` | `<hr>` |
## Screenshot Embedding
### Marker Format
```markdown
<!-- SCREENSHOT: id="ss-login" url="/login" description="Login form" -->
```
### Embedded Format
```html
<figure class="screenshot">
<img src="data:image/png;base64,{{BASE64_DATA}}"
alt="Login form"
loading="lazy">
<figcaption>Login form</figcaption>
</figure>
```
### Placeholder (if missing)
```html
<div class="screenshot-placeholder">
[Screenshot: ss-login - Login form]
</div>
```
## File Size Optimization
### CSS
- Minify before embedding
- Remove unused styles
- Combine duplicate rules
### JavaScript
- Minify before embedding
- Remove console.log statements
- Use IIFE for scoping
### Images
- Compress before Base64 encoding
- Use appropriate dimensions (max 1280px width)
- Consider WebP format if browser support is acceptable
## Validation
### HTML Validation
- W3C HTML5 compliance
- Proper nesting
- Required attributes present
### CSS Validation
- Valid property values
- No deprecated properties
- Vendor prefixes where needed
### JavaScript
- No syntax errors
- All functions defined
- Error handling for edge cases
## Testing Checklist
- [ ] Opens in Chrome/Firefox/Safari/Edge
- [ ] Search works correctly
- [ ] Collapse/expand works
- [ ] Tag filtering works
- [ ] Theme toggle works
- [ ] Print preview correct
- [ ] Keyboard navigation works
- [ ] Mobile responsive
- [ ] Offline functionality
- [ ] All links valid
- [ ] All images display
- [ ] No console errors

View File

@@ -0,0 +1,253 @@
# Quality Standards
Quality gates and standards for software manual generation.
## Quality Dimensions
### 1. Completeness (25%)
All required sections present and adequately covered.
| Requirement | Weight | Criteria |
|-------------|--------|----------|
| Overview section | 5 | Product intro, features, quick start |
| UI Guide | 5 | All major screens documented |
| API Reference | 5 | All public APIs documented |
| Configuration | 4 | All config options explained |
| Troubleshooting | 3 | Common issues addressed |
| Examples | 3 | Multi-level examples provided |
**Scoring**:
- 100%: All sections present with adequate depth
- 80%: All sections present, some lacking depth
- 60%: Missing 1-2 sections
- 40%: Missing 3+ sections
- 0%: Critical sections missing (overview, UI guide)
### 2. Consistency (25%)
Terminology, style, and structure uniform across sections.
| Aspect | Check |
|--------|-------|
| Terminology | Same term for same concept throughout |
| Formatting | Consistent heading levels, code block styles |
| Tone | Consistent formality level |
| Cross-references | All internal links valid |
| Screenshot naming | Follow `ss-{feature}-{action}` pattern |
**Scoring**:
- 100%: Zero inconsistencies
- 80%: 1-3 minor inconsistencies
- 60%: 4-6 inconsistencies
- 40%: 7-10 inconsistencies
- 0%: Pervasive inconsistencies
### 3. Depth (25%)
Content provides sufficient detail for target audience.
| Level | Criteria |
|-------|----------|
| Shallow | Basic descriptions only |
| Standard | Descriptions + usage examples |
| Deep | Descriptions + examples + edge cases + best practices |
**Per-Section Depth Check**:
- [ ] Explains "what" (definition)
- [ ] Explains "why" (rationale)
- [ ] Explains "how" (procedure)
- [ ] Provides examples
- [ ] Covers edge cases
- [ ] Includes tips/best practices
**Scoring**:
- 100%: Deep coverage on all critical sections
- 80%: Standard coverage on all sections
- 60%: Shallow coverage on some sections
- 40%: Missing depth in critical areas
- 0%: Superficial throughout
### 4. Readability (25%)
Clear, user-friendly writing that's easy to follow.
| Metric | Target |
|--------|--------|
| Sentence length | Average < 20 words |
| Paragraph length | Average < 5 sentences |
| Heading hierarchy | Proper H1 > H2 > H3 nesting |
| Code blocks | Language specified |
| Lists | Used for 3+ items |
| Screenshots | Placed near relevant text |
**Structural Elements**:
- [ ] Clear section headers
- [ ] Numbered steps for procedures
- [ ] Bullet lists for options/features
- [ ] Tables for comparisons
- [ ] Code blocks with syntax highlighting
- [ ] Screenshots with captions
**Scoring**:
- 100%: All readability criteria met
- 80%: Minor structural issues
- 60%: Some sections hard to follow
- 40%: Significant readability problems
- 0%: Unclear, poorly structured
## Overall Quality Score
```
Overall = (Completeness × 0.25) + (Consistency × 0.25) +
(Depth × 0.25) + (Readability × 0.25)
```
**Quality Gates**:
| Gate | Threshold | Action |
|------|-----------|--------|
| Pass | ≥ 80% | Proceed to HTML generation |
| Review | 60-79% | Address warnings, proceed with caution |
| Fail | < 60% | Must address errors before continuing |
## Issue Classification
### Errors (Must Fix)
- Missing required sections
- Invalid cross-references
- Broken screenshot markers
- Code blocks without language
- Incomplete procedures (missing steps)
### Warnings (Should Fix)
- Terminology inconsistencies
- Sections lacking depth
- Missing examples
- Long paragraphs (> 7 sentences)
- Screenshots missing captions
### Info (Nice to Have)
- Optimization suggestions
- Additional example opportunities
- Alternative explanations
- Enhancement ideas
## Quality Checklist
### Pre-Generation
- [ ] All agents completed successfully
- [ ] No errors in consolidation report
- [ ] Overall score ≥ 60%
### Post-Generation
- [ ] HTML renders correctly
- [ ] Search returns relevant results
- [ ] All screenshots display
- [ ] Theme toggle works
- [ ] Print preview looks good
### Final Review
- [ ] User previewed and approved
- [ ] File size reasonable (< 10MB)
- [ ] No console errors in browser
- [ ] Accessible (keyboard navigation works)
## Automated Checks
```javascript
function runQualityChecks(workDir) {
const results = {
completeness: checkCompleteness(workDir),
consistency: checkConsistency(workDir),
depth: checkDepth(workDir),
readability: checkReadability(workDir)
};
results.overall = (
results.completeness * 0.25 +
results.consistency * 0.25 +
results.depth * 0.25 +
results.readability * 0.25
);
return results;
}
function checkCompleteness(workDir) {
const requiredSections = [
'section-overview.md',
'section-ui-guide.md',
'section-api-reference.md',
'section-configuration.md',
'section-troubleshooting.md',
'section-examples.md'
];
const existing = Glob(`${workDir}/sections/section-*.md`);
const found = requiredSections.filter(s =>
existing.some(e => e.endsWith(s))
);
return (found.length / requiredSections.length) * 100;
}
function checkConsistency(workDir) {
// Check terminology, cross-references, naming conventions
const issues = [];
// ... implementation ...
return Math.max(0, 100 - issues.length * 10);
}
function checkDepth(workDir) {
// Check content length, examples, edge cases
const sections = Glob(`${workDir}/sections/section-*.md`);
let totalScore = 0;
for (const section of sections) {
const content = Read(section);
let sectionScore = 0;
if (content.length > 500) sectionScore += 20;
if (content.includes('```')) sectionScore += 20;
if (content.includes('Example')) sectionScore += 20;
if (content.match(/\d+\./g)?.length > 3) sectionScore += 20;
if (content.includes('Note:') || content.includes('Tip:')) sectionScore += 20;
totalScore += sectionScore;
}
return totalScore / sections.length;
}
function checkReadability(workDir) {
// Check structure, formatting, organization
const sections = Glob(`${workDir}/sections/section-*.md`);
let issues = 0;
for (const section of sections) {
const content = Read(section);
// Check heading hierarchy
if (!content.startsWith('# ')) issues++;
// Check code block languages
const codeBlocks = content.match(/```\w*/g);
if (codeBlocks?.some(b => b === '```')) issues++;
// Check paragraph length
const paragraphs = content.split('\n\n');
if (paragraphs.some(p => p.split('. ').length > 7)) issues++;
}
return Math.max(0, 100 - issues * 10);
}
```

View File

@@ -0,0 +1,298 @@
# Writing Style Guide
User-friendly writing standards for software manuals.
## Core Principles
### 1. User-Centered
Write for the user, not the developer.
**Do**:
- "Click the **Save** button to save your changes"
- "Enter your email address in the login form"
**Don't**:
- "The onClick handler triggers the save mutation"
- "POST to /api/auth/login with email in body"
### 2. Action-Oriented
Focus on what users can **do**, not what the system does.
**Do**:
- "You can export your data as CSV"
- "To create a new project, click **New Project**"
**Don't**:
- "The system exports data in CSV format"
- "A new project is created when the button is clicked"
### 3. Clear and Direct
Use simple, straightforward language.
**Do**:
- "Select a file to upload"
- "The maximum file size is 10MB"
**Don't**:
- "Utilize the file selection interface to designate a file for uploading"
- "File size constraints limit uploads to 10 megabytes"
## Tone
### Friendly but Professional
- Conversational but not casual
- Helpful but not condescending
- Confident but not arrogant
**Examples**:
| Too Casual | Just Right | Too Formal |
|------------|------------|------------|
| "Yo, here's how..." | "Here's how to..." | "The following procedure describes..." |
| "Easy peasy!" | "That's all you need to do." | "The procedure has been completed." |
| "Don't worry about it" | "You don't need to change this" | "This parameter does not require modification" |
### Second Person
Address the user directly as "you".
**Do**: "You can customize your dashboard..."
**Don't**: "Users can customize their dashboards..."
## Structure
### Headings
Use clear, descriptive headings that tell users what they'll learn.
**Good Headings**:
- "Getting Started"
- "Creating Your First Project"
- "Configuring Email Notifications"
- "Troubleshooting Login Issues"
**Weak Headings**:
- "Overview"
- "Step 1"
- "Settings"
- "FAQ"
### Procedures
Number steps for sequential tasks.
```markdown
## Creating a New User
1. Navigate to **Settings** > **Users**.
2. Click the **Add User** button.
3. Enter the user's email address.
4. Select a role from the dropdown.
5. Click **Save**.
The new user will receive an invitation email.
```
### Features/Options
Use bullet lists for non-sequential items.
```markdown
## Export Options
You can export your data in several formats:
- **CSV**: Compatible with spreadsheets
- **JSON**: Best for developers
- **PDF**: Ideal for sharing reports
```
### Comparisons
Use tables for comparing options.
```markdown
## Plan Comparison
| Feature | Free | Pro | Enterprise |
|---------|------|-----|------------|
| Projects | 3 | Unlimited | Unlimited |
| Storage | 1GB | 10GB | 100GB |
| Support | Community | Email | Dedicated |
```
## Content Types
### Conceptual (What Is)
Explain what something is and why it matters.
```markdown
## What is a Workspace?
A workspace is a container for your projects and team members. Each workspace
has its own settings, billing, and permissions. You might create separate
workspaces for different clients or departments.
```
### Procedural (How To)
Step-by-step instructions for completing a task.
```markdown
## How to Create a Workspace
1. Click your profile icon in the top-right corner.
2. Select **Create Workspace**.
3. Enter a name for your workspace.
4. Choose a plan (you can upgrade later).
5. Click **Create**.
Your new workspace is ready to use.
```
### Reference (API/Config)
Detailed specifications and parameters.
```markdown
## Configuration Options
### `DATABASE_URL`
- **Type**: String (required)
- **Format**: `postgresql://user:password@host:port/database`
- **Example**: `postgresql://admin:secret@localhost:5432/myapp`
Database connection string for PostgreSQL.
```
## Formatting
### Bold
Use for:
- UI elements: Click **Save**
- First use of key terms: **Workspaces** contain projects
- Emphasis: **Never** share your API key
### Italic
Use for:
- Introducing new terms: A *workspace* is...
- Placeholders: Replace *your-api-key* with...
- Emphasis (sparingly): This is *really* important
### Code
Use for:
- Commands: Run `npm install`
- File paths: Edit `config/settings.json`
- Environment variables: Set `DATABASE_URL`
- API endpoints: POST `/api/users`
- Code references: The `handleSubmit` function
### Code Blocks
Always specify the language.
```javascript
// Example: Fetching user data
const response = await fetch('/api/user');
const user = await response.json();
```
### Notes and Warnings
Use for important callouts.
```markdown
> **Note**: This feature requires a Pro plan.
> **Warning**: Deleting a workspace cannot be undone.
> **Tip**: Use keyboard shortcuts to work faster.
```
## Screenshots
### When to Include
- First time showing a UI element
- Complex interfaces
- Before/after comparisons
- Error states
### Guidelines
- Capture just the relevant area
- Use consistent dimensions
- Highlight important elements
- Add descriptive captions
```markdown
<!-- SCREENSHOT: id="ss-dashboard" description="Main dashboard showing project list" -->
*The dashboard displays all your projects with their status.*
```
## Examples
### Good Section Example
```markdown
## Inviting Team Members
You can invite colleagues to collaborate on your projects.
### To invite a team member:
1. Open **Settings** > **Team**.
2. Click **Invite Member**.
3. Enter their email address.
4. Select their role:
- **Admin**: Full access to all settings
- **Editor**: Can edit projects
- **Viewer**: Read-only access
5. Click **Send Invite**.
The person will receive an email with a link to join your workspace.
> **Note**: You can have up to 5 team members on the Free plan.
<!-- SCREENSHOT: id="ss-invite-team" description="Team invitation dialog" -->
```
## Language Guidelines
### Avoid Jargon
| Technical | User-Friendly |
|-----------|---------------|
| Execute | Run |
| Terminate | Stop, End |
| Instantiate | Create |
| Invoke | Call, Use |
| Parameterize | Set, Configure |
| Persist | Save |
### Be Specific
| Vague | Specific |
|-------|----------|
| "Click the button" | "Click **Save**" |
| "Enter information" | "Enter your email address" |
| "An error occurred" | "Your password must be at least 8 characters" |
| "It takes a moment" | "This typically takes 2-3 seconds" |
### Use Active Voice
| Passive | Active |
|---------|--------|
| "The file is uploaded" | "Upload the file" |
| "Settings are saved" | "Click **Save** to keep your changes" |
| "Errors are displayed" | "The form shows any errors" |

View File

@@ -0,0 +1,984 @@
/* ========================================
Docsify-Style Documentation CSS
Software Manual Skill - Modern Theme
======================================== */
/* ========== CSS Variables ========== */
:root {
/* Light Theme - Teal Accent */
--bg-color: #ffffff;
--bg-secondary: #f8fafc;
--bg-tertiary: #f1f5f9;
--text-color: #1e293b;
--text-secondary: #64748b;
--text-muted: #94a3b8;
--border-color: #e2e8f0;
--accent-color: #14b8a6;
--accent-hover: #0d9488;
--accent-light: rgba(20, 184, 166, 0.1);
--link-color: #14b8a6;
--sidebar-bg: #ffffff;
--sidebar-width: 280px;
--code-bg: #1e293b;
--code-color: #e2e8f0;
--shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
--shadow-md: 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1);
--shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1);
/* Callout Colors */
--tip-bg: rgba(20, 184, 166, 0.08);
--tip-border: #14b8a6;
--warning-bg: rgba(245, 158, 11, 0.08);
--warning-border: #f59e0b;
--danger-bg: rgba(239, 68, 68, 0.08);
--danger-border: #ef4444;
--info-bg: rgba(59, 130, 246, 0.08);
--info-border: #3b82f6;
/* Typography */
--font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Noto Sans SC', sans-serif;
--font-mono: 'JetBrains Mono', 'Fira Code', 'SF Mono', Monaco, Consolas, monospace;
--font-size-xs: 0.75rem;
--font-size-sm: 0.875rem;
--font-size-base: 1rem;
--font-size-lg: 1.125rem;
--line-height: 1.75;
/* Spacing */
--space-xs: 0.25rem;
--space-sm: 0.5rem;
--space-md: 1rem;
--space-lg: 1.5rem;
--space-xl: 2rem;
--space-2xl: 3rem;
/* Border Radius */
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 12px;
/* Transitions */
--transition: 0.2s ease;
--transition-slow: 0.3s ease;
}
/* Dark Theme */
[data-theme="dark"] {
--bg-color: #0f172a;
--bg-secondary: #1e293b;
--bg-tertiary: #334155;
--text-color: #f1f5f9;
--text-secondary: #94a3b8;
--text-muted: #64748b;
--border-color: #334155;
--sidebar-bg: #1e293b;
--code-bg: #0f172a;
--code-color: #e2e8f0;
--tip-bg: rgba(20, 184, 166, 0.15);
--warning-bg: rgba(245, 158, 11, 0.15);
--danger-bg: rgba(239, 68, 68, 0.15);
--info-bg: rgba(59, 130, 246, 0.15);
}
/* ========== Reset ========== */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html, body {
height: 100%;
}
body {
font-family: var(--font-family);
font-size: var(--font-size-base);
line-height: var(--line-height);
color: var(--text-color);
background-color: var(--bg-color);
-webkit-font-smoothing: antialiased;
}
/* ========== Layout ========== */
.docsify-container {
display: flex;
min-height: 100vh;
}
/* ========== Sidebar ========== */
.sidebar {
position: fixed;
top: 0;
left: 0;
width: var(--sidebar-width);
height: 100vh;
background: var(--sidebar-bg);
border-right: 1px solid var(--border-color);
display: flex;
flex-direction: column;
z-index: 100;
transition: transform var(--transition);
}
.sidebar-header {
padding: var(--space-lg);
border-bottom: 1px solid var(--border-color);
}
.logo {
display: flex;
align-items: center;
gap: var(--space-sm);
}
.logo-icon {
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, var(--accent-color), #3eaf7c);
border-radius: 8px;
color: #fff;
font-weight: bold;
font-size: 1.25rem;
}
.logo-text h1 {
font-size: var(--font-size-base);
font-weight: 600;
color: var(--text-color);
margin: 0;
line-height: 1.2;
}
.logo-text .version {
font-size: var(--font-size-sm);
color: var(--text-muted);
}
/* ========== Search ========== */
.sidebar-search {
padding: var(--space-md);
position: relative;
}
.search-box {
position: relative;
display: flex;
align-items: center;
}
.search-icon {
position: absolute;
left: 10px;
color: var(--text-muted);
pointer-events: none;
}
.search-box input {
width: 100%;
padding: 10px 60px 10px 36px;
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
font-size: var(--font-size-sm);
background: var(--bg-secondary);
color: var(--text-color);
transition: all var(--transition);
}
.search-box input:focus {
outline: none;
border-color: var(--accent-color);
box-shadow: 0 0 0 3px var(--accent-light);
background: var(--bg-color);
}
.search-box input::placeholder {
color: var(--text-muted);
}
/* Keyboard shortcut hint */
.search-box::after {
content: 'Ctrl K';
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
font-size: var(--font-size-xs);
color: var(--text-muted);
background: var(--bg-color);
padding: 2px 6px;
border-radius: var(--radius-sm);
border: 1px solid var(--border-color);
font-family: var(--font-mono);
pointer-events: none;
}
.search-results {
position: absolute;
top: 100%;
left: var(--space-md);
right: var(--space-md);
background: var(--bg-color);
border: 1px solid var(--border-color);
border-radius: 8px;
box-shadow: var(--shadow-lg);
max-height: 400px;
overflow-y: auto;
opacity: 0;
visibility: hidden;
transform: translateY(-4px);
transition: all var(--transition);
z-index: 200;
}
.search-results.visible {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.search-result-item {
display: block;
padding: var(--space-sm) var(--space-md);
text-decoration: none;
color: var(--text-color);
border-bottom: 1px solid var(--border-color);
transition: background var(--transition);
}
.search-result-item:last-child {
border-bottom: none;
}
.search-result-item:hover {
background: var(--bg-secondary);
}
.result-title {
font-weight: 600;
font-size: var(--font-size-sm);
margin-bottom: 2px;
}
.result-excerpt {
font-size: 0.8rem;
color: var(--text-secondary);
line-height: 1.4;
}
.result-excerpt mark {
background: var(--accent-light);
color: var(--accent-color);
padding: 1px 4px;
border-radius: var(--radius-sm);
font-weight: 500;
}
.no-results {
padding: var(--space-md);
text-align: center;
color: var(--text-muted);
font-size: var(--font-size-sm);
}
/* ========== Sidebar Navigation ========== */
.sidebar-nav {
flex: 1;
overflow-y: auto;
padding: var(--space-md) 0;
}
.nav-group {
margin-bottom: var(--space-xs);
}
.nav-group-header {
display: flex;
align-items: center;
padding: var(--space-sm) var(--space-md);
cursor: pointer;
user-select: none;
transition: background var(--transition);
}
.nav-group-header:hover {
background: var(--bg-secondary);
}
.nav-group-toggle {
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
margin-right: var(--space-xs);
background: none;
border: none;
color: var(--text-muted);
cursor: pointer;
transition: transform var(--transition);
}
.nav-group-toggle svg {
width: 12px;
height: 12px;
}
.nav-group.expanded .nav-group-toggle {
transform: rotate(90deg);
}
.nav-group-title {
font-size: var(--font-size-sm);
font-weight: 600;
color: var(--text-color);
}
.nav-group-items {
display: none;
padding-left: var(--space-lg);
}
.nav-group.expanded .nav-group-items {
display: block;
}
.nav-item {
display: block;
padding: 8px var(--space-md) 8px calc(var(--space-md) + 4px);
font-size: var(--font-size-sm);
color: var(--text-secondary);
text-decoration: none;
border-left: 2px solid transparent;
margin: 2px 8px 2px 0;
border-radius: 0 var(--radius-md) var(--radius-md) 0;
transition: all var(--transition);
cursor: pointer;
}
.nav-item:hover {
color: var(--text-color);
background: var(--bg-secondary);
}
.nav-item.active {
color: var(--accent-color);
border-left-color: var(--accent-color);
background: var(--accent-light);
font-weight: 500;
}
/* Top-level nav items (no group) */
.nav-item.top-level {
padding-left: var(--space-md);
border-left: none;
margin: 2px 8px;
border-radius: var(--radius-md);
}
.nav-item.top-level.active {
background: var(--accent-light);
}
/* ========== Main Content ========== */
.main-content {
flex: 1;
margin-left: var(--sidebar-width);
min-height: 100vh;
overflow-y: auto;
display: flex;
flex-direction: column;
}
.mobile-header {
display: none;
position: sticky;
top: 0;
padding: var(--space-sm) var(--space-md);
background: var(--bg-color);
border-bottom: 1px solid var(--border-color);
z-index: 50;
align-items: center;
gap: var(--space-sm);
}
.sidebar-toggle {
background: none;
border: none;
padding: var(--space-xs);
color: var(--text-color);
cursor: pointer;
border-radius: 4px;
transition: background var(--transition);
}
.sidebar-toggle:hover {
background: var(--bg-secondary);
}
.current-section {
flex: 1;
font-weight: 600;
font-size: var(--font-size-sm);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.theme-toggle-mobile {
background: none;
border: none;
padding: var(--space-xs);
font-size: 1.25rem;
cursor: pointer;
}
/* ========== Content Sections ========== */
.content-wrapper {
flex: 1;
max-width: 860px;
margin: 0 auto;
padding: var(--space-2xl) var(--space-xl);
width: 100%;
}
.content-section {
display: none;
animation: fadeIn 0.3s ease;
}
.content-section.active {
display: block;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
/* ========== Content Typography ========== */
.content-section h1 {
font-size: 2rem;
font-weight: 700;
margin-bottom: var(--space-lg);
padding-bottom: var(--space-md);
border-bottom: 1px solid var(--border-color);
}
.content-section h2 {
font-size: 1.5rem;
font-weight: 600;
margin-top: var(--space-2xl);
margin-bottom: var(--space-md);
padding-bottom: var(--space-sm);
border-bottom: 1px solid var(--border-color);
}
.content-section h3 {
font-size: 1.25rem;
font-weight: 600;
margin-top: var(--space-xl);
margin-bottom: var(--space-sm);
}
.content-section h4 {
font-size: 1.1rem;
font-weight: 600;
margin-top: var(--space-lg);
margin-bottom: var(--space-sm);
}
.content-section p {
margin-bottom: var(--space-md);
}
.content-section a {
color: var(--link-color);
text-decoration: none;
}
.content-section a:hover {
text-decoration: underline;
}
/* Lists */
.content-section ul,
.content-section ol {
margin: var(--space-md) 0;
padding-left: var(--space-xl);
}
.content-section li {
margin-bottom: var(--space-sm);
}
.content-section li::marker {
color: var(--accent-color);
}
/* Inline Code */
.content-section code {
font-family: var(--font-mono);
font-size: 0.85em;
padding: 3px 8px;
background: var(--bg-tertiary);
color: var(--accent-color);
border-radius: var(--radius-sm);
font-weight: 500;
}
/* Code Blocks */
.code-block-wrapper {
position: relative;
margin: var(--space-lg) 0;
border-radius: var(--radius-lg);
overflow: hidden;
box-shadow: var(--shadow-md);
}
.content-section pre {
margin: 0;
padding: var(--space-lg);
padding-top: calc(var(--space-lg) + 40px);
background: var(--code-bg);
overflow-x: auto;
border-radius: var(--radius-lg);
}
.content-section pre code {
display: block;
padding: 0;
background: none;
color: var(--code-color);
font-size: var(--font-size-sm);
line-height: 1.7;
}
/* Code Block Header */
.code-block-wrapper::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 40px;
background: rgba(255,255,255,0.03);
border-bottom: 1px solid rgba(255,255,255,0.05);
}
/* Code Block Actions */
.code-block-actions {
position: absolute;
top: 8px;
right: 12px;
display: flex;
gap: 8px;
z-index: 10;
}
.copy-code-btn {
position: absolute;
top: 8px;
right: 12px;
padding: 6px 12px;
background: rgba(255,255,255,0.08);
border: 1px solid rgba(255,255,255,0.1);
border-radius: var(--radius-md);
color: var(--code-color);
cursor: pointer;
opacity: 0;
transition: all var(--transition);
display: flex;
align-items: center;
gap: 6px;
font-size: var(--font-size-xs);
font-family: var(--font-family);
}
.code-block-wrapper:hover .copy-code-btn {
opacity: 1;
}
.copy-code-btn:hover {
background: rgba(255,255,255,0.15);
border-color: rgba(255,255,255,0.2);
}
.copy-code-btn.copied {
background: var(--accent-color);
border-color: var(--accent-color);
color: #fff;
}
/* Code syntax colors */
.content-section pre .keyword { color: #c678dd; }
.content-section pre .string { color: #98c379; }
.content-section pre .number { color: #d19a66; }
.content-section pre .comment { color: #5c6370; font-style: italic; }
.content-section pre .function { color: #61afef; }
.content-section pre .operator { color: #56b6c2; }
/* Tables */
.content-section table {
width: 100%;
margin: var(--space-lg) 0;
border-collapse: collapse;
font-size: var(--font-size-sm);
border-radius: var(--radius-md);
overflow: hidden;
box-shadow: var(--shadow-sm);
}
.content-section th {
padding: var(--space-md);
background: var(--accent-color);
color: #fff;
font-weight: 600;
text-align: left;
font-size: var(--font-size-sm);
letter-spacing: 0.02em;
}
.content-section th:first-child {
border-top-left-radius: var(--radius-md);
}
.content-section th:last-child {
border-top-right-radius: var(--radius-md);
}
.content-section td {
padding: var(--space-sm) var(--space-md);
border-bottom: 1px solid var(--border-color);
vertical-align: top;
}
.content-section tbody tr:nth-child(even) {
background: var(--bg-secondary);
}
.content-section tbody tr:hover {
background: var(--accent-light);
}
.content-section tbody tr:last-child td {
border-bottom: none;
}
.content-section tbody tr:last-child td:first-child {
border-bottom-left-radius: var(--radius-md);
}
.content-section tbody tr:last-child td:last-child {
border-bottom-right-radius: var(--radius-md);
}
/* Blockquote / Callouts */
.content-section blockquote {
position: relative;
margin: var(--space-lg) 0;
padding: var(--space-md) var(--space-lg);
padding-left: calc(var(--space-lg) + 32px);
background: var(--tip-bg);
border: 1px solid var(--tip-border);
border-radius: var(--radius-lg);
}
.content-section blockquote::before {
content: '💡';
position: absolute;
left: var(--space-md);
top: var(--space-md);
font-size: 1.25rem;
line-height: 1;
}
.content-section blockquote p:last-child {
margin-bottom: 0;
}
.content-section blockquote p:first-child {
font-weight: 500;
color: var(--text-color);
}
/* Warning callout */
.content-section blockquote.warning,
.content-section blockquote:has(strong:first-child:contains("警告")),
.content-section blockquote:has(strong:first-child:contains("Warning")) {
background: var(--warning-bg);
border-color: var(--warning-border);
}
.content-section blockquote.warning::before {
content: '⚠️';
}
/* Danger callout */
.content-section blockquote.danger,
.content-section blockquote:has(strong:first-child:contains("危险")),
.content-section blockquote:has(strong:first-child:contains("Danger")) {
background: var(--danger-bg);
border-color: var(--danger-border);
}
.content-section blockquote.danger::before {
content: '🚨';
}
/* Info callout */
.content-section blockquote.info,
.content-section blockquote:has(strong:first-child:contains("注意")),
.content-section blockquote:has(strong:first-child:contains("Note")) {
background: var(--info-bg);
border-color: var(--info-border);
}
.content-section blockquote.info::before {
content: '';
}
/* Images */
.content-section img {
max-width: 100%;
height: auto;
border-radius: 8px;
box-shadow: var(--shadow-md);
margin: var(--space-md) 0;
}
.screenshot-placeholder {
padding: var(--space-xl);
background: var(--bg-secondary);
border: 2px dashed var(--border-color);
border-radius: 8px;
text-align: center;
color: var(--text-muted);
margin: var(--space-md) 0;
}
/* ========== Footer ========== */
.main-footer {
padding: var(--space-lg);
text-align: center;
color: var(--text-muted);
font-size: var(--font-size-sm);
border-top: 1px solid var(--border-color);
margin-top: auto;
}
/* ========== Theme Toggle (Desktop) ========== */
.theme-toggle {
position: fixed;
bottom: var(--space-lg);
right: var(--space-lg);
width: 44px;
height: 44px;
border-radius: 50%;
border: 1px solid var(--border-color);
background: var(--bg-color);
box-shadow: var(--shadow-md);
cursor: pointer;
font-size: 1.25rem;
z-index: 100;
transition: transform var(--transition);
}
.theme-toggle:hover {
transform: scale(1.1);
}
[data-theme="light"] .moon-icon { display: inline; }
[data-theme="light"] .sun-icon { display: none; }
[data-theme="dark"] .moon-icon { display: none; }
[data-theme="dark"] .sun-icon { display: inline; }
/* ========== Back to Top ========== */
.back-to-top {
position: fixed;
bottom: calc(var(--space-lg) + 56px);
right: var(--space-lg);
width: 40px;
height: 40px;
border-radius: 50%;
border: 1px solid var(--border-color);
background: var(--bg-color);
box-shadow: var(--shadow-md);
color: var(--text-secondary);
cursor: pointer;
opacity: 0;
visibility: hidden;
transition: all var(--transition);
z-index: 100;
display: flex;
align-items: center;
justify-content: center;
}
.back-to-top.visible {
opacity: 1;
visibility: visible;
}
.back-to-top:hover {
color: var(--accent-color);
border-color: var(--accent-color);
}
/* ========== Responsive ========== */
@media (max-width: 960px) {
.sidebar {
transform: translateX(-100%);
}
.sidebar.open {
transform: translateX(0);
box-shadow: var(--shadow-lg);
}
.main-content {
margin-left: 0;
}
.mobile-header {
display: flex;
}
.content-wrapper {
padding: var(--space-lg);
}
.theme-toggle {
display: none;
}
}
@media (max-width: 640px) {
.content-section h1 {
font-size: 1.5rem;
}
.content-section h2 {
font-size: 1.25rem;
}
.content-wrapper {
padding: var(--space-md);
}
}
/* ========== Print Styles ========== */
@media print {
.sidebar,
.mobile-header,
.theme-toggle,
.back-to-top,
.copy-code-btn {
display: none !important;
}
.main-content {
margin-left: 0;
}
.content-section {
display: block !important;
page-break-after: always;
}
.content-section pre {
background: #f5f5f5 !important;
color: #333 !important;
}
}
/* ========== Pygments Syntax Highlighting (One Dark Theme) ========== */
/* Generated for CodeHilite extension */
.highlight { background: #282c34; border-radius: 8px; padding: 1em; overflow-x: auto; margin: var(--spacing-md) 0; }
.highlight pre { margin: 0; background: transparent; padding: 0; }
.highlight code { background: transparent; border: none; padding: 0; color: #abb2bf; font-size: var(--font-size-sm); }
/* Pygments Token Colors - One Dark Theme */
.highlight .hll { background-color: #3e4451; }
.highlight .c { color: #5c6370; font-style: italic; } /* Comment */
.highlight .err { color: #e06c75; } /* Error */
.highlight .k { color: #c678dd; } /* Keyword */
.highlight .l { color: #98c379; } /* Literal */
.highlight .n { color: #abb2bf; } /* Name */
.highlight .o { color: #56b6c2; } /* Operator */
.highlight .p { color: #abb2bf; } /* Punctuation */
.highlight .ch { color: #5c6370; font-style: italic; } /* Comment.Hashbang */
.highlight .cm { color: #5c6370; font-style: italic; } /* Comment.Multiline */
.highlight .cp { color: #5c6370; font-style: italic; } /* Comment.Preproc */
.highlight .cpf { color: #5c6370; font-style: italic; } /* Comment.PreprocFile */
.highlight .c1 { color: #5c6370; font-style: italic; } /* Comment.Single */
.highlight .cs { color: #5c6370; font-style: italic; } /* Comment.Special */
.highlight .gd { color: #e06c75; } /* Generic.Deleted */
.highlight .ge { font-style: italic; } /* Generic.Emph */
.highlight .gh { color: #abb2bf; font-weight: bold; } /* Generic.Heading */
.highlight .gi { color: #98c379; } /* Generic.Inserted */
.highlight .go { color: #5c6370; } /* Generic.Output */
.highlight .gp { color: #5c6370; } /* Generic.Prompt */
.highlight .gs { font-weight: bold; } /* Generic.Strong */
.highlight .gu { color: #56b6c2; font-weight: bold; } /* Generic.Subheading */
.highlight .gt { color: #e06c75; } /* Generic.Traceback */
.highlight .kc { color: #c678dd; } /* Keyword.Constant */
.highlight .kd { color: #c678dd; } /* Keyword.Declaration */
.highlight .kn { color: #c678dd; } /* Keyword.Namespace */
.highlight .kp { color: #c678dd; } /* Keyword.Pseudo */
.highlight .kr { color: #c678dd; } /* Keyword.Reserved */
.highlight .kt { color: #e5c07b; } /* Keyword.Type */
.highlight .ld { color: #98c379; } /* Literal.Date */
.highlight .m { color: #d19a66; } /* Literal.Number */
.highlight .s { color: #98c379; } /* Literal.String */
.highlight .na { color: #d19a66; } /* Name.Attribute */
.highlight .nb { color: #e5c07b; } /* Name.Builtin */
.highlight .nc { color: #e5c07b; } /* Name.Class */
.highlight .no { color: #d19a66; } /* Name.Constant */
.highlight .nd { color: #e5c07b; } /* Name.Decorator */
.highlight .ni { color: #abb2bf; } /* Name.Entity */
.highlight .ne { color: #e06c75; } /* Name.Exception */
.highlight .nf { color: #61afef; } /* Name.Function */
.highlight .nl { color: #abb2bf; } /* Name.Label */
.highlight .nn { color: #e5c07b; } /* Name.Namespace */
.highlight .nx { color: #abb2bf; } /* Name.Other */
.highlight .py { color: #abb2bf; } /* Name.Property */
.highlight .nt { color: #e06c75; } /* Name.Tag */
.highlight .nv { color: #e06c75; } /* Name.Variable */
.highlight .ow { color: #56b6c2; } /* Operator.Word */
.highlight .w { color: #abb2bf; } /* Text.Whitespace */
.highlight .mb { color: #d19a66; } /* Literal.Number.Bin */
.highlight .mf { color: #d19a66; } /* Literal.Number.Float */
.highlight .mh { color: #d19a66; } /* Literal.Number.Hex */
.highlight .mi { color: #d19a66; } /* Literal.Number.Integer */
.highlight .mo { color: #d19a66; } /* Literal.Number.Oct */
.highlight .sa { color: #98c379; } /* Literal.String.Affix */
.highlight .sb { color: #98c379; } /* Literal.String.Backtick */
.highlight .sc { color: #98c379; } /* Literal.String.Char */
.highlight .dl { color: #98c379; } /* Literal.String.Delimiter */
.highlight .sd { color: #98c379; } /* Literal.String.Doc */
.highlight .s2 { color: #98c379; } /* Literal.String.Double */
.highlight .se { color: #d19a66; } /* Literal.String.Escape */
.highlight .sh { color: #98c379; } /* Literal.String.Heredoc */
.highlight .si { color: #98c379; } /* Literal.String.Interpol */
.highlight .sx { color: #98c379; } /* Literal.String.Other */
.highlight .sr { color: #56b6c2; } /* Literal.String.Regex */
.highlight .s1 { color: #98c379; } /* Literal.String.Single */
.highlight .ss { color: #56b6c2; } /* Literal.String.Symbol */
.highlight .bp { color: #e5c07b; } /* Name.Builtin.Pseudo */
.highlight .fm { color: #61afef; } /* Name.Function.Magic */
.highlight .vc { color: #e06c75; } /* Name.Variable.Class */
.highlight .vg { color: #e06c75; } /* Name.Variable.Global */
.highlight .vi { color: #e06c75; } /* Name.Variable.Instance */
.highlight .vm { color: #e06c75; } /* Name.Variable.Magic */
.highlight .il { color: #d19a66; } /* Literal.Number.Integer.Long */
/* Dark theme override for highlight */
[data-theme="dark"] .highlight {
background: #1e2128;
border: 1px solid #3d4450;
}

View File

@@ -0,0 +1,788 @@
/* ========================================
TiddlyWiki-Style Base CSS
Software Manual Skill
======================================== */
/* ========== CSS Variables ========== */
:root {
/* Light Theme */
--bg-primary: #ffffff;
--bg-secondary: #f8f9fa;
--bg-tertiary: #e9ecef;
--text-primary: #212529;
--text-secondary: #495057;
--text-muted: #6c757d;
--border-color: #dee2e6;
--accent-color: #0d6efd;
--accent-hover: #0b5ed7;
--success-color: #198754;
--warning-color: #ffc107;
--danger-color: #dc3545;
--info-color: #0dcaf0;
/* Layout */
--sidebar-width: 280px;
--header-height: 60px;
--content-max-width: 900px;
--spacing-xs: 0.25rem;
--spacing-sm: 0.5rem;
--spacing-md: 1rem;
--spacing-lg: 1.5rem;
--spacing-xl: 2rem;
/* Typography */
--font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
--font-family-mono: 'SF Mono', Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
--font-size-sm: 0.875rem;
--font-size-base: 1rem;
--font-size-lg: 1.125rem;
--font-size-xl: 1.25rem;
--line-height: 1.6;
/* Shadows */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
/* Transitions */
--transition-fast: 150ms ease;
--transition-base: 300ms ease;
}
/* ========== Reset & Base ========== */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
scroll-behavior: smooth;
}
body {
font-family: var(--font-family);
font-size: var(--font-size-base);
line-height: var(--line-height);
color: var(--text-primary);
background-color: var(--bg-secondary);
}
/* ========== Layout ========== */
.wiki-container {
display: flex;
min-height: 100vh;
}
/* ========== Sidebar ========== */
.wiki-sidebar {
position: fixed;
top: 0;
left: 0;
width: var(--sidebar-width);
height: 100vh;
background-color: var(--bg-primary);
border-right: 1px solid var(--border-color);
overflow-y: auto;
z-index: 100;
display: flex;
flex-direction: column;
transition: transform var(--transition-base);
}
/* Logo Area */
.wiki-logo {
padding: var(--spacing-lg);
text-align: center;
border-bottom: 1px solid var(--border-color);
}
.wiki-logo .logo-placeholder {
width: 60px;
height: 60px;
margin: 0 auto var(--spacing-sm);
background: linear-gradient(135deg, var(--accent-color), var(--info-color));
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
font-size: var(--font-size-xl);
}
.wiki-logo h1 {
font-size: var(--font-size-lg);
font-weight: 600;
margin-bottom: var(--spacing-xs);
}
.wiki-logo .version {
font-size: var(--font-size-sm);
color: var(--text-muted);
}
/* Search */
.wiki-search {
padding: var(--spacing-md);
position: relative;
}
.wiki-search input {
width: 100%;
padding: var(--spacing-sm) var(--spacing-md);
border: 1px solid var(--border-color);
border-radius: 6px;
font-size: var(--font-size-sm);
background-color: var(--bg-secondary);
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
}
.wiki-search input:focus {
outline: none;
border-color: var(--accent-color);
box-shadow: 0 0 0 3px rgba(13, 110, 253, 0.15);
}
.search-results {
position: absolute;
top: 100%;
left: var(--spacing-md);
right: var(--spacing-md);
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 6px;
box-shadow: var(--shadow-lg);
max-height: 400px;
overflow-y: auto;
z-index: 200;
}
.search-result-item {
display: block;
padding: var(--spacing-sm) var(--spacing-md);
text-decoration: none;
color: var(--text-primary);
border-bottom: 1px solid var(--border-color);
transition: background-color var(--transition-fast);
}
.search-result-item:last-child {
border-bottom: none;
}
.search-result-item:hover {
background-color: var(--bg-secondary);
}
.result-title {
font-weight: 600;
margin-bottom: var(--spacing-xs);
}
.result-excerpt {
font-size: var(--font-size-sm);
color: var(--text-secondary);
}
.result-excerpt mark {
background-color: var(--warning-color);
padding: 0 2px;
border-radius: 2px;
}
.no-results {
padding: var(--spacing-md);
text-align: center;
color: var(--text-muted);
}
/* Tags */
.wiki-tags {
padding: var(--spacing-md);
display: flex;
flex-wrap: wrap;
gap: var(--spacing-xs);
border-bottom: 1px solid var(--border-color);
}
.wiki-tags .tag {
padding: var(--spacing-xs) var(--spacing-sm);
font-size: var(--font-size-sm);
border: 1px solid var(--border-color);
border-radius: 20px;
background: var(--bg-secondary);
color: var(--text-secondary);
cursor: pointer;
transition: all var(--transition-fast);
}
.wiki-tags .tag:hover {
border-color: var(--accent-color);
color: var(--accent-color);
}
.wiki-tags .tag.active {
background-color: var(--accent-color);
border-color: var(--accent-color);
color: white;
}
/* Table of Contents */
.wiki-toc {
flex: 1;
padding: var(--spacing-md);
overflow-y: auto;
}
.wiki-toc h3 {
font-size: var(--font-size-sm);
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-muted);
margin-bottom: var(--spacing-md);
}
.wiki-toc ul {
list-style: none;
}
.wiki-toc li {
margin-bottom: var(--spacing-xs);
}
.wiki-toc a {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--spacing-sm);
color: var(--text-secondary);
text-decoration: none;
border-radius: 6px;
font-size: var(--font-size-sm);
transition: all var(--transition-fast);
}
.wiki-toc a:hover {
background-color: var(--bg-secondary);
color: var(--accent-color);
}
/* ========== Main Content ========== */
.wiki-content {
flex: 1;
margin-left: var(--sidebar-width);
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* Header */
.content-header {
position: sticky;
top: 0;
background-color: var(--bg-primary);
border-bottom: 1px solid var(--border-color);
padding: var(--spacing-sm) var(--spacing-lg);
display: flex;
align-items: center;
justify-content: space-between;
z-index: 50;
}
.sidebar-toggle {
display: none;
flex-direction: column;
gap: 4px;
padding: var(--spacing-sm);
background: none;
border: none;
cursor: pointer;
}
.sidebar-toggle span {
display: block;
width: 20px;
height: 2px;
background-color: var(--text-primary);
transition: transform var(--transition-fast);
}
.header-actions {
display: flex;
gap: var(--spacing-sm);
}
.header-actions button {
padding: var(--spacing-xs) var(--spacing-sm);
font-size: var(--font-size-sm);
border: 1px solid var(--border-color);
border-radius: 4px;
background: var(--bg-primary);
color: var(--text-secondary);
cursor: pointer;
transition: all var(--transition-fast);
}
.header-actions button:hover {
border-color: var(--accent-color);
color: var(--accent-color);
}
/* Tiddler Container */
.tiddler-container {
flex: 1;
max-width: var(--content-max-width);
margin: 0 auto;
padding: var(--spacing-lg);
width: 100%;
}
/* ========== Tiddler (Content Block) ========== */
.tiddler {
background-color: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 8px;
margin-bottom: var(--spacing-lg);
box-shadow: var(--shadow-sm);
transition: box-shadow var(--transition-fast);
}
.tiddler:hover {
box-shadow: var(--shadow-md);
}
.tiddler-header {
padding: var(--spacing-md) var(--spacing-lg);
border-bottom: 1px solid var(--border-color);
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: var(--spacing-sm);
}
.tiddler-title {
display: flex;
align-items: center;
gap: var(--spacing-sm);
font-size: var(--font-size-xl);
font-weight: 600;
margin: 0;
}
.collapse-toggle {
background: none;
border: none;
font-size: var(--font-size-sm);
color: var(--text-muted);
cursor: pointer;
padding: var(--spacing-xs);
transition: transform var(--transition-fast);
}
.tiddler.collapsed .collapse-toggle {
transform: rotate(-90deg);
}
.tiddler-meta {
display: flex;
gap: var(--spacing-sm);
flex-wrap: wrap;
}
.difficulty-badge {
padding: var(--spacing-xs) var(--spacing-sm);
font-size: 0.75rem;
font-weight: 500;
border-radius: 4px;
text-transform: uppercase;
}
.difficulty-badge.beginner {
background-color: #d1fae5;
color: #065f46;
}
.difficulty-badge.intermediate {
background-color: #fef3c7;
color: #92400e;
}
.difficulty-badge.advanced {
background-color: #fee2e2;
color: #991b1b;
}
.tag-badge {
padding: var(--spacing-xs) var(--spacing-sm);
font-size: 0.75rem;
background-color: var(--bg-tertiary);
color: var(--text-secondary);
border-radius: 4px;
}
.tiddler-content {
padding: var(--spacing-lg);
}
.tiddler.collapsed .tiddler-content {
display: none;
}
/* ========== Content Typography ========== */
.tiddler-content h1,
.tiddler-content h2,
.tiddler-content h3,
.tiddler-content h4 {
margin-top: var(--spacing-lg);
margin-bottom: var(--spacing-md);
font-weight: 600;
}
.tiddler-content h1 { font-size: 1.75rem; }
.tiddler-content h2 { font-size: 1.5rem; }
.tiddler-content h3 { font-size: 1.25rem; }
.tiddler-content h4 { font-size: 1.125rem; }
.tiddler-content p {
margin-bottom: var(--spacing-md);
}
/* Lists - Enhanced Styling */
.tiddler-content ul,
.tiddler-content ol {
margin: var(--spacing-md) 0;
padding-left: var(--spacing-xl);
}
.tiddler-content ul {
list-style: none;
}
.tiddler-content ul > li {
position: relative;
margin-bottom: var(--spacing-sm);
padding-left: 8px;
}
.tiddler-content ul > li::before {
content: "•";
position: absolute;
left: -16px;
color: var(--accent-color);
font-weight: bold;
}
.tiddler-content ol {
list-style: none;
counter-reset: item;
}
.tiddler-content ol > li {
position: relative;
margin-bottom: var(--spacing-sm);
padding-left: 8px;
counter-increment: item;
}
.tiddler-content ol > li::before {
content: counter(item) ".";
position: absolute;
left: -24px;
color: var(--accent-color);
font-weight: 600;
}
/* Nested lists */
.tiddler-content ul ul,
.tiddler-content ol ol,
.tiddler-content ul ol,
.tiddler-content ol ul {
margin: var(--spacing-xs) 0;
}
.tiddler-content ul ul > li::before {
content: "◦";
}
.tiddler-content ul ul ul > li::before {
content: "▪";
}
.tiddler-content a {
color: var(--accent-color);
text-decoration: none;
}
.tiddler-content a:hover {
text-decoration: underline;
}
/* Inline Code - Red Highlight */
.tiddler-content code {
font-family: var(--font-family-mono);
font-size: 0.875em;
padding: 2px 6px;
background-color: #fff5f5;
color: #c92a2a;
border-radius: 4px;
border: 1px solid #ffc9c9;
}
/* Code Blocks - Dark Background */
.tiddler-content pre {
position: relative;
margin: var(--spacing-md) 0;
padding: 0;
background-color: #1e2128;
border-radius: 8px;
overflow: hidden;
border: 1px solid #3d4450;
}
.tiddler-content pre::before {
content: attr(data-language);
display: block;
padding: 8px 16px;
background-color: #2d333b;
color: #8b949e;
font-size: 0.75rem;
font-family: var(--font-family);
text-transform: uppercase;
letter-spacing: 0.05em;
border-bottom: 1px solid #3d4450;
}
.tiddler-content pre code {
display: block;
padding: var(--spacing-md);
background: none;
color: #e6edf3;
font-size: var(--font-size-sm);
line-height: 1.6;
overflow-x: auto;
border: none;
}
.copy-code-btn {
position: absolute;
top: 6px;
right: 12px;
padding: 4px 10px;
font-size: 0.7rem;
background-color: #3d4450;
color: #8b949e;
border: 1px solid #4d5566;
border-radius: 4px;
cursor: pointer;
opacity: 0;
transition: all var(--transition-fast);
}
.copy-code-btn:hover {
background-color: #4d5566;
color: #e6edf3;
}
.tiddler-content pre:hover .copy-code-btn {
opacity: 1;
}
/* Tables - Blue Header Style */
.tiddler-content table {
width: 100%;
margin: var(--spacing-md) 0;
border-collapse: collapse;
border: 1px solid #dee2e6;
border-radius: 8px;
overflow: hidden;
}
.tiddler-content th {
padding: 12px 16px;
background: linear-gradient(135deg, #1971c2, #228be6);
color: white;
font-weight: 600;
text-align: left;
border: none;
border-bottom: 2px solid #1864ab;
}
.tiddler-content td {
padding: 10px 16px;
border: 1px solid #e9ecef;
text-align: left;
}
.tiddler-content tbody tr:nth-child(odd) {
background-color: #f8f9fa;
}
.tiddler-content tbody tr:nth-child(even) {
background-color: #ffffff;
}
.tiddler-content tbody tr:hover {
background-color: #e7f5ff;
}
/* Screenshots */
.screenshot {
margin: var(--spacing-lg) 0;
text-align: center;
}
.screenshot img {
max-width: 100%;
border: 1px solid var(--border-color);
border-radius: 8px;
box-shadow: var(--shadow-md);
}
.screenshot figcaption {
margin-top: var(--spacing-sm);
font-size: var(--font-size-sm);
color: var(--text-muted);
font-style: italic;
}
.screenshot-placeholder {
padding: var(--spacing-xl);
background-color: var(--bg-tertiary);
border: 2px dashed var(--border-color);
border-radius: 8px;
color: var(--text-muted);
text-align: center;
}
/* ========== Footer ========== */
.wiki-footer {
padding: var(--spacing-lg);
text-align: center;
color: var(--text-muted);
font-size: var(--font-size-sm);
border-top: 1px solid var(--border-color);
background-color: var(--bg-primary);
}
/* ========== Theme Toggle ========== */
.theme-toggle {
position: fixed;
bottom: var(--spacing-lg);
right: var(--spacing-lg);
width: 48px;
height: 48px;
border-radius: 50%;
border: none;
background-color: var(--bg-primary);
box-shadow: var(--shadow-lg);
cursor: pointer;
font-size: 1.5rem;
z-index: 100;
transition: transform var(--transition-fast);
}
.theme-toggle:hover {
transform: scale(1.1);
}
[data-theme="light"] .moon-icon { display: inline; }
[data-theme="light"] .sun-icon { display: none; }
[data-theme="dark"] .moon-icon { display: none; }
[data-theme="dark"] .sun-icon { display: inline; }
/* ========== Back to Top ========== */
.back-to-top {
position: fixed;
bottom: calc(var(--spacing-lg) + 60px);
right: var(--spacing-lg);
width: 40px;
height: 40px;
border-radius: 50%;
border: none;
background-color: var(--accent-color);
color: white;
font-size: 1.25rem;
cursor: pointer;
opacity: 0;
visibility: hidden;
transition: all var(--transition-fast);
z-index: 100;
}
.back-to-top.visible {
opacity: 1;
visibility: visible;
}
.back-to-top:hover {
background-color: var(--accent-hover);
}
/* ========== Responsive ========== */
@media (max-width: 1024px) {
.wiki-sidebar {
transform: translateX(-100%);
}
.wiki-sidebar.open {
transform: translateX(0);
}
.wiki-content {
margin-left: 0;
}
.sidebar-toggle {
display: flex;
}
}
@media (max-width: 640px) {
.tiddler-header {
flex-direction: column;
align-items: flex-start;
}
.header-actions {
display: none;
}
.wiki-tags {
overflow-x: auto;
flex-wrap: nowrap;
padding-bottom: var(--spacing-md);
}
}
/* ========== Print Styles ========== */
@media print {
.wiki-sidebar,
.theme-toggle,
.back-to-top,
.content-header,
.collapse-toggle,
.copy-code-btn {
display: none !important;
}
.wiki-content {
margin-left: 0;
}
.tiddler {
break-inside: avoid;
box-shadow: none;
border: 1px solid #ccc;
}
.tiddler.collapsed .tiddler-content {
display: block;
}
.tiddler-content pre {
background-color: #f5f5f5 !important;
color: #333 !important;
}
}

View File

@@ -0,0 +1,278 @@
/* ========================================
TiddlyWiki-Style Dark Theme
Software Manual Skill
======================================== */
[data-theme="dark"] {
/* Dark Theme Colors */
--bg-primary: #1a1a2e;
--bg-secondary: #16213e;
--bg-tertiary: #0f3460;
--text-primary: #eaeaea;
--text-secondary: #b8b8b8;
--text-muted: #888888;
--border-color: #2d3748;
--accent-color: #4dabf7;
--accent-hover: #339af0;
--success-color: #51cf66;
--warning-color: #ffd43b;
--danger-color: #ff6b6b;
--info-color: #22b8cf;
/* Shadows */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.4);
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.5);
}
/* Dark theme specific overrides */
[data-theme="dark"] .wiki-logo .logo-placeholder {
background: linear-gradient(135deg, var(--accent-color), #6741d9);
}
[data-theme="dark"] .wiki-search input {
background-color: var(--bg-tertiary);
border-color: var(--border-color);
color: var(--text-primary);
}
[data-theme="dark"] .wiki-search input::placeholder {
color: var(--text-muted);
}
[data-theme="dark"] .search-results {
background-color: var(--bg-secondary);
border-color: var(--border-color);
}
[data-theme="dark"] .search-result-item {
border-color: var(--border-color);
}
[data-theme="dark"] .search-result-item:hover {
background-color: var(--bg-tertiary);
}
[data-theme="dark"] .result-excerpt mark {
background-color: rgba(255, 212, 59, 0.3);
color: var(--warning-color);
}
[data-theme="dark"] .wiki-tags .tag {
background-color: var(--bg-tertiary);
border-color: var(--border-color);
color: var(--text-secondary);
}
[data-theme="dark"] .wiki-tags .tag:hover {
border-color: var(--accent-color);
color: var(--accent-color);
}
[data-theme="dark"] .wiki-tags .tag.active {
background-color: var(--accent-color);
border-color: var(--accent-color);
color: #1a1a2e;
}
[data-theme="dark"] .wiki-toc a:hover {
background-color: var(--bg-tertiary);
}
[data-theme="dark"] .content-header {
background-color: var(--bg-primary);
border-color: var(--border-color);
}
[data-theme="dark"] .sidebar-toggle span {
background-color: var(--text-primary);
}
[data-theme="dark"] .header-actions button {
background-color: var(--bg-secondary);
border-color: var(--border-color);
color: var(--text-secondary);
}
[data-theme="dark"] .header-actions button:hover {
border-color: var(--accent-color);
color: var(--accent-color);
}
[data-theme="dark"] .tiddler {
background-color: var(--bg-primary);
border-color: var(--border-color);
}
[data-theme="dark"] .tiddler-header {
border-color: var(--border-color);
}
[data-theme="dark"] .difficulty-badge.beginner {
background-color: rgba(81, 207, 102, 0.2);
color: var(--success-color);
}
[data-theme="dark"] .difficulty-badge.intermediate {
background-color: rgba(255, 212, 59, 0.2);
color: var(--warning-color);
}
[data-theme="dark"] .difficulty-badge.advanced {
background-color: rgba(255, 107, 107, 0.2);
color: var(--danger-color);
}
[data-theme="dark"] .tag-badge {
background-color: var(--bg-tertiary);
color: var(--text-secondary);
}
[data-theme="dark"] .tiddler-content code {
background-color: var(--bg-tertiary);
color: var(--accent-color);
}
[data-theme="dark"] .tiddler-content pre {
background-color: #0d1117;
border: 1px solid var(--border-color);
}
[data-theme="dark"] .tiddler-content pre code {
color: #e6e6e6;
}
[data-theme="dark"] .copy-code-btn {
background-color: var(--bg-tertiary);
color: var(--text-secondary);
}
[data-theme="dark"] .tiddler-content th {
background-color: var(--bg-tertiary);
}
[data-theme="dark"] .tiddler-content tr:nth-child(even) {
background-color: var(--bg-secondary);
}
[data-theme="dark"] .tiddler-content th,
[data-theme="dark"] .tiddler-content td {
border-color: var(--border-color);
}
[data-theme="dark"] .screenshot img {
border-color: var(--border-color);
}
[data-theme="dark"] .screenshot-placeholder {
background-color: var(--bg-tertiary);
border-color: var(--border-color);
}
[data-theme="dark"] .wiki-footer {
background-color: var(--bg-primary);
border-color: var(--border-color);
}
[data-theme="dark"] .theme-toggle {
background-color: var(--bg-secondary);
color: var(--warning-color);
}
[data-theme="dark"] .back-to-top {
background-color: var(--accent-color);
}
[data-theme="dark"] .back-to-top:hover {
background-color: var(--accent-hover);
}
/* Scrollbar styling for dark theme */
[data-theme="dark"] ::-webkit-scrollbar {
width: 8px;
height: 8px;
}
[data-theme="dark"] ::-webkit-scrollbar-track {
background: var(--bg-secondary);
}
[data-theme="dark"] ::-webkit-scrollbar-thumb {
background: var(--bg-tertiary);
border-radius: 4px;
}
[data-theme="dark"] ::-webkit-scrollbar-thumb:hover {
background: var(--border-color);
}
/* Selection color */
[data-theme="dark"] ::selection {
background-color: rgba(77, 171, 247, 0.3);
color: var(--text-primary);
}
/* Focus styles for accessibility */
[data-theme="dark"] :focus {
outline-color: var(--accent-color);
}
[data-theme="dark"] .wiki-search input:focus {
border-color: var(--accent-color);
box-shadow: 0 0 0 3px rgba(77, 171, 247, 0.2);
}
/* Link colors */
[data-theme="dark"] .tiddler-content a {
color: var(--accent-color);
}
[data-theme="dark"] .tiddler-content a:hover {
color: var(--accent-hover);
}
/* Blockquote styling */
[data-theme="dark"] .tiddler-content blockquote {
border-left: 4px solid var(--accent-color);
background-color: var(--bg-tertiary);
padding: var(--spacing-md);
margin: var(--spacing-md) 0;
color: var(--text-secondary);
}
/* Horizontal rule */
[data-theme="dark"] .tiddler-content hr {
border: none;
border-top: 1px solid var(--border-color);
margin: var(--spacing-lg) 0;
}
/* Alert/Note boxes */
[data-theme="dark"] .note,
[data-theme="dark"] .warning,
[data-theme="dark"] .tip,
[data-theme="dark"] .danger {
padding: var(--spacing-md);
border-radius: 6px;
margin: var(--spacing-md) 0;
}
[data-theme="dark"] .note {
background-color: rgba(34, 184, 207, 0.1);
border-left: 4px solid var(--info-color);
}
[data-theme="dark"] .warning {
background-color: rgba(255, 212, 59, 0.1);
border-left: 4px solid var(--warning-color);
}
[data-theme="dark"] .tip {
background-color: rgba(81, 207, 102, 0.1);
border-left: 4px solid var(--success-color);
}
[data-theme="dark"] .danger {
background-color: rgba(255, 107, 107, 0.1);
border-left: 4px solid var(--danger-color);
}

View File

@@ -0,0 +1,466 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="{{SOFTWARE_NAME}} - Interactive Software Manual">
<meta name="generator" content="software-manual-skill">
<title>{{SOFTWARE_NAME}} v{{VERSION}} - User Manual</title>
<style>
{{EMBEDDED_CSS}}
</style>
</head>
<body class="docsify-container" data-theme="light">
<!-- Sidebar Navigation -->
<aside class="sidebar" id="sidebar">
<!-- Logo and Title -->
<div class="sidebar-header">
<div class="logo">
<span class="logo-icon">{{LOGO_ICON}}</span>
<div class="logo-text">
<h1>{{SOFTWARE_NAME}}</h1>
<span class="version">v{{VERSION}}</span>
</div>
</div>
</div>
<!-- Search Box -->
<div class="sidebar-search">
<div class="search-box">
<svg class="search-icon" viewBox="0 0 24 24" width="16" height="16">
<circle cx="11" cy="11" r="8" fill="none" stroke="currentColor" stroke-width="2"/>
<path d="M21 21l-4.35-4.35" fill="none" stroke="currentColor" stroke-width="2"/>
</svg>
<input type="text" id="searchInput" placeholder="搜索文档..." aria-label="Search">
</div>
<div id="searchResults" class="search-results"></div>
</div>
<!-- Hierarchical Navigation -->
<nav class="sidebar-nav" id="sidebarNav">
{{SIDEBAR_NAV_HTML}}
</nav>
</aside>
<!-- Main Content Area -->
<main class="main-content" id="mainContent">
<!-- Mobile Header -->
<header class="mobile-header">
<button class="sidebar-toggle" id="sidebarToggle" aria-label="Toggle sidebar">
<svg viewBox="0 0 24 24" width="24" height="24">
<path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z" fill="currentColor"/>
</svg>
</button>
<span class="current-section" id="currentSection">{{SOFTWARE_NAME}}</span>
<button class="theme-toggle-mobile" id="themeToggleMobile" aria-label="Toggle theme">
<span class="sun-icon">&#9728;</span>
<span class="moon-icon">&#9790;</span>
</button>
</header>
<!-- Content Sections (only one visible at a time) -->
<div class="content-wrapper">
{{SECTIONS_HTML}}
</div>
<!-- Footer -->
<footer class="main-footer">
<p>Generated by <strong>software-manual-skill</strong> | Last updated: {{TIMESTAMP}}</p>
</footer>
</main>
<!-- Theme Toggle (Desktop) -->
<button class="theme-toggle" id="themeToggle" aria-label="Toggle theme">
<span class="sun-icon">&#9728;</span>
<span class="moon-icon">&#9790;</span>
</button>
<!-- Back to Top -->
<button class="back-to-top" id="backToTop" aria-label="Back to top">
<svg viewBox="0 0 24 24" width="20" height="20">
<path d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z" fill="currentColor"/>
</svg>
</button>
<!-- Search Index Data -->
<script id="search-index" type="application/json">
{{SEARCH_INDEX_JSON}}
</script>
<!-- Navigation Structure Data -->
<script id="nav-structure" type="application/json">
{{NAV_STRUCTURE_JSON}}
</script>
<!-- Mermaid.js for diagram rendering -->
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
<script>
mermaid.initialize({
startOnLoad: false,
theme: document.body.dataset.theme === 'dark' ? 'dark' : 'default',
securityLevel: 'loose'
});
</script>
<!-- Embedded JavaScript -->
<script>
(function() {
'use strict';
// ========== State Management ==========
let currentSectionId = null;
const sections = document.querySelectorAll('.content-section');
const navItems = document.querySelectorAll('.nav-item');
// ========== Section Navigation ==========
function showSection(sectionId) {
// Hide all sections
sections.forEach(s => s.classList.remove('active'));
// Show target section
const target = document.getElementById('section-' + sectionId);
if (target) {
target.classList.add('active');
currentSectionId = sectionId;
// Update URL hash
history.pushState(null, '', '#/' + sectionId);
// Update nav active state
navItems.forEach(item => {
item.classList.remove('active');
if (item.dataset.section === sectionId) {
item.classList.add('active');
// Expand parent groups
expandParentGroups(item);
}
});
// Update mobile header
const currentSectionEl = document.getElementById('currentSection');
if (currentSectionEl && target.dataset.title) {
currentSectionEl.textContent = target.dataset.title;
}
// Scroll to top
document.getElementById('mainContent').scrollTop = 0;
}
}
function expandParentGroups(item) {
let parent = item.parentElement;
while (parent) {
if (parent.classList.contains('nav-group')) {
parent.classList.add('expanded');
const toggle = parent.querySelector('.nav-group-toggle');
if (toggle) toggle.setAttribute('aria-expanded', 'true');
}
parent = parent.parentElement;
}
}
// ========== Navigation Click Handlers ==========
navItems.forEach(item => {
item.addEventListener('click', function(e) {
e.preventDefault();
const sectionId = this.dataset.section;
if (sectionId) {
showSection(sectionId);
// Close sidebar on mobile
document.getElementById('sidebar').classList.remove('open');
}
});
});
// ========== Navigation Group Toggle ==========
document.querySelectorAll('.nav-group-toggle').forEach(toggle => {
toggle.addEventListener('click', function(e) {
e.stopPropagation();
const group = this.closest('.nav-group');
group.classList.toggle('expanded');
this.setAttribute('aria-expanded', group.classList.contains('expanded'));
});
});
// ========== Search Functionality ==========
const indexData = JSON.parse(document.getElementById('search-index').textContent);
const searchInput = document.getElementById('searchInput');
const searchResults = document.getElementById('searchResults');
function searchDocs(query) {
if (!query || query.length < 2) return [];
const results = [];
const lowerQuery = query.toLowerCase();
for (const [id, content] of Object.entries(indexData)) {
let score = 0;
const titleLower = content.title.toLowerCase();
const bodyLower = content.body.toLowerCase();
if (titleLower.includes(lowerQuery)) score += 10;
if (bodyLower.includes(lowerQuery)) score += 5;
if (score > 0) {
results.push({
id,
title: content.title,
excerpt: getExcerpt(content.body, query),
score
});
}
}
return results.sort((a, b) => b.score - a.score).slice(0, 8);
}
function getExcerpt(text, query) {
const maxLength = 120;
const lowerText = text.toLowerCase();
const lowerQuery = query.toLowerCase();
const index = lowerText.indexOf(lowerQuery);
if (index === -1) {
return text.substring(0, maxLength) + (text.length > maxLength ? '...' : '');
}
const start = Math.max(0, index - 30);
const end = Math.min(text.length, index + query.length + 60);
let excerpt = text.substring(start, end);
if (start > 0) excerpt = '...' + excerpt;
if (end < text.length) excerpt += '...';
const regex = new RegExp('(' + query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ')', 'gi');
return excerpt.replace(regex, '<mark>$1</mark>');
}
searchInput.addEventListener('input', function() {
const query = this.value.trim();
const results = searchDocs(query);
if (results.length === 0) {
searchResults.innerHTML = query.length >= 2
? '<div class="no-results">未找到结果</div>'
: '';
searchResults.classList.toggle('visible', query.length >= 2);
return;
}
searchResults.innerHTML = results.map(r => `
<a href="#/${r.id}" class="search-result-item" data-section="${r.id}">
<div class="result-title">${r.title}</div>
<div class="result-excerpt">${r.excerpt}</div>
</a>
`).join('');
searchResults.classList.add('visible');
});
searchResults.addEventListener('click', function(e) {
const item = e.target.closest('.search-result-item');
if (item) {
e.preventDefault();
searchInput.value = '';
searchResults.innerHTML = '';
searchResults.classList.remove('visible');
showSection(item.dataset.section);
}
});
// Close search results when clicking outside
document.addEventListener('click', function(e) {
if (!e.target.closest('.sidebar-search')) {
searchResults.classList.remove('visible');
}
});
// ========== Theme Toggle ==========
function setTheme(theme) {
document.body.dataset.theme = theme;
localStorage.setItem('docs-theme', theme);
}
const savedTheme = localStorage.getItem('docs-theme') || 'light';
setTheme(savedTheme);
document.getElementById('themeToggle').addEventListener('click', function() {
setTheme(document.body.dataset.theme === 'dark' ? 'light' : 'dark');
});
document.getElementById('themeToggleMobile').addEventListener('click', function() {
setTheme(document.body.dataset.theme === 'dark' ? 'light' : 'dark');
});
// ========== Sidebar Toggle (Mobile) ==========
document.getElementById('sidebarToggle').addEventListener('click', function() {
document.getElementById('sidebar').classList.toggle('open');
});
// Close sidebar when clicking outside on mobile
document.addEventListener('click', function(e) {
const sidebar = document.getElementById('sidebar');
const toggle = document.getElementById('sidebarToggle');
if (!sidebar.contains(e.target) && !toggle.contains(e.target)) {
sidebar.classList.remove('open');
}
});
// ========== Back to Top ==========
const backToTop = document.getElementById('backToTop');
const mainContent = document.getElementById('mainContent');
mainContent.addEventListener('scroll', function() {
backToTop.classList.toggle('visible', this.scrollTop > 300);
});
backToTop.addEventListener('click', function() {
mainContent.scrollTo({ top: 0, behavior: 'smooth' });
});
// ========== Code Block Copy ==========
document.querySelectorAll('pre').forEach(pre => {
const wrapper = document.createElement('div');
wrapper.className = 'code-block-wrapper';
pre.parentNode.insertBefore(wrapper, pre);
wrapper.appendChild(pre);
const copyBtn = document.createElement('button');
copyBtn.className = 'copy-code-btn';
copyBtn.innerHTML = '<svg viewBox="0 0 24 24" width="16" height="16"><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z" fill="currentColor"/></svg>';
copyBtn.addEventListener('click', function() {
const code = pre.querySelector('code') || pre;
navigator.clipboard.writeText(code.textContent).then(() => {
copyBtn.innerHTML = '<svg viewBox="0 0 24 24" width="16" height="16"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" fill="currentColor"/></svg>';
setTimeout(() => {
copyBtn.innerHTML = '<svg viewBox="0 0 24 24" width="16" height="16"><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z" fill="currentColor"/></svg>';
}, 2000);
});
});
wrapper.appendChild(copyBtn);
});
// ========== Mermaid Diagram Rendering ==========
function renderMermaidDiagrams() {
// Find all mermaid code blocks and convert them to diagrams
document.querySelectorAll('pre code.language-mermaid, pre code.highlight-mermaid').forEach((codeBlock, index) => {
const pre = codeBlock.parentElement;
const wrapper = pre.parentElement;
const code = codeBlock.textContent;
// Create mermaid container
const mermaidDiv = document.createElement('div');
mermaidDiv.className = 'mermaid';
mermaidDiv.textContent = code;
// Replace code block with mermaid div
if (wrapper && wrapper.classList.contains('code-block-wrapper')) {
wrapper.parentElement.replaceChild(mermaidDiv, wrapper);
} else {
pre.parentElement.replaceChild(mermaidDiv, pre);
}
});
// Also handle codehilite blocks with mermaid
document.querySelectorAll('.highlight').forEach((block) => {
const code = block.querySelector('code, pre');
if (code && code.textContent.trim().startsWith('graph ') ||
code && code.textContent.trim().startsWith('sequenceDiagram') ||
code && code.textContent.trim().startsWith('flowchart ') ||
code && code.textContent.trim().startsWith('classDiagram') ||
code && code.textContent.trim().startsWith('stateDiagram') ||
code && code.textContent.trim().startsWith('erDiagram') ||
code && code.textContent.trim().startsWith('gantt') ||
code && code.textContent.trim().startsWith('pie') ||
code && code.textContent.trim().startsWith('journey')) {
const mermaidDiv = document.createElement('div');
mermaidDiv.className = 'mermaid';
mermaidDiv.textContent = code.textContent;
block.parentElement.replaceChild(mermaidDiv, block);
}
});
// Render all mermaid diagrams
if (typeof mermaid !== 'undefined') {
mermaid.run();
}
}
// ========== Internal Anchor Links Handler ==========
// Handle clicks on internal anchor links (TOC links like #材料管理api)
document.addEventListener('click', function(e) {
const link = e.target.closest('a[href^="#"]');
if (!link) return;
const href = link.getAttribute('href');
// Skip section navigation links (handled by nav-item)
if (link.classList.contains('nav-item')) return;
// Skip search result links
if (link.classList.contains('search-result-item')) return;
// Check if it's an internal anchor (not a section link)
if (href && href.startsWith('#') && !href.startsWith('#/')) {
e.preventDefault();
const anchorId = href.substring(1);
const targetElement = document.getElementById(anchorId);
if (targetElement) {
// Scroll to the anchor within current section
targetElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
// Update URL without triggering popstate
history.pushState(null, '', '#/' + currentSectionId + '/' + anchorId);
}
}
});
// ========== Hash Parser ==========
function parseHash(hash) {
// Handle formats: #/sectionId, #/sectionId/anchorId, #anchorId
if (!hash || hash === '#' || hash === '#/') return { section: null, anchor: null };
if (hash.startsWith('#/')) {
const parts = hash.substring(2).split('/');
return { section: parts[0] || null, anchor: parts[1] || null };
} else {
// Plain anchor like #材料管理api - stay on current section
return { section: null, anchor: hash.substring(1) };
}
}
// ========== Initial Load ==========
// Check URL hash or show first section
const initialHash = parseHash(window.location.hash);
if (initialHash.section && document.getElementById('section-' + initialHash.section)) {
showSection(initialHash.section);
// Scroll to anchor if present
if (initialHash.anchor) {
setTimeout(() => {
const anchor = document.getElementById(initialHash.anchor);
if (anchor) anchor.scrollIntoView({ behavior: 'smooth', block: 'start' });
}, 100);
}
} else if (sections.length > 0) {
const firstSection = sections[0].id.replace('section-', '');
showSection(firstSection);
}
// Render mermaid diagrams after initial load
setTimeout(renderMermaidDiagrams, 100);
// Handle browser back/forward
window.addEventListener('popstate', function() {
const parsed = parseHash(window.location.hash);
if (parsed.section) {
showSection(parsed.section);
if (parsed.anchor) {
setTimeout(() => {
const anchor = document.getElementById(parsed.anchor);
if (anchor) anchor.scrollIntoView({ behavior: 'smooth', block: 'start' });
}, 100);
}
}
});
})();
</script>
</body>
</html>

View File

@@ -0,0 +1,327 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="{{SOFTWARE_NAME}} - Interactive Software Manual">
<meta name="generator" content="software-manual-skill">
<title>{{SOFTWARE_NAME}} v{{VERSION}} - User Manual</title>
<style>
{{EMBEDDED_CSS}}
</style>
</head>
<body class="wiki-container" data-theme="light">
<!-- Sidebar Navigation -->
<aside class="wiki-sidebar">
<!-- Logo and Title -->
<div class="wiki-logo">
<div class="logo-placeholder">{{SOFTWARE_NAME}}</div>
<h1>{{SOFTWARE_NAME}}</h1>
<span class="version">v{{VERSION}}</span>
</div>
<!-- Search Box -->
<div class="wiki-search">
<input type="text" id="searchInput" placeholder="Search documentation..." aria-label="Search">
<div id="searchResults" class="search-results" aria-live="polite"></div>
</div>
<!-- Tag Navigation (Dynamic) -->
<nav class="wiki-tags" aria-label="Filter by category">
<button class="tag active" data-tag="all">全部</button>
{{TAG_BUTTONS_HTML}}
</nav>
<!-- Table of Contents -->
{{TOC_HTML}}
</aside>
<!-- Main Content Area -->
<main class="wiki-content">
<!-- Header Bar -->
<header class="content-header">
<button class="sidebar-toggle" id="sidebarToggle" aria-label="Toggle sidebar">
<span></span>
<span></span>
<span></span>
</button>
<div class="header-actions">
<button class="expand-all" id="expandAll">Expand All</button>
<button class="collapse-all" id="collapseAll">Collapse All</button>
<button class="print-btn" id="printBtn">Print</button>
</div>
</header>
<!-- Tiddler Container -->
<div class="tiddler-container">
{{TIDDLERS_HTML}}
</div>
<!-- Footer -->
<footer class="wiki-footer">
<p>Generated by <strong>software-manual-skill</strong></p>
<p>Last updated: <time datetime="{{TIMESTAMP}}">{{TIMESTAMP}}</time></p>
</footer>
</main>
<!-- Theme Toggle Button -->
<button class="theme-toggle" id="themeToggle" aria-label="Toggle theme">
<span class="sun-icon">&#9728;</span>
<span class="moon-icon">&#9790;</span>
</button>
<!-- Back to Top Button -->
<button class="back-to-top" id="backToTop" aria-label="Back to top">&#8593;</button>
<!-- Search Index Data -->
<script id="search-index" type="application/json">
{{SEARCH_INDEX_JSON}}
</script>
<!-- Embedded JavaScript -->
<script>
(function() {
'use strict';
// ========== Search Functionality ==========
class WikiSearch {
constructor(indexData) {
this.index = indexData;
}
search(query) {
if (!query || query.length < 2) return [];
const results = [];
const lowerQuery = query.toLowerCase();
const queryWords = lowerQuery.split(/\s+/);
for (const [id, content] of Object.entries(this.index)) {
let score = 0;
// Title match (higher weight)
const titleLower = content.title.toLowerCase();
if (titleLower.includes(lowerQuery)) {
score += 10;
}
queryWords.forEach(word => {
if (titleLower.includes(word)) score += 3;
});
// Body match
const bodyLower = content.body.toLowerCase();
if (bodyLower.includes(lowerQuery)) {
score += 5;
}
queryWords.forEach(word => {
if (bodyLower.includes(word)) score += 1;
});
// Tag match
if (content.tags) {
content.tags.forEach(tag => {
if (tag.toLowerCase().includes(lowerQuery)) score += 4;
});
}
if (score > 0) {
results.push({
id,
title: content.title,
excerpt: this.highlight(content.body, query),
score
});
}
}
return results
.sort((a, b) => b.score - a.score)
.slice(0, 10);
}
highlight(text, query) {
const maxLength = 150;
const lowerText = text.toLowerCase();
const lowerQuery = query.toLowerCase();
const index = lowerText.indexOf(lowerQuery);
if (index === -1) {
return text.substring(0, maxLength) + (text.length > maxLength ? '...' : '');
}
const start = Math.max(0, index - 40);
const end = Math.min(text.length, index + query.length + 80);
let excerpt = text.substring(start, end);
if (start > 0) excerpt = '...' + excerpt;
if (end < text.length) excerpt += '...';
// Highlight matches
const regex = new RegExp('(' + query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ')', 'gi');
return excerpt.replace(regex, '<mark>$1</mark>');
}
}
// Initialize search
const indexData = JSON.parse(document.getElementById('search-index').textContent);
const search = new WikiSearch(indexData);
const searchInput = document.getElementById('searchInput');
const searchResults = document.getElementById('searchResults');
searchInput.addEventListener('input', function() {
const query = this.value.trim();
const results = search.search(query);
if (results.length === 0) {
searchResults.innerHTML = query.length >= 2
? '<div class="no-results">No results found</div>'
: '';
return;
}
searchResults.innerHTML = results.map(r => `
<a href="#${r.id}" class="search-result-item" data-tiddler="${r.id}">
<div class="result-title">${r.title}</div>
<div class="result-excerpt">${r.excerpt}</div>
</a>
`).join('');
});
// Clear search on result click
searchResults.addEventListener('click', function(e) {
const item = e.target.closest('.search-result-item');
if (item) {
searchInput.value = '';
searchResults.innerHTML = '';
// Expand target tiddler
const tiddlerId = item.dataset.tiddler;
const tiddler = document.getElementById(tiddlerId);
if (tiddler) {
tiddler.classList.remove('collapsed');
const toggle = tiddler.querySelector('.collapse-toggle');
if (toggle) toggle.textContent = '▼';
}
}
});
// ========== Collapse/Expand ==========
document.querySelectorAll('.collapse-toggle').forEach(btn => {
btn.addEventListener('click', function() {
const tiddler = this.closest('.tiddler');
tiddler.classList.toggle('collapsed');
this.textContent = tiddler.classList.contains('collapsed') ? '▶' : '▼';
});
});
// Expand/Collapse All
document.getElementById('expandAll').addEventListener('click', function() {
document.querySelectorAll('.tiddler').forEach(t => {
t.classList.remove('collapsed');
const toggle = t.querySelector('.collapse-toggle');
if (toggle) toggle.textContent = '▼';
});
});
document.getElementById('collapseAll').addEventListener('click', function() {
document.querySelectorAll('.tiddler').forEach(t => {
t.classList.add('collapsed');
const toggle = t.querySelector('.collapse-toggle');
if (toggle) toggle.textContent = '▶';
});
});
// ========== Tag Filtering ==========
document.querySelectorAll('.wiki-tags .tag').forEach(tag => {
tag.addEventListener('click', function() {
const filter = this.dataset.tag;
// Update active state
document.querySelectorAll('.wiki-tags .tag').forEach(t => t.classList.remove('active'));
this.classList.add('active');
// Filter tiddlers
document.querySelectorAll('.tiddler').forEach(tiddler => {
if (filter === 'all') {
tiddler.style.display = '';
} else {
const tags = tiddler.dataset.tags || '';
tiddler.style.display = tags.includes(filter) ? '' : 'none';
}
});
});
});
// ========== Theme Toggle ==========
const themeToggle = document.getElementById('themeToggle');
const savedTheme = localStorage.getItem('wiki-theme');
if (savedTheme) {
document.body.dataset.theme = savedTheme;
}
themeToggle.addEventListener('click', function() {
const isDark = document.body.dataset.theme === 'dark';
document.body.dataset.theme = isDark ? 'light' : 'dark';
localStorage.setItem('wiki-theme', document.body.dataset.theme);
});
// ========== Sidebar Toggle (Mobile) ==========
document.getElementById('sidebarToggle').addEventListener('click', function() {
document.querySelector('.wiki-sidebar').classList.toggle('open');
});
// ========== Back to Top ==========
const backToTop = document.getElementById('backToTop');
window.addEventListener('scroll', function() {
backToTop.classList.toggle('visible', window.scrollY > 300);
});
backToTop.addEventListener('click', function() {
window.scrollTo({ top: 0, behavior: 'smooth' });
});
// ========== Print ==========
document.getElementById('printBtn').addEventListener('click', function() {
window.print();
});
// ========== TOC Navigation ==========
document.querySelectorAll('.wiki-toc a').forEach(link => {
link.addEventListener('click', function(e) {
const tiddlerId = this.getAttribute('href').substring(1);
const tiddler = document.getElementById(tiddlerId);
if (tiddler) {
// Expand if collapsed
tiddler.classList.remove('collapsed');
const toggle = tiddler.querySelector('.collapse-toggle');
if (toggle) toggle.textContent = '▼';
// Close sidebar on mobile
document.querySelector('.wiki-sidebar').classList.remove('open');
}
});
});
// ========== Code Block Copy ==========
document.querySelectorAll('pre').forEach(pre => {
const copyBtn = document.createElement('button');
copyBtn.className = 'copy-code-btn';
copyBtn.textContent = 'Copy';
copyBtn.addEventListener('click', function() {
const code = pre.querySelector('code');
navigator.clipboard.writeText(code.textContent).then(() => {
copyBtn.textContent = 'Copied!';
setTimeout(() => copyBtn.textContent = 'Copy', 2000);
});
});
pre.appendChild(copyBtn);
});
})();
</script>
</body>
</html>

View File

@@ -0,0 +1,219 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "discovery-finding-schema",
"title": "Discovery Finding Schema",
"description": "Schema for perspective-based issue discovery results",
"type": "object",
"required": ["perspective", "discovery_id", "analysis_timestamp", "cli_tool_used", "summary", "findings"],
"properties": {
"perspective": {
"type": "string",
"enum": ["bug", "ux", "test", "quality", "security", "performance", "maintainability", "best-practices"],
"description": "Discovery perspective"
},
"discovery_id": {
"type": "string",
"pattern": "^DSC-\\d{8}-\\d{6}$",
"description": "Parent discovery session ID"
},
"analysis_timestamp": {
"type": "string",
"format": "date-time",
"description": "ISO 8601 timestamp of analysis"
},
"cli_tool_used": {
"type": "string",
"enum": ["gemini", "qwen", "codex"],
"description": "CLI tool that performed the analysis"
},
"model": {
"type": "string",
"description": "Specific model version used",
"examples": ["gemini-2.5-pro", "qwen-max"]
},
"analysis_duration_ms": {
"type": "integer",
"minimum": 0,
"description": "Analysis duration in milliseconds"
},
"summary": {
"type": "object",
"required": ["total_findings"],
"properties": {
"total_findings": { "type": "integer", "minimum": 0 },
"critical": { "type": "integer", "minimum": 0 },
"high": { "type": "integer", "minimum": 0 },
"medium": { "type": "integer", "minimum": 0 },
"low": { "type": "integer", "minimum": 0 },
"files_analyzed": { "type": "integer", "minimum": 0 }
},
"description": "Summary statistics (FLAT structure, NOT nested)"
},
"findings": {
"type": "array",
"items": {
"type": "object",
"required": ["id", "title", "perspective", "priority", "category", "description", "file", "line"],
"properties": {
"id": {
"type": "string",
"pattern": "^dsc-[a-z]+-\\d{3}-[a-f0-9]{8}$",
"description": "Unique finding ID: dsc-{perspective}-{seq}-{uuid8}",
"examples": ["dsc-bug-001-a1b2c3d4"]
},
"title": {
"type": "string",
"minLength": 10,
"maxLength": 200,
"description": "Concise finding title"
},
"perspective": {
"type": "string",
"enum": ["bug", "ux", "test", "quality", "security", "performance", "maintainability", "best-practices"]
},
"priority": {
"type": "string",
"enum": ["critical", "high", "medium", "low"],
"description": "Priority level (lowercase only)"
},
"category": {
"type": "string",
"description": "Perspective-specific category",
"examples": ["null-check", "edge-case", "missing-test", "complexity", "injection"]
},
"description": {
"type": "string",
"minLength": 20,
"description": "Detailed description of the finding"
},
"file": {
"type": "string",
"description": "File path relative to project root"
},
"line": {
"type": "integer",
"minimum": 1,
"description": "Line number of the finding"
},
"snippet": {
"type": "string",
"description": "Relevant code snippet"
},
"suggested_issue": {
"type": "object",
"required": ["title", "type", "priority"],
"properties": {
"title": {
"type": "string",
"description": "Suggested issue title for export"
},
"type": {
"type": "string",
"enum": ["bug", "feature", "enhancement", "refactor", "test", "docs"],
"description": "Issue type"
},
"priority": {
"type": "integer",
"minimum": 1,
"maximum": 5,
"description": "Priority 1-5 (1=critical, 5=low)"
},
"tags": {
"type": "array",
"items": { "type": "string" },
"description": "Suggested tags for the issue"
}
},
"description": "Pre-filled issue suggestion for export"
},
"external_reference": {
"type": ["object", "null"],
"properties": {
"source": { "type": "string" },
"url": { "type": "string", "format": "uri" },
"relevance": { "type": "string" }
},
"description": "External reference from Exa research (if applicable)"
},
"confidence": {
"type": "number",
"minimum": 0,
"maximum": 1,
"description": "Confidence score 0.0-1.0"
},
"impact": {
"type": "string",
"description": "Description of potential impact"
},
"recommendation": {
"type": "string",
"description": "Specific recommendation to address the finding"
},
"metadata": {
"type": "object",
"additionalProperties": true,
"description": "Additional metadata (CWE ID, OWASP category, etc.)"
}
}
},
"description": "Array of discovered findings"
},
"cross_references": {
"type": "array",
"items": {
"type": "object",
"properties": {
"finding_id": { "type": "string" },
"related_perspectives": {
"type": "array",
"items": { "type": "string" }
},
"reason": { "type": "string" }
}
},
"description": "Cross-references to findings in other perspectives"
}
},
"examples": [
{
"perspective": "bug",
"discovery_id": "DSC-20250128-143022",
"analysis_timestamp": "2025-01-28T14:35:00Z",
"cli_tool_used": "gemini",
"model": "gemini-2.5-pro",
"analysis_duration_ms": 45000,
"summary": {
"total_findings": 8,
"critical": 1,
"high": 2,
"medium": 3,
"low": 2,
"files_analyzed": 5
},
"findings": [
{
"id": "dsc-bug-001-a1b2c3d4",
"title": "Missing null check in user validation",
"perspective": "bug",
"priority": "high",
"category": "null-check",
"description": "User object is accessed without null check after database query, which may fail if user doesn't exist",
"file": "src/auth/validator.ts",
"line": 45,
"snippet": "const user = await db.findUser(id);\nreturn user.email; // user may be null",
"suggested_issue": {
"title": "Add null check in user validation",
"type": "bug",
"priority": 2,
"tags": ["bug", "auth"]
},
"external_reference": null,
"confidence": 0.85,
"impact": "Runtime error when user not found",
"recommendation": "Add null check: if (!user) throw new NotFoundError('User not found');"
}
],
"cross_references": []
}
]
}

View File

@@ -0,0 +1,125 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "discovery-state-schema",
"title": "Discovery State Schema (Merged)",
"description": "Unified schema for issue discovery session (state + progress merged)",
"type": "object",
"required": ["discovery_id", "target_pattern", "phase", "created_at"],
"properties": {
"discovery_id": {
"type": "string",
"description": "Unique discovery session ID",
"pattern": "^DSC-\\d{8}-\\d{6}$",
"examples": ["DSC-20250128-143022"]
},
"target_pattern": {
"type": "string",
"description": "File/directory pattern being analyzed",
"examples": ["src/auth/**", "codex-lens/**/*.py"]
},
"phase": {
"type": "string",
"enum": ["initialization", "parallel", "aggregation", "complete"],
"description": "Current execution phase"
},
"created_at": {
"type": "string",
"format": "date-time"
},
"updated_at": {
"type": "string",
"format": "date-time"
},
"target": {
"type": "object",
"description": "Target module information",
"properties": {
"files_count": {
"type": "object",
"properties": {
"source": { "type": "integer" },
"tests": { "type": "integer" },
"total": { "type": "integer" }
}
},
"project": {
"type": "object",
"properties": {
"name": { "type": "string" },
"version": { "type": "string" }
}
}
}
},
"perspectives": {
"type": "array",
"description": "Perspective analysis status (merged from progress)",
"items": {
"type": "object",
"required": ["name", "status"],
"properties": {
"name": {
"type": "string",
"enum": ["bug", "ux", "test", "quality", "security", "performance", "maintainability", "best-practices"]
},
"status": {
"type": "string",
"enum": ["pending", "in_progress", "completed", "failed"]
},
"findings": {
"type": "integer",
"minimum": 0
}
}
}
},
"external_research": {
"type": "object",
"properties": {
"enabled": { "type": "boolean", "default": false },
"completed": { "type": "boolean", "default": false }
}
},
"results": {
"type": "object",
"description": "Aggregated results (final phase)",
"properties": {
"total_findings": { "type": "integer", "minimum": 0 },
"issues_generated": { "type": "integer", "minimum": 0 },
"priority_distribution": {
"type": "object",
"properties": {
"critical": { "type": "integer" },
"high": { "type": "integer" },
"medium": { "type": "integer" },
"low": { "type": "integer" }
}
}
}
}
},
"examples": [
{
"discovery_id": "DSC-20251228-182237",
"target_pattern": "codex-lens/**/*.py",
"phase": "complete",
"created_at": "2025-12-28T18:22:37+08:00",
"updated_at": "2025-12-28T18:35:00+08:00",
"target": {
"files_count": { "source": 48, "tests": 44, "total": 93 },
"project": { "name": "codex-lens", "version": "0.1.0" }
},
"perspectives": [
{ "name": "bug", "status": "completed", "findings": 15 },
{ "name": "test", "status": "completed", "findings": 11 },
{ "name": "quality", "status": "completed", "findings": 12 }
],
"external_research": { "enabled": false, "completed": false },
"results": {
"total_findings": 37,
"issues_generated": 15,
"priority_distribution": { "critical": 4, "high": 13, "medium": 16, "low": 6 }
}
}
]
}

View File

@@ -0,0 +1,141 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Issues JSONL Schema",
"description": "Schema for each line in issues.jsonl (flat storage)",
"type": "object",
"required": ["id", "title", "status", "created_at"],
"properties": {
"id": {
"type": "string",
"description": "Issue ID (GH-123, ISS-xxx, DSC-001)"
},
"title": {
"type": "string"
},
"status": {
"type": "string",
"enum": ["registered", "planning", "planned", "queued", "executing", "completed", "failed", "paused"],
"default": "registered"
},
"priority": {
"type": "integer",
"minimum": 1,
"maximum": 5,
"default": 3,
"description": "1=critical, 2=high, 3=medium, 4=low, 5=trivial"
},
"context": {
"type": "string",
"description": "Issue context/description (markdown)"
},
"source": {
"type": "string",
"enum": ["github", "text", "discovery"],
"description": "Source of the issue"
},
"source_url": {
"type": "string",
"description": "Original source URL (for GitHub issues)"
},
"tags": {
"type": "array",
"items": { "type": "string" },
"description": "Issue tags"
},
"extended_context": {
"type": "object",
"description": "Minimal extended context for planning hints",
"properties": {
"location": {
"type": "string",
"description": "file:line format (e.g., 'src/auth.ts:42')"
},
"suggested_fix": {
"type": "string",
"description": "Suggested remediation"
},
"notes": {
"type": "string",
"description": "Additional notes (user clarifications or discovery hints)"
}
}
},
"affected_components": {
"type": "array",
"items": { "type": "string" },
"description": "Files/modules affected"
},
"lifecycle_requirements": {
"type": "object",
"properties": {
"test_strategy": {
"type": "string",
"enum": ["unit", "integration", "e2e", "manual", "auto"]
},
"regression_scope": {
"type": "string",
"enum": ["affected", "related", "full"]
},
"acceptance_type": {
"type": "string",
"enum": ["automated", "manual", "both"]
},
"commit_strategy": {
"type": "string",
"enum": ["per-task", "squash", "atomic"]
}
}
},
"bound_solution_id": {
"type": "string",
"description": "ID of the bound solution (null if none bound)"
},
"solution_count": {
"type": "integer",
"default": 0,
"description": "Number of candidate solutions"
},
"created_at": {
"type": "string",
"format": "date-time"
},
"updated_at": {
"type": "string",
"format": "date-time"
},
"planned_at": {
"type": "string",
"format": "date-time"
},
"completed_at": {
"type": "string",
"format": "date-time"
}
},
"examples": [
{
"id": "DSC-001",
"title": "Fix: SQLite connection pool memory leak",
"status": "registered",
"priority": 1,
"context": "Connection pool cleanup only happens when MAX_POOL_SIZE is reached...",
"source": "discovery",
"tags": ["bug", "resource-leak", "critical"],
"extended_context": {
"location": "storage/sqlite_store.py:59",
"suggested_fix": "Implement periodic cleanup or weak references",
"notes": null
},
"affected_components": ["storage/sqlite_store.py"],
"lifecycle_requirements": {
"test_strategy": "unit",
"regression_scope": "affected",
"acceptance_type": "automated",
"commit_strategy": "per-task"
},
"bound_solution_id": null,
"solution_count": 0,
"created_at": "2025-12-28T18:22:37Z"
}
]
}

View File

@@ -0,0 +1,248 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Issue Execution Queue Schema",
"description": "Execution queue supporting both task-level (T-N) and solution-level (S-N) granularity",
"type": "object",
"properties": {
"id": {
"type": "string",
"pattern": "^QUE-[0-9]{8}-[0-9]{6}$",
"description": "Queue ID in format QUE-YYYYMMDD-HHMMSS"
},
"status": {
"type": "string",
"enum": ["active", "paused", "completed", "archived"],
"default": "active"
},
"issue_ids": {
"type": "array",
"items": { "type": "string" },
"description": "Issues included in this queue"
},
"solutions": {
"type": "array",
"description": "Solution-level queue items (preferred for new queues)",
"items": {
"$ref": "#/definitions/solutionItem"
}
},
"tasks": {
"type": "array",
"description": "Task-level queue items (legacy format)",
"items": {
"$ref": "#/definitions/taskItem"
}
},
"conflicts": {
"type": "array",
"description": "Detected conflicts between items",
"items": {
"$ref": "#/definitions/conflict"
}
},
"execution_groups": {
"type": "array",
"description": "Parallel/Sequential execution groups",
"items": {
"$ref": "#/definitions/executionGroup"
}
},
"_metadata": {
"type": "object",
"properties": {
"version": { "type": "string", "default": "2.0" },
"queue_type": {
"type": "string",
"enum": ["solution", "task"],
"description": "Queue granularity level"
},
"total_solutions": { "type": "integer" },
"total_tasks": { "type": "integer" },
"pending_count": { "type": "integer" },
"ready_count": { "type": "integer" },
"executing_count": { "type": "integer" },
"completed_count": { "type": "integer" },
"failed_count": { "type": "integer" },
"last_queue_formation": { "type": "string", "format": "date-time" },
"last_updated": { "type": "string", "format": "date-time" }
}
}
},
"definitions": {
"solutionItem": {
"type": "object",
"required": ["item_id", "issue_id", "solution_id", "status", "task_count", "files_touched"],
"properties": {
"item_id": {
"type": "string",
"pattern": "^S-[0-9]+$",
"description": "Solution-level queue item ID (S-1, S-2, ...)"
},
"issue_id": {
"type": "string",
"description": "Source issue ID"
},
"solution_id": {
"type": "string",
"description": "Bound solution ID"
},
"status": {
"type": "string",
"enum": ["pending", "ready", "executing", "completed", "failed", "blocked"],
"default": "pending"
},
"task_count": {
"type": "integer",
"minimum": 1,
"description": "Number of tasks in this solution"
},
"files_touched": {
"type": "array",
"items": { "type": "string" },
"description": "All files modified by this solution"
},
"execution_order": {
"type": "integer",
"description": "Order in execution sequence"
},
"execution_group": {
"type": "string",
"description": "Parallel (P*) or Sequential (S*) group ID"
},
"depends_on": {
"type": "array",
"items": { "type": "string" },
"description": "Solution IDs this item depends on"
},
"semantic_priority": {
"type": "number",
"minimum": 0,
"maximum": 1,
"description": "Semantic importance score (0.0-1.0)"
},
"queued_at": { "type": "string", "format": "date-time" },
"started_at": { "type": "string", "format": "date-time" },
"completed_at": { "type": "string", "format": "date-time" },
"result": {
"type": "object",
"properties": {
"summary": { "type": "string" },
"files_modified": { "type": "array", "items": { "type": "string" } },
"tasks_completed": { "type": "integer" },
"commit_hashes": { "type": "array", "items": { "type": "string" } }
}
},
"failure_reason": { "type": "string" }
}
},
"taskItem": {
"type": "object",
"required": ["item_id", "issue_id", "solution_id", "task_id", "status"],
"properties": {
"item_id": {
"type": "string",
"pattern": "^T-[0-9]+$",
"description": "Task-level queue item ID (T-1, T-2, ...)"
},
"issue_id": { "type": "string" },
"solution_id": { "type": "string" },
"task_id": { "type": "string" },
"status": {
"type": "string",
"enum": ["pending", "ready", "executing", "completed", "failed", "blocked"],
"default": "pending"
},
"execution_order": { "type": "integer" },
"execution_group": { "type": "string" },
"depends_on": { "type": "array", "items": { "type": "string" } },
"semantic_priority": { "type": "number", "minimum": 0, "maximum": 1 },
"queued_at": { "type": "string", "format": "date-time" },
"started_at": { "type": "string", "format": "date-time" },
"completed_at": { "type": "string", "format": "date-time" },
"result": {
"type": "object",
"properties": {
"files_modified": { "type": "array", "items": { "type": "string" } },
"files_created": { "type": "array", "items": { "type": "string" } },
"summary": { "type": "string" },
"commit_hash": { "type": "string" }
}
},
"failure_reason": { "type": "string" }
}
},
"conflict": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["file_conflict", "dependency_conflict", "resource_conflict"]
},
"file": {
"type": "string",
"description": "Conflicting file path"
},
"solutions": {
"type": "array",
"items": { "type": "string" },
"description": "Solution IDs involved (for solution-level queues)"
},
"tasks": {
"type": "array",
"items": { "type": "string" },
"description": "Task IDs involved (for task-level queues)"
},
"resolution": {
"type": "string",
"enum": ["sequential", "merge", "manual"]
},
"resolution_order": {
"type": "array",
"items": { "type": "string" },
"description": "Execution order to resolve conflict"
},
"rationale": {
"type": "string",
"description": "Explanation of resolution decision"
},
"resolved": {
"type": "boolean",
"default": false
}
}
},
"executionGroup": {
"type": "object",
"required": ["id", "type"],
"properties": {
"id": {
"type": "string",
"pattern": "^[PS][0-9]+$",
"description": "Group ID (P1, P2 for parallel, S1, S2 for sequential)"
},
"type": {
"type": "string",
"enum": ["parallel", "sequential"]
},
"solutions": {
"type": "array",
"items": { "type": "string" },
"description": "Solution IDs in this group"
},
"tasks": {
"type": "array",
"items": { "type": "string" },
"description": "Task IDs in this group (legacy)"
},
"solution_count": {
"type": "integer",
"description": "Number of solutions in group"
},
"task_count": {
"type": "integer",
"description": "Number of tasks in group (legacy)"
}
}
}
}
}

View File

@@ -0,0 +1,94 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Issue Registry Schema",
"description": "Global registry of all issues and their solutions",
"type": "object",
"properties": {
"issues": {
"type": "array",
"description": "List of registered issues",
"items": {
"type": "object",
"required": ["id", "title", "status", "created_at"],
"properties": {
"id": {
"type": "string",
"description": "Issue ID (e.g., GH-123, TEXT-xxx)"
},
"title": {
"type": "string"
},
"status": {
"type": "string",
"enum": ["registered", "planning", "planned", "queued", "executing", "completed", "failed", "paused"],
"default": "registered"
},
"priority": {
"type": "integer",
"minimum": 1,
"maximum": 5,
"default": 3
},
"solution_count": {
"type": "integer",
"default": 0,
"description": "Number of candidate solutions"
},
"bound_solution_id": {
"type": "string",
"description": "ID of the bound solution (null if none bound)"
},
"source": {
"type": "string",
"enum": ["github", "text", "file"],
"description": "Source of the issue"
},
"source_url": {
"type": "string",
"description": "Original source URL (for GitHub issues)"
},
"created_at": {
"type": "string",
"format": "date-time"
},
"updated_at": {
"type": "string",
"format": "date-time"
},
"planned_at": {
"type": "string",
"format": "date-time"
},
"queued_at": {
"type": "string",
"format": "date-time"
},
"completed_at": {
"type": "string",
"format": "date-time"
}
}
}
},
"_metadata": {
"type": "object",
"properties": {
"version": { "type": "string", "default": "1.0" },
"total_issues": { "type": "integer" },
"by_status": {
"type": "object",
"properties": {
"registered": { "type": "integer" },
"planning": { "type": "integer" },
"planned": { "type": "integer" },
"queued": { "type": "integer" },
"executing": { "type": "integer" },
"completed": { "type": "integer" },
"failed": { "type": "integer" }
}
},
"last_updated": { "type": "string", "format": "date-time" }
}
}
}
}

View File

@@ -0,0 +1,166 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Issue Solution Schema",
"description": "Schema for solution registered to an issue",
"type": "object",
"required": ["id", "tasks", "is_bound", "created_at"],
"properties": {
"id": {
"type": "string",
"description": "Unique solution identifier: SOL-{issue-id}-{seq}",
"pattern": "^SOL-.+-[0-9]+$",
"examples": ["SOL-GH-123-1", "SOL-ISS-20251229-1"]
},
"description": {
"type": "string",
"description": "High-level summary of the solution"
},
"approach": {
"type": "string",
"description": "Technical approach or strategy"
},
"tasks": {
"type": "array",
"description": "Task breakdown for this solution",
"items": {
"type": "object",
"required": ["id", "title", "scope", "action", "implementation", "acceptance"],
"properties": {
"id": {
"type": "string",
"pattern": "^T[0-9]+$"
},
"title": {
"type": "string",
"description": "Action verb + target"
},
"scope": {
"type": "string",
"description": "Module path or feature area"
},
"action": {
"type": "string",
"enum": ["Create", "Update", "Implement", "Refactor", "Add", "Delete", "Configure", "Test", "Fix"]
},
"description": {
"type": "string",
"description": "1-2 sentences describing what to implement"
},
"modification_points": {
"type": "array",
"items": {
"type": "object",
"properties": {
"file": { "type": "string" },
"target": { "type": "string" },
"change": { "type": "string" }
}
}
},
"implementation": {
"type": "array",
"items": { "type": "string" },
"description": "Step-by-step implementation guide"
},
"test": {
"type": "object",
"description": "Test requirements",
"properties": {
"unit": { "type": "array", "items": { "type": "string" } },
"integration": { "type": "array", "items": { "type": "string" } },
"commands": { "type": "array", "items": { "type": "string" } },
"coverage_target": { "type": "number" }
}
},
"regression": {
"type": "array",
"items": { "type": "string" },
"description": "Regression check points"
},
"acceptance": {
"type": "object",
"description": "Acceptance criteria & verification",
"required": ["criteria", "verification"],
"properties": {
"criteria": { "type": "array", "items": { "type": "string" } },
"verification": { "type": "array", "items": { "type": "string" } },
"manual_checks": { "type": "array", "items": { "type": "string" } }
}
},
"commit": {
"type": "object",
"description": "Commit specification",
"properties": {
"type": { "type": "string", "enum": ["feat", "fix", "refactor", "test", "docs", "chore"] },
"scope": { "type": "string" },
"message_template": { "type": "string" },
"breaking": { "type": "boolean" }
}
},
"depends_on": {
"type": "array",
"items": { "type": "string" },
"default": [],
"description": "Task IDs this task depends on"
},
"estimated_minutes": {
"type": "integer",
"description": "Estimated time to complete"
},
"status": {
"type": "string",
"description": "Task status (optional, for tracking)"
},
"priority": {
"type": "integer",
"minimum": 1,
"maximum": 5,
"default": 3
}
}
}
},
"exploration_context": {
"type": "object",
"description": "ACE exploration results",
"properties": {
"project_structure": { "type": "string" },
"relevant_files": {
"type": "array",
"items": { "type": "string" }
},
"patterns": { "type": "string" },
"integration_points": { "type": "string" }
}
},
"analysis": {
"type": "object",
"description": "Solution risk assessment",
"properties": {
"risk": { "type": "string", "enum": ["low", "medium", "high"] },
"impact": { "type": "string", "enum": ["low", "medium", "high"] },
"complexity": { "type": "string", "enum": ["low", "medium", "high"] }
}
},
"score": {
"type": "number",
"minimum": 0,
"maximum": 1,
"description": "Solution quality score (0.0-1.0)"
},
"is_bound": {
"type": "boolean",
"default": false,
"description": "Whether this solution is bound to the issue"
},
"created_at": {
"type": "string",
"format": "date-time"
},
"bound_at": {
"type": "string",
"format": "date-time",
"description": "When this solution was bound to the issue"
}
}
}

View File

@@ -1,105 +0,0 @@
## MCP Tools Usage
### search_context (ACE) - Code Search (REQUIRED - HIGHEST PRIORITY)
**OVERRIDES**: All other search/discovery rules in other workflow files
**When**: ANY code discovery task, including:
- Find code, understand codebase structure, locate implementations
- Explore unknown locations
- Verify file existence before reading
- Pattern-based file discovery
- Semantic code understanding
**Priority Rule**:
1. **Always use mcp__ace-tool__search_context FIRST** for any code/file discovery
2. Only use Built-in Grep for single-file exact line search (after location confirmed)
3. Only use Built-in Read for known, confirmed file paths
**How**:
```javascript
// Natural language code search - best for understanding and exploration
mcp__ace-tool__search_context({
project_root_path: "/path/to/project",
query: "authentication logic"
})
// With keywords for better semantic matching
mcp__ace-tool__search_context({
project_root_path: "/path/to/project",
query: "I want to find where the server handles user login. Keywords: auth, login, session"
})
```
**Good Query Examples**:
- "Where is the function that handles user authentication?"
- "What tests are there for the login functionality?"
- "How is the database connected to the application?"
- "I want to find where the server handles chunk merging. Keywords: upload chunk merge"
- "Locate where the system refreshes cached data. Keywords: cache refresh, invalidation"
**Bad Query Examples** (use grep or file view instead):
- "Find definition of constructor of class Foo" (use grep tool instead)
- "Find all references to function bar" (use grep tool instead)
- "Show me how Checkout class is used in services/payment.py" (use file view tool instead)
**Key Features**:
- Real-time index of the codebase (always up-to-date)
- Cross-language retrieval support
- Semantic search with embeddings
- No manual index initialization required
---
### read_file - Read File Contents
**When**: Read files found by search_context
**How**:
```javascript
read_file(path="/path/to/file.ts") // Single file
read_file(path="/src/**/*.config.ts") // Pattern matching
```
---
### edit_file - Modify Files
**When**: Built-in Edit tool fails or need advanced features
**How**:
```javascript
edit_file(path="/file.ts", old_string="...", new_string="...", mode="update")
edit_file(path="/file.ts", line=10, content="...", mode="insert_after")
```
**Modes**: `update` (replace text), `insert_after`, `insert_before`, `delete_line`
---
### write_file - Create/Overwrite Files
**When**: Create new files or completely replace content
**How**:
```javascript
write_file(path="/new-file.ts", content="...")
```
---
### Exa - External Search
**When**: Find documentation/examples outside codebase
**How**:
```javascript
mcp__exa__search(query="React hooks 2025 documentation")
mcp__exa__search(query="FastAPI auth example", numResults=10)
mcp__exa__search(query="latest API docs", livecrawl="always")
```
**Parameters**:
- `query` (required): Search query string
- `numResults` (optional): Number of results to return (default: 5)
- `livecrawl` (optional): `"always"` or `"fallback"` for live crawling

View File

@@ -1,35 +1,27 @@
## MCP Tools Usage
## Context Acquisition (MCP Tools Priority)
### smart_search - Code Search (REQUIRED - HIGHEST PRIORITY)
**For task context gathering and analysis, ALWAYS prefer MCP tools**:
**OVERRIDES**: All other search/discovery rules in other workflow files
1. **mcp__ace-tool__search_context** - HIGHEST PRIORITY for code discovery
- Semantic search with real-time codebase index
- Use for: finding implementations, understanding architecture, locating patterns
- Example: `mcp__ace-tool__search_context(project_root_path="/path", query="authentication logic")`
**When**: ANY code discovery task, including:
- Find code, understand codebase structure, locate implementations
- Explore unknown locations
- Verify file existence before reading
- Pattern-based file discovery
2. **smart_search** - Fallback for structured search
- Use `smart_search(query="...")` for keyword/regex search
- Use `smart_search(action="find_files", pattern="*.ts")` for file discovery
- Supports modes: `auto`, `hybrid`, `exact`, `ripgrep`
**Priority Rule**:
1. **Always use smart_search FIRST** for any code/file discovery
2. Only use Built-in Grep for single-file exact line search (after location confirmed)
3. Only use Built-in Read for known, confirmed file paths
3. **read_file** - Batch file reading
- Read multiple files in parallel: `read_file(path="file1.ts")`, `read_file(path="file2.ts")`
- Supports glob patterns: `read_file(path="src/**/*.config.ts")`
**Workflow** (search first, init if needed):
```javascript
// Step 1: Try search directly (works if index exists or uses ripgrep fallback)
smart_search(query="authentication logic")
// Step 2: Only if search warns "No CodexLens index found", then init
smart_search(action="init", path=".") // Creates FTS index only
// Note: For semantic/vector search, use "ccw view" dashboard to create vector index
**Priority Order**:
```
ACE search_context (semantic) → smart_search (structured) → read_file (batch read) → shell commands (fallback)
```
**Modes**: `auto` (intelligent routing), `hybrid` (semantic, needs vector index), `exact` (FTS), `ripgrep` (no index)
---
**NEVER** use shell commands (`cat`, `find`, `grep`) when MCP tools are available.
### read_file - Read File Contents
**When**: Read files found by smart_search

View File

@@ -76,18 +76,23 @@ chcp 65001 > $null
**For task context gathering and analysis, ALWAYS prefer MCP tools**:
1. **smart_search** - First choice for code discovery
- Use `smart_search(query="...")` for semantic/keyword search
1. **mcp__ace-tool__search_context** - HIGHEST PRIORITY for code discovery
- Semantic search with real-time codebase index
- Use for: finding implementations, understanding architecture, locating patterns
- Example: `mcp__ace-tool__search_context(project_root_path="/path", query="authentication logic")`
2. **smart_search** - Fallback for structured search
- Use `smart_search(query="...")` for keyword/regex search
- Use `smart_search(action="find_files", pattern="*.ts")` for file discovery
- Supports modes: `auto`, `hybrid`, `exact`, `ripgrep`
2. **read_file** - Batch file reading
3. **read_file** - Batch file reading
- Read multiple files in parallel: `read_file(path="file1.ts")`, `read_file(path="file2.ts")`
- Supports glob patterns: `read_file(path="src/**/*.config.ts")`
**Priority Order**:
```
smart_search (discovery) → read_file (batch read) → shell commands (fallback)
ACE search_context (semantic) → smart_search (structured) → read_file (batch read) → shell commands (fallback)
```
**NEVER** use shell commands (`cat`, `find`, `grep`) when MCP tools are available.
@@ -96,7 +101,7 @@ smart_search (discovery) → read_file (batch read) → shell commands (fallback
**Before**:
- [ ] Understand PURPOSE and TASK clearly
- [ ] Use smart_search to discover relevant files
- [ ] Use ACE search_context first, fallback to smart_search for discovery
- [ ] Use read_file to batch read context files, find 3+ patterns
- [ ] Check RULES templates and constraints

View File

@@ -0,0 +1,356 @@
---
description: Execute all solutions from issue queue with git commit after each task
argument-hint: ""
---
# Issue Execute (Codex Version)
## Core Principle
**Serial Execution**: Execute solutions ONE BY ONE from the issue queue via `ccw issue next`. For each solution, complete all tasks sequentially (implement → test → commit per task). Continue autonomously until queue is empty.
## Execution Flow
```
INIT: Fetch first solution via ccw issue next
WHILE solution exists:
1. Receive solution JSON from ccw issue next
2. Execute all tasks in solution.tasks sequentially:
FOR each task:
- IMPLEMENT: Follow task.implementation steps
- TEST: Run task.test commands
- VERIFY: Check task.acceptance criteria
- COMMIT: Stage files, commit with task.commit.message_template
3. Report completion via ccw issue done <item_id>
4. Fetch next solution via ccw issue next
WHEN queue empty:
Output final summary
```
## Step 1: Fetch First Solution
Run this command to get your first solution:
```bash
ccw issue next
```
This returns JSON with the full solution definition:
- `item_id`: Solution identifier in queue (e.g., "S-1")
- `issue_id`: Parent issue ID (e.g., "ISS-20251227-001")
- `solution_id`: Solution ID (e.g., "SOL-ISS-20251227-001-1")
- `solution`: Full solution with all tasks
- `execution_hints`: Timing and executor hints
If response contains `{ "status": "empty" }`, all solutions are complete - skip to final summary.
## Step 2: Parse Solution Response
Expected solution structure:
```json
{
"item_id": "S-1",
"issue_id": "ISS-20251227-001",
"solution_id": "SOL-ISS-20251227-001-1",
"status": "pending",
"solution": {
"id": "SOL-ISS-20251227-001-1",
"description": "Description of solution approach",
"tasks": [
{
"id": "T1",
"title": "Task title",
"scope": "src/module/",
"action": "Create|Modify|Fix|Refactor|Add",
"description": "What to do",
"modification_points": [
{ "file": "path/to/file.ts", "target": "function name", "change": "description" }
],
"implementation": [
"Step 1: Do this",
"Step 2: Do that"
],
"test": {
"commands": ["npm test -- --filter=xxx"],
"unit": ["Unit test requirement 1", "Unit test requirement 2"]
},
"regression": ["Verify existing tests still pass"],
"acceptance": {
"criteria": ["Criterion 1: Must pass", "Criterion 2: Must verify"],
"verification": ["Run test command", "Manual verification step"]
},
"commit": {
"type": "feat|fix|test|refactor",
"scope": "module",
"message_template": "feat(scope): description"
},
"depends_on": [],
"estimated_minutes": 30,
"priority": 1
}
],
"exploration_context": {
"relevant_files": ["path/to/reference.ts"],
"patterns": "Follow existing pattern in xxx",
"integration_points": "Used by other modules"
},
"analysis": {
"risk": "low|medium|high",
"impact": "low|medium|high",
"complexity": "low|medium|high"
},
"score": 0.95,
"is_bound": true
},
"execution_hints": {
"executor": "codex",
"estimated_minutes": 180
}
}
```
## Step 2.5: Initialize Todo Tracking
After parsing solution, create todo list to track each task:
```javascript
// Create todos for all tasks in current solution
TodoWrite({
todos: solution.tasks.map(task => ({
content: `${task.id}: ${task.title}`,
activeForm: `Executing ${task.id}: ${task.title}`,
status: "pending"
}))
})
```
## Step 3: Execute Tasks Sequentially
Iterate through `solution.tasks` array and execute each task.
**Before starting each task**, mark it as in_progress:
```javascript
// Update current task status to in_progress
TodoWrite({ todos: [...existingTodos.map(t =>
t.content.startsWith(currentTask.id) ? {...t, status: "in_progress"} : t
)] })
```
**After completing each task** (commit done), mark it as completed:
```javascript
// Update completed task status
TodoWrite({ todos: [...existingTodos.map(t =>
t.content.startsWith(completedTask.id) ? {...t, status: "completed"} : t
)] })
```
### Phase A: IMPLEMENT
1. Read all `solution.exploration_context.relevant_files` to understand existing patterns
2. Follow `task.implementation` steps in order
3. Apply changes to `task.modification_points` files
4. Follow `solution.exploration_context.patterns` for code style consistency
5. Run `task.regression` checks if specified to ensure no breakage
**Output format:**
```
## Implementing: [task.title] (Task [N]/[Total])
**Scope**: [task.scope]
**Action**: [task.action]
**Steps**:
1. ✓ [implementation step 1]
2. ✓ [implementation step 2]
...
**Files Modified**:
- path/to/file1.ts
- path/to/file2.ts
```
### Phase B: TEST
1. Run all commands in `task.test.commands`
2. Verify unit tests pass (`task.test.unit`)
3. Run integration tests if specified (`task.test.integration`)
**If tests fail**: Fix the code and re-run. Do NOT proceed until tests pass.
**Output format:**
```
## Testing: [task.title]
**Test Results**:
- [x] Unit tests: PASSED
- [x] Integration tests: PASSED (or N/A)
```
### Phase C: VERIFY
Check all `task.acceptance.criteria` are met using `task.acceptance.verification` steps:
```
## Verifying: [task.title]
**Acceptance Criteria**:
- [x] Criterion 1: Verified
- [x] Criterion 2: Verified
...
**Verification Steps**:
- [x] Run test command
- [x] Manual verification step
All criteria met: YES
```
**If any criterion fails**: Go back to IMPLEMENT phase and fix.
### Phase D: COMMIT
After all phases pass, commit the changes for this task:
```bash
# Stage all modified files
git add path/to/file1.ts path/to/file2.ts ...
# Commit with task message template
git commit -m "$(cat <<'EOF'
[task.commit.message_template]
Solution-ID: [solution_id]
Issue-ID: [issue_id]
Task-ID: [task.id]
EOF
)"
```
**Output format:**
```
## Committed: [task.title]
**Commit**: [commit hash]
**Message**: [commit message]
**Files**: N files changed
```
### Repeat for Next Task
Continue to next task in `solution.tasks` array until all tasks are complete.
## Step 4: Report Completion
After ALL tasks in the solution are complete, report to queue system:
```bash
ccw issue done <item_id> --result '{
"files_modified": ["path1", "path2"],
"tests_passed": true,
"acceptance_passed": true,
"committed": true,
"commits": [
{ "task_id": "T1", "hash": "abc123" },
{ "task_id": "T2", "hash": "def456" }
],
"summary": "[What was accomplished]"
}'
```
**If solution failed and cannot be fixed:**
```bash
ccw issue done <item_id> --fail --reason "Task [task.id] failed: [details]"
```
## Step 5: Continue to Next Solution
Clear current todo list and fetch next solution:
```javascript
// Reset todos for next solution
TodoWrite({ todos: [] })
```
Then fetch next solution:
```bash
ccw issue next
```
**Output progress:**
```
✓ [N/M] Completed: [item_id] - [solution.approach]
→ Fetching next solution...
```
**DO NOT STOP.** Return to Step 2 and continue until queue is empty.
## Final Summary
When `ccw issue next` returns `{ "status": "empty" }`:
```markdown
## Issue Queue Execution Complete
**Total Solutions Executed**: N
**Total Tasks Executed**: M
**All Commits**:
| # | Solution | Task | Commit |
|---|----------|------|--------|
| 1 | S-1 | T1 | abc123 |
| 2 | S-1 | T2 | def456 |
| 3 | S-2 | T1 | ghi789 |
**Files Modified**:
- path/to/file1.ts
- path/to/file2.ts
**Summary**:
[Overall what was accomplished]
```
## Execution Rules
1. **Never stop mid-queue** - Continue until queue is empty
2. **One solution at a time** - Fully complete (all tasks + report) before moving on
3. **Sequential within solution** - Complete each task (including commit) before next
4. **Tests MUST pass** - Do not proceed to commit if tests fail
5. **Commit after each task** - Each task gets its own commit
6. **Self-verify** - All acceptance criteria must pass before commit
7. **Report accurately** - Use `ccw issue done` after each solution
8. **Handle failures gracefully** - If a solution fails, report via `ccw issue done --fail` and continue to next
9. **Track with todos** - Use TodoWrite to track task progress within each solution
## Error Handling
| Situation | Action |
|-----------|--------|
| `ccw issue next` returns empty | All done - output final summary |
| Tests fail | Fix code, re-run tests |
| Verification fails | Go back to implement phase |
| Git commit fails | Check staging, retry commit |
| `ccw issue done` fails | Log error, continue to next solution |
| Unrecoverable error | Call `ccw issue done --fail`, continue to next |
## CLI Command Reference
| Command | Purpose |
|---------|---------|
| `ccw issue next` | Fetch next solution from queue |
| `ccw issue done <id>` | Mark solution complete with result |
| `ccw issue done <id> --fail` | Mark solution failed with reason |
## Start Execution
Begin by running:
```bash
ccw issue next
```
Then follow the solution lifecycle for each solution until queue is empty.

View File

@@ -0,0 +1,103 @@
---
description: Plan issue(s) into bound solutions (writes solutions JSONL via ccw issue bind)
argument-hint: "<issue-id>[,<issue-id>,...] [--all-pending] [--batch-size 3]"
---
# Issue Plan (Codex Version)
## Goal
Create executable solution(s) for issue(s) and bind the selected solution to each issue using `ccw issue bind`.
This workflow is **planning + registration** (no implementation): it explores the codebase just enough to produce a high-quality task breakdown that can be executed later (e.g., by `issue-execute.md`).
## Inputs
- **Explicit issues**: comma-separated IDs, e.g. `ISS-123,ISS-124`
- **All pending**: `--all-pending` → plan all issues in `registered` status
- **Batch size**: `--batch-size N` (default `3`) → max issues per batch
## Output Requirements
For each issue:
- Register at least one solution and bind one solution to the issue (updates `.workflow/issues/issues.jsonl` and appends to `.workflow/issues/solutions/{issue-id}.jsonl`).
- Ensure tasks conform to `.claude/workflows/cli-templates/schemas/solution-schema.json`.
- Each task includes quantified `acceptance.criteria` and concrete `acceptance.verification`.
Return a final summary JSON:
```json
{
"bound": [{ "issue_id": "...", "solution_id": "...", "task_count": 0 }],
"pending_selection": [{ "issue_id": "...", "solutions": [{ "id": "...", "task_count": 0, "description": "..." }] }],
"conflicts": [{ "file": "...", "issues": ["..."] }]
}
```
## Workflow
### Step 1: Resolve issue list
- If `--all-pending`:
- Run `ccw issue list --status registered --json` and plan all returned issues.
- Else:
- Parse IDs from user input (split by `,`), and ensure each issue exists:
- `ccw issue init <issue-id> --title "Issue <issue-id>"` (safe if already exists)
### Step 2: Load issue details
For each issue ID:
- `ccw issue status <issue-id> --json`
- Extract the issue title/context/labels and any discovery hints (affected files, snippets, etc. if present).
### Step 3: Minimal exploration (evidence-based)
- If issue context names specific files or symbols: open them first.
- Otherwise:
- Use `rg` to locate relevant code paths by keywords from the title/context.
- Read 3+ similar patterns before proposing refactors or API changes.
### Step 4: Draft solutions and tasks (schema-driven)
Default to **one** solution per issue unless there are genuinely different approaches.
Task rules (from schema):
- `id`: `T1`, `T2`, ...
- `action`: one of `Create|Update|Implement|Refactor|Add|Delete|Configure|Test|Fix`
- `implementation`: step-by-step, executable instructions
- `test.commands`: include at least one command per task when feasible
- `acceptance.criteria`: testable statements
- `acceptance.verification`: concrete steps/commands mapping to criteria
- Prefer small, independently testable tasks; encode dependencies in `depends_on`.
### Step 5: Register & bind solutions via CLI
**Create solution** (via CLI endpoint):
```bash
ccw issue solution <issue-id> --data '{"description":"...", "approach":"...", "tasks":[...]}'
# Output: {"id":"SOL-{issue-id}-1", ...}
```
**CLI Features:**
| Feature | Description |
|---------|-------------|
| Auto-increment ID | `SOL-{issue-id}-{seq}` (e.g., `SOL-GH-123-1`) |
| Multi-solution | Appends to existing JSONL, supports multiple per issue |
| Trailing newline | Proper JSONL format, no corruption |
**Binding:**
- **Single solution**: Auto-bind: `ccw issue bind <issue-id> <solution-id>`
- **Multiple solutions**: Present alternatives in `pending_selection`, wait for user choice
### Step 6: Detect cross-issue file conflicts (best-effort)
Across the issues planned in this run:
- Build a set of touched files from each solutions `modification_points.file` (and/or task `scope` when explicit files are missing).
- If the same file appears in multiple issues, add it to `conflicts` with all involved issue IDs.
- Recommend a safe execution order (sequential) when conflicts exist.
## Done Criteria
- A bound solution exists for each issue unless explicitly deferred for user selection.
- All tasks validate against the solution schema fields (especially acceptance criteria + verification).
- The final summary JSON matches the required shape.

View File

@@ -0,0 +1,224 @@
---
description: Form execution queue from bound solutions (orders solutions, detects conflicts, assigns groups)
argument-hint: "[--issue <id>]"
---
# Issue Queue (Codex Version)
## Goal
Create an ordered execution queue from all bound solutions. Analyze inter-solution file conflicts, calculate semantic priorities, and assign parallel/sequential execution groups.
This workflow is **ordering only** (no execution): it reads bound solutions, detects conflicts, and produces a queue file that `issue-execute.md` can consume.
## Inputs
- **All planned**: Default behavior → queue all issues with `planned` status and bound solutions
- **Specific issue**: `--issue <id>` → queue only that issue's solution
## Output Requirements
**Generate Files (EXACTLY 2):**
1. `.workflow/issues/queues/{queue-id}.json` - Full queue with solutions, conflicts, groups
2. `.workflow/issues/queues/index.json` - Update with new queue entry
**Return Summary:**
```json
{
"queue_id": "QUE-YYYYMMDD-HHMMSS",
"total_solutions": 3,
"total_tasks": 12,
"execution_groups": [{ "id": "P1", "type": "parallel", "count": 2 }],
"conflicts_resolved": 1,
"issues_queued": ["ISS-xxx", "ISS-yyy"]
}
```
## Workflow
### Step 1: Generate Queue ID
Generate queue ID ONCE at start, reuse throughout:
```bash
# Format: QUE-YYYYMMDD-HHMMSS (UTC)
QUEUE_ID="QUE-$(date -u +%Y%m%d-%H%M%S)"
```
### Step 2: Load Planned Issues
Get all issues with bound solutions:
```bash
ccw issue list --status planned --json
```
For each issue in the result:
- Extract `id`, `bound_solution_id`, `priority`
- Read solution from `.workflow/issues/solutions/{issue-id}.jsonl`
- Find the bound solution by matching `solution.id === bound_solution_id`
- Collect `files_touched` from all tasks' `modification_points.file`
Build solution list:
```json
[
{
"issue_id": "ISS-xxx",
"solution_id": "SOL-xxx",
"task_count": 3,
"files_touched": ["src/auth.ts", "src/utils.ts"],
"priority": "medium"
}
]
```
### Step 3: Detect File Conflicts
Build a file → solutions mapping:
```javascript
fileModifications = {
"src/auth.ts": ["SOL-ISS-001-1", "SOL-ISS-003-1"],
"src/api.ts": ["SOL-ISS-002-1"]
}
```
Conflicts exist when a file has multiple solutions. For each conflict:
- Record the file and involved solutions
- Will be resolved in Step 4
### Step 4: Resolve Conflicts & Build DAG
**Resolution Rules (in priority order):**
1. Higher issue priority first: `critical > high > medium > low`
2. Foundation solutions first: fewer dependencies
3. More tasks = higher priority: larger impact
For each file conflict:
- Apply resolution rules to determine order
- Add dependency edge: later solution `depends_on` earlier solution
- Record rationale
**Semantic Priority Formula:**
```
Base: critical=0.9, high=0.7, medium=0.5, low=0.3
Boost: task_count>=5 → +0.1, task_count>=3 → +0.05
Final: clamp(base + boost, 0.0, 1.0)
```
### Step 5: Assign Execution Groups
- **Parallel (P1, P2, ...)**: Solutions with NO file overlaps between them
- **Sequential (S1, S2, ...)**: Solutions that share files must run in order
Group assignment:
1. Start with all solutions in potential parallel group
2. For each file conflict, move later solution to sequential group
3. Assign group IDs: P1 for first parallel batch, S2 for first sequential, etc.
### Step 6: Generate Queue Files
**Queue file structure** (`.workflow/issues/queues/{QUEUE_ID}.json`):
```json
{
"id": "QUE-20251228-120000",
"status": "active",
"issue_ids": ["ISS-001", "ISS-002"],
"solutions": [
{
"item_id": "S-1",
"issue_id": "ISS-001",
"solution_id": "SOL-ISS-001-1",
"status": "pending",
"execution_order": 1,
"execution_group": "P1",
"depends_on": [],
"semantic_priority": 0.8,
"files_touched": ["src/auth.ts"],
"task_count": 3
}
],
"conflicts": [
{
"type": "file_conflict",
"file": "src/auth.ts",
"solutions": ["S-1", "S-3"],
"resolution": "sequential",
"resolution_order": ["S-1", "S-3"],
"rationale": "S-1 creates auth module, S-3 extends it"
}
],
"execution_groups": [
{ "id": "P1", "type": "parallel", "solutions": ["S-1", "S-2"], "solution_count": 2 },
{ "id": "S2", "type": "sequential", "solutions": ["S-3"], "solution_count": 1 }
]
}
```
**Update index** (`.workflow/issues/queues/index.json`):
```json
{
"active_queue_id": "QUE-20251228-120000",
"queues": [
{
"id": "QUE-20251228-120000",
"status": "active",
"issue_ids": ["ISS-001", "ISS-002"],
"total_solutions": 3,
"completed_solutions": 0,
"created_at": "2025-12-28T12:00:00Z"
}
]
}
```
### Step 7: Update Issue Statuses
For each queued issue, update status to `queued`:
```bash
ccw issue update <issue-id> --status queued
```
## Queue Item ID Format
- Solution items: `S-1`, `S-2`, `S-3`, ...
- Sequential numbering starting from 1
## Done Criteria
- [ ] Exactly 2 files generated: queue JSON + index update
- [ ] Queue has valid DAG (no circular dependencies)
- [ ] All file conflicts resolved with rationale
- [ ] Semantic priority calculated for each solution (0.0-1.0)
- [ ] Execution groups assigned (P* for parallel, S* for sequential)
- [ ] Issue statuses updated to `queued`
- [ ] Summary JSON returned with correct shape
## Validation Rules
1. **No cycles**: If resolution creates a cycle, abort and report
2. **Parallel safety**: Solutions in same P* group must have NO file overlaps
3. **Sequential order**: Solutions in S* group must be in correct dependency order
4. **Single queue ID**: Use the same queue ID throughout (generated in Step 1)
## Error Handling
| Situation | Action |
|-----------|--------|
| No planned issues | Return empty queue summary |
| Circular dependency detected | Abort, report cycle details |
| Missing solution file | Skip issue, log warning |
| Index file missing | Create new index |
## Start Execution
Begin by listing planned issues:
```bash
ccw issue list --status planned --json
```
Then follow the workflow to generate the queue.

41
.github/workflows/visual-tests.yml vendored Normal file
View File

@@ -0,0 +1,41 @@
name: Visual Regression Tests
on:
pull_request:
push:
branches:
- main
jobs:
visual-tests:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
- name: Run visual tests
run: npm run test:visual
- name: Upload visual artifacts on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: visual-regression-artifacts
path: |
ccw/tests/visual/snapshots/current
ccw/tests/visual/snapshots/diff
retention-days: 7

View File

@@ -5,6 +5,52 @@ All notable changes to Claude Code Workflow (CCW) will be documented in this fil
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [6.3.11] - 2025-12-28
### 🔧 Issue System Enhancements | Issue系统增强
#### CLI Improvements | CLI改进
- **Added**: `ccw issue update <id> --status <status>` command for pure field updates
- **Added**: Support for `--priority`, `--title`, `--description` in update command
- **Added**: Auto-timestamp setting based on status (planned_at, queued_at, completed_at)
#### Issue Plan Command | Issue Plan命令
- **Changed**: Agent execution from sequential to parallel (max 10 concurrent)
- **Added**: Multi-solution user selection prompt with clear notification
- **Added**: Explicit binding check (`solutions.length === 1`) before auto-bind
#### Issue Queue Command | Issue Queue命令
- **Fixed**: Queue ID generation moved from agent to command (avoid duplicate IDs)
- **Fixed**: Strict output file control (exactly 2 files per execution)
- **Added**: Clear documentation for `update` vs `done`/`queue add` usage
#### Discovery System | Discovery系统
- **Enhanced**: Discovery progress reading with new schema support
- **Enhanced**: Discovery index reading and issue exporting
## [6.3.9] - 2025-12-27
### 🔧 Issue System Consistency | Issue系统一致性修复
#### Schema Unification | Schema统一
- **Upgraded**: `solution-schema.json` to Rich Plan model with full lifecycle fields
- **Added**: `test`, `regression`, `commit`, `lifecycle_status` objects to task schema
- **Changed**: `acceptance` from string[] to object `{criteria[], verification[]}`
- **Added**: `analysis` and `score` fields for multi-solution evaluation
- **Removed**: Redundant `issue-task-jsonl-schema.json` and `solutions-jsonl-schema.json`
- **Fixed**: `queue-schema.json` field naming (`queue_id``item_id`)
#### Agent Updates | Agent更新
- **Added**: Multi-solution generation support based on complexity
- **Added**: Search tool fallback chain (ACE → smart_search → Grep → rg → Glob)
- **Added**: `lifecycle_requirements` propagation from issue to tasks
- **Added**: Priority mapping formula (1-5 → 0.0-1.0 semantic priority)
- **Fixed**: Task decomposition to match Rich Plan schema
#### Type Safety | 类型安全
- **Added**: `QueueConflict` and `ExecutionGroup` interfaces to `issue.ts`
- **Fixed**: `conflicts` array typing (from `any[]` to `QueueConflict[]`)
## [6.2.0] - 2025-12-21
### 🎯 Native CodexLens & Dashboard Revolution | 原生CodexLens与Dashboard革新

View File

@@ -9,6 +9,7 @@
[![npm](https://img.shields.io/npm/v/claude-code-workflow.svg)](https://www.npmjs.com/package/claude-code-workflow)
[![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
[![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20Linux%20%7C%20macOS-lightgrey.svg)]()
[![Visual Tests](https://github.com/catlog22/Claude-Code-Workflow/actions/workflows/visual-tests.yml/badge.svg)](https://github.com/catlog22/Claude-Code-Workflow/actions/workflows/visual-tests.yml)
**Languages:** [English](README.md) | [中文](README_CN.md)

View File

@@ -12,6 +12,7 @@ import { cliCommand } from './commands/cli.js';
import { memoryCommand } from './commands/memory.js';
import { coreMemoryCommand } from './commands/core-memory.js';
import { hookCommand } from './commands/hook.js';
import { issueCommand } from './commands/issue.js';
import { readFileSync, existsSync } from 'fs';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
@@ -260,5 +261,33 @@ export function run(argv: string[]): void {
.option('--type <type>', 'Context type: session-start, context')
.action((subcommand, args, options) => hookCommand(subcommand, args, options));
// Issue command - Issue lifecycle management with JSONL task tracking
program
.command('issue [subcommand] [args...]')
.description('Issue lifecycle management with JSONL task tracking')
.option('--title <title>', 'Task title')
.option('--type <type>', 'Task type: feature, bug, refactor, test, chore, docs')
.option('--status <status>', 'Task status')
.option('--phase <phase>', 'Execution phase')
.option('--description <desc>', 'Task description')
.option('--depends-on <ids>', 'Comma-separated dependency task IDs')
.option('--delivery-criteria <items>', 'Pipe-separated delivery criteria')
.option('--pause-criteria <items>', 'Pipe-separated pause criteria')
.option('--executor <type>', 'Executor: agent, codex, gemini, auto')
.option('--priority <n>', 'Task priority (1-5)')
.option('--format <fmt>', 'Output format: json, markdown')
.option('--json', 'Output as JSON')
.option('--brief', 'Brief JSON output (minimal fields)')
.option('--force', 'Force operation')
// New options for solution/queue management
.option('--solution <path>', 'Solution JSON file path')
.option('--solution-id <id>', 'Solution ID')
.option('--data <json>', 'JSON data for create/solution')
.option('--result <json>', 'Execution result JSON')
.option('--reason <text>', 'Failure reason')
.option('--fail', 'Mark task as failed')
.option('--from-queue [queue-id]', 'Sync issue statuses from queue (default: active queue)')
.action((subcommand, args, options) => issueCommand(subcommand, args, options));
program.parse(argv);
}

2029
ccw/src/commands/issue.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -21,6 +21,7 @@ const MODULE_FILES = [
'dashboard-js/components/tabs-other.js',
'dashboard-js/components/carousel.js',
'dashboard-js/components/notifications.js',
'dashboard-js/components/cli-stream-viewer.js',
'dashboard-js/components/global-notifications.js',
'dashboard-js/components/cli-status.js',
'dashboard-js/components/cli-history.js',

View File

@@ -47,7 +47,8 @@ const MODULE_CSS_FILES = [
'28-mcp-manager.css',
'29-help.css',
'30-core-memory.css',
'31-api-settings.css'
'31-api-settings.css',
'34-discovery.css'
];
const MODULE_FILES = [
@@ -97,6 +98,8 @@ const MODULE_FILES = [
'views/rules-manager.js',
'views/claude-manager.js',
'views/api-settings.js',
'views/issue-manager.js',
'views/issue-discovery.js',
'views/help.js',
'main.js'
];

View File

@@ -110,6 +110,34 @@ interface SessionReviewData {
findings: Array<Finding & { dimension: string }>;
}
interface ProjectGuidelines {
conventions: {
coding_style: string[];
naming_patterns: string[];
file_structure: string[];
documentation: string[];
};
constraints: {
architecture: string[];
tech_stack: string[];
performance: string[];
security: string[];
};
quality_rules: Array<{ rule: string; scope: string; enforced_by?: string }>;
learnings: Array<{
date: string;
session_id?: string;
insight: string;
context?: string;
category?: string;
}>;
_metadata?: {
created_at: string;
updated_at?: string;
version: string;
};
}
interface ProjectOverview {
projectName: string;
description: string;
@@ -144,6 +172,7 @@ interface ProjectOverview {
analysis_timestamp: string | null;
analysis_mode: string;
};
guidelines: ProjectGuidelines | null;
}
/**
@@ -156,11 +185,13 @@ export async function aggregateData(sessions: ScanSessionsResult, workflowDir: s
// Initialize cache manager
const cache = createDashboardCache(workflowDir);
// Prepare paths to watch for changes
// Prepare paths to watch for changes (includes both new dual files and legacy)
const watchPaths = [
join(workflowDir, 'active'),
join(workflowDir, 'archives'),
join(workflowDir, 'project.json'),
join(workflowDir, 'project-tech.json'),
join(workflowDir, 'project-guidelines.json'),
join(workflowDir, 'project.json'), // Legacy support
...sessions.active.map(s => s.path),
...sessions.archived.map(s => s.path)
];
@@ -516,12 +547,19 @@ function sortTaskIds(a: string, b: string): number {
}
/**
* Load project overview from project.json
* Load project overview from project-tech.json and project-guidelines.json
* Supports dual file structure with backward compatibility for legacy project.json
* @param workflowDir - Path to .workflow directory
* @returns Project overview data or null if not found
*/
function loadProjectOverview(workflowDir: string): ProjectOverview | null {
const projectFile = join(workflowDir, 'project.json');
const techFile = join(workflowDir, 'project-tech.json');
const guidelinesFile = join(workflowDir, 'project-guidelines.json');
const legacyFile = join(workflowDir, 'project.json');
// Check for new dual file structure first, fallback to legacy
const useLegacy = !existsSync(techFile) && existsSync(legacyFile);
const projectFile = useLegacy ? legacyFile : techFile;
if (!existsSync(projectFile)) {
console.log(`Project file not found at: ${projectFile}`);
@@ -532,15 +570,59 @@ function loadProjectOverview(workflowDir: string): ProjectOverview | null {
const fileContent = readFileSync(projectFile, 'utf8');
const projectData = JSON.parse(fileContent) as Record<string, unknown>;
console.log(`Successfully loaded project overview: ${projectData.project_name || 'Unknown'}`);
console.log(`Successfully loaded project overview: ${projectData.project_name || 'Unknown'} (${useLegacy ? 'legacy' : 'tech'})`);
// Parse tech data (compatible with both legacy and new structure)
const overview = projectData.overview as Record<string, unknown> | undefined;
const technologyStack = overview?.technology_stack as Record<string, unknown[]> | undefined;
const architecture = overview?.architecture as Record<string, unknown> | undefined;
const developmentIndex = projectData.development_index as Record<string, unknown[]> | undefined;
const statistics = projectData.statistics as Record<string, unknown> | undefined;
const technologyAnalysis = projectData.technology_analysis as Record<string, unknown> | undefined;
const developmentStatus = projectData.development_status as Record<string, unknown> | undefined;
// Support both old and new schema field names
const technologyStack = (overview?.technology_stack || technologyAnalysis?.technology_stack) as Record<string, unknown[]> | undefined;
const architecture = (overview?.architecture || technologyAnalysis?.architecture) as Record<string, unknown> | undefined;
const developmentIndex = (projectData.development_index || developmentStatus?.development_index) as Record<string, unknown[]> | undefined;
const statistics = (projectData.statistics || developmentStatus?.statistics) as Record<string, unknown> | undefined;
const metadata = projectData._metadata as Record<string, unknown> | undefined;
// Load guidelines from separate file if exists
let guidelines: ProjectGuidelines | null = null;
if (existsSync(guidelinesFile)) {
try {
const guidelinesContent = readFileSync(guidelinesFile, 'utf8');
const guidelinesData = JSON.parse(guidelinesContent) as Record<string, unknown>;
const conventions = guidelinesData.conventions as Record<string, string[]> | undefined;
const constraints = guidelinesData.constraints as Record<string, string[]> | undefined;
guidelines = {
conventions: {
coding_style: conventions?.coding_style || [],
naming_patterns: conventions?.naming_patterns || [],
file_structure: conventions?.file_structure || [],
documentation: conventions?.documentation || []
},
constraints: {
architecture: constraints?.architecture || [],
tech_stack: constraints?.tech_stack || [],
performance: constraints?.performance || [],
security: constraints?.security || []
},
quality_rules: (guidelinesData.quality_rules as Array<{ rule: string; scope: string; enforced_by?: string }>) || [],
learnings: (guidelinesData.learnings as Array<{
date: string;
session_id?: string;
insight: string;
context?: string;
category?: string;
}>) || [],
_metadata: guidelinesData._metadata as ProjectGuidelines['_metadata'] | undefined
};
console.log(`Successfully loaded project guidelines`);
} catch (guidelinesErr) {
console.error(`Failed to parse project-guidelines.json:`, (guidelinesErr as Error).message);
}
}
return {
projectName: (projectData.project_name as string) || 'Unknown',
description: (overview?.description as string) || '',
@@ -574,10 +656,11 @@ function loadProjectOverview(workflowDir: string): ProjectOverview | null {
initialized_by: (metadata?.initialized_by as string) || 'unknown',
analysis_timestamp: (metadata?.analysis_timestamp as string) || null,
analysis_mode: (metadata?.analysis_mode as string) || 'unknown'
}
},
guidelines
};
} catch (err) {
console.error(`Failed to parse project.json at ${projectFile}:`, (err as Error).message);
console.error(`Failed to parse project file at ${projectFile}:`, (err as Error).message);
console.error('Error stack:', (err as Error).stack);
return null;
}

View File

@@ -0,0 +1,607 @@
// @ts-nocheck
/**
* Discovery Routes Module
*
* Storage Structure:
* .workflow/issues/discoveries/
* ├── index.json # Discovery session index
* └── {discovery-id}/
* ├── discovery-state.json # State machine
* ├── discovery-progress.json # Real-time progress
* ├── perspectives/ # Per-perspective results
* │ ├── bug.json
* │ └── ...
* ├── external-research.json # Exa research results
* ├── discovery-issues.jsonl # Generated candidate issues
* └── reports/
*
* API Endpoints:
* - GET /api/discoveries - List all discovery sessions
* - GET /api/discoveries/:id - Get discovery session detail
* - GET /api/discoveries/:id/findings - Get all findings
* - GET /api/discoveries/:id/progress - Get real-time progress
* - POST /api/discoveries/:id/export - Export findings as issues
* - PATCH /api/discoveries/:id/findings/:fid - Update finding status
* - DELETE /api/discoveries/:id - Delete discovery session
*/
import type { IncomingMessage, ServerResponse } from 'http';
import { readFileSync, existsSync, writeFileSync, mkdirSync, readdirSync, rmSync } from 'fs';
import { join } from 'path';
export interface RouteContext {
pathname: string;
url: URL;
req: IncomingMessage;
res: ServerResponse;
initialPath: string;
handlePostRequest: (req: IncomingMessage, res: ServerResponse, handler: (body: unknown) => Promise<any>) => void;
broadcastToClients: (data: unknown) => void;
}
// ========== Helper Functions ==========
function getDiscoveriesDir(projectPath: string): string {
return join(projectPath, '.workflow', 'issues', 'discoveries');
}
function readDiscoveryIndex(discoveriesDir: string): { discoveries: any[]; total: number } {
const indexPath = join(discoveriesDir, 'index.json');
// Try to read index.json first
if (existsSync(indexPath)) {
try {
return JSON.parse(readFileSync(indexPath, 'utf8'));
} catch {
// Fall through to scan
}
}
// Fallback: scan directory for discovery folders
if (!existsSync(discoveriesDir)) {
return { discoveries: [], total: 0 };
}
try {
const entries = readdirSync(discoveriesDir, { withFileTypes: true });
const discoveries: any[] = [];
for (const entry of entries) {
if (entry.isDirectory() && entry.name.startsWith('DSC-')) {
const statePath = join(discoveriesDir, entry.name, 'discovery-state.json');
if (existsSync(statePath)) {
try {
const state = JSON.parse(readFileSync(statePath, 'utf8'));
discoveries.push({
discovery_id: entry.name,
target_pattern: state.target_pattern,
perspectives: state.metadata?.perspectives || [],
created_at: state.metadata?.created_at,
completed_at: state.completed_at
});
} catch {
// Skip invalid entries
}
}
}
}
// Sort by creation time descending
discoveries.sort((a, b) => {
const timeA = new Date(a.created_at || 0).getTime();
const timeB = new Date(b.created_at || 0).getTime();
return timeB - timeA;
});
return { discoveries, total: discoveries.length };
} catch {
return { discoveries: [], total: 0 };
}
}
function writeDiscoveryIndex(discoveriesDir: string, index: any) {
if (!existsSync(discoveriesDir)) {
mkdirSync(discoveriesDir, { recursive: true });
}
writeFileSync(join(discoveriesDir, 'index.json'), JSON.stringify(index, null, 2));
}
function readDiscoveryState(discoveriesDir: string, discoveryId: string): any | null {
const statePath = join(discoveriesDir, discoveryId, 'discovery-state.json');
if (!existsSync(statePath)) return null;
try {
return JSON.parse(readFileSync(statePath, 'utf8'));
} catch {
return null;
}
}
function readDiscoveryProgress(discoveriesDir: string, discoveryId: string): any | null {
// Try merged state first (new schema)
const statePath = join(discoveriesDir, discoveryId, 'discovery-state.json');
if (existsSync(statePath)) {
try {
const state = JSON.parse(readFileSync(statePath, 'utf8'));
// New merged schema: perspectives array + results object
if (state.perspectives && Array.isArray(state.perspectives)) {
const completed = state.perspectives.filter((p: any) => p.status === 'completed').length;
const total = state.perspectives.length;
return {
discovery_id: discoveryId,
phase: state.phase,
last_update: state.updated_at || state.created_at,
progress: {
perspective_analysis: {
total,
completed,
in_progress: state.perspectives.filter((p: any) => p.status === 'in_progress').length,
percent_complete: total > 0 ? Math.round((completed / total) * 100) : 0
},
external_research: state.external_research || { enabled: false, completed: false },
aggregation: { completed: state.phase === 'aggregation' || state.phase === 'complete' },
issue_generation: { completed: state.phase === 'complete', issues_count: state.results?.issues_generated || 0 }
},
agent_status: state.perspectives
};
}
// Old schema: metadata.perspectives (backward compat)
if (state.metadata?.perspectives) {
return {
discovery_id: discoveryId,
phase: state.phase,
progress: { perspective_analysis: { total: state.metadata.perspectives.length, completed: state.perspectives_completed?.length || 0 } }
};
}
} catch {
// Fall through
}
}
// Fallback: try legacy progress file
const progressPath = join(discoveriesDir, discoveryId, 'discovery-progress.json');
if (existsSync(progressPath)) {
try { return JSON.parse(readFileSync(progressPath, 'utf8')); } catch { return null; }
}
return null;
}
function readPerspectiveFindings(discoveriesDir: string, discoveryId: string): any[] {
const perspectivesDir = join(discoveriesDir, discoveryId, 'perspectives');
if (!existsSync(perspectivesDir)) return [];
const allFindings: any[] = [];
const files = readdirSync(perspectivesDir).filter(f => f.endsWith('.json'));
for (const file of files) {
try {
const content = JSON.parse(readFileSync(join(perspectivesDir, file), 'utf8'));
const perspective = file.replace('.json', '');
if (content.findings && Array.isArray(content.findings)) {
allFindings.push({
perspective,
summary: content.summary || {},
findings: content.findings.map((f: any) => ({
...f,
perspective: f.perspective || perspective
}))
});
}
} catch {
// Skip invalid files
}
}
return allFindings;
}
function readDiscoveryIssues(discoveriesDir: string, discoveryId: string): any[] {
const issuesPath = join(discoveriesDir, discoveryId, 'discovery-issues.jsonl');
if (!existsSync(issuesPath)) return [];
try {
const content = readFileSync(issuesPath, 'utf8');
return content.split('\n').filter(line => line.trim()).map(line => JSON.parse(line));
} catch {
return [];
}
}
function writeDiscoveryIssues(discoveriesDir: string, discoveryId: string, issues: any[]) {
const issuesPath = join(discoveriesDir, discoveryId, 'discovery-issues.jsonl');
writeFileSync(issuesPath, issues.map(i => JSON.stringify(i)).join('\n'));
}
function flattenFindings(perspectiveResults: any[]): any[] {
const allFindings: any[] = [];
for (const result of perspectiveResults) {
if (result.findings) {
allFindings.push(...result.findings);
}
}
return allFindings;
}
function appendToIssuesJsonl(projectPath: string, issues: any[]): { added: number; skipped: number; skippedIds: string[] } {
const issuesDir = join(projectPath, '.workflow', 'issues');
const issuesPath = join(issuesDir, 'issues.jsonl');
if (!existsSync(issuesDir)) {
mkdirSync(issuesDir, { recursive: true });
}
// Read existing issues
let existingIssues: any[] = [];
if (existsSync(issuesPath)) {
try {
const content = readFileSync(issuesPath, 'utf8');
existingIssues = content.split('\n').filter(line => line.trim()).map(line => JSON.parse(line));
} catch {
// Start fresh
}
}
// Build set of existing IDs and source_finding combinations for deduplication
const existingIds = new Set(existingIssues.map(i => i.id));
const existingSourceFindings = new Set(
existingIssues
.filter(i => i.source === 'discovery' && i.source_finding_id)
.map(i => `${i.source_discovery_id}:${i.source_finding_id}`)
);
// Convert and filter duplicates
const skippedIds: string[] = [];
const newIssues: any[] = [];
for (const di of issues) {
// Check for duplicate by ID
if (existingIds.has(di.id)) {
skippedIds.push(di.id);
continue;
}
// Check for duplicate by source_discovery_id + source_finding_id
const sourceKey = `${di.source_discovery_id}:${di.source_finding_id}`;
if (di.source_finding_id && existingSourceFindings.has(sourceKey)) {
skippedIds.push(di.id);
continue;
}
newIssues.push({
id: di.id,
title: di.title,
status: 'registered',
priority: di.priority || 3,
context: di.context || di.description || '',
source: 'discovery',
source_discovery_id: di.source_discovery_id,
source_finding_id: di.source_finding_id,
perspective: di.perspective,
file: di.file,
line: di.line,
labels: di.labels || [],
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
});
}
if (newIssues.length > 0) {
const allIssues = [...existingIssues, ...newIssues];
writeFileSync(issuesPath, allIssues.map(i => JSON.stringify(i)).join('\n'));
}
return { added: newIssues.length, skipped: skippedIds.length, skippedIds };
}
// ========== Route Handler ==========
export async function handleDiscoveryRoutes(ctx: RouteContext): Promise<boolean> {
const { pathname, url, req, res, initialPath, handlePostRequest } = ctx;
const projectPath = url.searchParams.get('path') || initialPath;
const discoveriesDir = getDiscoveriesDir(projectPath);
// GET /api/discoveries - List all discovery sessions
if (pathname === '/api/discoveries' && req.method === 'GET') {
const index = readDiscoveryIndex(discoveriesDir);
// Enrich with state info
const enrichedDiscoveries = index.discoveries.map((d: any) => {
const state = readDiscoveryState(discoveriesDir, d.discovery_id);
const progress = readDiscoveryProgress(discoveriesDir, d.discovery_id);
return {
...d,
phase: state?.phase || 'unknown',
total_findings: state?.total_findings || 0,
issues_generated: state?.issues_generated || 0,
priority_distribution: state?.priority_distribution || {},
progress: progress?.progress || null
};
});
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
discoveries: enrichedDiscoveries,
total: enrichedDiscoveries.length,
_metadata: { updated_at: new Date().toISOString() }
}));
return true;
}
// GET /api/discoveries/:id - Get discovery detail
const detailMatch = pathname.match(/^\/api\/discoveries\/([^/]+)$/);
if (detailMatch && req.method === 'GET') {
const discoveryId = detailMatch[1];
const state = readDiscoveryState(discoveriesDir, discoveryId);
if (!state) {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: `Discovery ${discoveryId} not found` }));
return true;
}
const progress = readDiscoveryProgress(discoveriesDir, discoveryId);
const perspectiveResults = readPerspectiveFindings(discoveriesDir, discoveryId);
const discoveryIssues = readDiscoveryIssues(discoveriesDir, discoveryId);
// Read external research if exists
let externalResearch = null;
const externalPath = join(discoveriesDir, discoveryId, 'external-research.json');
if (existsSync(externalPath)) {
try {
externalResearch = JSON.parse(readFileSync(externalPath, 'utf8'));
} catch {
// Ignore
}
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
...state,
progress: progress?.progress || null,
perspectives: perspectiveResults,
external_research: externalResearch,
discovery_issues: discoveryIssues
}));
return true;
}
// GET /api/discoveries/:id/findings - Get all findings
const findingsMatch = pathname.match(/^\/api\/discoveries\/([^/]+)\/findings$/);
if (findingsMatch && req.method === 'GET') {
const discoveryId = findingsMatch[1];
if (!existsSync(join(discoveriesDir, discoveryId))) {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: `Discovery ${discoveryId} not found` }));
return true;
}
const perspectiveResults = readPerspectiveFindings(discoveriesDir, discoveryId);
const allFindings = flattenFindings(perspectiveResults);
// Support filtering
const perspectiveFilter = url.searchParams.get('perspective');
const priorityFilter = url.searchParams.get('priority');
let filtered = allFindings;
if (perspectiveFilter) {
filtered = filtered.filter(f => f.perspective === perspectiveFilter);
}
if (priorityFilter) {
filtered = filtered.filter(f => f.priority === priorityFilter);
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
findings: filtered,
total: filtered.length,
perspectives: [...new Set(allFindings.map(f => f.perspective))],
_metadata: { discovery_id: discoveryId }
}));
return true;
}
// GET /api/discoveries/:id/progress - Get real-time progress
const progressMatch = pathname.match(/^\/api\/discoveries\/([^/]+)\/progress$/);
if (progressMatch && req.method === 'GET') {
const discoveryId = progressMatch[1];
const progress = readDiscoveryProgress(discoveriesDir, discoveryId);
if (!progress) {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: `Progress for ${discoveryId} not found` }));
return true;
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(progress));
return true;
}
// POST /api/discoveries/:id/export - Export findings as issues
const exportMatch = pathname.match(/^\/api\/discoveries\/([^/]+)\/export$/);
if (exportMatch && req.method === 'POST') {
handlePostRequest(req, res, async (body: any) => {
const discoveryId = exportMatch[1];
const { finding_ids, export_all } = body as { finding_ids?: string[]; export_all?: boolean };
if (!existsSync(join(discoveriesDir, discoveryId))) {
return { error: `Discovery ${discoveryId} not found` };
}
const perspectiveResults = readPerspectiveFindings(discoveriesDir, discoveryId);
const allFindings = flattenFindings(perspectiveResults);
let toExport: any[];
if (export_all) {
toExport = allFindings;
} else if (finding_ids && finding_ids.length > 0) {
toExport = allFindings.filter(f => finding_ids.includes(f.id));
} else {
return { error: 'Either finding_ids or export_all required' };
}
if (toExport.length === 0) {
return { error: 'No findings to export' };
}
// Convert findings to issue format
const issuesToExport = toExport.map((f, idx) => {
const suggestedIssue = f.suggested_issue || {};
return {
id: `ISS-${Date.now()}-${idx}`,
title: suggestedIssue.title || f.title,
priority: suggestedIssue.priority || 3,
context: f.description || '',
source: 'discovery',
source_discovery_id: discoveryId,
source_finding_id: f.id, // Track original finding ID for deduplication
perspective: f.perspective,
file: f.file,
line: f.line,
labels: suggestedIssue.labels || [f.perspective]
};
});
// Append to main issues.jsonl (with deduplication)
const result = appendToIssuesJsonl(projectPath, issuesToExport);
// Mark exported findings in perspective files
if (result.added > 0) {
const exportedFindingIds = new Set(
issuesToExport
.filter((_, idx) => !result.skippedIds.includes(issuesToExport[idx].id))
.map(i => i.source_finding_id)
);
// Update each perspective file to mark findings as exported
const perspectivesDir = join(discoveriesDir, discoveryId, 'perspectives');
if (existsSync(perspectivesDir)) {
const files = readdirSync(perspectivesDir).filter(f => f.endsWith('.json'));
for (const file of files) {
const filePath = join(perspectivesDir, file);
try {
const content = JSON.parse(readFileSync(filePath, 'utf8'));
if (content.findings) {
let modified = false;
for (const finding of content.findings) {
if (exportedFindingIds.has(finding.id) && !finding.exported) {
finding.exported = true;
finding.exported_at = new Date().toISOString();
modified = true;
}
}
if (modified) {
writeFileSync(filePath, JSON.stringify(content, null, 2));
}
}
} catch {
// Skip invalid files
}
}
}
}
// Update discovery state
const state = readDiscoveryState(discoveriesDir, discoveryId);
if (state) {
state.issues_generated = (state.issues_generated || 0) + result.added;
writeFileSync(
join(discoveriesDir, discoveryId, 'discovery-state.json'),
JSON.stringify(state, null, 2)
);
}
return {
success: true,
exported_count: result.added,
skipped_count: result.skipped,
skipped_ids: result.skippedIds,
message: result.skipped > 0
? `Exported ${result.added} issues, skipped ${result.skipped} duplicates`
: `Exported ${result.added} issues`
};
});
return true;
}
// PATCH /api/discoveries/:id/findings/:fid - Update finding status
const updateFindingMatch = pathname.match(/^\/api\/discoveries\/([^/]+)\/findings\/([^/]+)$/);
if (updateFindingMatch && req.method === 'PATCH') {
handlePostRequest(req, res, async (body: any) => {
const [, discoveryId, findingId] = updateFindingMatch;
const { status, dismissed } = body as { status?: string; dismissed?: boolean };
const perspectivesDir = join(discoveriesDir, discoveryId, 'perspectives');
if (!existsSync(perspectivesDir)) {
return { error: `Discovery ${discoveryId} not found` };
}
// Find and update the finding
const files = readdirSync(perspectivesDir).filter(f => f.endsWith('.json'));
let updated = false;
for (const file of files) {
const filePath = join(perspectivesDir, file);
try {
const content = JSON.parse(readFileSync(filePath, 'utf8'));
if (content.findings) {
const findingIndex = content.findings.findIndex((f: any) => f.id === findingId);
if (findingIndex !== -1) {
if (status !== undefined) {
content.findings[findingIndex].status = status;
}
if (dismissed !== undefined) {
content.findings[findingIndex].dismissed = dismissed;
}
content.findings[findingIndex].updated_at = new Date().toISOString();
writeFileSync(filePath, JSON.stringify(content, null, 2));
updated = true;
break;
}
}
} catch {
// Skip invalid files
}
}
if (!updated) {
return { error: `Finding ${findingId} not found` };
}
return { success: true, finding_id: findingId };
});
return true;
}
// DELETE /api/discoveries/:id - Delete discovery session
const deleteMatch = pathname.match(/^\/api\/discoveries\/([^/]+)$/);
if (deleteMatch && req.method === 'DELETE') {
const discoveryId = deleteMatch[1];
const discoveryPath = join(discoveriesDir, discoveryId);
if (!existsSync(discoveryPath)) {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: `Discovery ${discoveryId} not found` }));
return true;
}
try {
// Remove directory
rmSync(discoveryPath, { recursive: true, force: true });
// Update index
const index = readDiscoveryIndex(discoveriesDir);
index.discoveries = index.discoveries.filter((d: any) => d.discovery_id !== discoveryId);
index.total = index.discoveries.length;
writeDiscoveryIndex(discoveriesDir, index);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: true, deleted: discoveryId }));
} catch (err) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Failed to delete discovery' }));
}
return true;
}
// Not handled
return false;
}

View File

@@ -0,0 +1,717 @@
// @ts-nocheck
/**
* Issue Routes Module (Optimized - Flat JSONL Storage)
*
* Storage Structure:
* .workflow/issues/
* ├── issues.jsonl # All issues (one per line)
* ├── queues/ # Queue history directory
* │ ├── index.json # Queue index (active + history)
* │ └── {queue-id}.json # Individual queue files
* └── solutions/
* ├── {issue-id}.jsonl # Solutions for issue (one per line)
* └── ...
*
* API Endpoints (8 total):
* - GET /api/issues - List all issues
* - POST /api/issues - Create new issue
* - GET /api/issues/:id - Get issue detail
* - PATCH /api/issues/:id - Update issue (includes binding logic)
* - DELETE /api/issues/:id - Delete issue
* - POST /api/issues/:id/solutions - Add solution
* - PATCH /api/issues/:id/tasks/:taskId - Update task
* - GET /api/queue - Get execution queue
* - POST /api/queue/reorder - Reorder queue items
*/
import type { IncomingMessage, ServerResponse } from 'http';
import { readFileSync, existsSync, writeFileSync, mkdirSync, unlinkSync } from 'fs';
import { join } from 'path';
export interface RouteContext {
pathname: string;
url: URL;
req: IncomingMessage;
res: ServerResponse;
initialPath: string;
handlePostRequest: (req: IncomingMessage, res: ServerResponse, handler: (body: unknown) => Promise<any>) => void;
broadcastToClients: (data: unknown) => void;
}
// ========== JSONL Helper Functions ==========
function readIssuesJsonl(issuesDir: string): any[] {
const issuesPath = join(issuesDir, 'issues.jsonl');
if (!existsSync(issuesPath)) return [];
try {
const content = readFileSync(issuesPath, 'utf8');
return content.split('\n').filter(line => line.trim()).map(line => JSON.parse(line));
} catch {
return [];
}
}
function writeIssuesJsonl(issuesDir: string, issues: any[]) {
if (!existsSync(issuesDir)) mkdirSync(issuesDir, { recursive: true });
const issuesPath = join(issuesDir, 'issues.jsonl');
writeFileSync(issuesPath, issues.map(i => JSON.stringify(i)).join('\n'));
}
function readSolutionsJsonl(issuesDir: string, issueId: string): any[] {
const solutionsPath = join(issuesDir, 'solutions', `${issueId}.jsonl`);
if (!existsSync(solutionsPath)) return [];
try {
const content = readFileSync(solutionsPath, 'utf8');
return content.split('\n').filter(line => line.trim()).map(line => JSON.parse(line));
} catch {
return [];
}
}
function readIssueHistoryJsonl(issuesDir: string): any[] {
const historyPath = join(issuesDir, 'issue-history.jsonl');
if (!existsSync(historyPath)) return [];
try {
const content = readFileSync(historyPath, 'utf8');
return content.split('\n').filter(line => line.trim()).map(line => JSON.parse(line));
} catch {
return [];
}
}
function writeSolutionsJsonl(issuesDir: string, issueId: string, solutions: any[]) {
const solutionsDir = join(issuesDir, 'solutions');
if (!existsSync(solutionsDir)) mkdirSync(solutionsDir, { recursive: true });
writeFileSync(join(solutionsDir, `${issueId}.jsonl`), solutions.map(s => JSON.stringify(s)).join('\n'));
}
function readQueue(issuesDir: string) {
// Try new multi-queue structure first
const queuesDir = join(issuesDir, 'queues');
const indexPath = join(queuesDir, 'index.json');
if (existsSync(indexPath)) {
try {
const index = JSON.parse(readFileSync(indexPath, 'utf8'));
const activeQueueId = index.active_queue_id;
if (activeQueueId) {
const queueFilePath = join(queuesDir, `${activeQueueId}.json`);
if (existsSync(queueFilePath)) {
return JSON.parse(readFileSync(queueFilePath, 'utf8'));
}
}
} catch {
// Fall through to legacy check
}
}
// Fallback to legacy queue.json
const legacyQueuePath = join(issuesDir, 'queue.json');
if (existsSync(legacyQueuePath)) {
try {
return JSON.parse(readFileSync(legacyQueuePath, 'utf8'));
} catch {
// Return empty queue
}
}
return { tasks: [], conflicts: [], execution_groups: [], _metadata: { version: '1.0', total_tasks: 0 } };
}
function writeQueue(issuesDir: string, queue: any) {
if (!existsSync(issuesDir)) mkdirSync(issuesDir, { recursive: true });
// Support both solution-based and task-based queues
const items = queue.solutions || queue.tasks || [];
const isSolutionBased = Array.isArray(queue.solutions) && queue.solutions.length > 0;
queue._metadata = {
...queue._metadata,
updated_at: new Date().toISOString(),
...(isSolutionBased
? { total_solutions: items.length }
: { total_tasks: items.length })
};
// Check if using new multi-queue structure
const queuesDir = join(issuesDir, 'queues');
const indexPath = join(queuesDir, 'index.json');
if (existsSync(indexPath) && queue.id) {
// Write to new structure
const queueFilePath = join(queuesDir, `${queue.id}.json`);
writeFileSync(queueFilePath, JSON.stringify(queue, null, 2));
// Update index metadata
try {
const index = JSON.parse(readFileSync(indexPath, 'utf8'));
const queueEntry = index.queues?.find((q: any) => q.id === queue.id);
if (queueEntry) {
if (isSolutionBased) {
queueEntry.total_solutions = items.length;
queueEntry.completed_solutions = items.filter((i: any) => i.status === 'completed').length;
} else {
queueEntry.total_tasks = items.length;
queueEntry.completed_tasks = items.filter((i: any) => i.status === 'completed').length;
}
writeFileSync(indexPath, JSON.stringify(index, null, 2));
}
} catch {
// Ignore index update errors
}
} else {
// Fallback to legacy queue.json
writeFileSync(join(issuesDir, 'queue.json'), JSON.stringify(queue, null, 2));
}
}
function getIssueDetail(issuesDir: string, issueId: string) {
const issues = readIssuesJsonl(issuesDir);
const issue = issues.find(i => i.id === issueId);
if (!issue) return null;
const solutions = readSolutionsJsonl(issuesDir, issueId);
let tasks: any[] = [];
if (issue.bound_solution_id) {
const boundSol = solutions.find(s => s.id === issue.bound_solution_id);
if (boundSol?.tasks) tasks = boundSol.tasks;
}
return { ...issue, solutions, tasks };
}
function enrichIssues(issues: any[], issuesDir: string) {
return issues.map(issue => {
const solutions = readSolutionsJsonl(issuesDir, issue.id);
let taskCount = 0;
// Get task count from bound solution
if (issue.bound_solution_id) {
const boundSol = solutions.find(s => s.id === issue.bound_solution_id);
if (boundSol?.tasks) {
taskCount = boundSol.tasks.length;
}
}
return {
...issue,
solution_count: solutions.length,
task_count: taskCount
};
});
}
/**
* Get queue items (supports both solution-based and task-based queues)
*/
function getQueueItems(queue: any): any[] {
return queue.solutions || queue.tasks || [];
}
/**
* Check if queue is solution-based
*/
function isSolutionBasedQueue(queue: any): boolean {
return Array.isArray(queue.solutions) && queue.solutions.length > 0;
}
function groupQueueByExecutionGroup(queue: any) {
const groups: { [key: string]: any[] } = {};
const items = getQueueItems(queue);
const isSolutionBased = isSolutionBasedQueue(queue);
for (const item of items) {
const groupId = item.execution_group || 'ungrouped';
if (!groups[groupId]) groups[groupId] = [];
groups[groupId].push(item);
}
for (const groupId of Object.keys(groups)) {
groups[groupId].sort((a, b) => (a.execution_order || 0) - (b.execution_order || 0));
}
const executionGroups = Object.entries(groups).map(([id, groupItems]) => ({
id,
type: id.startsWith('P') ? 'parallel' : id.startsWith('S') ? 'sequential' : 'unknown',
// Use appropriate count field based on queue type
...(isSolutionBased
? { solution_count: groupItems.length, solutions: groupItems.map(i => i.item_id) }
: { task_count: groupItems.length, tasks: groupItems.map(i => i.item_id) })
})).sort((a, b) => {
const aFirst = groups[a.id]?.[0]?.execution_order || 0;
const bFirst = groups[b.id]?.[0]?.execution_order || 0;
return aFirst - bFirst;
});
return { ...queue, execution_groups: executionGroups, grouped_items: groups };
}
/**
* Bind solution to issue with proper side effects
*/
function bindSolutionToIssue(issuesDir: string, issueId: string, solutionId: string, issues: any[], issueIndex: number) {
const solutions = readSolutionsJsonl(issuesDir, issueId);
const solIndex = solutions.findIndex(s => s.id === solutionId);
if (solIndex === -1) return { error: `Solution ${solutionId} not found` };
// Unbind all, bind new
solutions.forEach(s => { s.is_bound = false; });
solutions[solIndex].is_bound = true;
solutions[solIndex].bound_at = new Date().toISOString();
writeSolutionsJsonl(issuesDir, issueId, solutions);
// Update issue
issues[issueIndex].bound_solution_id = solutionId;
issues[issueIndex].status = 'planned';
issues[issueIndex].planned_at = new Date().toISOString();
return { success: true, bound: solutionId };
}
// ========== Route Handler ==========
export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
const { pathname, url, req, res, initialPath, handlePostRequest } = ctx;
const projectPath = url.searchParams.get('path') || initialPath;
const issuesDir = join(projectPath, '.workflow', 'issues');
// ===== Queue Routes (top-level /api/queue) =====
// GET /api/queue - Get execution queue
if (pathname === '/api/queue' && req.method === 'GET') {
const queue = groupQueueByExecutionGroup(readQueue(issuesDir));
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(queue));
return true;
}
// GET /api/queue/history - Get queue history (all queues from index)
if (pathname === '/api/queue/history' && req.method === 'GET') {
const queuesDir = join(issuesDir, 'queues');
const indexPath = join(queuesDir, 'index.json');
if (!existsSync(indexPath)) {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ queues: [], active_queue_id: null }));
return true;
}
try {
const index = JSON.parse(readFileSync(indexPath, 'utf8'));
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(index));
} catch {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ queues: [], active_queue_id: null }));
}
return true;
}
// GET /api/queue/:id - Get specific queue by ID
const queueDetailMatch = pathname.match(/^\/api\/queue\/([^/]+)$/);
if (queueDetailMatch && req.method === 'GET' && queueDetailMatch[1] !== 'history' && queueDetailMatch[1] !== 'reorder') {
const queueId = queueDetailMatch[1];
const queuesDir = join(issuesDir, 'queues');
const queueFilePath = join(queuesDir, `${queueId}.json`);
if (!existsSync(queueFilePath)) {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: `Queue ${queueId} not found` }));
return true;
}
try {
const queue = JSON.parse(readFileSync(queueFilePath, 'utf8'));
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(groupQueueByExecutionGroup(queue)));
} catch {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Failed to read queue' }));
}
return true;
}
// POST /api/queue/switch - Switch active queue
if (pathname === '/api/queue/switch' && req.method === 'POST') {
handlePostRequest(req, res, async (body: any) => {
const { queueId } = body;
if (!queueId) return { error: 'queueId required' };
const queuesDir = join(issuesDir, 'queues');
const indexPath = join(queuesDir, 'index.json');
const queueFilePath = join(queuesDir, `${queueId}.json`);
if (!existsSync(queueFilePath)) {
return { error: `Queue ${queueId} not found` };
}
try {
const index = existsSync(indexPath)
? JSON.parse(readFileSync(indexPath, 'utf8'))
: { active_queue_id: null, queues: [] };
index.active_queue_id = queueId;
writeFileSync(indexPath, JSON.stringify(index, null, 2));
return { success: true, active_queue_id: queueId };
} catch (err) {
return { error: 'Failed to switch queue' };
}
});
return true;
}
// POST /api/queue/reorder - Reorder queue items (supports both solutions and tasks)
if (pathname === '/api/queue/reorder' && req.method === 'POST') {
handlePostRequest(req, res, async (body: any) => {
const { groupId, newOrder } = body;
if (!groupId || !Array.isArray(newOrder)) {
return { error: 'groupId and newOrder (array) required' };
}
const queue = readQueue(issuesDir);
const items = getQueueItems(queue);
const isSolutionBased = isSolutionBasedQueue(queue);
const groupItems = items.filter((item: any) => item.execution_group === groupId);
const otherItems = items.filter((item: any) => item.execution_group !== groupId);
if (groupItems.length === 0) return { error: `No items in group ${groupId}` };
const groupItemIds = new Set(groupItems.map((i: any) => i.item_id));
if (groupItemIds.size !== new Set(newOrder).size) {
return { error: 'newOrder must contain all group items' };
}
for (const id of newOrder) {
if (!groupItemIds.has(id)) return { error: `Invalid item_id: ${id}` };
}
const itemMap = new Map(groupItems.map((i: any) => [i.item_id, i]));
const reorderedItems = newOrder.map((qid: string, idx: number) => ({ ...itemMap.get(qid), _idx: idx }));
const newQueueItems = [...otherItems, ...reorderedItems].sort((a, b) => {
const aGroup = parseInt(a.execution_group?.match(/\d+/)?.[0] || '999');
const bGroup = parseInt(b.execution_group?.match(/\d+/)?.[0] || '999');
if (aGroup !== bGroup) return aGroup - bGroup;
if (a.execution_group === b.execution_group) {
return (a._idx ?? a.execution_order ?? 999) - (b._idx ?? b.execution_order ?? 999);
}
return (a.execution_order || 0) - (b.execution_order || 0);
});
newQueueItems.forEach((item, idx) => { item.execution_order = idx + 1; delete item._idx; });
// Write back to appropriate array based on queue type
if (isSolutionBased) {
queue.solutions = newQueueItems;
} else {
queue.tasks = newQueueItems;
}
writeQueue(issuesDir, queue);
return { success: true, groupId, reordered: newOrder.length };
});
return true;
}
// Legacy: GET /api/issues/queue (backward compat)
if (pathname === '/api/issues/queue' && req.method === 'GET') {
const queue = groupQueueByExecutionGroup(readQueue(issuesDir));
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(queue));
return true;
}
// ===== Issue Routes =====
// GET /api/issues - List all issues
if (pathname === '/api/issues' && req.method === 'GET') {
const issues = enrichIssues(readIssuesJsonl(issuesDir), issuesDir);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
issues,
_metadata: { version: '2.0', storage: 'jsonl', total_issues: issues.length, last_updated: new Date().toISOString() }
}));
return true;
}
// GET /api/issues/history - List completed issues from history
if (pathname === '/api/issues/history' && req.method === 'GET') {
const history = readIssueHistoryJsonl(issuesDir);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
issues: history,
_metadata: { version: '1.0', storage: 'jsonl', total_issues: history.length, last_updated: new Date().toISOString() }
}));
return true;
}
// POST /api/issues - Create issue
if (pathname === '/api/issues' && req.method === 'POST') {
handlePostRequest(req, res, async (body: any) => {
if (!body.id || !body.title) return { error: 'id and title required' };
const issues = readIssuesJsonl(issuesDir);
if (issues.find(i => i.id === body.id)) return { error: `Issue ${body.id} exists` };
const newIssue = {
id: body.id,
title: body.title,
status: body.status || 'registered',
priority: body.priority || 3,
context: body.context || '',
source: body.source || 'text',
source_url: body.source_url || null,
tags: body.tags || [],
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
};
issues.push(newIssue);
writeIssuesJsonl(issuesDir, issues);
return { success: true, issue: newIssue };
});
return true;
}
// GET /api/issues/:id - Get issue detail
const detailMatch = pathname.match(/^\/api\/issues\/([^/]+)$/);
if (detailMatch && req.method === 'GET') {
const issueId = decodeURIComponent(detailMatch[1]);
if (issueId === 'queue') return false;
const detail = getIssueDetail(issuesDir, issueId);
if (!detail) {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Issue not found' }));
return true;
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(detail));
return true;
}
// PATCH /api/issues/:id - Update issue (with binding support)
const updateMatch = pathname.match(/^\/api\/issues\/([^/]+)$/);
if (updateMatch && req.method === 'PATCH') {
const issueId = decodeURIComponent(updateMatch[1]);
if (issueId === 'queue') return false;
handlePostRequest(req, res, async (body: any) => {
const issues = readIssuesJsonl(issuesDir);
const issueIndex = issues.findIndex(i => i.id === issueId);
if (issueIndex === -1) return { error: 'Issue not found' };
const updates: string[] = [];
// Handle binding if bound_solution_id provided
if (body.bound_solution_id !== undefined) {
if (body.bound_solution_id) {
const bindResult = bindSolutionToIssue(issuesDir, issueId, body.bound_solution_id, issues, issueIndex);
if (bindResult.error) return bindResult;
updates.push('bound_solution_id');
} else {
// Unbind
const solutions = readSolutionsJsonl(issuesDir, issueId);
solutions.forEach(s => { s.is_bound = false; });
writeSolutionsJsonl(issuesDir, issueId, solutions);
issues[issueIndex].bound_solution_id = null;
updates.push('bound_solution_id (unbound)');
}
}
// Update other fields
for (const field of ['title', 'context', 'status', 'priority', 'tags']) {
if (body[field] !== undefined) {
issues[issueIndex][field] = body[field];
updates.push(field);
}
}
issues[issueIndex].updated_at = new Date().toISOString();
writeIssuesJsonl(issuesDir, issues);
return { success: true, issueId, updated: updates };
});
return true;
}
// DELETE /api/issues/:id
const deleteMatch = pathname.match(/^\/api\/issues\/([^/]+)$/);
if (deleteMatch && req.method === 'DELETE') {
const issueId = decodeURIComponent(deleteMatch[1]);
const issues = readIssuesJsonl(issuesDir);
const filtered = issues.filter(i => i.id !== issueId);
if (filtered.length === issues.length) {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Issue not found' }));
return true;
}
writeIssuesJsonl(issuesDir, filtered);
// Clean up solutions file
const solPath = join(issuesDir, 'solutions', `${issueId}.jsonl`);
if (existsSync(solPath)) {
try { unlinkSync(solPath); } catch {}
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: true, issueId }));
return true;
}
// POST /api/issues/:id/solutions - Add solution
const addSolMatch = pathname.match(/^\/api\/issues\/([^/]+)\/solutions$/);
if (addSolMatch && req.method === 'POST') {
const issueId = decodeURIComponent(addSolMatch[1]);
handlePostRequest(req, res, async (body: any) => {
if (!body.id || !body.tasks) return { error: 'id and tasks required' };
const solutions = readSolutionsJsonl(issuesDir, issueId);
if (solutions.find(s => s.id === body.id)) return { error: `Solution ${body.id} exists` };
const newSolution = {
id: body.id,
description: body.description || '',
tasks: body.tasks,
exploration_context: body.exploration_context || {},
analysis: body.analysis || {},
score: body.score || 0,
is_bound: false,
created_at: new Date().toISOString()
};
solutions.push(newSolution);
writeSolutionsJsonl(issuesDir, issueId, solutions);
// Update issue solution_count
const issues = readIssuesJsonl(issuesDir);
const idx = issues.findIndex(i => i.id === issueId);
if (idx !== -1) {
issues[idx].solution_count = solutions.length;
issues[idx].updated_at = new Date().toISOString();
writeIssuesJsonl(issuesDir, issues);
}
return { success: true, solution: newSolution };
});
return true;
}
// PATCH /api/issues/:id/tasks/:taskId - Update task
const taskMatch = pathname.match(/^\/api\/issues\/([^/]+)\/tasks\/([^/]+)$/);
if (taskMatch && req.method === 'PATCH') {
const issueId = decodeURIComponent(taskMatch[1]);
const taskId = decodeURIComponent(taskMatch[2]);
handlePostRequest(req, res, async (body: any) => {
const issues = readIssuesJsonl(issuesDir);
const issue = issues.find(i => i.id === issueId);
if (!issue?.bound_solution_id) return { error: 'Issue or bound solution not found' };
const solutions = readSolutionsJsonl(issuesDir, issueId);
const solIdx = solutions.findIndex(s => s.id === issue.bound_solution_id);
if (solIdx === -1) return { error: 'Bound solution not found' };
const taskIdx = solutions[solIdx].tasks?.findIndex((t: any) => t.id === taskId);
if (taskIdx === -1 || taskIdx === undefined) return { error: 'Task not found' };
const updates: string[] = [];
for (const field of ['status', 'priority', 'result', 'error']) {
if (body[field] !== undefined) {
solutions[solIdx].tasks[taskIdx][field] = body[field];
updates.push(field);
}
}
solutions[solIdx].tasks[taskIdx].updated_at = new Date().toISOString();
writeSolutionsJsonl(issuesDir, issueId, solutions);
return { success: true, issueId, taskId, updated: updates };
});
return true;
}
// Legacy: PUT /api/issues/:id/task/:taskId (backward compat)
const legacyTaskMatch = pathname.match(/^\/api\/issues\/([^/]+)\/task\/([^/]+)$/);
if (legacyTaskMatch && req.method === 'PUT') {
const issueId = decodeURIComponent(legacyTaskMatch[1]);
const taskId = decodeURIComponent(legacyTaskMatch[2]);
handlePostRequest(req, res, async (body: any) => {
const issues = readIssuesJsonl(issuesDir);
const issue = issues.find(i => i.id === issueId);
if (!issue?.bound_solution_id) return { error: 'Issue or bound solution not found' };
const solutions = readSolutionsJsonl(issuesDir, issueId);
const solIdx = solutions.findIndex(s => s.id === issue.bound_solution_id);
if (solIdx === -1) return { error: 'Bound solution not found' };
const taskIdx = solutions[solIdx].tasks?.findIndex((t: any) => t.id === taskId);
if (taskIdx === -1 || taskIdx === undefined) return { error: 'Task not found' };
const updates: string[] = [];
if (body.status !== undefined) { solutions[solIdx].tasks[taskIdx].status = body.status; updates.push('status'); }
if (body.priority !== undefined) { solutions[solIdx].tasks[taskIdx].priority = body.priority; updates.push('priority'); }
solutions[solIdx].tasks[taskIdx].updated_at = new Date().toISOString();
writeSolutionsJsonl(issuesDir, issueId, solutions);
return { success: true, issueId, taskId, updated: updates };
});
return true;
}
// Legacy: PUT /api/issues/:id/bind/:solutionId (backward compat)
const legacyBindMatch = pathname.match(/^\/api\/issues\/([^/]+)\/bind\/([^/]+)$/);
if (legacyBindMatch && req.method === 'PUT') {
const issueId = decodeURIComponent(legacyBindMatch[1]);
const solutionId = decodeURIComponent(legacyBindMatch[2]);
const issues = readIssuesJsonl(issuesDir);
const issueIndex = issues.findIndex(i => i.id === issueId);
if (issueIndex === -1) {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Issue not found' }));
return true;
}
const result = bindSolutionToIssue(issuesDir, issueId, solutionId, issues, issueIndex);
if (result.error) {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(result));
return true;
}
issues[issueIndex].updated_at = new Date().toISOString();
writeIssuesJsonl(issuesDir, issues);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: true, issueId, solutionId }));
return true;
}
// Legacy: PUT /api/issues/:id (backward compat for PATCH)
const legacyUpdateMatch = pathname.match(/^\/api\/issues\/([^/]+)$/);
if (legacyUpdateMatch && req.method === 'PUT') {
const issueId = decodeURIComponent(legacyUpdateMatch[1]);
if (issueId === 'queue') return false;
handlePostRequest(req, res, async (body: any) => {
const issues = readIssuesJsonl(issuesDir);
const issueIndex = issues.findIndex(i => i.id === issueId);
if (issueIndex === -1) return { error: 'Issue not found' };
const updates: string[] = [];
for (const field of ['title', 'context', 'status', 'priority', 'bound_solution_id', 'tags']) {
if (body[field] !== undefined) {
issues[issueIndex][field] = body[field];
updates.push(field);
}
}
issues[issueIndex].updated_at = new Date().toISOString();
writeIssuesJsonl(issuesDir, issues);
return { success: true, issueId, updated: updates };
});
return true;
}
return false;
}

View File

@@ -17,6 +17,8 @@ import { handleGraphRoutes } from './routes/graph-routes.js';
import { handleSystemRoutes } from './routes/system-routes.js';
import { handleFilesRoutes } from './routes/files-routes.js';
import { handleSkillsRoutes } from './routes/skills-routes.js';
import { handleIssueRoutes } from './routes/issue-routes.js';
import { handleDiscoveryRoutes } from './routes/discovery-routes.js';
import { handleRulesRoutes } from './routes/rules-routes.js';
import { handleSessionRoutes } from './routes/session-routes.js';
import { handleCcwRoutes } from './routes/ccw-routes.js';
@@ -86,7 +88,10 @@ const MODULE_CSS_FILES = [
'28-mcp-manager.css',
'29-help.css',
'30-core-memory.css',
'31-api-settings.css'
'31-api-settings.css',
'32-issue-manager.css',
'33-cli-stream-viewer.css',
'34-discovery.css'
];
// Modular JS files in dependency order
@@ -107,6 +112,7 @@ const MODULE_FILES = [
'components/flowchart.js',
'components/carousel.js',
'components/notifications.js',
'components/cli-stream-viewer.js',
'components/global-notifications.js',
'components/task-queue-sidebar.js',
'components/cli-status.js',
@@ -142,6 +148,8 @@ const MODULE_FILES = [
'views/claude-manager.js',
'views/api-settings.js',
'views/help.js',
'views/issue-manager.js',
'views/issue-discovery.js',
'main.js'
];
@@ -244,7 +252,7 @@ export async function startServer(options: ServerOptions = {}): Promise<http.Ser
// CORS headers for API requests
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
if (req.method === 'OPTIONS') {
@@ -340,6 +348,21 @@ export async function startServer(options: ServerOptions = {}): Promise<http.Ser
if (await handleSkillsRoutes(routeContext)) return;
}
// Queue routes (/api/queue*) - top-level queue API
if (pathname.startsWith('/api/queue')) {
if (await handleIssueRoutes(routeContext)) return;
}
// Issue routes (/api/issues*)
if (pathname.startsWith('/api/issues')) {
if (await handleIssueRoutes(routeContext)) return;
}
// Discovery routes (/api/discoveries*)
if (pathname.startsWith('/api/discoveries')) {
if (await handleDiscoveryRoutes(routeContext)) return;
}
// Rules routes (/api/rules*)
if (pathname.startsWith('/api/rules')) {
if (await handleRulesRoutes(routeContext)) return;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,580 @@
/**
* CLI Stream Viewer Styles
* Right-side popup panel for viewing CLI streaming output
*/
/* ===== Overlay ===== */
.cli-stream-overlay {
position: fixed;
inset: 0;
background: rgb(0 0 0 / 0.3);
z-index: 1050;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
}
.cli-stream-overlay.open {
opacity: 1;
visibility: visible;
}
/* ===== Main Panel ===== */
.cli-stream-viewer {
position: fixed;
top: 60px;
right: 16px;
width: 650px;
max-width: calc(100vw - 32px);
max-height: calc(100vh - 80px);
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-radius: 8px;
box-shadow: 0 8px 32px rgb(0 0 0 / 0.2);
z-index: 1100;
display: flex;
flex-direction: column;
transform: translateX(calc(100% + 20px));
opacity: 0;
visibility: hidden;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.cli-stream-viewer.open {
transform: translateX(0);
opacity: 1;
visibility: visible;
}
/* ===== Header ===== */
.cli-stream-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
border-bottom: 1px solid hsl(var(--border));
background: hsl(var(--muted) / 0.3);
}
.cli-stream-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.875rem;
font-weight: 600;
color: hsl(var(--foreground));
}
.cli-stream-title svg,
.cli-stream-title i {
width: 18px;
height: 18px;
color: hsl(var(--primary));
}
.cli-stream-count-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 20px;
height: 20px;
padding: 0 6px;
background: hsl(var(--muted));
color: hsl(var(--muted-foreground));
border-radius: 10px;
font-size: 0.6875rem;
font-weight: 600;
}
.cli-stream-count-badge.has-running {
background: hsl(var(--warning));
color: hsl(var(--warning-foreground, white));
}
/* ===== Search Box ===== */
.cli-stream-search {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
background: hsl(var(--background));
border: 1px solid hsl(var(--border));
border-radius: 6px;
transition: all 0.2s;
}
.cli-stream-search:focus-within {
border-color: hsl(var(--primary));
box-shadow: 0 0 0 2px hsl(var(--primary) / 0.1);
}
.cli-stream-search-input {
width: 140px;
padding: 2px 4px;
background: transparent;
border: none;
outline: none;
font-size: 0.75rem;
color: hsl(var(--foreground));
}
.cli-stream-search-input::placeholder {
color: hsl(var(--muted-foreground));
}
.cli-stream-search-icon {
width: 14px;
height: 14px;
color: hsl(var(--muted-foreground));
flex-shrink: 0;
}
.cli-stream-search-clear {
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
padding: 0;
background: transparent;
border: none;
border-radius: 50%;
font-size: 0.75rem;
color: hsl(var(--muted-foreground));
cursor: pointer;
opacity: 0;
transition: all 0.15s;
}
.cli-stream-search:focus-within .cli-stream-search-clear,
.cli-stream-search-input:not(:placeholder-shown) + .cli-stream-search-clear {
opacity: 1;
}
.cli-stream-search-clear:hover {
background: hsl(var(--muted));
color: hsl(var(--foreground));
}
.cli-stream-actions {
display: flex;
align-items: center;
gap: 8px;
}
.cli-stream-action-btn {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 10px;
background: transparent;
border: 1px solid hsl(var(--border));
border-radius: 4px;
font-size: 0.75rem;
color: hsl(var(--muted-foreground));
cursor: pointer;
transition: all 0.15s;
}
.cli-stream-action-btn:hover {
background: hsl(var(--hover));
color: hsl(var(--foreground));
}
.cli-stream-close-btn {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
padding: 0;
background: transparent;
border: none;
border-radius: 4px;
font-size: 1.25rem;
color: hsl(var(--muted-foreground));
cursor: pointer;
transition: all 0.15s;
}
.cli-stream-close-btn:hover {
background: hsl(var(--destructive) / 0.1);
color: hsl(var(--destructive));
}
/* ===== Tab Bar ===== */
.cli-stream-tabs {
display: flex;
gap: 2px;
padding: 8px 12px;
border-bottom: 1px solid hsl(var(--border));
background: hsl(var(--muted) / 0.2);
overflow-x: auto;
scrollbar-width: thin;
}
.cli-stream-tabs::-webkit-scrollbar {
height: 4px;
}
.cli-stream-tabs::-webkit-scrollbar-thumb {
background: hsl(var(--border));
border-radius: 2px;
}
.cli-stream-tab {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: transparent;
border: 1px solid transparent;
border-radius: 6px;
font-size: 0.75rem;
color: hsl(var(--muted-foreground));
cursor: pointer;
white-space: nowrap;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.cli-stream-tab:hover {
background: hsl(var(--hover));
color: hsl(var(--foreground));
transform: translateY(-1px);
}
.cli-stream-tab.active {
background: hsl(var(--card));
border-color: hsl(var(--primary));
color: hsl(var(--foreground));
box-shadow: 0 2px 8px rgb(0 0 0 / 0.15);
transform: translateY(-1px);
}
.cli-stream-tab.active .cli-stream-tab-tool {
color: hsl(var(--primary));
}
.cli-stream-tab-status {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.cli-stream-tab-status.running {
background: hsl(var(--warning));
animation: streamStatusPulse 1.5s ease-in-out infinite;
}
.cli-stream-tab-status.completed {
background: hsl(var(--success));
}
.cli-stream-tab-status.error {
background: hsl(var(--destructive));
}
@keyframes streamStatusPulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.6; transform: scale(1.2); }
}
.cli-stream-tab-tool {
font-weight: 500;
text-transform: capitalize;
}
.cli-stream-tab-mode {
font-size: 0.625rem;
padding: 1px 4px;
background: hsl(var(--muted));
border-radius: 3px;
color: hsl(var(--muted-foreground));
}
.cli-stream-tab-close {
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
margin-left: 4px;
background: transparent;
border: none;
border-radius: 50%;
font-size: 0.75rem;
color: hsl(var(--muted-foreground));
cursor: pointer;
opacity: 0;
transition: all 0.15s;
}
.cli-stream-tab:hover .cli-stream-tab-close {
opacity: 1;
}
.cli-stream-tab-close:hover {
background: hsl(var(--destructive) / 0.2);
color: hsl(var(--destructive));
}
.cli-stream-tab-close.disabled {
cursor: not-allowed;
opacity: 0.3 !important;
}
/* ===== Empty State ===== */
.cli-stream-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 48px 24px;
color: hsl(var(--muted-foreground));
text-align: center;
}
.cli-stream-empty svg,
.cli-stream-empty i {
width: 48px;
height: 48px;
margin-bottom: 16px;
opacity: 0.5;
}
.cli-stream-empty-title {
font-size: 0.875rem;
font-weight: 500;
margin-bottom: 4px;
}
.cli-stream-empty-hint {
font-size: 0.75rem;
opacity: 0.7;
}
/* ===== Terminal Content ===== */
.cli-stream-content {
flex: 1;
min-height: 300px;
max-height: 500px;
overflow-y: auto;
padding: 12px 16px;
background: hsl(220 13% 8%);
font-family: var(--font-mono, 'Consolas', 'Monaco', 'Courier New', monospace);
font-size: 0.75rem;
line-height: 1.6;
scrollbar-width: thin;
}
.cli-stream-content::-webkit-scrollbar {
width: 8px;
}
.cli-stream-content::-webkit-scrollbar-track {
background: transparent;
}
.cli-stream-content::-webkit-scrollbar-thumb {
background: hsl(0 0% 40%);
border-radius: 4px;
}
.cli-stream-line {
white-space: pre-wrap;
word-break: break-all;
margin: 0;
padding: 2px 0;
border-radius: 2px;
transition: background-color 0.15s;
}
.cli-stream-line:hover {
background: hsl(0 0% 100% / 0.03);
}
.cli-stream-line.stdout {
color: hsl(0 0% 85%);
}
.cli-stream-line.stderr {
color: hsl(8 75% 65%);
background: hsl(8 75% 65% / 0.05);
}
.cli-stream-line.system {
color: hsl(210 80% 65%);
font-style: italic;
padding-left: 8px;
border-left: 2px solid hsl(210 80% 65% / 0.5);
}
.cli-stream-line.info {
color: hsl(200 80% 70%);
}
/* JSON/Code syntax coloring in output */
.cli-stream-line .json-key {
color: hsl(200 80% 70%);
}
.cli-stream-line .json-string {
color: hsl(100 50% 60%);
}
.cli-stream-line .json-number {
color: hsl(40 80% 65%);
}
/* Search highlight */
.cli-stream-highlight {
background: hsl(50 100% 50% / 0.4);
color: inherit;
padding: 0 2px;
border-radius: 2px;
}
/* Filter result info */
.cli-stream-filter-info {
display: inline-block;
padding: 4px 10px;
margin-bottom: 8px;
background: hsl(var(--primary) / 0.15);
color: hsl(var(--primary));
border-radius: 4px;
font-size: 0.6875rem;
font-weight: 500;
}
/* Auto-scroll indicator */
.cli-stream-scroll-btn {
position: sticky;
bottom: 8px;
left: 50%;
transform: translateX(-50%);
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 12px;
background: hsl(var(--primary));
color: white;
border: none;
border-radius: 12px;
font-size: 0.625rem;
cursor: pointer;
opacity: 0;
transition: opacity 0.2s;
}
.cli-stream-content.has-new-content .cli-stream-scroll-btn {
opacity: 1;
}
/* ===== Status Bar ===== */
.cli-stream-status {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 16px;
border-top: 1px solid hsl(var(--border));
background: hsl(var(--muted) / 0.3);
font-size: 0.6875rem;
color: hsl(var(--muted-foreground));
}
.cli-stream-status-info {
display: flex;
align-items: center;
gap: 12px;
}
.cli-stream-status-item {
display: flex;
align-items: center;
gap: 4px;
}
.cli-stream-status-item svg,
.cli-stream-status-item i {
width: 12px;
height: 12px;
}
.cli-stream-status-actions {
display: flex;
align-items: center;
gap: 8px;
}
.cli-stream-toggle-btn {
display: flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
background: transparent;
border: 1px solid hsl(var(--border));
border-radius: 3px;
font-size: 0.625rem;
color: hsl(var(--muted-foreground));
cursor: pointer;
transition: all 0.15s;
}
.cli-stream-toggle-btn:hover {
background: hsl(var(--hover));
}
.cli-stream-toggle-btn.active {
background: hsl(var(--primary) / 0.1);
border-color: hsl(var(--primary));
color: hsl(var(--primary));
}
/* ===== Header Button & Badge ===== */
.cli-stream-btn {
position: relative;
}
.cli-stream-badge {
position: absolute;
top: -2px;
right: -2px;
min-width: 14px;
height: 14px;
padding: 0 4px;
background: hsl(var(--warning));
color: white;
border-radius: 7px;
font-size: 0.5625rem;
font-weight: 600;
display: none;
align-items: center;
justify-content: center;
}
.cli-stream-badge.has-running {
display: flex;
animation: streamBadgePulse 1.5s ease-in-out infinite;
}
@keyframes streamBadgePulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.15); }
}
/* ===== Responsive ===== */
@media (max-width: 768px) {
.cli-stream-viewer {
top: 56px;
right: 8px;
left: 8px;
width: auto;
max-height: calc(100vh - 72px);
}
.cli-stream-content {
min-height: 200px;
max-height: 350px;
}
}

View File

@@ -0,0 +1,783 @@
/* ==========================================
ISSUE DISCOVERY STYLES
========================================== */
/* Discovery Manager Container */
.discovery-manager {
width: 100%;
}
.discovery-manager.loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 300px;
color: hsl(var(--muted-foreground));
}
/* Discovery Header */
.discovery-header {
margin-bottom: 1.5rem;
}
.discovery-back-btn {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
font-size: 0.875rem;
font-weight: 500;
color: hsl(var(--muted-foreground));
background: hsl(var(--muted));
border: none;
cursor: pointer;
transition: all 0.15s ease;
}
.discovery-back-btn:hover {
color: hsl(var(--foreground));
background: hsl(var(--hover));
}
/* Discovery List */
.discovery-list-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
gap: 1rem;
}
/* Discovery Card */
.discovery-card {
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-radius: 0.75rem;
padding: 1rem;
cursor: pointer;
transition: all 0.15s ease;
}
.discovery-card:hover {
border-color: hsl(var(--primary) / 0.5);
box-shadow: 0 4px 12px hsl(var(--foreground) / 0.05);
}
.discovery-card.running {
border-color: hsl(var(--warning) / 0.5);
}
.discovery-card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.75rem;
}
.discovery-id {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
font-weight: 600;
color: hsl(var(--foreground));
}
.discovery-phase {
padding: 0.25rem 0.5rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 500;
text-transform: capitalize;
}
.discovery-phase.complete {
background: hsl(var(--success) / 0.1);
color: hsl(var(--success));
}
.discovery-phase.parallel,
.discovery-phase.external,
.discovery-phase.aggregation {
background: hsl(var(--warning) / 0.1);
color: hsl(var(--warning));
}
.discovery-phase.initialization {
background: hsl(var(--primary) / 0.1);
color: hsl(var(--primary));
}
.discovery-card-body {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.discovery-target {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
}
/* Perspective Badges */
.discovery-perspectives {
display: flex;
flex-wrap: wrap;
gap: 0.375rem;
}
.perspective-badge {
padding: 0.125rem 0.5rem;
border-radius: 9999px;
font-size: 0.625rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.025em;
}
.perspective-badge.bug {
background: hsl(0 84% 60% / 0.1);
color: hsl(0 84% 60%);
}
.perspective-badge.ux {
background: hsl(262 84% 60% / 0.1);
color: hsl(262 84% 60%);
}
.perspective-badge.test {
background: hsl(200 84% 50% / 0.1);
color: hsl(200 84% 50%);
}
.perspective-badge.quality {
background: hsl(142 76% 36% / 0.1);
color: hsl(142 76% 36%);
}
.perspective-badge.security {
background: hsl(0 84% 50% / 0.1);
color: hsl(0 84% 50%);
}
.perspective-badge.performance {
background: hsl(38 92% 50% / 0.1);
color: hsl(38 92% 50%);
}
.perspective-badge.maintainability {
background: hsl(280 60% 50% / 0.1);
color: hsl(280 60% 50%);
}
.perspective-badge.best-practices {
background: hsl(170 60% 45% / 0.1);
color: hsl(170 60% 45%);
}
.perspective-badge.more {
background: hsl(var(--muted));
color: hsl(var(--muted-foreground));
}
/* Progress Bar */
.discovery-progress-bar {
height: 4px;
background: hsl(var(--muted));
border-radius: 2px;
overflow: hidden;
}
.discovery-progress-bar .progress-fill {
height: 100%;
background: hsl(var(--primary));
transition: width 0.3s ease;
}
/* Stats */
.discovery-stats {
display: flex;
gap: 1.5rem;
}
.discovery-stats .stat {
display: flex;
flex-direction: column;
}
.discovery-stats .stat-value {
font-size: 1.25rem;
font-weight: 700;
color: hsl(var(--foreground));
}
.discovery-stats .stat-label {
font-size: 0.75rem;
color: hsl(var(--muted-foreground));
}
/* Priority Distribution Bar */
.discovery-priority-bar {
display: flex;
height: 4px;
border-radius: 2px;
overflow: hidden;
}
.discovery-priority-bar .priority-segment {
height: 100%;
}
.discovery-priority-bar .priority-segment.critical {
background: hsl(0 84% 60%);
}
.discovery-priority-bar .priority-segment.high {
background: hsl(38 92% 50%);
}
.discovery-priority-bar .priority-segment.medium {
background: hsl(48 96% 53%);
}
.discovery-priority-bar .priority-segment.low {
background: hsl(142 76% 36%);
}
.discovery-card-footer {
display: flex;
justify-content: flex-end;
margin-top: 0.75rem;
padding-top: 0.75rem;
border-top: 1px solid hsl(var(--border));
}
.discovery-action-btn {
padding: 0.375rem;
border-radius: 0.375rem;
color: hsl(var(--muted-foreground));
background: transparent;
border: none;
cursor: pointer;
transition: all 0.15s ease;
}
.discovery-action-btn:hover {
color: hsl(var(--destructive));
background: hsl(var(--destructive) / 0.1);
}
/* Empty State */
.discovery-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 300px;
text-align: center;
color: hsl(var(--muted-foreground));
}
.discovery-empty .empty-icon {
margin-bottom: 1rem;
opacity: 0.5;
}
/* Discovery Detail Container */
.discovery-detail-container {
display: grid;
grid-template-columns: 1fr 400px;
gap: 1.5rem;
height: calc(100vh - 200px);
min-height: 500px;
}
@media (max-width: 1024px) {
.discovery-detail-container {
grid-template-columns: 1fr;
height: auto;
}
}
/* Findings Panel */
.discovery-findings-panel {
display: flex;
flex-direction: column;
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-radius: 0.75rem;
overflow: hidden;
}
.discovery-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0.75rem 1rem;
border-bottom: 1px solid hsl(var(--border));
flex-wrap: wrap;
}
.toolbar-filters {
display: flex;
gap: 0.5rem;
}
.filter-select {
padding: 0.375rem 0.75rem;
border-radius: 0.375rem;
font-size: 0.75rem;
color: hsl(var(--foreground));
background: hsl(var(--muted));
border: 1px solid hsl(var(--border));
cursor: pointer;
}
.toolbar-search {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.375rem 0.75rem;
border-radius: 0.375rem;
background: hsl(var(--muted));
border: 1px solid hsl(var(--border));
flex: 1;
max-width: 250px;
}
.toolbar-search input {
flex: 1;
background: transparent;
border: none;
font-size: 0.75rem;
color: hsl(var(--foreground));
outline: none;
}
.findings-count {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.5rem 1rem;
font-size: 0.75rem;
color: hsl(var(--muted-foreground));
border-bottom: 1px solid hsl(var(--border));
}
.findings-count-left {
display: flex;
align-items: center;
gap: 0.5rem;
}
.findings-count .selected-count {
color: hsl(var(--primary));
font-weight: 500;
}
.findings-count-actions {
display: flex;
gap: 0.5rem;
}
.select-action-btn {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.625rem;
font-weight: 500;
color: hsl(var(--muted-foreground));
background: transparent;
border: 1px solid hsl(var(--border));
cursor: pointer;
transition: all 0.15s ease;
}
.select-action-btn:hover {
color: hsl(var(--foreground));
background: hsl(var(--muted));
border-color: hsl(var(--primary) / 0.3);
}
/* Findings List */
.findings-list {
flex: 1;
overflow-y: auto;
padding: 0.5rem;
}
.findings-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 200px;
color: hsl(var(--muted-foreground));
text-align: center;
}
/* Finding Item */
.finding-item {
display: flex;
gap: 0.75rem;
padding: 0.75rem;
border-radius: 0.5rem;
cursor: pointer;
transition: all 0.15s ease;
}
.finding-item:hover {
background: hsl(var(--muted) / 0.5);
}
.finding-item.active {
background: hsl(var(--primary) / 0.1);
border: 1px solid hsl(var(--primary) / 0.3);
}
.finding-item.selected {
background: hsl(var(--primary) / 0.05);
}
.finding-item.dismissed {
opacity: 0.5;
}
.finding-item.exported {
opacity: 0.6;
background: hsl(var(--success) / 0.05);
border: 1px solid hsl(var(--success) / 0.2);
}
.finding-item.exported:hover {
background: hsl(var(--success) / 0.08);
}
/* Exported Badge */
.exported-badge {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.125rem 0.5rem;
border-radius: 9999px;
font-size: 0.625rem;
font-weight: 600;
text-transform: uppercase;
background: hsl(var(--success) / 0.1);
color: hsl(var(--success));
}
.finding-checkbox input:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.finding-checkbox {
display: flex;
align-items: flex-start;
padding-top: 0.125rem;
}
.finding-checkbox input {
width: 16px;
height: 16px;
cursor: pointer;
}
.finding-content {
flex: 1;
min-width: 0;
}
.finding-header {
display: flex;
gap: 0.375rem;
margin-bottom: 0.375rem;
}
.finding-title {
font-size: 0.875rem;
font-weight: 500;
color: hsl(var(--foreground));
line-height: 1.3;
margin-bottom: 0.25rem;
}
.finding-location {
display: flex;
align-items: center;
gap: 0.25rem;
font-size: 0.75rem;
color: hsl(var(--muted-foreground));
}
/* Priority Badge */
.priority-badge {
padding: 0.125rem 0.375rem;
border-radius: 0.25rem;
font-size: 0.625rem;
font-weight: 600;
text-transform: uppercase;
}
.priority-badge.critical {
background: hsl(0 84% 60% / 0.1);
color: hsl(0 84% 60%);
}
.priority-badge.high {
background: hsl(38 92% 50% / 0.1);
color: hsl(38 92% 50%);
}
.priority-badge.medium {
background: hsl(48 96% 53% / 0.1);
color: hsl(48 70% 40%);
}
.priority-badge.low {
background: hsl(142 76% 36% / 0.1);
color: hsl(142 76% 36%);
}
/* Bulk Actions */
.bulk-actions {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
border-top: 1px solid hsl(var(--border));
background: hsl(var(--muted) / 0.3);
}
.bulk-count {
font-size: 0.75rem;
font-weight: 500;
color: hsl(var(--foreground));
}
.bulk-action-btn {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.375rem 0.75rem;
border-radius: 0.375rem;
font-size: 0.75rem;
font-weight: 500;
border: none;
cursor: pointer;
transition: all 0.15s ease;
}
.bulk-action-btn.export {
background: hsl(var(--primary));
color: hsl(var(--primary-foreground));
}
.bulk-action-btn.export:hover {
opacity: 0.9;
}
.bulk-action-btn.dismiss {
background: hsl(var(--muted));
color: hsl(var(--muted-foreground));
}
.bulk-action-btn.dismiss:hover {
background: hsl(var(--destructive) / 0.1);
color: hsl(var(--destructive));
}
/* Preview Panel */
.discovery-preview-panel {
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-radius: 0.75rem;
overflow: hidden;
}
.preview-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
min-height: 300px;
color: hsl(var(--muted-foreground));
text-align: center;
}
/* Finding Preview */
.finding-preview {
padding: 1.25rem;
height: 100%;
overflow-y: auto;
}
.preview-header {
margin-bottom: 1.25rem;
}
.preview-badges {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.confidence-badge {
padding: 0.125rem 0.5rem;
border-radius: 9999px;
font-size: 0.625rem;
font-weight: 500;
background: hsl(var(--muted));
color: hsl(var(--muted-foreground));
}
.preview-title {
font-size: 1.125rem;
font-weight: 600;
color: hsl(var(--foreground));
line-height: 1.3;
}
.preview-section {
margin-bottom: 1rem;
}
.preview-section h4 {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: hsl(var(--muted-foreground));
margin-bottom: 0.5rem;
}
.preview-location code {
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.75rem;
background: hsl(var(--muted));
color: hsl(var(--primary));
}
.preview-snippet {
padding: 0.75rem;
border-radius: 0.5rem;
background: hsl(var(--muted));
overflow-x: auto;
}
.preview-snippet code {
font-size: 0.75rem;
white-space: pre;
}
.preview-description,
.preview-impact,
.preview-recommendation {
font-size: 0.875rem;
line-height: 1.5;
color: hsl(var(--foreground));
}
/* Suggested Issue */
.preview-section.suggested-issue {
padding: 0.75rem;
background: hsl(var(--primary) / 0.05);
border: 1px solid hsl(var(--primary) / 0.2);
border-radius: 0.5rem;
}
.suggested-issue-content {
margin-top: 0.5rem;
}
.suggested-issue-content .issue-title {
font-size: 0.875rem;
font-weight: 500;
color: hsl(var(--foreground));
margin-bottom: 0.5rem;
}
.suggested-issue-content .issue-meta {
display: flex;
flex-wrap: wrap;
gap: 0.375rem;
}
.issue-type,
.issue-priority,
.issue-label {
padding: 0.125rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.625rem;
font-weight: 500;
}
.issue-type {
background: hsl(var(--primary) / 0.1);
color: hsl(var(--primary));
}
.issue-priority {
background: hsl(var(--warning) / 0.1);
color: hsl(var(--warning));
}
.issue-label {
background: hsl(var(--muted));
color: hsl(var(--muted-foreground));
}
/* Preview Actions */
.preview-actions {
display: flex;
gap: 0.75rem;
margin-top: 1.5rem;
padding-top: 1rem;
border-top: 1px solid hsl(var(--border));
}
.preview-action-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
font-size: 0.875rem;
font-weight: 500;
border: none;
cursor: pointer;
transition: all 0.15s ease;
flex: 1;
}
.preview-action-btn.primary {
background: hsl(var(--primary));
color: hsl(var(--primary-foreground));
}
.preview-action-btn.primary:hover {
opacity: 0.9;
}
.preview-action-btn.secondary {
background: hsl(var(--muted));
color: hsl(var(--muted-foreground));
}
.preview-action-btn.secondary:hover {
color: hsl(var(--destructive));
background: hsl(var(--destructive) / 0.1);
}

View File

@@ -21,24 +21,6 @@ let nativeResumeEnabled = localStorage.getItem('ccw-native-resume') !== 'false';
// Recursive Query settings (for hierarchical storage aggregation)
let recursiveQueryEnabled = localStorage.getItem('ccw-recursive-query') !== 'false'; // default true
// Code Index MCP provider (codexlens, ace, or none)
let codeIndexMcpProvider = 'codexlens';
// ========== Helper Functions ==========
/**
* Get the context-tools filename based on provider
*/
function getContextToolsFileName(provider) {
switch (provider) {
case 'ace':
return 'context-tools-ace.md';
case 'none':
return 'context-tools-none.md';
default:
return 'context-tools.md';
}
}
// ========== Initialization ==========
function initCliStatus() {
// Load all statuses in one call using aggregated endpoint
@@ -259,12 +241,7 @@ async function loadCliToolsConfig() {
defaultCliTool = data.defaultTool;
}
// Load Code Index MCP provider from config
if (data.settings?.codeIndexMcp) {
codeIndexMcpProvider = data.settings.codeIndexMcp;
}
console.log('[CLI Config] Loaded from:', data._configInfo?.source || 'unknown', '| Default:', data.defaultTool, '| CodeIndexMCP:', codeIndexMcpProvider);
console.log('[CLI Config] Loaded from:', data._configInfo?.source || 'unknown', '| Default:', data.defaultTool);
return data;
} catch (err) {
console.error('Failed to load CLI tools config:', err);
@@ -637,33 +614,6 @@ function renderCliStatus() {
</div>
<p class="cli-setting-desc">Cache prefix/suffix injection mode for prompts</p>
</div>
<div class="cli-setting-item">
<label class="cli-setting-label">
<i data-lucide="search" class="w-3 h-3"></i>
Code Index MCP
</label>
<div class="cli-setting-control">
<div class="flex items-center bg-muted rounded-lg p-0.5">
<button class="code-mcp-btn px-3 py-1.5 text-xs font-medium rounded-md transition-all ${codeIndexMcpProvider === 'codexlens' ? 'bg-primary text-primary-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'}"
onclick="setCodeIndexMcpProvider('codexlens')">
CodexLens
</button>
<button class="code-mcp-btn px-3 py-1.5 text-xs font-medium rounded-md transition-all ${codeIndexMcpProvider === 'ace' ? 'bg-primary text-primary-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'}"
onclick="setCodeIndexMcpProvider('ace')">
ACE
</button>
<button class="code-mcp-btn px-3 py-1.5 text-xs font-medium rounded-md transition-all ${codeIndexMcpProvider === 'none' ? 'bg-primary text-primary-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'}"
onclick="setCodeIndexMcpProvider('none')">
None
</button>
</div>
</div>
<p class="cli-setting-desc">Code search provider (updates CLAUDE.md context-tools reference)</p>
<p class="cli-setting-desc text-xs text-muted-foreground mt-1">
<i data-lucide="file-text" class="w-3 h-3 inline-block mr-1"></i>
Current: <code class="bg-muted px-1 rounded">${getContextToolsFileName(codeIndexMcpProvider)}</code>
</p>
</div>
</div>
</div>
`;
@@ -786,33 +736,6 @@ async function setCacheInjectionMode(mode) {
}
}
async function setCodeIndexMcpProvider(provider) {
try {
const response = await fetch('/api/cli/code-index-mcp', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ provider: provider })
});
if (response.ok) {
codeIndexMcpProvider = provider;
if (window.claudeCliToolsConfig && window.claudeCliToolsConfig.settings) {
window.claudeCliToolsConfig.settings.codeIndexMcp = provider;
}
const providerName = provider === 'ace' ? 'ACE (Augment)' : provider === 'none' ? 'None (Built-in only)' : 'CodexLens';
showRefreshToast(`Code Index MCP switched to ${providerName}`, 'success');
// Re-render both CLI status and settings section
if (typeof renderCliStatus === 'function') renderCliStatus();
if (typeof renderCliSettingsSection === 'function') renderCliSettingsSection();
} else {
const data = await response.json();
showRefreshToast(`Failed to switch Code Index MCP: ${data.error}`, 'error');
}
} catch (err) {
console.error('Failed to switch Code Index MCP:', err);
showRefreshToast('Failed to switch Code Index MCP', 'error');
}
}
async function refreshAllCliStatus() {
await loadAllStatuses();
renderCliStatus();

View File

@@ -0,0 +1,532 @@
/**
* CLI Stream Viewer Component
* Real-time streaming output viewer for CLI executions
*/
// ===== State Management =====
let cliStreamExecutions = {}; // { executionId: { tool, mode, output, status, startTime, endTime } }
let activeStreamTab = null;
let autoScrollEnabled = true;
let isCliStreamViewerOpen = false;
let searchFilter = ''; // Search filter for output content
const MAX_OUTPUT_LINES = 5000; // Prevent memory issues
// ===== Initialization =====
function initCliStreamViewer() {
// Initialize keyboard shortcuts
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && isCliStreamViewerOpen) {
if (searchFilter) {
clearSearch();
} else {
toggleCliStreamViewer();
}
}
// Ctrl+F to focus search when viewer is open
if ((e.ctrlKey || e.metaKey) && e.key === 'f' && isCliStreamViewerOpen) {
e.preventDefault();
const searchInput = document.getElementById('cliStreamSearchInput');
if (searchInput) {
searchInput.focus();
searchInput.select();
}
}
});
// Initialize scroll detection for auto-scroll
const content = document.getElementById('cliStreamContent');
if (content) {
content.addEventListener('scroll', handleStreamContentScroll);
}
}
// ===== Panel Control =====
function toggleCliStreamViewer() {
const viewer = document.getElementById('cliStreamViewer');
const overlay = document.getElementById('cliStreamOverlay');
if (!viewer || !overlay) return;
isCliStreamViewerOpen = !isCliStreamViewerOpen;
if (isCliStreamViewerOpen) {
viewer.classList.add('open');
overlay.classList.add('open');
// If no active tab but have executions, select the first one
if (!activeStreamTab && Object.keys(cliStreamExecutions).length > 0) {
const firstId = Object.keys(cliStreamExecutions)[0];
switchStreamTab(firstId);
} else {
renderStreamContent(activeStreamTab);
}
// Re-init lucide icons
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
} else {
viewer.classList.remove('open');
overlay.classList.remove('open');
}
}
// ===== WebSocket Event Handlers =====
function handleCliStreamStarted(payload) {
const { executionId, tool, mode, timestamp } = payload;
// Create new execution record
cliStreamExecutions[executionId] = {
tool: tool || 'cli',
mode: mode || 'analysis',
output: [],
status: 'running',
startTime: timestamp ? new Date(timestamp).getTime() : Date.now(),
endTime: null
};
// Add system message
cliStreamExecutions[executionId].output.push({
type: 'system',
content: `[${new Date().toLocaleTimeString()}] CLI execution started: ${tool} (${mode} mode)`,
timestamp: Date.now()
});
// If this is the first execution or panel is open, select it
if (!activeStreamTab || isCliStreamViewerOpen) {
activeStreamTab = executionId;
}
renderStreamTabs();
renderStreamContent(activeStreamTab);
updateStreamBadge();
// Auto-open panel if configured (optional)
// if (!isCliStreamViewerOpen) toggleCliStreamViewer();
}
function handleCliStreamOutput(payload) {
const { executionId, chunkType, data } = payload;
const exec = cliStreamExecutions[executionId];
if (!exec) return;
// Parse and add output lines
const content = typeof data === 'string' ? data : JSON.stringify(data);
const lines = content.split('\n');
lines.forEach(line => {
if (line.trim() || lines.length === 1) { // Keep empty lines if it's the only content
exec.output.push({
type: chunkType || 'stdout',
content: line,
timestamp: Date.now()
});
}
});
// Trim if too long
if (exec.output.length > MAX_OUTPUT_LINES) {
exec.output = exec.output.slice(-MAX_OUTPUT_LINES);
}
// Update UI if this is the active tab
if (activeStreamTab === executionId && isCliStreamViewerOpen) {
requestAnimationFrame(() => {
renderStreamContent(executionId);
});
}
// Update badge to show activity
updateStreamBadge();
}
function handleCliStreamCompleted(payload) {
const { executionId, success, duration, timestamp } = payload;
const exec = cliStreamExecutions[executionId];
if (!exec) return;
exec.status = success ? 'completed' : 'error';
exec.endTime = timestamp ? new Date(timestamp).getTime() : Date.now();
// Add completion message
const durationText = duration ? ` (${formatDuration(duration)})` : '';
const statusText = success ? 'completed successfully' : 'failed';
exec.output.push({
type: 'system',
content: `[${new Date().toLocaleTimeString()}] CLI execution ${statusText}${durationText}`,
timestamp: Date.now()
});
renderStreamTabs();
if (activeStreamTab === executionId) {
renderStreamContent(executionId);
}
updateStreamBadge();
}
function handleCliStreamError(payload) {
const { executionId, error, timestamp } = payload;
const exec = cliStreamExecutions[executionId];
if (!exec) return;
exec.status = 'error';
exec.endTime = timestamp ? new Date(timestamp).getTime() : Date.now();
// Add error message
exec.output.push({
type: 'stderr',
content: `[ERROR] ${error || 'Unknown error occurred'}`,
timestamp: Date.now()
});
renderStreamTabs();
if (activeStreamTab === executionId) {
renderStreamContent(executionId);
}
updateStreamBadge();
}
// ===== UI Rendering =====
function renderStreamTabs() {
const tabsContainer = document.getElementById('cliStreamTabs');
if (!tabsContainer) return;
const execIds = Object.keys(cliStreamExecutions);
if (execIds.length === 0) {
tabsContainer.innerHTML = '';
return;
}
// Sort: running first, then by start time (newest first)
execIds.sort((a, b) => {
const execA = cliStreamExecutions[a];
const execB = cliStreamExecutions[b];
if (execA.status === 'running' && execB.status !== 'running') return -1;
if (execA.status !== 'running' && execB.status === 'running') return 1;
return execB.startTime - execA.startTime;
});
tabsContainer.innerHTML = execIds.map(id => {
const exec = cliStreamExecutions[id];
const isActive = id === activeStreamTab;
const canClose = exec.status !== 'running';
return `
<div class="cli-stream-tab ${isActive ? 'active' : ''}"
onclick="switchStreamTab('${id}')"
data-execution-id="${id}">
<span class="cli-stream-tab-status ${exec.status}"></span>
<span class="cli-stream-tab-tool">${escapeHtml(exec.tool)}</span>
<span class="cli-stream-tab-mode">${exec.mode}</span>
<button class="cli-stream-tab-close ${canClose ? '' : 'disabled'}"
onclick="event.stopPropagation(); closeStream('${id}')"
title="${canClose ? _streamT('cliStream.close') : _streamT('cliStream.cannotCloseRunning')}"
${canClose ? '' : 'disabled'}>×</button>
</div>
`;
}).join('');
// Update count badge
const countBadge = document.getElementById('cliStreamCountBadge');
if (countBadge) {
const runningCount = execIds.filter(id => cliStreamExecutions[id].status === 'running').length;
countBadge.textContent = execIds.length;
countBadge.classList.toggle('has-running', runningCount > 0);
}
}
function renderStreamContent(executionId) {
const contentContainer = document.getElementById('cliStreamContent');
if (!contentContainer) return;
const exec = executionId ? cliStreamExecutions[executionId] : null;
if (!exec) {
// Show empty state
contentContainer.innerHTML = `
<div class="cli-stream-empty">
<i data-lucide="terminal"></i>
<div class="cli-stream-empty-title" data-i18n="cliStream.noStreams">${_streamT('cliStream.noStreams')}</div>
<div class="cli-stream-empty-hint" data-i18n="cliStream.noStreamsHint">${_streamT('cliStream.noStreamsHint')}</div>
</div>
`;
if (typeof lucide !== 'undefined') lucide.createIcons();
return;
}
// Check if should auto-scroll
const wasAtBottom = contentContainer.scrollHeight - contentContainer.scrollTop <= contentContainer.clientHeight + 50;
// Filter output lines based on search
let filteredOutput = exec.output;
if (searchFilter.trim()) {
const searchLower = searchFilter.toLowerCase();
filteredOutput = exec.output.filter(line =>
line.content.toLowerCase().includes(searchLower)
);
}
// Render output lines with search highlighting
contentContainer.innerHTML = filteredOutput.map(line => {
let content = escapeHtml(line.content);
// Highlight search matches
if (searchFilter.trim()) {
const searchRegex = new RegExp(`(${escapeRegex(searchFilter)})`, 'gi');
content = content.replace(searchRegex, '<mark class="cli-stream-highlight">$1</mark>');
}
return `<div class="cli-stream-line ${line.type}">${content}</div>`;
}).join('');
// Show filter result count if filtering
if (searchFilter.trim() && filteredOutput.length !== exec.output.length) {
const filterInfo = document.createElement('div');
filterInfo.className = 'cli-stream-filter-info';
filterInfo.textContent = `${filteredOutput.length} / ${exec.output.length} lines`;
contentContainer.insertBefore(filterInfo, contentContainer.firstChild);
}
// Auto-scroll if enabled and was at bottom
if (autoScrollEnabled && wasAtBottom) {
contentContainer.scrollTop = contentContainer.scrollHeight;
}
// Update status bar
renderStreamStatus(executionId);
}
function renderStreamStatus(executionId) {
const statusContainer = document.getElementById('cliStreamStatus');
if (!statusContainer) return;
const exec = executionId ? cliStreamExecutions[executionId] : null;
if (!exec) {
statusContainer.innerHTML = '';
return;
}
const duration = exec.endTime
? formatDuration(exec.endTime - exec.startTime)
: formatDuration(Date.now() - exec.startTime);
const statusLabel = exec.status === 'running'
? _streamT('cliStream.running')
: exec.status === 'completed'
? _streamT('cliStream.completed')
: _streamT('cliStream.error');
statusContainer.innerHTML = `
<div class="cli-stream-status-info">
<div class="cli-stream-status-item">
<span class="cli-stream-tab-status ${exec.status}"></span>
<span>${statusLabel}</span>
</div>
<div class="cli-stream-status-item">
<i data-lucide="clock"></i>
<span>${duration}</span>
</div>
<div class="cli-stream-status-item">
<i data-lucide="file-text"></i>
<span>${exec.output.length} ${_streamT('cliStream.lines') || 'lines'}</span>
</div>
</div>
<div class="cli-stream-status-actions">
<button class="cli-stream-toggle-btn ${autoScrollEnabled ? 'active' : ''}"
onclick="toggleAutoScroll()"
title="${_streamT('cliStream.autoScroll')}">
<i data-lucide="arrow-down-to-line"></i>
<span data-i18n="cliStream.autoScroll">${_streamT('cliStream.autoScroll')}</span>
</button>
</div>
`;
if (typeof lucide !== 'undefined') lucide.createIcons();
// Update duration periodically for running executions
if (exec.status === 'running') {
setTimeout(() => {
if (activeStreamTab === executionId && cliStreamExecutions[executionId]?.status === 'running') {
renderStreamStatus(executionId);
}
}, 1000);
}
}
function switchStreamTab(executionId) {
if (!cliStreamExecutions[executionId]) return;
activeStreamTab = executionId;
renderStreamTabs();
renderStreamContent(executionId);
}
function updateStreamBadge() {
const badge = document.getElementById('cliStreamBadge');
if (!badge) return;
const runningCount = Object.values(cliStreamExecutions).filter(e => e.status === 'running').length;
if (runningCount > 0) {
badge.textContent = runningCount;
badge.classList.add('has-running');
} else {
badge.textContent = '';
badge.classList.remove('has-running');
}
}
// ===== User Actions =====
function closeStream(executionId) {
const exec = cliStreamExecutions[executionId];
if (!exec || exec.status === 'running') return;
delete cliStreamExecutions[executionId];
// Switch to another tab if this was active
if (activeStreamTab === executionId) {
const remaining = Object.keys(cliStreamExecutions);
activeStreamTab = remaining.length > 0 ? remaining[0] : null;
}
renderStreamTabs();
renderStreamContent(activeStreamTab);
updateStreamBadge();
}
function clearCompletedStreams() {
const toRemove = Object.keys(cliStreamExecutions).filter(
id => cliStreamExecutions[id].status !== 'running'
);
toRemove.forEach(id => delete cliStreamExecutions[id]);
// Update active tab if needed
if (activeStreamTab && !cliStreamExecutions[activeStreamTab]) {
const remaining = Object.keys(cliStreamExecutions);
activeStreamTab = remaining.length > 0 ? remaining[0] : null;
}
renderStreamTabs();
renderStreamContent(activeStreamTab);
updateStreamBadge();
}
function toggleAutoScroll() {
autoScrollEnabled = !autoScrollEnabled;
if (autoScrollEnabled && activeStreamTab) {
const content = document.getElementById('cliStreamContent');
if (content) {
content.scrollTop = content.scrollHeight;
}
}
renderStreamStatus(activeStreamTab);
}
function handleStreamContentScroll() {
const content = document.getElementById('cliStreamContent');
if (!content) return;
// If user scrolls up, disable auto-scroll
const isAtBottom = content.scrollHeight - content.scrollTop <= content.clientHeight + 50;
if (!isAtBottom && autoScrollEnabled) {
autoScrollEnabled = false;
renderStreamStatus(activeStreamTab);
}
}
// ===== Helper Functions =====
function formatDuration(ms) {
if (ms < 1000) return `${ms}ms`;
const seconds = Math.floor(ms / 1000);
if (seconds < 60) return `${seconds}s`;
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
if (minutes < 60) return `${minutes}m ${remainingSeconds}s`;
const hours = Math.floor(minutes / 60);
const remainingMinutes = minutes % 60;
return `${hours}h ${remainingMinutes}m`;
}
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function escapeRegex(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
// ===== Search Functions =====
function handleSearchInput(event) {
searchFilter = event.target.value;
renderStreamContent(activeStreamTab);
}
function clearSearch() {
searchFilter = '';
const searchInput = document.getElementById('cliStreamSearchInput');
if (searchInput) {
searchInput.value = '';
}
renderStreamContent(activeStreamTab);
}
// Translation helper with fallback (uses global t from i18n.js)
function _streamT(key) {
// First try global t() from i18n.js
if (typeof t === 'function' && t !== _streamT) {
try {
return t(key);
} catch (e) {
// Fall through to fallbacks
}
}
// Fallback values
const fallbacks = {
'cliStream.noStreams': 'No active CLI executions',
'cliStream.noStreamsHint': 'Start a CLI command to see streaming output',
'cliStream.running': 'Running',
'cliStream.completed': 'Completed',
'cliStream.error': 'Error',
'cliStream.autoScroll': 'Auto-scroll',
'cliStream.close': 'Close',
'cliStream.cannotCloseRunning': 'Cannot close running execution',
'cliStream.lines': 'lines',
'cliStream.searchPlaceholder': 'Search output...',
'cliStream.filterResults': 'results'
};
return fallbacks[key] || key;
}
// Initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initCliStreamViewer);
} else {
initCliStreamViewer();
}
// ===== Global Exposure =====
window.toggleCliStreamViewer = toggleCliStreamViewer;
window.handleCliStreamStarted = handleCliStreamStarted;
window.handleCliStreamOutput = handleCliStreamOutput;
window.handleCliStreamCompleted = handleCliStreamCompleted;
window.handleCliStreamError = handleCliStreamError;
window.switchStreamTab = switchStreamTab;
window.closeStream = closeStream;
window.clearCompletedStreams = clearCompletedStreams;
window.toggleAutoScroll = toggleAutoScroll;
window.handleSearchInput = handleSearchInput;
window.clearSearch = clearSearch;

View File

@@ -155,6 +155,18 @@ function initNavigation() {
} else {
console.error('renderApiSettings not defined - please refresh the page');
}
} else if (currentView === 'issue-manager') {
if (typeof renderIssueManager === 'function') {
renderIssueManager();
} else {
console.error('renderIssueManager not defined - please refresh the page');
}
} else if (currentView === 'issue-discovery') {
if (typeof renderIssueDiscovery === 'function') {
renderIssueDiscovery();
} else {
console.error('renderIssueDiscovery not defined - please refresh the page');
}
}
});
});
@@ -199,6 +211,10 @@ function updateContentTitle() {
titleEl.textContent = t('title.codexLensManager');
} else if (currentView === 'api-settings') {
titleEl.textContent = t('title.apiSettings');
} else if (currentView === 'issue-manager') {
titleEl.textContent = t('title.issueManager');
} else if (currentView === 'issue-discovery') {
titleEl.textContent = t('title.issueDiscovery');
} else if (currentView === 'liteTasks') {
const names = { 'lite-plan': t('title.litePlanSessions'), 'lite-fix': t('title.liteFixSessions') };
titleEl.textContent = names[currentLiteType] || t('title.liteTasks');

View File

@@ -217,24 +217,40 @@ function handleNotification(data) {
if (typeof handleCliExecutionStarted === 'function') {
handleCliExecutionStarted(payload);
}
// Route to CLI Stream Viewer
if (typeof handleCliStreamStarted === 'function') {
handleCliStreamStarted(payload);
}
break;
case 'CLI_OUTPUT':
if (typeof handleCliOutput === 'function') {
handleCliOutput(payload);
}
// Route to CLI Stream Viewer
if (typeof handleCliStreamOutput === 'function') {
handleCliStreamOutput(payload);
}
break;
case 'CLI_EXECUTION_COMPLETED':
if (typeof handleCliExecutionCompleted === 'function') {
handleCliExecutionCompleted(payload);
}
// Route to CLI Stream Viewer
if (typeof handleCliStreamCompleted === 'function') {
handleCliStreamCompleted(payload);
}
break;
case 'CLI_EXECUTION_ERROR':
if (typeof handleCliExecutionError === 'function') {
handleCliExecutionError(payload);
}
// Route to CLI Stream Viewer
if (typeof handleCliStreamError === 'function') {
handleCliStreamError(payload);
}
break;
// CLI Review Events

View File

@@ -28,6 +28,8 @@ const i18n = {
'common.deleteFailed': 'Delete failed',
'common.retry': 'Retry',
'common.refresh': 'Refresh',
'common.back': 'Back',
'common.search': 'Search...',
'common.minutes': 'minutes',
'common.enabled': 'Enabled',
'common.disabled': 'Disabled',
@@ -39,7 +41,23 @@ const i18n = {
'header.refreshWorkspace': 'Refresh workspace',
'header.toggleTheme': 'Toggle theme',
'header.language': 'Language',
'header.cliStream': 'CLI Stream Viewer',
// CLI Stream Viewer
'cliStream.title': 'CLI Stream',
'cliStream.clearCompleted': 'Clear Completed',
'cliStream.noStreams': 'No active CLI executions',
'cliStream.noStreamsHint': 'Start a CLI command to see streaming output',
'cliStream.running': 'Running',
'cliStream.completed': 'Completed',
'cliStream.error': 'Error',
'cliStream.autoScroll': 'Auto-scroll',
'cliStream.close': 'Close',
'cliStream.cannotCloseRunning': 'Cannot close running execution',
'cliStream.lines': 'lines',
'cliStream.searchPlaceholder': 'Search output...',
'cliStream.filterResults': 'results',
// Sidebar - Project section
'nav.project': 'Project',
'nav.overview': 'Overview',
@@ -546,8 +564,6 @@ const i18n = {
'cli.recursiveQueryDesc': 'Aggregate CLI history and memory data from parent and child projects',
'cli.maxContextFiles': 'Max Context Files',
'cli.maxContextFilesDesc': 'Maximum files to include in smart context',
'cli.codeIndexMcp': 'Code Index MCP',
'cli.codeIndexMcpDesc': 'Code search provider (updates CLAUDE.md context-tools reference)',
// CCW Install
'ccw.install': 'CCW Install',
@@ -1711,6 +1727,203 @@ const i18n = {
'coreMemory.belongsToClusters': 'Belongs to Clusters',
'coreMemory.relationsError': 'Failed to load relations',
// Issue Manager
'nav.issues': 'Issues',
'nav.issueManager': 'Manager',
'nav.issueDiscovery': 'Discovery',
'title.issueManager': 'Issue Manager',
'title.issueDiscovery': 'Issue Discovery',
// Issue Discovery
'discovery.title': 'Issue Discovery',
'discovery.description': 'Discover potential issues from multiple perspectives',
'discovery.noSessions': 'No discovery sessions',
'discovery.noDiscoveries': 'No discoveries yet',
'discovery.runHint': 'Run /issue:discover to start discovering issues',
'discovery.runCommand': 'Run /issue:discover to start discovering issues',
'discovery.sessions': 'Sessions',
'discovery.findings': 'Findings',
'discovery.phase': 'Phase',
'discovery.perspectives': 'Perspectives',
'discovery.progress': 'Progress',
'discovery.total': 'Total',
'discovery.exported': 'Exported',
'discovery.dismissed': 'Dismissed',
'discovery.pending': 'Pending',
'discovery.external': 'External Research',
'discovery.selectAll': 'Select All',
'discovery.deselectAll': 'Deselect All',
'discovery.exportSelected': 'Export Selected',
'discovery.dismissSelected': 'Dismiss Selected',
'discovery.exportAsIssue': 'Export as Issue',
'discovery.dismiss': 'Dismiss',
'discovery.keep': 'Keep',
'discovery.priority.critical': 'Critical',
'discovery.priority.high': 'High',
'discovery.priority.medium': 'Medium',
'discovery.priority.low': 'Low',
'discovery.perspective.bug': 'Bug',
'discovery.perspective.ux': 'UX',
'discovery.perspective.test': 'Test',
'discovery.perspective.quality': 'Quality',
'discovery.perspective.security': 'Security',
'discovery.perspective.performance': 'Performance',
'discovery.perspective.maintainability': 'Maintainability',
'discovery.perspective.best-practices': 'Best Practices',
'discovery.file': 'File',
'discovery.line': 'Line',
'discovery.confidence': 'Confidence',
'discovery.suggestedIssue': 'Suggested Issue',
'discovery.externalRef': 'External Reference',
'discovery.noFindings': 'No findings match your filters',
'discovery.filterPerspective': 'Filter by Perspective',
'discovery.filterPriority': 'Filter by Priority',
'discovery.filterAll': 'All',
'discovery.allPerspectives': 'All Perspectives',
'discovery.allPriorities': 'All Priorities',
'discovery.selectFinding': 'Select a finding to preview',
'discovery.location': 'Location',
'discovery.code': 'Code',
'discovery.impact': 'Impact',
'discovery.recommendation': 'Recommendation',
'discovery.exportAsIssues': 'Export as Issues',
'discovery.selectAll': 'Select All',
'discovery.deselectAll': 'Deselect All',
'discovery.deleteSession': 'Delete Session',
'discovery.confirmDelete': 'Are you sure you want to delete this discovery session?',
'discovery.deleted': 'Discovery session deleted',
'discovery.exportSuccess': 'Findings exported as issues',
'discovery.dismissSuccess': 'Findings dismissed',
'discovery.backToList': 'Back to Sessions',
'discovery.viewDetails': 'View Details',
'discovery.inProgress': 'In Progress',
'discovery.completed': 'Completed',
// issues.* keys (used by issue-manager.js)
'issues.title': 'Issue Manager',
'issues.description': 'Manage issues, solutions, and execution queue',
'issues.viewIssues': 'Issues',
'issues.viewQueue': 'Queue',
'issues.filterStatus': 'Status',
'issues.filterAll': 'All',
'issues.noIssues': 'No issues found',
'issues.createHint': 'Click "Create" to add your first issue',
'issues.priority': 'Priority',
'issues.tasks': 'tasks',
'issues.solutions': 'solutions',
'issues.boundSolution': 'Bound',
'issues.queueEmpty': 'Queue is empty',
'issues.reorderHint': 'Drag items within a group to reorder',
'issues.parallelGroup': 'Parallel',
'issues.sequentialGroup': 'Sequential',
'issues.dependsOn': 'Depends on',
// Create & Search
'issues.create': 'Create',
'issues.createTitle': 'Create New Issue',
'issues.issueId': 'Issue ID',
'issues.issueTitle': 'Title',
'issues.issueContext': 'Context',
'issues.issuePriority': 'Priority',
'issues.titlePlaceholder': 'Brief description of the issue',
'issues.contextPlaceholder': 'Detailed description, requirements, etc.',
'issues.priorityLowest': 'Lowest',
'issues.priorityLow': 'Low',
'issues.priorityMedium': 'Medium',
'issues.priorityHigh': 'High',
'issues.priorityCritical': 'Critical',
'issues.searchPlaceholder': 'Search issues...',
'issues.showing': 'Showing',
'issues.of': 'of',
'issues.issues': 'issues',
'issues.tryDifferentFilter': 'Try adjusting your search or filters',
'issues.createFirst': 'Create First Issue',
'issues.idRequired': 'Issue ID is required',
'issues.titleRequired': 'Title is required',
'issues.created': 'Issue created successfully',
'issues.confirmDelete': 'Are you sure you want to delete this issue?',
'issues.deleted': 'Issue deleted',
'issues.idAutoGenerated': 'Auto-generated',
'issues.regenerateId': 'Regenerate ID',
// Solution detail
'issues.solutionDetail': 'Solution Details',
'issues.bind': 'Bind',
'issues.unbind': 'Unbind',
'issues.bound': 'Bound',
'issues.totalTasks': 'Total Tasks',
'issues.bindStatus': 'Bind Status',
'issues.createdAt': 'Created',
'issues.taskList': 'Task List',
'issues.noTasks': 'No tasks in this solution',
'issues.noSolutions': 'No solutions',
'issues.viewJson': 'View Raw JSON',
'issues.scope': 'Scope',
'issues.modificationPoints': 'Modification Points',
'issues.implementationSteps': 'Implementation Steps',
'issues.acceptanceCriteria': 'Acceptance Criteria',
'issues.dependencies': 'Dependencies',
'issues.solutionBound': 'Solution bound successfully',
'issues.solutionUnbound': 'Solution unbound',
// Queue operations
'issues.queueEmptyHint': 'Generate execution queue from bound solutions',
'issues.createQueue': 'Create Queue',
'issues.regenerate': 'Regenerate',
'issues.regenerateQueue': 'Regenerate Queue',
'issues.refreshQueue': 'Refresh',
'issues.executionGroups': 'groups',
'issues.totalItems': 'items',
'issues.queueRefreshed': 'Queue refreshed',
'issues.confirmCreateQueue': 'This will execute /issue:queue command via Claude Code CLI to generate execution queue from bound solutions.\n\nContinue?',
'issues.creatingQueue': 'Creating execution queue...',
'issues.queueExecutionStarted': 'Queue generation started',
'issues.queueCreated': 'Queue created successfully',
'issues.queueCreationFailed': 'Queue creation failed',
'issues.queueCommandHint': 'Run one of the following commands in your terminal to generate the execution queue from bound solutions:',
'issues.queueCommandInfo': 'After running the command, click "Refresh" to see the updated queue.',
'issues.alternative': 'Alternative',
'issues.refreshAfter': 'Refresh Queue',
// issue.* keys (legacy)
'issue.viewIssues': 'Issues',
'issue.viewQueue': 'Queue',
'issue.filterAll': 'All',
'issue.filterStatus': 'Status',
'issue.filterPriority': 'Priority',
'issue.noIssues': 'No issues found',
'issue.noIssuesHint': 'Issues will appear here when created via /issue:plan command',
'issue.noQueue': 'No tasks in queue',
'issue.noQueueHint': 'Run /issue:queue to form execution queue from bound solutions',
'issue.tasks': 'tasks',
'issue.solutions': 'solutions',
'issue.parallel': 'Parallel',
'issue.sequential': 'Sequential',
'issue.status.registered': 'Registered',
'issue.status.planned': 'Planned',
'issue.status.queued': 'Queued',
'issue.status.executing': 'Executing',
'issue.status.completed': 'Completed',
'issue.status.failed': 'Failed',
'issue.priority.critical': 'Critical',
'issue.priority.high': 'High',
'issue.priority.medium': 'Medium',
'issue.priority.low': 'Low',
'issue.detail.context': 'Context',
'issue.detail.solutions': 'Solutions',
'issue.detail.tasks': 'Tasks',
'issue.detail.noSolutions': 'No solutions available',
'issue.detail.noTasks': 'No tasks available',
'issue.detail.bound': 'Bound',
'issue.detail.modificationPoints': 'Modification Points',
'issue.detail.implementation': 'Implementation Steps',
'issue.detail.acceptance': 'Acceptance Criteria',
'issue.queue.reordered': 'Queue reordered',
'issue.queue.reorderFailed': 'Failed to reorder queue',
'issue.saved': 'Issue saved',
'issue.saveFailed': 'Failed to save issue',
'issue.taskUpdated': 'Task updated',
'issue.taskUpdateFailed': 'Failed to update task',
'issue.conflicts': 'Conflicts',
'issue.noConflicts': 'No conflicts detected',
'issue.conflict.resolved': 'Resolved',
'issue.conflict.pending': 'Pending',
// Common additions
'common.copyId': 'Copy ID',
'common.copied': 'Copied!',
@@ -1737,6 +1950,8 @@ const i18n = {
'common.deleteFailed': '删除失败',
'common.retry': '重试',
'common.refresh': '刷新',
'common.back': '返回',
'common.search': '搜索...',
'common.minutes': '分钟',
'common.enabled': '已启用',
'common.disabled': '已禁用',
@@ -1748,7 +1963,23 @@ const i18n = {
'header.refreshWorkspace': '刷新工作区',
'header.toggleTheme': '切换主题',
'header.language': '语言',
'header.cliStream': 'CLI 流式输出',
// CLI Stream Viewer
'cliStream.title': 'CLI 流式输出',
'cliStream.clearCompleted': '清除已完成',
'cliStream.noStreams': '没有活动的 CLI 执行',
'cliStream.noStreamsHint': '启动 CLI 命令以查看流式输出',
'cliStream.running': '运行中',
'cliStream.completed': '已完成',
'cliStream.error': '错误',
'cliStream.autoScroll': '自动滚动',
'cliStream.close': '关闭',
'cliStream.cannotCloseRunning': '无法关闭运行中的执行',
'cliStream.lines': '行',
'cliStream.searchPlaceholder': '搜索输出...',
'cliStream.filterResults': '条结果',
// Sidebar - Project section
'nav.project': '项目',
'nav.overview': '概览',
@@ -2256,8 +2487,6 @@ const i18n = {
'cli.recursiveQueryDesc': '聚合显示父项目和子项目的 CLI 历史与内存数据',
'cli.maxContextFiles': '最大上下文文件数',
'cli.maxContextFilesDesc': '智能上下文包含的最大文件数',
'cli.codeIndexMcp': '代码索引 MCP',
'cli.codeIndexMcpDesc': '代码搜索提供者 (更新 CLAUDE.md 的 context-tools 引用)',
// CCW Install
'ccw.install': 'CCW 安装',
@@ -3308,6 +3537,8 @@ const i18n = {
'common.edit': '编辑',
'common.close': '关闭',
'common.refresh': '刷新',
'common.back': '返回',
'common.search': '搜索...',
'common.refreshed': '已刷新',
'common.refreshing': '刷新中...',
'common.loading': '加载中...',
@@ -3429,6 +3660,203 @@ const i18n = {
'coreMemory.belongsToClusters': '所属聚类',
'coreMemory.relationsError': '加载关联失败',
// Issue Manager
'nav.issues': '议题',
'nav.issueManager': '管理器',
'nav.issueDiscovery': '发现',
'title.issueManager': '议题管理器',
'title.issueDiscovery': '议题发现',
// Issue Discovery
'discovery.title': '议题发现',
'discovery.description': '从多个视角发现潜在问题',
'discovery.noSessions': '暂无发现会话',
'discovery.noDiscoveries': '暂无发现',
'discovery.runHint': '运行 /issue:discover 开始发现问题',
'discovery.runCommand': '运行 /issue:discover 开始发现问题',
'discovery.sessions': '会话',
'discovery.findings': '发现',
'discovery.phase': '阶段',
'discovery.perspectives': '视角',
'discovery.progress': '进度',
'discovery.total': '总计',
'discovery.exported': '已导出',
'discovery.dismissed': '已忽略',
'discovery.pending': '待处理',
'discovery.external': '外部研究',
'discovery.selectAll': '全选',
'discovery.deselectAll': '取消全选',
'discovery.exportSelected': '导出选中',
'discovery.dismissSelected': '忽略选中',
'discovery.exportAsIssue': '导出为议题',
'discovery.dismiss': '忽略',
'discovery.keep': '保留',
'discovery.priority.critical': '紧急',
'discovery.priority.high': '高',
'discovery.priority.medium': '中',
'discovery.priority.low': '低',
'discovery.perspective.bug': 'Bug',
'discovery.perspective.ux': '用户体验',
'discovery.perspective.test': '测试',
'discovery.perspective.quality': '代码质量',
'discovery.perspective.security': '安全',
'discovery.perspective.performance': '性能',
'discovery.perspective.maintainability': '可维护性',
'discovery.perspective.best-practices': '最佳实践',
'discovery.file': '文件',
'discovery.line': '行号',
'discovery.confidence': '置信度',
'discovery.suggestedIssue': '建议议题',
'discovery.externalRef': '外部参考',
'discovery.noFindings': '没有匹配的发现',
'discovery.filterPerspective': '按视角筛选',
'discovery.filterPriority': '按优先级筛选',
'discovery.filterAll': '全部',
'discovery.allPerspectives': '所有视角',
'discovery.allPriorities': '所有优先级',
'discovery.selectFinding': '选择一个发现以预览',
'discovery.location': '位置',
'discovery.code': '代码',
'discovery.impact': '影响',
'discovery.recommendation': '建议',
'discovery.exportAsIssues': '导出为议题',
'discovery.selectAll': '全选',
'discovery.deselectAll': '取消全选',
'discovery.deleteSession': '删除会话',
'discovery.confirmDelete': '确定要删除此发现会话吗?',
'discovery.deleted': '发现会话已删除',
'discovery.exportSuccess': '发现已导出为议题',
'discovery.dismissSuccess': '发现已忽略',
'discovery.backToList': '返回列表',
'discovery.viewDetails': '查看详情',
'discovery.inProgress': '进行中',
'discovery.completed': '已完成',
// issues.* keys (used by issue-manager.js)
'issues.title': '议题管理器',
'issues.description': '管理议题、解决方案和执行队列',
'issues.viewIssues': '议题',
'issues.viewQueue': '队列',
'issues.filterStatus': '状态',
'issues.filterAll': '全部',
'issues.noIssues': '暂无议题',
'issues.createHint': '点击"创建"添加您的第一个议题',
'issues.priority': '优先级',
'issues.tasks': '任务',
'issues.solutions': '解决方案',
'issues.boundSolution': '已绑定',
'issues.queueEmpty': '队列为空',
'issues.reorderHint': '在组内拖拽项目以重新排序',
'issues.parallelGroup': '并行',
'issues.sequentialGroup': '顺序',
'issues.dependsOn': '依赖于',
// Create & Search
'issues.create': '创建',
'issues.createTitle': '创建新议题',
'issues.issueId': '议题ID',
'issues.issueTitle': '标题',
'issues.issueContext': '上下文',
'issues.issuePriority': '优先级',
'issues.titlePlaceholder': '简要描述议题',
'issues.contextPlaceholder': '详细描述、需求等',
'issues.priorityLowest': '最低',
'issues.priorityLow': '低',
'issues.priorityMedium': '中',
'issues.priorityHigh': '高',
'issues.priorityCritical': '紧急',
'issues.searchPlaceholder': '搜索议题...',
'issues.showing': '显示',
'issues.of': '共',
'issues.issues': '条议题',
'issues.tryDifferentFilter': '尝试调整搜索或筛选条件',
'issues.createFirst': '创建第一个议题',
'issues.idRequired': '议题ID为必填',
'issues.titleRequired': '标题为必填',
'issues.created': '议题创建成功',
'issues.confirmDelete': '确定要删除此议题吗?',
'issues.deleted': '议题已删除',
'issues.idAutoGenerated': '自动生成',
'issues.regenerateId': '重新生成ID',
// Solution detail
'issues.solutionDetail': '解决方案详情',
'issues.bind': '绑定',
'issues.unbind': '解绑',
'issues.bound': '已绑定',
'issues.totalTasks': '任务总数',
'issues.bindStatus': '绑定状态',
'issues.createdAt': '创建时间',
'issues.taskList': '任务列表',
'issues.noTasks': '此解决方案无任务',
'issues.noSolutions': '暂无解决方案',
'issues.viewJson': '查看原始JSON',
'issues.scope': '作用域',
'issues.modificationPoints': '修改点',
'issues.implementationSteps': '实现步骤',
'issues.acceptanceCriteria': '验收标准',
'issues.dependencies': '依赖项',
'issues.solutionBound': '解决方案已绑定',
'issues.solutionUnbound': '解决方案已解绑',
// Queue operations
'issues.queueEmptyHint': '从绑定的解决方案生成执行队列',
'issues.createQueue': '创建队列',
'issues.regenerate': '重新生成',
'issues.regenerateQueue': '重新生成队列',
'issues.refreshQueue': '刷新',
'issues.executionGroups': '个执行组',
'issues.totalItems': '个任务',
'issues.queueRefreshed': '队列已刷新',
'issues.confirmCreateQueue': '这将通过 Claude Code CLI 执行 /issue:queue 命令,从绑定的解决方案生成执行队列。\n\n是否继续',
'issues.creatingQueue': '正在创建执行队列...',
'issues.queueExecutionStarted': '队列生成已启动',
'issues.queueCreated': '队列创建成功',
'issues.queueCreationFailed': '队列创建失败',
'issues.queueCommandHint': '在终端中运行以下命令之一,从绑定的解决方案生成执行队列:',
'issues.queueCommandInfo': '运行命令后,点击"刷新"查看更新后的队列。',
'issues.alternative': '或者',
'issues.refreshAfter': '刷新队列',
// issue.* keys (legacy)
'issue.viewIssues': '议题',
'issue.viewQueue': '队列',
'issue.filterAll': '全部',
'issue.filterStatus': '状态',
'issue.filterPriority': '优先级',
'issue.noIssues': '暂无议题',
'issue.noIssuesHint': '通过 /issue:plan 命令创建的议题将显示在此处',
'issue.noQueue': '队列中暂无任务',
'issue.noQueueHint': '运行 /issue:queue 从绑定的解决方案生成执行队列',
'issue.tasks': '任务',
'issue.solutions': '解决方案',
'issue.parallel': '并行',
'issue.sequential': '顺序',
'issue.status.registered': '已注册',
'issue.status.planned': '已规划',
'issue.status.queued': '已入队',
'issue.status.executing': '执行中',
'issue.status.completed': '已完成',
'issue.status.failed': '失败',
'issue.priority.critical': '紧急',
'issue.priority.high': '高',
'issue.priority.medium': '中',
'issue.priority.low': '低',
'issue.detail.context': '上下文',
'issue.detail.solutions': '解决方案',
'issue.detail.tasks': '任务',
'issue.detail.noSolutions': '暂无解决方案',
'issue.detail.noTasks': '暂无任务',
'issue.detail.bound': '已绑定',
'issue.detail.modificationPoints': '修改点',
'issue.detail.implementation': '实现步骤',
'issue.detail.acceptance': '验收标准',
'issue.queue.reordered': '队列已重排',
'issue.queue.reorderFailed': '队列重排失败',
'issue.saved': '议题已保存',
'issue.saveFailed': '保存议题失败',
'issue.taskUpdated': '任务已更新',
'issue.taskUpdateFailed': '更新任务失败',
'issue.conflicts': '冲突',
'issue.noConflicts': '未检测到冲突',
'issue.conflict.resolved': '已解决',
'issue.conflict.pending': '待处理',
// Common additions
'common.copyId': '复制 ID',
'common.copied': '已复制!',

View File

@@ -987,24 +987,6 @@ function renderCliSettingsSection() {
'</div>' +
'<p class="cli-setting-desc">' + t('cli.maxContextFilesDesc') + '</p>' +
'</div>' +
'<div class="cli-setting-item">' +
'<label class="cli-setting-label">' +
'<i data-lucide="search" class="w-3 h-3"></i>' +
t('cli.codeIndexMcp') +
'</label>' +
'<div class="cli-setting-control">' +
'<select class="cli-setting-select" onchange="setCodeIndexMcpProvider(this.value)">' +
'<option value="codexlens"' + (codeIndexMcpProvider === 'codexlens' ? ' selected' : '') + '>CodexLens</option>' +
'<option value="ace"' + (codeIndexMcpProvider === 'ace' ? ' selected' : '') + '>ACE (Augment)</option>' +
'<option value="none"' + (codeIndexMcpProvider === 'none' ? ' selected' : '') + '>None (Built-in)</option>' +
'</select>' +
'</div>' +
'<p class="cli-setting-desc">' + t('cli.codeIndexMcpDesc') + '</p>' +
'<p class="cli-setting-desc text-xs text-muted-foreground">' +
'<i data-lucide="file-text" class="w-3 h-3 inline-block mr-1"></i>' +
'Current: <code class="bg-muted px-1 rounded">' + getContextToolsFileName(codeIndexMcpProvider) + '</code>' +
'</p>' +
'</div>' +
'</div>';
container.innerHTML = settingsHtml;

View File

@@ -446,6 +446,12 @@ async function loadSemanticDepsStatus() {
* Build GPU mode selector HTML
*/
function buildGpuModeSelector(gpuInfo) {
// Check if DirectML is unavailable due to Python environment
var directmlUnavailableReason = null;
if (!gpuInfo.available.includes('directml') && gpuInfo.pythonEnv && gpuInfo.pythonEnv.error) {
directmlUnavailableReason = gpuInfo.pythonEnv.error;
}
var modes = [
{
id: 'cpu',
@@ -457,10 +463,13 @@ function buildGpuModeSelector(gpuInfo) {
{
id: 'directml',
label: 'DirectML',
desc: t('codexlens.directmlModeDesc') || 'Windows GPU (NVIDIA/AMD/Intel)',
desc: directmlUnavailableReason
? directmlUnavailableReason
: (t('codexlens.directmlModeDesc') || 'Windows GPU (NVIDIA/AMD/Intel)'),
icon: 'cpu',
available: gpuInfo.available.includes('directml'),
recommended: gpuInfo.mode === 'directml'
recommended: gpuInfo.mode === 'directml',
warning: directmlUnavailableReason
},
{
id: 'cuda',
@@ -487,6 +496,7 @@ function buildGpuModeSelector(gpuInfo) {
var isDisabled = !mode.available;
var isRecommended = mode.recommended;
var isDefault = mode.id === gpuInfo.mode;
var hasWarning = mode.warning;
html +=
'<label class="flex items-center gap-3 p-2 rounded border cursor-pointer hover:bg-muted/50 transition-colors ' +
@@ -502,7 +512,7 @@ function buildGpuModeSelector(gpuInfo) {
(isRecommended ? '<span class="text-xs bg-primary/20 text-primary px-1.5 py-0.5 rounded">' + (t('common.recommended') || 'Recommended') + '</span>' : '') +
(isDisabled ? '<span class="text-xs text-muted-foreground">(' + (t('common.unavailable') || 'Unavailable') + ')</span>' : '') +
'</div>' +
'<div class="text-xs text-muted-foreground">' + mode.desc + '</div>' +
'<div class="text-xs ' + (hasWarning ? 'text-warning' : 'text-muted-foreground') + '">' + mode.desc + '</div>' +
'</div>' +
'</label>';
});

View File

@@ -168,16 +168,22 @@ async function loadAvailableSkills() {
if (!response.ok) throw new Error('Failed to load skills');
const data = await response.json();
// Combine project and user skills (API returns { projectSkills: [], userSkills: [] })
const allSkills = [
...(data.projectSkills || []).map(s => ({ ...s, scope: 'project' })),
...(data.userSkills || []).map(s => ({ ...s, scope: 'user' }))
];
const container = document.getElementById('skill-discovery-skill-context');
if (container && data.skills) {
if (data.skills.length === 0) {
if (container) {
if (allSkills.length === 0) {
container.innerHTML = `
<span class="font-mono bg-muted px-1.5 py-0.5 rounded">${t('hook.wizard.availableSkills')}</span>
<span class="text-muted-foreground ml-2">${t('hook.wizard.noSkillsFound').split('.')[0]}</span>
`;
} else {
const skillBadges = data.skills.map(skill => `
<span class="px-2 py-0.5 bg-emerald-500/10 text-emerald-500 rounded" title="${escapeHtml(skill.description)}">${escapeHtml(skill.name)}</span>
const skillBadges = allSkills.map(skill => `
<span class="px-2 py-0.5 bg-emerald-500/10 text-emerald-500 rounded" title="${escapeHtml(skill.description || '')}">${escapeHtml(skill.name)}</span>
`).join('');
container.innerHTML = `
<span class="font-mono bg-muted px-1.5 py-0.5 rounded">${t('hook.wizard.availableSkills')}</span>
@@ -187,7 +193,7 @@ async function loadAvailableSkills() {
}
// Store skills for wizard use
window.availableSkills = data.skills || [];
window.availableSkills = allSkills;
} catch (err) {
console.error('Failed to load skills:', err);
const container = document.getElementById('skill-discovery-skill-context');

View File

@@ -0,0 +1,730 @@
// ==========================================
// ISSUE DISCOVERY VIEW
// Manages discovery sessions and findings
// ==========================================
// ========== Discovery State ==========
var discoveryData = {
discoveries: [],
selectedDiscovery: null,
selectedFinding: null,
findings: [],
perspectiveFilter: 'all',
priorityFilter: 'all',
searchQuery: '',
selectedFindings: new Set(),
viewMode: 'list' // 'list' | 'detail'
};
var discoveryLoading = false;
var discoveryPollingInterval = null;
// ========== Helper Functions ==========
function getFilteredFindings() {
const findings = discoveryData.findings || [];
let filtered = findings;
if (discoveryData.perspectiveFilter !== 'all') {
filtered = filtered.filter(f => f.perspective === discoveryData.perspectiveFilter);
}
if (discoveryData.priorityFilter !== 'all') {
filtered = filtered.filter(f => f.priority === discoveryData.priorityFilter);
}
if (discoveryData.searchQuery) {
const q = discoveryData.searchQuery.toLowerCase();
filtered = filtered.filter(f =>
(f.title && f.title.toLowerCase().includes(q)) ||
(f.file && f.file.toLowerCase().includes(q)) ||
(f.description && f.description.toLowerCase().includes(q))
);
}
return filtered;
}
// ========== Main Render Function ==========
async function renderIssueDiscovery() {
const container = document.getElementById('mainContent');
if (!container) return;
// Hide stats grid and carousel
hideStatsAndCarousel();
// Show loading state
container.innerHTML = '<div class="discovery-manager loading">' +
'<div class="loading-spinner"><i data-lucide="loader-2" class="w-8 h-8 animate-spin"></i></div>' +
'<p>' + t('common.loading') + '</p>' +
'</div>';
lucide.createIcons();
// Load data
await loadDiscoveryData();
// Render the main view
renderDiscoveryView();
}
// ========== Data Loading ==========
async function loadDiscoveryData() {
discoveryLoading = true;
try {
const response = await fetch('/api/discoveries?path=' + encodeURIComponent(projectPath));
if (!response.ok) throw new Error('Failed to load discoveries');
const data = await response.json();
discoveryData.discoveries = data.discoveries || [];
updateDiscoveryBadge();
} catch (err) {
console.error('Failed to load discoveries:', err);
discoveryData.discoveries = [];
} finally {
discoveryLoading = false;
}
}
async function loadDiscoveryDetail(discoveryId) {
try {
const response = await fetch('/api/discoveries/' + encodeURIComponent(discoveryId) + '?path=' + encodeURIComponent(projectPath));
if (!response.ok) throw new Error('Failed to load discovery detail');
return await response.json();
} catch (err) {
console.error('Failed to load discovery detail:', err);
return null;
}
}
async function loadDiscoveryFindings(discoveryId) {
try {
let url = '/api/discoveries/' + encodeURIComponent(discoveryId) + '/findings?path=' + encodeURIComponent(projectPath);
if (discoveryData.perspectiveFilter !== 'all') {
url += '&perspective=' + encodeURIComponent(discoveryData.perspectiveFilter);
}
if (discoveryData.priorityFilter !== 'all') {
url += '&priority=' + encodeURIComponent(discoveryData.priorityFilter);
}
const response = await fetch(url);
if (!response.ok) throw new Error('Failed to load findings');
const data = await response.json();
return data.findings || [];
} catch (err) {
console.error('Failed to load findings:', err);
return [];
}
}
async function loadDiscoveryProgress(discoveryId) {
try {
const response = await fetch('/api/discoveries/' + encodeURIComponent(discoveryId) + '/progress?path=' + encodeURIComponent(projectPath));
if (!response.ok) return null;
return await response.json();
} catch (err) {
return null;
}
}
function updateDiscoveryBadge() {
const badge = document.getElementById('badgeDiscovery');
if (badge) {
badge.textContent = discoveryData.discoveries.length;
}
}
// ========== Main View Render ==========
function renderDiscoveryView() {
const container = document.getElementById('mainContent');
if (!container) return;
container.innerHTML = `
<div class="discovery-manager">
<!-- Header -->
<div class="discovery-header mb-6">
<div class="flex items-center justify-between flex-wrap gap-4">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-primary/10 rounded-lg flex items-center justify-center">
<i data-lucide="search-code" class="w-5 h-5 text-primary"></i>
</div>
<div>
<h2 class="text-lg font-semibold text-foreground">${t('discovery.title') || 'Issue Discovery'}</h2>
<p class="text-sm text-muted-foreground">${t('discovery.description') || 'Discover potential issues from multiple perspectives'}</p>
</div>
</div>
<div class="flex items-center gap-3">
${discoveryData.viewMode === 'detail' ? `
<button class="discovery-back-btn" onclick="backToDiscoveryList()">
<i data-lucide="arrow-left" class="w-4 h-4"></i>
<span>${t('common.back') || 'Back'}</span>
</button>
` : ''}
</div>
</div>
</div>
${discoveryData.viewMode === 'list' ? renderDiscoveryListSection() : renderDiscoveryDetailSection()}
</div>
`;
lucide.createIcons();
}
// ========== Discovery List Section ==========
function renderDiscoveryListSection() {
const discoveries = discoveryData.discoveries || [];
if (discoveries.length === 0) {
return `
<div class="discovery-empty">
<div class="empty-icon">
<i data-lucide="search-x" class="w-12 h-12 text-muted-foreground"></i>
</div>
<h3 class="text-lg font-medium text-foreground mt-4">${t('discovery.noDiscoveries') || 'No discoveries yet'}</h3>
<p class="text-sm text-muted-foreground mt-2">${t('discovery.runCommand') || 'Run /issue:discover to start discovering issues'}</p>
<div class="mt-4 p-3 bg-muted/50 rounded-lg">
<code class="text-sm text-primary">/issue:discover src/auth/**</code>
</div>
</div>
`;
}
return `
<div class="discovery-list-container">
${discoveries.map(d => renderDiscoveryCard(d)).join('')}
</div>
`;
}
function renderDiscoveryCard(discovery) {
const { discovery_id, target_pattern, perspectives, phase, total_findings, issues_generated, priority_distribution, progress } = discovery;
const isComplete = phase === 'complete';
const isRunning = phase && phase !== 'complete' && phase !== 'failed';
// Calculate progress percentage
let progressPercent = 0;
if (progress && progress.perspective_analysis) {
progressPercent = progress.perspective_analysis.percent_complete || 0;
} else if (isComplete) {
progressPercent = 100;
}
// Priority distribution bar
const critical = priority_distribution?.critical || 0;
const high = priority_distribution?.high || 0;
const medium = priority_distribution?.medium || 0;
const low = priority_distribution?.low || 0;
const total = critical + high + medium + low || 1;
return `
<div class="discovery-card ${isComplete ? 'complete' : ''} ${isRunning ? 'running' : ''}" onclick="viewDiscoveryDetail('${discovery_id}')">
<div class="discovery-card-header">
<div class="discovery-id">
<i data-lucide="search" class="w-4 h-4"></i>
<span>${discovery_id}</span>
</div>
<span class="discovery-phase ${phase}">${phase || 'unknown'}</span>
</div>
<div class="discovery-card-body">
<div class="discovery-target">
<i data-lucide="folder" class="w-4 h-4 text-muted-foreground"></i>
<span class="text-sm text-foreground">${target_pattern || 'N/A'}</span>
</div>
${perspectives && perspectives.length > 0 ? `
<div class="discovery-perspectives">
${perspectives.slice(0, 5).map(p => `<span class="perspective-badge ${p}">${p}</span>`).join('')}
${perspectives.length > 5 ? `<span class="perspective-badge more">+${perspectives.length - 5}</span>` : ''}
</div>
` : ''}
${isRunning ? `
<div class="discovery-progress-bar">
<div class="progress-fill" style="width: ${progressPercent}%"></div>
</div>
<div class="text-xs text-muted-foreground mt-1">${progressPercent}% complete</div>
` : ''}
<div class="discovery-stats">
<div class="stat">
<span class="stat-value">${total_findings || 0}</span>
<span class="stat-label">${t('discovery.findings') || 'Findings'}</span>
</div>
<div class="stat">
<span class="stat-value">${issues_generated || 0}</span>
<span class="stat-label">${t('discovery.exported') || 'Exported'}</span>
</div>
</div>
${total_findings > 0 ? `
<div class="discovery-priority-bar">
<div class="priority-segment critical" style="width: ${(critical / total) * 100}%" title="Critical: ${critical}"></div>
<div class="priority-segment high" style="width: ${(high / total) * 100}%" title="High: ${high}"></div>
<div class="priority-segment medium" style="width: ${(medium / total) * 100}%" title="Medium: ${medium}"></div>
<div class="priority-segment low" style="width: ${(low / total) * 100}%" title="Low: ${low}"></div>
</div>
` : ''}
</div>
<div class="discovery-card-footer">
<button class="discovery-action-btn" onclick="event.stopPropagation(); deleteDiscovery('${discovery_id}')">
<i data-lucide="trash-2" class="w-4 h-4"></i>
</button>
</div>
</div>
`;
}
// ========== Discovery Detail Section ==========
function renderDiscoveryDetailSection() {
const discovery = discoveryData.selectedDiscovery;
if (!discovery) {
return '<div class="loading-spinner"><i data-lucide="loader-2" class="w-8 h-8 animate-spin"></i></div>';
}
const findings = discoveryData.findings || [];
const perspectives = [...new Set(findings.map(f => f.perspective))];
const filteredFindings = getFilteredFindings();
return `
<div class="discovery-detail-container">
<!-- Left Panel: Findings List -->
<div class="discovery-findings-panel">
<!-- Toolbar -->
<div class="discovery-toolbar">
<div class="toolbar-filters">
<select class="filter-select" onchange="filterDiscoveryByPerspective(this.value)">
<option value="all" ${discoveryData.perspectiveFilter === 'all' ? 'selected' : ''}>${t('discovery.allPerspectives') || 'All Perspectives'}</option>
${perspectives.map(p => `<option value="${p}" ${discoveryData.perspectiveFilter === p ? 'selected' : ''}>${p}</option>`).join('')}
</select>
<select class="filter-select" onchange="filterDiscoveryByPriority(this.value)">
<option value="all" ${discoveryData.priorityFilter === 'all' ? 'selected' : ''}>${t('discovery.allPriorities') || 'All Priorities'}</option>
<option value="critical" ${discoveryData.priorityFilter === 'critical' ? 'selected' : ''}>Critical</option>
<option value="high" ${discoveryData.priorityFilter === 'high' ? 'selected' : ''}>High</option>
<option value="medium" ${discoveryData.priorityFilter === 'medium' ? 'selected' : ''}>Medium</option>
<option value="low" ${discoveryData.priorityFilter === 'low' ? 'selected' : ''}>Low</option>
</select>
</div>
<div class="toolbar-search">
<i data-lucide="search" class="w-4 h-4"></i>
<input type="text" placeholder="${t('common.search') || 'Search...'}"
value="${discoveryData.searchQuery}"
oninput="searchDiscoveryFindings(this.value)">
</div>
</div>
<!-- Findings Count -->
<div class="findings-count">
<div class="findings-count-left">
<span>${filteredFindings.length} ${t('discovery.findings') || 'findings'}</span>
${discoveryData.selectedFindings.size > 0 ? `
<span class="selected-count">(${discoveryData.selectedFindings.size} selected)</span>
` : ''}
</div>
<div class="findings-count-actions">
<button class="select-action-btn" onclick="selectAllFindings()">
<i data-lucide="check-square" class="w-3 h-3"></i>
<span>${t('discovery.selectAll') || 'Select All'}</span>
</button>
<button class="select-action-btn" onclick="deselectAllFindings()">
<i data-lucide="square" class="w-3 h-3"></i>
<span>${t('discovery.deselectAll') || 'Deselect All'}</span>
</button>
</div>
</div>
<!-- Findings List -->
<div class="findings-list">
${filteredFindings.length === 0 ? `
<div class="findings-empty">
<i data-lucide="inbox" class="w-8 h-8 text-muted-foreground"></i>
<p>${t('discovery.noFindings') || 'No findings match your filters'}</p>
</div>
` : filteredFindings.map(f => renderFindingItem(f)).join('')}
</div>
<!-- Bulk Actions -->
${discoveryData.selectedFindings.size > 0 ? `
<div class="bulk-actions">
<span class="bulk-count">${discoveryData.selectedFindings.size} selected</span>
<button class="bulk-action-btn export" onclick="exportSelectedFindings()">
<i data-lucide="upload" class="w-4 h-4"></i>
<span>${t('discovery.exportAsIssues') || 'Export as Issues'}</span>
</button>
<button class="bulk-action-btn dismiss" onclick="dismissSelectedFindings()">
<i data-lucide="x" class="w-4 h-4"></i>
<span>${t('discovery.dismiss') || 'Dismiss'}</span>
</button>
</div>
` : ''}
</div>
<!-- Right Panel: Finding Preview -->
<div class="discovery-preview-panel">
${discoveryData.selectedFinding ? renderFindingPreview(discoveryData.selectedFinding) : `
<div class="preview-empty">
<i data-lucide="mouse-pointer-click" class="w-12 h-12 text-muted-foreground"></i>
<p>${t('discovery.selectFinding') || 'Select a finding to preview'}</p>
</div>
`}
</div>
</div>
`;
}
function renderFindingItem(finding) {
const isSelected = discoveryData.selectedFindings.has(finding.id);
const isActive = discoveryData.selectedFinding?.id === finding.id;
const isExported = finding.exported === true;
return `
<div class="finding-item ${isActive ? 'active' : ''} ${isSelected ? 'selected' : ''} ${finding.dismissed ? 'dismissed' : ''} ${isExported ? 'exported' : ''}"
onclick="selectFinding('${finding.id}')">
<div class="finding-checkbox" onclick="event.stopPropagation(); toggleFindingSelection('${finding.id}')">
<input type="checkbox" ${isSelected ? 'checked' : ''} ${isExported ? 'disabled' : ''}>
</div>
<div class="finding-content">
<div class="finding-header">
<span class="perspective-badge ${finding.perspective}">${finding.perspective}</span>
<span class="priority-badge ${finding.priority}">${finding.priority}</span>
${isExported ? '<span class="exported-badge">' + (t('discovery.exported') || 'Exported') + '</span>' : ''}
</div>
<div class="finding-title">${finding.title || 'Untitled'}</div>
<div class="finding-location">
<i data-lucide="file" class="w-3 h-3"></i>
<span>${finding.file || 'Unknown'}${finding.line ? ':' + finding.line : ''}</span>
</div>
</div>
</div>
`;
}
function renderFindingPreview(finding) {
return `
<div class="finding-preview">
<div class="preview-header">
<div class="preview-badges">
<span class="perspective-badge ${finding.perspective}">${finding.perspective}</span>
<span class="priority-badge ${finding.priority}">${finding.priority}</span>
${finding.confidence ? `<span class="confidence-badge">${Math.round(finding.confidence * 100)}% confidence</span>` : ''}
</div>
<h3 class="preview-title">${finding.title || 'Untitled'}</h3>
</div>
<div class="preview-section">
<h4><i data-lucide="file-code" class="w-4 h-4"></i> ${t('discovery.location') || 'Location'}</h4>
<div class="preview-location">
<code>${finding.file || 'Unknown'}${finding.line ? ':' + finding.line : ''}</code>
</div>
</div>
${finding.snippet ? `
<div class="preview-section">
<h4><i data-lucide="code" class="w-4 h-4"></i> ${t('discovery.code') || 'Code'}</h4>
<pre class="preview-snippet"><code>${escapeHtml(finding.snippet)}</code></pre>
</div>
` : ''}
<div class="preview-section">
<h4><i data-lucide="info" class="w-4 h-4"></i> ${t('discovery.description') || 'Description'}</h4>
<p class="preview-description">${finding.description || 'No description'}</p>
</div>
${finding.impact ? `
<div class="preview-section">
<h4><i data-lucide="alert-triangle" class="w-4 h-4"></i> ${t('discovery.impact') || 'Impact'}</h4>
<p class="preview-impact">${finding.impact}</p>
</div>
` : ''}
${finding.recommendation ? `
<div class="preview-section">
<h4><i data-lucide="lightbulb" class="w-4 h-4"></i> ${t('discovery.recommendation') || 'Recommendation'}</h4>
<p class="preview-recommendation">${finding.recommendation}</p>
</div>
` : ''}
${finding.suggested_issue ? `
<div class="preview-section suggested-issue">
<h4><i data-lucide="clipboard-list" class="w-4 h-4"></i> ${t('discovery.suggestedIssue') || 'Suggested Issue'}</h4>
<div class="suggested-issue-content">
<div class="issue-title">${finding.suggested_issue.title || finding.title}</div>
<div class="issue-meta">
<span class="issue-type">${finding.suggested_issue.type || 'bug'}</span>
<span class="issue-priority">P${finding.suggested_issue.priority || 3}</span>
${finding.suggested_issue.labels ? finding.suggested_issue.labels.map(l => `<span class="issue-label">${l}</span>`).join('') : ''}
</div>
</div>
</div>
` : ''}
<div class="preview-actions">
<button class="preview-action-btn primary" onclick="exportSingleFinding('${finding.id}')">
<i data-lucide="upload" class="w-4 h-4"></i>
<span>${t('discovery.exportAsIssue') || 'Export as Issue'}</span>
</button>
<button class="preview-action-btn secondary" onclick="dismissFinding('${finding.id}')">
<i data-lucide="x" class="w-4 h-4"></i>
<span>${t('discovery.dismiss') || 'Dismiss'}</span>
</button>
</div>
</div>
`;
}
// ========== Actions ==========
async function viewDiscoveryDetail(discoveryId) {
discoveryData.viewMode = 'detail';
discoveryData.selectedFinding = null;
discoveryData.selectedFindings.clear();
discoveryData.perspectiveFilter = 'all';
discoveryData.priorityFilter = 'all';
discoveryData.searchQuery = '';
// Show loading
renderDiscoveryView();
// Load detail
const detail = await loadDiscoveryDetail(discoveryId);
if (detail) {
discoveryData.selectedDiscovery = detail;
// Flatten findings from perspectives
const allFindings = [];
if (detail.perspectives) {
for (const p of detail.perspectives) {
if (p.findings) {
allFindings.push(...p.findings);
}
}
}
discoveryData.findings = allFindings;
}
// Start polling if running
if (detail && detail.phase && detail.phase !== 'complete' && detail.phase !== 'failed') {
startDiscoveryPolling(discoveryId);
}
renderDiscoveryView();
}
function backToDiscoveryList() {
stopDiscoveryPolling();
discoveryData.viewMode = 'list';
discoveryData.selectedDiscovery = null;
discoveryData.selectedFinding = null;
discoveryData.findings = [];
discoveryData.selectedFindings.clear();
renderDiscoveryView();
}
function selectFinding(findingId) {
const finding = discoveryData.findings.find(f => f.id === findingId);
discoveryData.selectedFinding = finding || null;
renderDiscoveryView();
}
function toggleFindingSelection(findingId) {
if (discoveryData.selectedFindings.has(findingId)) {
discoveryData.selectedFindings.delete(findingId);
} else {
discoveryData.selectedFindings.add(findingId);
}
renderDiscoveryView();
}
function selectAllFindings() {
// Get filtered findings (respecting current filters)
const filteredFindings = getFilteredFindings();
// Select only non-exported findings
for (const finding of filteredFindings) {
if (!finding.exported) {
discoveryData.selectedFindings.add(finding.id);
}
}
renderDiscoveryView();
}
function deselectAllFindings() {
discoveryData.selectedFindings.clear();
renderDiscoveryView();
}
function filterDiscoveryByPerspective(perspective) {
discoveryData.perspectiveFilter = perspective;
renderDiscoveryView();
}
function filterDiscoveryByPriority(priority) {
discoveryData.priorityFilter = priority;
renderDiscoveryView();
}
function searchDiscoveryFindings(query) {
discoveryData.searchQuery = query;
renderDiscoveryView();
}
async function exportSelectedFindings() {
if (discoveryData.selectedFindings.size === 0) return;
const discoveryId = discoveryData.selectedDiscovery?.discovery_id;
if (!discoveryId) return;
try {
const response = await fetch('/api/discoveries/' + encodeURIComponent(discoveryId) + '/export?path=' + encodeURIComponent(projectPath), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ finding_ids: Array.from(discoveryData.selectedFindings) })
});
const result = await response.json();
if (result.success) {
// Show detailed message if duplicates were skipped
const msg = result.skipped_count > 0
? `Exported ${result.exported_count} issues, skipped ${result.skipped_count} duplicates`
: `Exported ${result.exported_count} issues`;
showNotification('success', msg);
discoveryData.selectedFindings.clear();
// Reload discovery data to reflect exported status
await loadDiscoveryData();
if (discoveryData.selectedDiscovery) {
await viewDiscoveryDetail(discoveryData.selectedDiscovery.discovery_id);
} else {
renderDiscoveryView();
}
} else {
showNotification('error', result.error || 'Export failed');
}
} catch (err) {
console.error('Export failed:', err);
showNotification('error', 'Export failed');
}
}
async function exportSingleFinding(findingId) {
const discoveryId = discoveryData.selectedDiscovery?.discovery_id;
if (!discoveryId) return;
try {
const response = await fetch('/api/discoveries/' + encodeURIComponent(discoveryId) + '/export?path=' + encodeURIComponent(projectPath), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ finding_ids: [findingId] })
});
const result = await response.json();
if (result.success) {
showNotification('success', 'Exported 1 issue');
// Reload discovery data
await loadDiscoveryData();
renderDiscoveryView();
} else {
showNotification('error', result.error || 'Export failed');
}
} catch (err) {
console.error('Export failed:', err);
showNotification('error', 'Export failed');
}
}
async function dismissFinding(findingId) {
const discoveryId = discoveryData.selectedDiscovery?.discovery_id;
if (!discoveryId) return;
try {
const response = await fetch('/api/discoveries/' + encodeURIComponent(discoveryId) + '/findings/' + encodeURIComponent(findingId) + '?path=' + encodeURIComponent(projectPath), {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ dismissed: true })
});
const result = await response.json();
if (result.success) {
// Update local state
const finding = discoveryData.findings.find(f => f.id === findingId);
if (finding) {
finding.dismissed = true;
}
if (discoveryData.selectedFinding?.id === findingId) {
discoveryData.selectedFinding = null;
}
renderDiscoveryView();
}
} catch (err) {
console.error('Dismiss failed:', err);
}
}
async function dismissSelectedFindings() {
for (const findingId of discoveryData.selectedFindings) {
await dismissFinding(findingId);
}
discoveryData.selectedFindings.clear();
renderDiscoveryView();
}
async function deleteDiscovery(discoveryId) {
if (!confirm(`Delete discovery ${discoveryId}? This cannot be undone.`)) return;
try {
const response = await fetch('/api/discoveries/' + encodeURIComponent(discoveryId) + '?path=' + encodeURIComponent(projectPath), {
method: 'DELETE'
});
const result = await response.json();
if (result.success) {
showNotification('success', 'Discovery deleted');
await loadDiscoveryData();
renderDiscoveryView();
} else {
showNotification('error', result.error || 'Delete failed');
}
} catch (err) {
console.error('Delete failed:', err);
showNotification('error', 'Delete failed');
}
}
// ========== Progress Polling ==========
function startDiscoveryPolling(discoveryId) {
stopDiscoveryPolling();
discoveryPollingInterval = setInterval(async () => {
const progress = await loadDiscoveryProgress(discoveryId);
if (progress) {
// Update progress in UI
if (discoveryData.selectedDiscovery) {
discoveryData.selectedDiscovery.progress = progress.progress;
discoveryData.selectedDiscovery.phase = progress.phase;
}
// Stop polling if complete
if (progress.phase === 'complete' || progress.phase === 'failed') {
stopDiscoveryPolling();
// Reload full detail
viewDiscoveryDetail(discoveryId);
}
}
}, 3000); // Poll every 3 seconds
}
function stopDiscoveryPolling() {
if (discoveryPollingInterval) {
clearInterval(discoveryPollingInterval);
discoveryPollingInterval = null;
}
}
// ========== Utilities ==========
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// ========== Cleanup ==========
function cleanupDiscoveryView() {
stopDiscoveryPolling();
discoveryData.selectedDiscovery = null;
discoveryData.selectedFinding = null;
discoveryData.findings = [];
discoveryData.selectedFindings.clear();
discoveryData.viewMode = 'list';
}

File diff suppressed because it is too large Load Diff

View File

@@ -169,6 +169,9 @@ function renderProjectOverview() {
${renderDevelopmentIndex(project.developmentIndex)}
</div>
<!-- Project Guidelines -->
${renderProjectGuidelines(project.guidelines)}
<!-- Statistics -->
<div class="bg-card border border-border rounded-lg p-6">
<h3 class="text-lg font-semibold text-foreground mb-4 flex items-center gap-2">
@@ -248,3 +251,153 @@ function renderDevelopmentIndex(devIndex) {
</div>
`;
}
function renderProjectGuidelines(guidelines) {
if (!guidelines) {
return `
<div class="bg-card border border-border rounded-lg p-6 mb-6">
<h3 class="text-lg font-semibold text-foreground mb-4 flex items-center gap-2">
<i data-lucide="scroll-text" class="w-5 h-5"></i> Project Guidelines
</h3>
<p class="text-muted-foreground text-sm">
No guidelines configured. Run <code class="px-2 py-1 bg-muted rounded text-xs font-mono">/session:solidify</code> to add project constraints and conventions.
</p>
</div>
`;
}
// Count total items
const conventionCount = Object.values(guidelines.conventions || {}).flat().length;
const constraintCount = Object.values(guidelines.constraints || {}).flat().length;
const rulesCount = (guidelines.quality_rules || []).length;
const learningsCount = (guidelines.learnings || []).length;
const totalCount = conventionCount + constraintCount + rulesCount + learningsCount;
if (totalCount === 0) {
return `
<div class="bg-card border border-border rounded-lg p-6 mb-6">
<h3 class="text-lg font-semibold text-foreground mb-4 flex items-center gap-2">
<i data-lucide="scroll-text" class="w-5 h-5"></i> Project Guidelines
</h3>
<p class="text-muted-foreground text-sm">
Guidelines file exists but is empty. Run <code class="px-2 py-1 bg-muted rounded text-xs font-mono">/session:solidify</code> to add project constraints and conventions.
</p>
</div>
`;
}
return `
<div class="bg-card border border-border rounded-lg p-6 mb-6">
<h3 class="text-lg font-semibold text-foreground mb-4 flex items-center gap-2">
<i data-lucide="scroll-text" class="w-5 h-5"></i> Project Guidelines
<span class="text-xs px-2 py-0.5 bg-primary-light text-primary rounded-full">${totalCount} items</span>
</h3>
<div class="space-y-6">
<!-- Conventions -->
${renderGuidelinesSection('Conventions', guidelines.conventions, 'book-marked', 'bg-success-light text-success', [
{ key: 'coding_style', label: 'Coding Style' },
{ key: 'naming_patterns', label: 'Naming Patterns' },
{ key: 'file_structure', label: 'File Structure' },
{ key: 'documentation', label: 'Documentation' }
])}
<!-- Constraints -->
${renderGuidelinesSection('Constraints', guidelines.constraints, 'shield-alert', 'bg-destructive/10 text-destructive', [
{ key: 'architecture', label: 'Architecture' },
{ key: 'tech_stack', label: 'Tech Stack' },
{ key: 'performance', label: 'Performance' },
{ key: 'security', label: 'Security' }
])}
<!-- Quality Rules -->
${renderQualityRules(guidelines.quality_rules)}
<!-- Learnings -->
${renderLearnings(guidelines.learnings)}
</div>
</div>
`;
}
function renderGuidelinesSection(title, data, icon, badgeClass, categories) {
if (!data) return '';
const items = categories.flatMap(cat => (data[cat.key] || []).map(item => ({ category: cat.label, value: item })));
if (items.length === 0) return '';
return `
<div>
<h4 class="text-sm font-semibold text-foreground mb-3 flex items-center gap-2">
<i data-lucide="${icon}" class="w-4 h-4"></i>
<span>${title}</span>
<span class="text-xs px-2 py-0.5 ${badgeClass} rounded-full">${items.length}</span>
</h4>
<div class="space-y-2">
${items.slice(0, 8).map(item => `
<div class="flex items-start gap-3 p-3 bg-background border border-border rounded-lg">
<span class="text-xs px-2 py-0.5 bg-muted text-muted-foreground rounded whitespace-nowrap">${escapeHtml(item.category)}</span>
<span class="text-sm text-foreground">${escapeHtml(item.value)}</span>
</div>
`).join('')}
${items.length > 8 ? `<div class="text-sm text-muted-foreground text-center py-2">... and ${items.length - 8} more</div>` : ''}
</div>
</div>
`;
}
function renderQualityRules(rules) {
if (!rules || rules.length === 0) return '';
return `
<div>
<h4 class="text-sm font-semibold text-foreground mb-3 flex items-center gap-2">
<i data-lucide="check-square" class="w-4 h-4"></i>
<span>Quality Rules</span>
<span class="text-xs px-2 py-0.5 bg-warning-light text-warning rounded-full">${rules.length}</span>
</h4>
<div class="space-y-2">
${rules.slice(0, 6).map(rule => `
<div class="p-3 bg-background border border-border rounded-lg">
<div class="flex items-start justify-between mb-1">
<span class="text-sm text-foreground font-medium">${escapeHtml(rule.rule)}</span>
${rule.enforced_by ? `<span class="text-xs px-2 py-0.5 bg-muted text-muted-foreground rounded">${escapeHtml(rule.enforced_by)}</span>` : ''}
</div>
<span class="text-xs text-muted-foreground">Scope: ${escapeHtml(rule.scope)}</span>
</div>
`).join('')}
${rules.length > 6 ? `<div class="text-sm text-muted-foreground text-center py-2">... and ${rules.length - 6} more</div>` : ''}
</div>
</div>
`;
}
function renderLearnings(learnings) {
if (!learnings || learnings.length === 0) return '';
return `
<div>
<h4 class="text-sm font-semibold text-foreground mb-3 flex items-center gap-2">
<i data-lucide="lightbulb" class="w-4 h-4"></i>
<span>Session Learnings</span>
<span class="text-xs px-2 py-0.5 bg-accent text-accent-foreground rounded-full">${learnings.length}</span>
</h4>
<div class="space-y-2">
${learnings.slice(0, 5).map(learning => `
<div class="p-3 bg-background border border-border rounded-lg border-l-4 border-l-primary">
<div class="flex items-start justify-between mb-2">
<span class="text-sm text-foreground">${escapeHtml(learning.insight)}</span>
<span class="text-xs text-muted-foreground whitespace-nowrap ml-2">${formatDate(learning.date)}</span>
</div>
<div class="flex items-center gap-2 text-xs">
${learning.category ? `<span class="px-2 py-0.5 bg-muted text-muted-foreground rounded">${escapeHtml(learning.category)}</span>` : ''}
${learning.session_id ? `<span class="px-2 py-0.5 bg-primary-light text-primary rounded font-mono">${escapeHtml(learning.session_id)}</span>` : ''}
</div>
${learning.context ? `<p class="text-xs text-muted-foreground mt-2">${escapeHtml(learning.context)}</p>` : ''}
</div>
`).join('')}
${learnings.length > 5 ? `<div class="text-sm text-muted-foreground text-center py-2">... and ${learnings.length - 5} more</div>` : ''}
</div>
</div>
`;
}

View File

@@ -275,6 +275,18 @@
</div>
</div>
</div>
<!-- CLI Stream Viewer Button -->
<button class="cli-stream-btn p-1.5 text-muted-foreground hover:text-foreground hover:bg-hover rounded relative"
id="cliStreamBtn"
onclick="toggleCliStreamViewer()"
data-i18n-title="header.cliStream"
title="CLI Stream Viewer">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="4 17 10 11 4 5"/>
<line x1="12" y1="19" x2="20" y2="19"/>
</svg>
<span class="cli-stream-badge" id="cliStreamBadge"></span>
</button>
<!-- Refresh Button -->
<button class="refresh-btn p-1.5 text-muted-foreground hover:text-foreground hover:bg-hover rounded" id="refreshWorkspace" data-i18n-title="header.refreshWorkspace" title="Refresh workspace">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
@@ -394,6 +406,26 @@
</ul>
</div>
<!-- Issues Section -->
<div class="mb-2" id="issuesNav">
<div class="flex items-center px-4 py-2 text-sm font-semibold text-muted-foreground uppercase tracking-wide">
<i data-lucide="clipboard-list" class="nav-section-icon mr-2"></i>
<span class="nav-section-title" data-i18n="nav.issues">Issues</span>
</div>
<ul class="space-y-0.5">
<li class="nav-item flex items-center gap-2 px-3 py-2.5 text-sm text-muted-foreground hover:bg-hover hover:text-foreground rounded cursor-pointer transition-colors" data-view="issue-manager" data-tooltip="Issue Manager">
<i data-lucide="list-checks" class="nav-icon"></i>
<span class="nav-text flex-1" data-i18n="nav.issueManager">Manager</span>
<span class="badge px-2 py-0.5 text-xs font-semibold rounded-full bg-hover text-muted-foreground" id="badgeIssues">0</span>
</li>
<li class="nav-item flex items-center gap-2 px-3 py-2.5 text-sm text-muted-foreground hover:bg-hover hover:text-foreground rounded cursor-pointer transition-colors" data-view="issue-discovery" data-tooltip="Issue Discovery">
<i data-lucide="search-code" class="nav-icon"></i>
<span class="nav-text flex-1" data-i18n="nav.issueDiscovery">Discovery</span>
<span class="badge px-2 py-0.5 text-xs font-semibold rounded-full bg-hover text-muted-foreground" id="badgeDiscovery">0</span>
</li>
</ul>
</div>
<!-- MCP Servers Section -->
<div class="mb-2" id="mcpServersNav">
<div class="flex items-center px-4 py-2 text-sm font-semibold text-muted-foreground uppercase tracking-wide">
@@ -578,6 +610,44 @@
<div class="drawer-overlay hidden fixed inset-0 bg-black/50 z-40" id="drawerOverlay" onclick="closeTaskDrawer()"></div>
</div>
<!-- CLI Stream Viewer Panel -->
<div class="cli-stream-viewer" id="cliStreamViewer">
<div class="cli-stream-header">
<div class="cli-stream-title">
<i data-lucide="terminal"></i>
<span data-i18n="cliStream.title">CLI Stream</span>
<span class="cli-stream-count-badge" id="cliStreamCountBadge">0</span>
</div>
<div class="cli-stream-search">
<i data-lucide="search" class="cli-stream-search-icon"></i>
<input type="text"
id="cliStreamSearchInput"
class="cli-stream-search-input"
placeholder="Search output..."
oninput="handleSearchInput(event)"
data-i18n-placeholder="cliStream.searchPlaceholder">
<button class="cli-stream-search-clear" onclick="clearSearch()" title="Clear search">&times;</button>
</div>
<div class="cli-stream-actions">
<button class="cli-stream-action-btn" onclick="clearCompletedStreams()" data-i18n="cliStream.clearCompleted">
<i data-lucide="trash-2"></i>
<span>Clear</span>
</button>
<button class="cli-stream-close-btn" onclick="toggleCliStreamViewer()" title="Close">&times;</button>
</div>
</div>
<div class="cli-stream-tabs" id="cliStreamTabs">
<!-- Dynamic tabs -->
</div>
<div class="cli-stream-content" id="cliStreamContent">
<!-- Terminal output -->
</div>
<div class="cli-stream-status" id="cliStreamStatus">
<!-- Status bar -->
</div>
</div>
<div class="cli-stream-overlay" id="cliStreamOverlay" onclick="toggleCliStreamViewer()"></div>
<!-- Markdown Preview Modal -->
<div id="markdownModal" class="markdown-modal hidden fixed inset-0 z-[100] flex items-center justify-center">
<div class="markdown-modal-backdrop absolute inset-0 bg-black/60" onclick="closeMarkdownModal()"></div>

View File

@@ -596,7 +596,7 @@ async function executeCliTool(
ensureHistoryDir(workingDir); // Ensure history directory exists
// NEW: Check if model is a custom LiteLLM endpoint ID
if (model && !['gemini', 'qwen', 'codex'].includes(tool)) {
if (model) {
const endpoint = findEndpointById(workingDir, model);
if (endpoint) {
// Route to LiteLLM executor

View File

@@ -379,11 +379,63 @@ async function ensureLiteLLMEmbedderReady(): Promise<BootstrapResult> {
*/
type GpuMode = 'cpu' | 'cuda' | 'directml';
/**
* Python environment info for compatibility checks
*/
interface PythonEnvInfo {
version: string; // e.g., "3.11.5"
majorMinor: string; // e.g., "3.11"
architecture: number; // 32 or 64
compatible: boolean; // true if 64-bit and Python 3.8-3.12
error?: string;
}
/**
* Check Python environment in venv for DirectML compatibility
* DirectML requires: 64-bit Python, version 3.8-3.12
*/
async function checkPythonEnvForDirectML(): Promise<PythonEnvInfo> {
const pythonPath =
process.platform === 'win32'
? join(CODEXLENS_VENV, 'Scripts', 'python.exe')
: join(CODEXLENS_VENV, 'bin', 'python');
if (!existsSync(pythonPath)) {
return { version: '', majorMinor: '', architecture: 0, compatible: false, error: 'Python not found in venv' };
}
try {
// Get Python version and architecture in one call
const checkScript = `import sys, struct; print(f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}|{struct.calcsize('P') * 8}")`;
const result = execSync(`"${pythonPath}" -c "${checkScript}"`, { encoding: 'utf-8', timeout: 10000 }).trim();
const [version, archStr] = result.split('|');
const architecture = parseInt(archStr, 10);
const [major, minor] = version.split('.').map(Number);
const majorMinor = `${major}.${minor}`;
// DirectML wheels available for Python 3.8-3.12, 64-bit only
const versionCompatible = major === 3 && minor >= 8 && minor <= 12;
const archCompatible = architecture === 64;
const compatible = versionCompatible && archCompatible;
let error: string | undefined;
if (!archCompatible) {
error = `Python is ${architecture}-bit. onnxruntime-directml requires 64-bit Python. Please reinstall Python as 64-bit.`;
} else if (!versionCompatible) {
error = `Python ${majorMinor} is not supported. onnxruntime-directml requires Python 3.8-3.12.`;
}
return { version, majorMinor, architecture, compatible, error };
} catch (e) {
return { version: '', majorMinor: '', architecture: 0, compatible: false, error: `Failed to check Python: ${(e as Error).message}` };
}
}
/**
* Detect available GPU acceleration
* @returns Detected GPU mode and info
*/
async function detectGpuSupport(): Promise<{ mode: GpuMode; available: GpuMode[]; info: string }> {
async function detectGpuSupport(): Promise<{ mode: GpuMode; available: GpuMode[]; info: string; pythonEnv?: PythonEnvInfo }> {
const available: GpuMode[] = ['cpu'];
let detectedInfo = 'CPU only';
@@ -402,19 +454,20 @@ async function detectGpuSupport(): Promise<{ mode: GpuMode; available: GpuMode[]
// NVIDIA not available
}
// On Windows, DirectML is always available if DirectX 12 is supported
// On Windows, DirectML requires 64-bit Python 3.8-3.12
let pythonEnv: PythonEnvInfo | undefined;
if (process.platform === 'win32') {
try {
// Check for DirectX 12 support via dxdiag or registry
// DirectML works on most modern Windows 10/11 systems
pythonEnv = await checkPythonEnvForDirectML();
if (pythonEnv.compatible) {
available.push('directml');
if (available.includes('cuda')) {
detectedInfo = 'NVIDIA GPU detected (CUDA & DirectML available)';
} else {
detectedInfo = 'DirectML available (Windows GPU acceleration)';
}
} catch {
// DirectML check failed
} else if (pythonEnv.error) {
// DirectML not available due to Python environment
console.log(`[CodexLens] DirectML unavailable: ${pythonEnv.error}`);
}
}
@@ -426,7 +479,7 @@ async function detectGpuSupport(): Promise<{ mode: GpuMode; available: GpuMode[]
recommendedMode = 'cuda';
}
return { mode: recommendedMode, available, info: detectedInfo };
return { mode: recommendedMode, available, info: detectedInfo, pythonEnv };
}
/**
@@ -441,6 +494,19 @@ async function installSemantic(gpuMode: GpuMode = 'cpu'): Promise<BootstrapResul
return { success: false, error: 'CodexLens not installed. Install CodexLens first.' };
}
// Check Python environment compatibility for DirectML
if (gpuMode === 'directml') {
const pythonEnv = await checkPythonEnvForDirectML();
if (!pythonEnv.compatible) {
const errorDetails = pythonEnv.error || 'Unknown compatibility issue';
return {
success: false,
error: `DirectML installation failed: ${errorDetails}\n\nTo fix this:\n1. Uninstall current Python\n2. Install 64-bit Python 3.10, 3.11, or 3.12 from python.org\n3. Delete ~/.codexlens/venv folder\n4. Reinstall CodexLens`
};
}
console.log(`[CodexLens] Python ${pythonEnv.version} (${pythonEnv.architecture}-bit) - DirectML compatible`);
}
const pipPath =
process.platform === 'win32'
? join(CODEXLENS_VENV, 'Scripts', 'pip.exe')
@@ -1411,7 +1477,7 @@ export {
cancelIndexing,
isIndexingInProgress,
};
export type { GpuMode };
export type { GpuMode, PythonEnvInfo };
// Backward-compatible export for tests
export const codexLensTool = {

View File

@@ -19,6 +19,10 @@ export interface LiteLLMExecutionOptions {
includeDirs?: string[]; // Additional directories for @patterns
enableCache?: boolean; // Override endpoint cache setting
onOutput?: (data: { type: string; data: string }) => void;
/** Number of retries after the initial attempt (default: 0) */
maxRetries?: number;
/** Base delay for exponential backoff in milliseconds (default: 1000) */
retryBaseDelayMs?: number;
}
export interface LiteLLMExecutionResult {
@@ -180,7 +184,15 @@ export async function executeLiteLLMEndpoint(
}
// Use litellm-client to call chat
const response = await client.chat(finalPrompt, endpoint.model);
const response = await callWithRetries(
() => client.chat(finalPrompt, endpoint.model),
{
maxRetries: options.maxRetries ?? 0,
baseDelayMs: options.retryBaseDelayMs ?? 1000,
onOutput,
rateLimitKey: `${provider.type}:${endpoint.model}`,
},
);
if (onOutput) {
onOutput({ type: 'stdout', data: response });
@@ -239,3 +251,74 @@ function getProviderBaseUrlEnvVarName(providerType: string): string | null {
return envVarMap[providerType] || null;
}
const rateLimitRetryQueueNextAt = new Map<string, number>();
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function isRateLimitError(errorMessage: string): boolean {
return /429|rate limit|too many requests/i.test(errorMessage);
}
function isRetryableError(errorMessage: string): boolean {
// Never retry auth/config errors
if (/401|403|unauthorized|forbidden/i.test(errorMessage)) {
return false;
}
// Retry rate limits, transient server errors, and network timeouts
return /(429|500|502|503|504|timeout|timed out|econnreset|enotfound|econnrefused|socket hang up)/i.test(
errorMessage,
);
}
async function callWithRetries(
call: () => Promise<string>,
options: {
maxRetries: number;
baseDelayMs: number;
onOutput?: (data: { type: string; data: string }) => void;
rateLimitKey: string;
},
): Promise<string> {
const { maxRetries, baseDelayMs, onOutput, rateLimitKey } = options;
let attempt = 0;
while (true) {
try {
return await call();
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err);
if (attempt >= maxRetries || !isRetryableError(errorMessage)) {
throw err;
}
const delayMs = baseDelayMs * 2 ** attempt;
if (onOutput) {
onOutput({
type: 'stderr',
data: `[LiteLLM retry ${attempt + 1}/${maxRetries}: waiting ${delayMs}ms] ${errorMessage}\n`,
});
}
attempt += 1;
if (isRateLimitError(errorMessage)) {
const now = Date.now();
const earliestAt = now + delayMs;
const queuedAt = rateLimitRetryQueueNextAt.get(rateLimitKey) ?? 0;
const scheduledAt = Math.max(queuedAt, earliestAt);
rateLimitRetryQueueNextAt.set(rateLimitKey, scheduledAt + delayMs);
await sleep(scheduledAt - now);
continue;
}
await sleep(delayMs);
}
}
}

View File

@@ -247,7 +247,13 @@ function generatePreviewMd(metadata) {
* Main execute function
*/
async function execute(params) {
const { prototypesDir = '.', template: templatePath } = params;
const {
prototypesDir = '.',
template: templatePath,
runId: runIdParam,
sessionId: sessionIdParam,
timestamp: timestampParam,
} = params;
const targetPath = resolve(process.cwd(), prototypesDir);
@@ -262,11 +268,16 @@ async function execute(params) {
throw new Error('No prototype files found matching pattern {target}-style-{s}-layout-{l}.html');
}
const now = new Date();
const runId = runIdParam || `run-${now.toISOString().replace(/[:.]/g, '-').slice(0, -5)}`;
const sessionId = sessionIdParam || 'standalone';
const timestamp = timestampParam || now.toISOString();
// Generate metadata
const metadata = {
runId: `run-${new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5)}`,
sessionId: 'standalone',
timestamp: new Date().toISOString(),
runId,
sessionId,
timestamp,
styles,
layouts,
targets
@@ -319,6 +330,18 @@ Auto-detects matrix dimensions from file pattern: {target}-style-{s}-layout-{l}.
template: {
type: 'string',
description: 'Optional path to compare.html template'
},
runId: {
type: 'string',
description: 'Optional run identifier to inject into compare.html (defaults to generated timestamp-based run id)'
},
sessionId: {
type: 'string',
description: 'Optional session identifier to inject into compare.html (default: standalone)'
},
timestamp: {
type: 'string',
description: 'Optional ISO timestamp to inject into compare.html (defaults to current time)'
}
},
required: []

View File

@@ -0,0 +1,182 @@
/**
* Unit tests for browser-launcher utility module.
*
* Notes:
* - Targets the runtime implementation shipped in `ccw/dist`.
* - Prevents real browser launches by stubbing `child_process.spawn` used by the `open` package.
* - Stubs `os.platform` and `path.resolve` to exercise platform-specific URL formatting.
*/
import { after, beforeEach, describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { Buffer } from 'node:buffer';
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
// eslint-disable-next-line @typescript-eslint/no-var-requires
const os = require('node:os') as typeof import('node:os');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const childProcess = require('node:child_process') as typeof import('node:child_process');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const pathModule = require('node:path') as typeof import('node:path');
const ORIGINAL_ENV = { ...process.env };
type SpawnCall = { command: string; args: string[] };
const spawnCalls: SpawnCall[] = [];
const spawnPlan: Array<{ type: 'return' } | { type: 'throw'; error: Error }> = [];
const originalPlatform = os.platform;
const originalSpawn = childProcess.spawn;
const originalResolve = pathModule.resolve;
let platformValue: NodeJS.Platform = originalPlatform();
let resolveImpl = (...args: string[]) => originalResolve(...args);
os.platform = (() => platformValue) as any;
pathModule.resolve = ((...args: string[]) => resolveImpl(...args)) as any;
childProcess.spawn = ((command: string, args: string[] = []) => {
spawnCalls.push({ command: String(command), args: args.map(String) });
const next = spawnPlan.shift();
if (next?.type === 'throw') {
throw next.error;
}
return { unref() {} } as any;
}) as any;
const browserLauncherUrl = new URL('../dist/utils/browser-launcher.js', import.meta.url).href;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let mod: any;
function extractOpenTargets(): string[] {
const targets: string[] = [];
for (const call of spawnCalls) {
const encodedIndex = call.args.indexOf('-EncodedCommand');
if (encodedIndex !== -1) {
const base64 = call.args[encodedIndex + 1];
if (!base64) continue;
const decoded = Buffer.from(base64, 'base64').toString('utf16le');
const match = decoded.match(/Start\\s+\"([^\"]+)\"/);
targets.push(match?.[1] ?? decoded);
continue;
}
targets.push([call.command, ...call.args].join(' '));
}
return targets;
}
function normalizedTargets(): string[] {
return extractOpenTargets().map((t) => t.replace(/\\/g, '/'));
}
beforeEach(() => {
spawnCalls.length = 0;
spawnPlan.length = 0;
for (const key of Object.keys(process.env)) {
if (!(key in ORIGINAL_ENV)) delete process.env[key];
}
for (const [key, value] of Object.entries(ORIGINAL_ENV)) {
process.env[key] = value;
}
platformValue = originalPlatform();
resolveImpl = (...args: string[]) => originalResolve(...args);
});
describe('browser-launcher utility module', async () => {
mod = await import(browserLauncherUrl);
it('launchBrowser opens HTTP/HTTPS URLs', async () => {
await mod.launchBrowser('http://example.com');
await mod.launchBrowser('https://example.com');
const targets = normalizedTargets().join('\n');
assert.ok(targets.includes('http://example.com'));
assert.ok(targets.includes('https://example.com'));
});
it('launchBrowser converts file path to file:// URL (Windows)', async () => {
platformValue = 'win32';
resolveImpl = () => 'C:\\tmp\\file.html';
await mod.launchBrowser('file.html');
assert.ok(normalizedTargets().join('\n').includes('file:///C:/tmp/file.html'));
});
it('launchBrowser converts file path to file:// URL (Unix)', async () => {
platformValue = 'linux';
resolveImpl = () => '/tmp/file.html';
await mod.launchBrowser('file.html');
assert.ok(normalizedTargets().join('\n').includes('file:///tmp/file.html'));
});
it('isHeadlessEnvironment detects common CI env vars', () => {
for (const key of ['CI', 'CONTINUOUS_INTEGRATION', 'GITHUB_ACTIONS', 'GITLAB_CI', 'JENKINS_URL']) {
delete process.env[key];
}
assert.equal(mod.isHeadlessEnvironment(), false);
process.env.CI = '1';
assert.equal(mod.isHeadlessEnvironment(), true);
delete process.env.CI;
process.env.GITHUB_ACTIONS = 'true';
assert.equal(mod.isHeadlessEnvironment(), true);
});
it('wraps browser launch errors for URLs', async () => {
spawnPlan.push({ type: 'throw', error: new Error('boom') });
await assert.rejects(
mod.launchBrowser('https://example.com'),
(err: any) => err instanceof Error && err.message.includes('Failed to open browser: boom'),
);
});
it('falls back to opening file path directly when URL open fails', async () => {
platformValue = 'win32';
resolveImpl = () => 'C:\\tmp\\file.html';
spawnPlan.push({ type: 'throw', error: new Error('primary fail') });
spawnPlan.push({ type: 'return' });
await mod.launchBrowser('file.html');
const targets = normalizedTargets().join('\n');
assert.ok(targets.includes('file:///C:/tmp/file.html'));
assert.ok(targets.includes('C:/tmp/file.html'));
});
it('throws when both primary and fallback file opens fail', async () => {
platformValue = 'win32';
resolveImpl = () => 'C:\\tmp\\file.html';
spawnPlan.push({ type: 'throw', error: new Error('primary fail') });
spawnPlan.push({ type: 'throw', error: new Error('fallback fail') });
await assert.rejects(
mod.launchBrowser('file.html'),
(err: any) =>
err instanceof Error && err.message.includes('Failed to open browser: primary fail'),
);
});
});
after(() => {
os.platform = originalPlatform;
childProcess.spawn = originalSpawn;
pathModule.resolve = originalResolve;
for (const key of Object.keys(process.env)) {
if (!(key in ORIGINAL_ENV)) delete process.env[key];
}
for (const [key, value] of Object.entries(ORIGINAL_ENV)) {
process.env[key] = value;
}
});

Some files were not shown because too many files have changed in this diff Show More