mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-28 09:23:08 +08:00
feat: Enhance team lifecycle roles with checkpoint handling and inner loop execution
- Added checkpoint gate handling to the coordinator role, defining behavior based on quality gate results. - Updated planner role to utilize inner loop pattern for structured implementation planning and reporting. - Revised writer role to implement inner loop for document generation, delegating CLI execution to a subagent. - Introduced a new doc-generation subagent for isolated CLI calls and document generation strategies. - Enhanced UI components in the frontend to display job statuses, last run times, and improved error handling. - Updated localization files to include new strings for job details and status banners. - Improved CSS styles for markdown previews to enhance readability and presentation.
This commit is contained in:
@@ -77,6 +77,7 @@ Parse `$ARGUMENTS` to extract `--role`. If absent -> Orchestration Mode (auto ro
|
||||
|----------|------|-------------|---------|
|
||||
| discuss | [subagents/discuss-subagent.md](subagents/discuss-subagent.md) | analyst, writer, reviewer | Multi-perspective critique |
|
||||
| explore | [subagents/explore-subagent.md](subagents/explore-subagent.md) | analyst, planner, any role | Codebase exploration with cache |
|
||||
| doc-generation | [subagents/doc-generation-subagent.md](subagents/doc-generation-subagent.md) | writer | Document generation (CLI execution) |
|
||||
|
||||
### Dispatch
|
||||
|
||||
@@ -105,6 +106,10 @@ User provides task description
|
||||
|---------|--------|
|
||||
| `check` / `status` | Output execution status graph, no advancement |
|
||||
| `resume` / `continue` | Check worker states, advance next step |
|
||||
| `revise <TASK-ID> [feedback]` | Create revision task for specified document + cascade downstream |
|
||||
| `feedback <text>` | Analyze feedback impact, create targeted revision chain |
|
||||
| `recheck` | Re-run QUALITY-001 quality check (after manual edits) |
|
||||
| `improve [dimension]` | Auto-improve weakest dimension from readiness-report |
|
||||
|
||||
---
|
||||
|
||||
@@ -144,7 +149,8 @@ Task completion with optional fast-advance to skip coordinator round-trip:
|
||||
|
||||
| Condition | Action |
|
||||
|-----------|--------|
|
||||
| 1 ready task, simple linear successor | Spawn directly via Task(run_in_background: true) |
|
||||
| 同前缀后续任务 (Inner Loop 角色) | 不 spawn,主 agent 内循环 (Phase 5-L) |
|
||||
| 1 ready task, simple linear successor, 不同前缀 | Spawn directly via Task(run_in_background: true) |
|
||||
| Multiple ready tasks (parallel window) | SendMessage to coordinator (needs orchestration) |
|
||||
| No ready tasks + others running | SendMessage to coordinator (status update) |
|
||||
| No ready tasks + nothing running | SendMessage to coordinator (pipeline may be complete) |
|
||||
@@ -152,6 +158,92 @@ Task completion with optional fast-advance to skip coordinator round-trip:
|
||||
|
||||
**Fast-advance failure recovery**: If a fast-advanced task fails (worker exits without completing), the coordinator detects it as an orphaned in_progress task on next `resume`/`check` and resets it to pending for re-spawn. Self-healing, no manual intervention required. See [monitor.md](roles/coordinator/commands/monitor.md) Fast-Advance Failure Recovery.
|
||||
|
||||
### Worker Inner Loop (同前缀多任务角色)
|
||||
|
||||
适用角色:writer (DRAFT-*)、planner (PLAN-*)、executor (IMPL-*)
|
||||
|
||||
当一个角色拥有**同前缀的多个串行任务**时,不再每完成一个就 spawn 新 agent,而是在同一 agent 内循环处理:
|
||||
|
||||
**Inner Loop 流程**:
|
||||
|
||||
```
|
||||
Phase 1: 发现任务 (首次)
|
||||
│
|
||||
├─ 找到任务 → Phase 2-3: 加载上下文 + Subagent 执行
|
||||
│ │
|
||||
│ v
|
||||
│ Phase 4: 验证 (+ Inline Discuss if applicable)
|
||||
│ │
|
||||
│ v
|
||||
│ Phase 5-L: 轻量完成 (Loop variant)
|
||||
│ │
|
||||
│ ├─ TaskUpdate 标完成
|
||||
│ ├─ team_msg 记录
|
||||
│ ├─ 累积摘要到 context_accumulator
|
||||
│ │
|
||||
│ ├─ 检查:还有同前缀待处理任务?
|
||||
│ │ ├─ YES → 回到 Phase 1 (内循环)
|
||||
│ │ └─ NO → Phase 5-F: 最终报告
|
||||
│ │
|
||||
│ └─ 异常中断条件?
|
||||
│ ├─ consensus_blocked HIGH → SendMessage → STOP
|
||||
│ └─ 错误累计 ≥ 3 → SendMessage → STOP
|
||||
│
|
||||
└─ Phase 5-F: 最终报告 (Final)
|
||||
├─ SendMessage (含全部任务摘要)
|
||||
└─ STOP
|
||||
```
|
||||
|
||||
**context_accumulator** (主 agent 上下文中维护,不写文件):
|
||||
|
||||
每个 subagent 返回后,主 agent 将结果压缩为摘要追加到 accumulator:
|
||||
|
||||
```
|
||||
context_accumulator = []
|
||||
|
||||
# DRAFT-001 subagent 返回后
|
||||
context_accumulator.append({
|
||||
task: "DRAFT-001",
|
||||
artifact: "spec/product-brief.md",
|
||||
key_decisions: ["聚焦 B2B 场景", "MVP 不含移动端"],
|
||||
discuss_verdict: "consensus_reached",
|
||||
discuss_rating: 4.2
|
||||
})
|
||||
|
||||
# DRAFT-002 subagent 返回后
|
||||
context_accumulator.append({
|
||||
task: "DRAFT-002",
|
||||
artifact: "spec/requirements/_index.md",
|
||||
key_decisions: ["REQ-003 降级为 P2", "NFR-perf 新增 SLA"],
|
||||
discuss_verdict: "consensus_reached",
|
||||
discuss_rating: 3.8
|
||||
})
|
||||
```
|
||||
|
||||
后续 subagent 调用时,将 accumulator 摘要作为 CONTEXT 传入,实现知识传递。
|
||||
|
||||
**Phase 5-L vs Phase 5-F 区别**:
|
||||
|
||||
| 步骤 | Phase 5-L (循环中) | Phase 5-F (最终) |
|
||||
|------|-------------------|-----------------|
|
||||
| TaskUpdate completed | YES | YES |
|
||||
| team_msg log | YES | YES |
|
||||
| 累积摘要 | YES | - |
|
||||
| SendMessage to coordinator | NO | YES (含所有任务汇总) |
|
||||
| Fast-Advance 到下一前缀 | - | YES (检查跨前缀后续) |
|
||||
|
||||
**中断恢复**:
|
||||
|
||||
如果 Inner Loop agent 在 DRAFT-003 崩溃:
|
||||
1. DRAFT-001, DRAFT-002 已落盘 + 已标完成 → 安全
|
||||
2. DRAFT-003 状态为 in_progress → coordinator resume 时检测到无 active_worker → 重置为 pending
|
||||
3. 重新 spawn writer → Phase 1 找到 DRAFT-003 → Resume Artifact Check:
|
||||
- DRAFT-003 产物不存在 → 正常执行
|
||||
- DRAFT-003 产物已写但未标完成 → 验证后标完成
|
||||
4. 新 writer 从 DRAFT-003 开始循环,丢失的只是 001+002 的隐性摘要(可从磁盘重建基础信息)
|
||||
|
||||
**恢复增强** (可选):在每个 Phase 5-L 后将 context_accumulator 写入 `<session>/shared-memory.json` 的 `context_accumulator` 字段,crash 后可读回。
|
||||
|
||||
### Inline Discuss Protocol (produce roles: analyst, writer, reviewer)
|
||||
|
||||
After completing their primary output, produce roles call the discuss subagent inline:
|
||||
@@ -354,10 +446,41 @@ Beat 1 2 3 4
|
||||
|
||||
| Trigger | Position | Behavior |
|
||||
|---------|----------|----------|
|
||||
| Spec->Impl transition | QUALITY-001 completed | Pause, wait for user `resume` |
|
||||
| Spec->Impl transition | QUALITY-001 completed | Read readiness-report.md, extract gate + scores, display Checkpoint Output Template, pause for user action |
|
||||
| GC loop max | QA-FE max 2 rounds | Stop iteration, report current state |
|
||||
| Pipeline stall | No ready + no running | Check missing tasks, report to user |
|
||||
|
||||
**Checkpoint Output Template** (QUALITY-001 completion):
|
||||
|
||||
Coordinator reads `<session>/spec/readiness-report.md`, extracts gate + dimension scores, displays:
|
||||
|
||||
```
|
||||
[coordinator] ══════════════════════════════════════════
|
||||
[coordinator] SPEC PHASE COMPLETE
|
||||
[coordinator] Quality Gate: <PASS|REVIEW|FAIL> (<score>%)
|
||||
[coordinator]
|
||||
[coordinator] Dimension Scores:
|
||||
[coordinator] Completeness: <bar> <n>%
|
||||
[coordinator] Consistency: <bar> <n>%
|
||||
[coordinator] Traceability: <bar> <n>%
|
||||
[coordinator] Depth: <bar> <n>%
|
||||
[coordinator] Coverage: <bar> <n>%
|
||||
[coordinator]
|
||||
[coordinator] Available Actions:
|
||||
[coordinator] resume → Proceed to implementation
|
||||
[coordinator] improve → Auto-improve weakest dimension
|
||||
[coordinator] improve <dimension> → Improve specific dimension
|
||||
[coordinator] revise <TASK-ID> → Revise specific document
|
||||
[coordinator] recheck → Re-run quality check
|
||||
[coordinator] feedback <text> → Inject feedback, create revision
|
||||
[coordinator] ══════════════════════════════════════════
|
||||
```
|
||||
|
||||
Gate-specific guidance:
|
||||
- PASS: All actions available, resume is primary suggestion
|
||||
- REVIEW: Recommend improve/revise before resume, warn on resume
|
||||
- FAIL: Recommend improve/revise, do not suggest resume (user can force)
|
||||
|
||||
**Stall detection** (coordinator `handleCheck`):
|
||||
|
||||
| Check | Condition | Resolution |
|
||||
@@ -385,6 +508,8 @@ Beat 1 2 3 4
|
||||
|
||||
## Coordinator Spawn Template
|
||||
|
||||
### 标准 Worker (单任务角色: analyst, tester, reviewer, architect)
|
||||
|
||||
When coordinator spawns workers, use background mode (Spawn-and-Stop):
|
||||
|
||||
```
|
||||
@@ -419,6 +544,42 @@ Session: <session-folder>
|
||||
})
|
||||
```
|
||||
|
||||
### Inner Loop Worker (多任务角色: writer, planner, executor)
|
||||
|
||||
```
|
||||
Task({
|
||||
subagent_type: "general-purpose",
|
||||
description: "Spawn <role> worker (inner loop)",
|
||||
team_name: <team-name>,
|
||||
name: "<role>",
|
||||
run_in_background: true,
|
||||
prompt: `You are team "<team-name>" <ROLE>.
|
||||
|
||||
## Primary Instruction
|
||||
All your work MUST be executed by calling Skill to get role definition:
|
||||
Skill(skill="team-lifecycle-v4", args="--role=<role>")
|
||||
|
||||
Current requirement: <task-description>
|
||||
Session: <session-folder>
|
||||
|
||||
## Inner Loop Mode
|
||||
You will handle ALL <PREFIX>-* tasks in this session, not just the first one.
|
||||
After completing each task, loop back to find the next <PREFIX>-* task.
|
||||
Only SendMessage to coordinator when:
|
||||
- All <PREFIX>-* tasks are done
|
||||
- A consensus_blocked HIGH occurs
|
||||
- Errors accumulate (≥ 3)
|
||||
|
||||
## Role Guidelines
|
||||
- Only process <PREFIX>-* tasks, do not execute other role work
|
||||
- All output prefixed with [<role>] tag
|
||||
- Only communicate with coordinator
|
||||
- Do not use TaskCreate to create tasks for other roles
|
||||
- Before each SendMessage, call mcp__ccw-tools__team_msg to log
|
||||
- Use subagent calls for heavy work, retain summaries in context`
|
||||
})
|
||||
```
|
||||
|
||||
## Session Directory
|
||||
|
||||
```
|
||||
|
||||
@@ -106,12 +106,21 @@ Every task description includes session, scope, and inline discuss metadata:
|
||||
TaskCreate({
|
||||
subject: "<TASK-ID>",
|
||||
owner: "<role>",
|
||||
description: "<task description from pipeline table>\nSession: <session-folder>\nScope: <scope>\nInlineDiscuss: <DISCUSS-NNN or none>",
|
||||
description: "<task description from pipeline table>\nSession: <session-folder>\nScope: <scope>\nInlineDiscuss: <DISCUSS-NNN or none>\nInnerLoop: <true|false>",
|
||||
blockedBy: [<dependency-list>],
|
||||
status: "pending"
|
||||
})
|
||||
```
|
||||
|
||||
**InnerLoop Flag Rules**:
|
||||
|
||||
| Role | InnerLoop |
|
||||
|------|-----------|
|
||||
| writer (DRAFT-*) | true |
|
||||
| planner (PLAN-*) | true |
|
||||
| executor (IMPL-*) | true |
|
||||
| analyst, tester, reviewer, architect, fe-developer, fe-qa | false |
|
||||
|
||||
### Execution Method
|
||||
|
||||
| Method | Behavior |
|
||||
@@ -129,6 +138,56 @@ TaskCreate({
|
||||
| Session reference | Every task description contains `Session: <session-folder>` |
|
||||
| Inline discuss | Spec tasks have InlineDiscuss field matching round config |
|
||||
|
||||
### Revision Task Template
|
||||
|
||||
When handleRevise/handleFeedback creates revision tasks:
|
||||
|
||||
```
|
||||
TaskCreate({
|
||||
subject: "<ORIGINAL-ID>-R1",
|
||||
owner: "<same-role-as-original>",
|
||||
description: "<revision-type> revision of <ORIGINAL-ID>.\n
|
||||
Session: <session-folder>\n
|
||||
Original artifact: <artifact-path>\n
|
||||
User feedback: <feedback-text or 'system-initiated'>\n
|
||||
Revision scope: <targeted|full>\n
|
||||
InlineDiscuss: <same-discuss-round-as-original>\n
|
||||
InnerLoop: <true|false based on role>",
|
||||
status: "pending",
|
||||
blockedBy: [<predecessor-R1 if cascaded>]
|
||||
})
|
||||
```
|
||||
|
||||
**Revision naming**: `<ORIGINAL-ID>-R1` (max 1 revision per task; second revision -> `-R2`; third -> escalate to user)
|
||||
|
||||
**Cascade blockedBy chain example** (revise DRAFT-002):
|
||||
- DRAFT-002-R1 (no blockedBy)
|
||||
- DRAFT-003-R1 (blockedBy: DRAFT-002-R1)
|
||||
- DRAFT-004-R1 (blockedBy: DRAFT-003-R1)
|
||||
- QUALITY-001-R1 (blockedBy: DRAFT-004-R1)
|
||||
|
||||
### Improvement Task Template
|
||||
|
||||
When handleImprove creates improvement tasks:
|
||||
|
||||
```
|
||||
TaskCreate({
|
||||
subject: "IMPROVE-<dimension>-001",
|
||||
owner: "writer",
|
||||
description: "Quality improvement: <dimension>.\n
|
||||
Session: <session-folder>\n
|
||||
Current score: <X>%\n
|
||||
Target: 80%\n
|
||||
Readiness report: <session>/spec/readiness-report.md\n
|
||||
Weak areas: <extracted-from-report>\n
|
||||
Strategy: <from-dimension-strategy-table>\n
|
||||
InnerLoop: true",
|
||||
status: "pending"
|
||||
})
|
||||
```
|
||||
|
||||
Improvement tasks are always followed by a QUALITY-001-R1 recheck (blockedBy: IMPROVE task).
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Scenario | Resolution |
|
||||
|
||||
@@ -33,6 +33,10 @@ Parse `$ARGUMENTS` to determine handler:
|
||||
| 2 | Contains "check" or "status" | handleCheck |
|
||||
| 3 | Contains "resume", "continue", or "next" | handleResume |
|
||||
| 4 | None of the above (initial spawn after dispatch) | handleSpawnNext |
|
||||
| 5 | Contains "revise" + task ID pattern | handleRevise |
|
||||
| 6 | Contains "feedback" + quoted/unquoted text | handleFeedback |
|
||||
| 7 | Contains "recheck" | handleRecheck |
|
||||
| 8 | Contains "improve" (optionally + dimension name) | handleImprove |
|
||||
|
||||
Known worker roles: analyst, writer, planner, executor, tester, reviewer, architect, fe-developer, fe-qa.
|
||||
|
||||
@@ -47,6 +51,8 @@ Worker completed a task. Verify completion, update state, auto-advance.
|
||||
```
|
||||
Receive callback from [<role>]
|
||||
+- Find matching active worker by role
|
||||
+- Is this a progress update (not final)? (Inner Loop intermediate task completion)
|
||||
| +- YES -> Update session state, do NOT remove from active_workers -> STOP
|
||||
+- Task status = completed?
|
||||
| +- YES -> remove from active_workers -> update session
|
||||
| | +- Handle checkpoints (see below)
|
||||
@@ -87,7 +93,7 @@ Read-only status report. No pipeline advancement.
|
||||
done=completed >>>=running o=pending .=not created
|
||||
|
||||
[coordinator] Active Workers:
|
||||
> <subject> (<role>) - running <elapsed>
|
||||
> <subject> (<role>) - running <elapsed> [inner-loop: N/M tasks done]
|
||||
|
||||
[coordinator] Ready to spawn: <subjects>
|
||||
[coordinator] Commands: 'resume' to advance | 'check' to refresh
|
||||
@@ -132,6 +138,9 @@ Ready tasks found?
|
||||
+- NONE + work in progress -> report waiting -> STOP
|
||||
+- NONE + nothing in progress -> PIPELINE_COMPLETE -> Phase 5
|
||||
+- HAS ready tasks -> for each:
|
||||
+- Is task owner an Inner Loop role AND that role already has an active_worker?
|
||||
| +- YES -> SKIP spawn (existing worker will pick it up via inner loop)
|
||||
| +- NO -> normal spawn below
|
||||
+- TaskUpdate -> in_progress
|
||||
+- team_msg log -> task_unblocked
|
||||
+- Spawn worker (see tool call below)
|
||||
@@ -154,11 +163,144 @@ Task({
|
||||
|
||||
---
|
||||
|
||||
### Handler: handleRevise
|
||||
|
||||
User requests targeted revision of a completed task.
|
||||
|
||||
```
|
||||
Parse: revise <TASK-ID> [feedback-text]
|
||||
+- Validate TASK-ID exists and is completed
|
||||
| +- NOT completed -> error: "Task <ID> is not completed, cannot revise"
|
||||
+- Determine role and doc type from TASK-ID prefix
|
||||
+- Create revision task:
|
||||
| TaskCreate({
|
||||
| subject: "<TASK-ID>-R1",
|
||||
| owner: "<same-role>",
|
||||
| description: "User-requested revision of <TASK-ID>.\n
|
||||
| Session: <session-folder>\n
|
||||
| Original artifact: <artifact-path>\n
|
||||
| User feedback: <feedback-text or 'general revision requested'>\n
|
||||
| Revision scope: targeted\n
|
||||
| InlineDiscuss: <same-discuss-round>\n
|
||||
| InnerLoop: true",
|
||||
| status: "pending"
|
||||
| })
|
||||
+- Cascade check (auto):
|
||||
| +- Find all completed tasks downstream of TASK-ID
|
||||
| +- For each downstream completed task -> create <ID>-R1
|
||||
| +- Chain blockedBy: each R1 blockedBy its predecessor R1
|
||||
| +- Always end with QUALITY-001-R1 (recheck)
|
||||
+- Spawn worker for first revision task -> STOP
|
||||
```
|
||||
|
||||
**Cascade Rules**:
|
||||
|
||||
| Revised Task | Downstream (auto-cascade) |
|
||||
|-------------|--------------------------|
|
||||
| RESEARCH-001 | DRAFT-001~004-R1, QUALITY-001-R1 |
|
||||
| DRAFT-001 | DRAFT-002~004-R1, QUALITY-001-R1 |
|
||||
| DRAFT-002 | DRAFT-003~004-R1, QUALITY-001-R1 |
|
||||
| DRAFT-003 | DRAFT-004-R1, QUALITY-001-R1 |
|
||||
| DRAFT-004 | QUALITY-001-R1 |
|
||||
| QUALITY-001 | (no cascade, just recheck) |
|
||||
|
||||
**Cascade depth control**: Only cascade tasks that are already completed. Pending/in_progress tasks will naturally pick up changes.
|
||||
|
||||
---
|
||||
|
||||
### Handler: handleFeedback
|
||||
|
||||
User injects feedback into pipeline context.
|
||||
|
||||
```
|
||||
Parse: feedback <text>
|
||||
+- Determine pipeline state:
|
||||
| +- Spec phase in progress -> find earliest affected DRAFT task
|
||||
| +- Spec phase complete (at checkpoint) -> analyze full impact
|
||||
| +- Impl phase in progress -> log to wisdom/decisions.md, no revision
|
||||
+- Analyze feedback impact:
|
||||
| +- Keyword match against doc types:
|
||||
| "vision/market/MVP/scope" -> DRAFT-001 (product-brief)
|
||||
| "requirement/feature/NFR/user story" -> DRAFT-002 (requirements)
|
||||
| "architecture/ADR/component/tech stack" -> DRAFT-003 (architecture)
|
||||
| "epic/story/sprint/priority" -> DRAFT-004 (epics)
|
||||
| +- If unclear -> default to earliest incomplete or most recent completed
|
||||
+- Write feedback to wisdom/decisions.md
|
||||
+- Create revision chain (same as handleRevise from determined start point)
|
||||
+- Spawn worker -> STOP
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Handler: handleRecheck
|
||||
|
||||
Re-run quality check after manual edits or revisions.
|
||||
|
||||
```
|
||||
Parse: recheck
|
||||
+- Validate QUALITY-001 exists and is completed
|
||||
| +- NOT completed -> error: "Quality check hasn't run yet"
|
||||
+- Create recheck task:
|
||||
| TaskCreate({
|
||||
| subject: "QUALITY-001-R1",
|
||||
| owner: "reviewer",
|
||||
| description: "Re-run spec quality check.\n
|
||||
| Session: <session-folder>\n
|
||||
| Scope: full recheck\n
|
||||
| InlineDiscuss: DISCUSS-006",
|
||||
| status: "pending"
|
||||
| })
|
||||
+- Spawn reviewer -> STOP
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Handler: handleImprove
|
||||
|
||||
Quality-driven improvement based on readiness report dimensions.
|
||||
|
||||
```
|
||||
Parse: improve [dimension]
|
||||
+- Read <session>/spec/readiness-report.md
|
||||
| +- NOT found -> error: "No readiness report. Run quality check first."
|
||||
+- Extract dimension scores
|
||||
+- Select target:
|
||||
| +- dimension specified -> use it
|
||||
| +- not specified -> pick lowest scoring dimension
|
||||
+- Map dimension to improvement strategy:
|
||||
|
|
||||
| | Dimension | Strategy | Target Tasks |
|
||||
| |-----------|----------|-------------|
|
||||
| | completeness | Fill missing sections | DRAFT with missing sections |
|
||||
| | consistency | Unify terminology/format | All DRAFT (batch) |
|
||||
| | traceability | Strengthen Goals->Reqs->Arch->Stories chain | DRAFT-002, DRAFT-003, DRAFT-004 |
|
||||
| | depth | Enhance AC/ADR detail | Weakest sub-dimension's DRAFT |
|
||||
| | coverage | Add uncovered requirements | DRAFT-002 |
|
||||
|
|
||||
+- Create improvement task:
|
||||
| TaskCreate({
|
||||
| subject: "IMPROVE-<dimension>-001",
|
||||
| owner: "writer",
|
||||
| description: "Quality improvement: <dimension>.\n
|
||||
| Session: <session-folder>\n
|
||||
| Current score: <X>%\n
|
||||
| Target: 80%\n
|
||||
| Weak areas: <from readiness-report>\n
|
||||
| Strategy: <from table>\n
|
||||
| InnerLoop: true",
|
||||
| status: "pending"
|
||||
| })
|
||||
+- Create QUALITY-001-R1 (blockedBy: IMPROVE task)
|
||||
+- Spawn writer -> STOP
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Checkpoints
|
||||
|
||||
| Completed Task | Mode Condition | Action |
|
||||
|---------------|----------------|--------|
|
||||
| QUALITY-001 | full-lifecycle or full-lifecycle-fe | Output "SPEC PHASE COMPLETE" checkpoint, pause for user review before impl |
|
||||
| QUALITY-001 | full-lifecycle or full-lifecycle-fe | Read readiness-report.md -> extract gate + scores -> output Checkpoint Output Template (see SKILL.md) -> pause for user action |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -151,6 +151,25 @@ Delegate to `commands/dispatch.md` which creates the full task chain:
|
||||
- User "check" -> handleCheck (status only)
|
||||
- User "resume" -> handleResume (advance)
|
||||
|
||||
### Checkpoint Gate Handling
|
||||
|
||||
When QUALITY-001 completes (spec->impl transition checkpoint):
|
||||
|
||||
1. Read `<session-folder>/spec/readiness-report.md`
|
||||
2. Parse quality gate: extract `Quality Gate:` line -> PASS/REVIEW/FAIL + score
|
||||
3. Parse dimension scores: extract `Dimension Scores` table
|
||||
4. Output Checkpoint Output Template (see SKILL.md Checkpoints) with gate-specific guidance
|
||||
5. Write gate result to team-session.json: `checkpoint_gate: { gate, score, dimensions }`
|
||||
6. Pause and wait for user command
|
||||
|
||||
**Gate-specific behavior**:
|
||||
|
||||
| Gate | Primary Suggestion | Warning |
|
||||
|------|-------------------|---------|
|
||||
| PASS (>=80%) | `resume` to proceed | None |
|
||||
| REVIEW (60-79%) | `improve` or `revise` first | Warn on `resume`: "Quality below target, proceed at risk" |
|
||||
| FAIL (<60%) | `improve` or `revise` required | Block `resume` suggestion, user can force |
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Report + Next Steps
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
# Role: planner
|
||||
|
||||
Multi-angle code exploration (via shared explore subagent with cache) and structured implementation planning.
|
||||
Multi-angle code exploration and structured implementation planning.
|
||||
Uses **Inner Loop** pattern for consistency (currently single PLAN-* task, extensible).
|
||||
|
||||
## Identity
|
||||
|
||||
- **Name**: `planner` | **Prefix**: `PLAN-*` | **Tag**: `[planner]`
|
||||
- **Mode**: Inner Loop
|
||||
- **Responsibility**: Complexity assessment -> Code exploration (shared cache) -> Plan generation -> Approval
|
||||
|
||||
## Boundaries
|
||||
@@ -117,6 +119,15 @@ Requirements: 2-7 tasks with id, title, files[].change, convergence.criteria, de
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Report (Inner Loop)
|
||||
|
||||
Currently planner only has PLAN-001, so it directly executes Phase 5-F (Final Report).
|
||||
If future extensions add multiple PLAN-* tasks, Phase 5-L loop activates automatically:
|
||||
- Phase 5-L: Mark task completed, accumulate summary, loop back to Phase 1
|
||||
- Phase 5-F: All PLAN-* done, send final report to coordinator with full summary
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Scenario | Resolution |
|
||||
|
||||
@@ -2,7 +2,12 @@
|
||||
|
||||
## Purpose
|
||||
|
||||
Multi-CLI document generation for 4 document types. Each uses parallel or staged CLI analysis, then synthesizes into templated documents.
|
||||
Document generation strategy reference. Used by doc-generation-subagent.md as prompt source.
|
||||
Writer 主 agent 不再直接执行此文件中的 CLI 调用,而是将对应段落传入 subagent prompt。
|
||||
|
||||
## Usage
|
||||
|
||||
Writer Phase 3 加载此文件中对应 doc-type 的策略段落,嵌入 subagent prompt 的 "Execution Strategy" 字段。
|
||||
|
||||
## Phase 2: Context Loading
|
||||
|
||||
|
||||
@@ -1,27 +1,29 @@
|
||||
# Role: writer
|
||||
|
||||
Product Brief, Requirements/PRD, Architecture, and Epics & Stories document generation. Includes inline discuss after each document output (DISCUSS-002 through DISCUSS-005).
|
||||
Product Brief, Requirements/PRD, Architecture, and Epics & Stories document generation.
|
||||
Uses **Inner Loop** pattern: one agent handles all DRAFT-* tasks sequentially,
|
||||
delegating document generation to subagent, retaining summaries across tasks.
|
||||
|
||||
## Identity
|
||||
|
||||
- **Name**: `writer` | **Prefix**: `DRAFT-*` | **Tag**: `[writer]`
|
||||
- **Responsibility**: Load Context -> Generate Document -> **Inline Discuss** -> Report
|
||||
- **Mode**: Inner Loop (处理全部 DRAFT-* 任务)
|
||||
- **Responsibility**: [Loop: Load Context -> Subagent Generate -> Validate + Discuss -> Accumulate] -> Final Report
|
||||
|
||||
## Boundaries
|
||||
|
||||
### MUST
|
||||
- Only process DRAFT-* tasks
|
||||
- Read templates before generating (from `../../templates/`)
|
||||
- Follow document-standards.md (from `../../specs/`)
|
||||
- Integrate prior discussion feedback when available
|
||||
- Generate proper YAML frontmatter
|
||||
- Call discuss subagent after document output (round from InlineDiscuss field)
|
||||
- Use subagent for document generation (不在主 agent 内执行 CLI)
|
||||
- Maintain context_accumulator across tasks
|
||||
- Call discuss subagent after each document output
|
||||
- Loop through all DRAFT-* tasks before reporting to coordinator
|
||||
|
||||
### MUST NOT
|
||||
- Create tasks for other roles
|
||||
- Skip template loading
|
||||
- Modify discussion records from prior rounds
|
||||
- Skip inline discuss
|
||||
- Execute CLI document generation in main agent (delegate to subagent)
|
||||
- SendMessage to coordinator mid-loop (除非 consensus_blocked HIGH)
|
||||
|
||||
## Message Types
|
||||
|
||||
@@ -35,13 +37,22 @@ Product Brief, Requirements/PRD, Architecture, and Epics & Stories document gene
|
||||
|
||||
| Tool | Purpose |
|
||||
|------|---------|
|
||||
| commands/generate-doc.md | Multi-CLI document generation |
|
||||
| gemini, codex, claude CLI | Multi-perspective content generation |
|
||||
| subagents/doc-generation-subagent.md | Document generation (per task) |
|
||||
| discuss subagent | Inline discuss critique |
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Context & Discussion Loading
|
||||
## Phase 1: Task Discovery (Inner Loop)
|
||||
|
||||
**首次进入**:标准 Phase 1 流程,找到第一个 DRAFT-* pending 任务。
|
||||
|
||||
**循环重入**:Phase 5-L 完成后回到此处,TaskList 查找下一个 DRAFT-* pending 且 blockedBy 已全部 completed 的任务。
|
||||
|
||||
**终止条件**:无更多 DRAFT-* 可处理 → Phase 5-F。
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Context Loading
|
||||
|
||||
**Objective**: Load all required inputs for document generation.
|
||||
|
||||
@@ -72,15 +83,62 @@ Product Brief, Requirements/PRD, Architecture, and Epics & Stories document gene
|
||||
| architecture | + requirements/_index.md |
|
||||
| epics | + architecture/_index.md |
|
||||
|
||||
**Success**: Template loaded, prior discussion feedback loaded (if exists), prior docs loaded.
|
||||
**Prior decisions from accumulator**: 将 context_accumulator 中的前序摘要作为 "Prior Decisions" 传入。
|
||||
|
||||
| Input | Source | Required |
|
||||
|-------|--------|----------|
|
||||
| Document standards | `../../specs/document-standards.md` | Yes |
|
||||
| Template | From routing table | Yes |
|
||||
| Spec config | `<session-folder>/spec/spec-config.json` | Yes |
|
||||
| Discovery context | `<session-folder>/spec/discovery-context.json` | Yes |
|
||||
| Discussion feedback | `<session-folder>/discussions/<discuss-file>` | If exists |
|
||||
| Prior decisions | context_accumulator (内存) | 如果有前序任务 |
|
||||
|
||||
**Success**: Template loaded, prior discussion feedback loaded (if exists), prior docs loaded, accumulator context prepared.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Document Generation
|
||||
## Phase 3: Subagent Document Generation
|
||||
|
||||
**Objective**: Generate document using template and multi-CLI analysis.
|
||||
**Objective**: Delegate document generation to doc-generation subagent.
|
||||
|
||||
Delegate to `commands/generate-doc.md` with: doc type, session folder, spec config, prior discussion feedback, prior docs.
|
||||
**变化**:不再在主 agent 内执行 CLI 调用,而是委托给 doc-generation subagent。
|
||||
|
||||
```
|
||||
Task({
|
||||
subagent_type: "universal-executor",
|
||||
run_in_background: false,
|
||||
description: "Generate <doc-type> document",
|
||||
prompt: `<从 subagents/doc-generation-subagent.md 加载 prompt>
|
||||
|
||||
## Task
|
||||
- Document type: <doc-type>
|
||||
- Session folder: <session-folder>
|
||||
- Template: <template-path>
|
||||
|
||||
## Context
|
||||
- Spec config: <spec-config 内容>
|
||||
- Discovery context: <discovery-context 摘要>
|
||||
- Prior discussion feedback: <discussion-file 内容 if exists>
|
||||
- Prior decisions (from writer accumulator):
|
||||
<context_accumulator 序列化>
|
||||
|
||||
## Instructions
|
||||
<从 commands/generate-doc.md 加载该 doc-type 的具体策略>
|
||||
|
||||
## Expected Output
|
||||
Return JSON:
|
||||
{
|
||||
"artifact_path": "<output-path>",
|
||||
"summary": "<100-200字摘要>",
|
||||
"key_decisions": ["<decision-1>", "<decision-2>", ...],
|
||||
"sections_generated": ["<section-1>", ...],
|
||||
"warnings": ["<warning if any>"]
|
||||
}`
|
||||
})
|
||||
```
|
||||
|
||||
**主 agent 拿到的只是上述 JSON 摘要**,不是整篇文档。文档已由 subagent 写入磁盘。
|
||||
|
||||
---
|
||||
|
||||
@@ -140,11 +198,49 @@ Discussion: <session-folder>/discussions/<DISCUSS-NNN>-discussion.md
|
||||
|
||||
---
|
||||
|
||||
## Phase 5-L: 循环完成 (Loop Completion)
|
||||
|
||||
在还有后续 DRAFT-* 任务时执行:
|
||||
|
||||
1. **TaskUpdate**: 标记当前任务 completed
|
||||
2. **team_msg**: 记录任务完成
|
||||
3. **累积摘要**:
|
||||
```
|
||||
context_accumulator.append({
|
||||
task: "<DRAFT-NNN>",
|
||||
artifact: "<output-path>",
|
||||
key_decisions: <from subagent return>,
|
||||
discuss_verdict: <from Phase 4>,
|
||||
discuss_rating: <from Phase 4>,
|
||||
summary: <from subagent return>
|
||||
})
|
||||
```
|
||||
4. **中断检查**:
|
||||
- consensus_blocked HIGH → SendMessage → STOP
|
||||
- 累计错误 >= 3 → SendMessage → STOP
|
||||
5. **Loop**: 回到 Phase 1
|
||||
|
||||
**不做**:不 SendMessage、不 Fast-Advance spawn。
|
||||
|
||||
## Phase 5-F: 最终报告 (Final Report)
|
||||
|
||||
当所有 DRAFT-* 任务完成后:
|
||||
|
||||
1. **TaskUpdate**: 标记最后一个任务 completed
|
||||
2. **team_msg**: 记录完成
|
||||
3. **汇总报告**: 所有任务摘要 + discuss 结果 + 产出路径
|
||||
4. **Fast-Advance 检查**: 检查跨前缀后续 (如 QUALITY-001 是否 ready)
|
||||
5. **SendMessage** 或 **spawn successor**
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Scenario | Resolution |
|
||||
|----------|------------|
|
||||
| Subagent 失败 | 重试 1 次,换 subagent_type;仍失败则记录错误,继续下一任务 |
|
||||
| Discuss subagent 失败 | 跳过 discuss,记录 warning |
|
||||
| 累计 3 个任务失败 | SendMessage 报告 coordinator,STOP |
|
||||
| Agent crash mid-loop | Coordinator resume 检测 orphan → 重新 spawn → 从断点恢复 |
|
||||
| Prior doc not found | Notify coordinator, request prerequisite |
|
||||
| CLI failure | Retry with fallback tool |
|
||||
| Discussion contradicts prior docs | Note conflict, flag for coordinator |
|
||||
| Discuss subagent fails | Proceed without discuss, log warning in report |
|
||||
|
||||
@@ -22,17 +22,21 @@
|
||||
"writer": {
|
||||
"task_prefix": "DRAFT",
|
||||
"responsibility": "Product Brief / PRD / Architecture / Epics document generation + inline discuss",
|
||||
"execution_mode": "inner_loop",
|
||||
"subagent_type": "universal-executor",
|
||||
"inline_discuss": ["DISCUSS-002", "DISCUSS-003", "DISCUSS-004", "DISCUSS-005"],
|
||||
"message_types": ["draft_ready", "draft_revision", "error"]
|
||||
},
|
||||
"planner": {
|
||||
"task_prefix": "PLAN",
|
||||
"responsibility": "Multi-angle code exploration (via shared explore), structured implementation planning",
|
||||
"execution_mode": "inner_loop",
|
||||
"message_types": ["plan_ready", "plan_revision", "error"]
|
||||
},
|
||||
"executor": {
|
||||
"task_prefix": "IMPL",
|
||||
"responsibility": "Code implementation following approved plans",
|
||||
"execution_mode": "inner_loop",
|
||||
"message_types": ["impl_complete", "impl_progress", "error"]
|
||||
},
|
||||
"tester": {
|
||||
@@ -42,10 +46,10 @@
|
||||
},
|
||||
"reviewer": {
|
||||
"task_prefix": "REVIEW",
|
||||
"additional_prefixes": ["QUALITY"],
|
||||
"responsibility": "Code review (REVIEW-*) + Spec quality validation (QUALITY-*) + inline discuss for sign-off",
|
||||
"additional_prefixes": ["QUALITY", "IMPROVE"],
|
||||
"responsibility": "Code review (REVIEW-*) + Spec quality validation (QUALITY-*) + Quality improvement recheck (IMPROVE-*) + inline discuss for sign-off",
|
||||
"inline_discuss": "DISCUSS-006",
|
||||
"message_types": ["review_result", "quality_result", "fix_required", "error"]
|
||||
"message_types": ["review_result", "quality_result", "quality_recheck", "fix_required", "error"]
|
||||
},
|
||||
"architect": {
|
||||
"task_prefix": "ARCH",
|
||||
@@ -83,6 +87,33 @@
|
||||
}
|
||||
},
|
||||
|
||||
"checkpoint_commands": {
|
||||
"revise": {
|
||||
"handler": "handleRevise",
|
||||
"pattern": "revise <TASK-ID> [feedback]",
|
||||
"cascade": true,
|
||||
"creates": "revision_task"
|
||||
},
|
||||
"feedback": {
|
||||
"handler": "handleFeedback",
|
||||
"pattern": "feedback <text>",
|
||||
"cascade": true,
|
||||
"creates": "revision_chain"
|
||||
},
|
||||
"recheck": {
|
||||
"handler": "handleRecheck",
|
||||
"pattern": "recheck",
|
||||
"cascade": false,
|
||||
"creates": "quality_recheck"
|
||||
},
|
||||
"improve": {
|
||||
"handler": "handleImprove",
|
||||
"pattern": "improve [dimension]",
|
||||
"cascade": false,
|
||||
"creates": "improvement_task + quality_recheck"
|
||||
}
|
||||
},
|
||||
|
||||
"pipelines": {
|
||||
"spec-only": {
|
||||
"description": "Specification pipeline: research+discuss -> draft+discuss x4 -> quality+discuss",
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
# Doc Generation Subagent
|
||||
|
||||
文档生成执行引擎。由 writer 主 agent 通过 Inner Loop 调用。
|
||||
负责 CLI 多视角分析 + 模板填充 + 文件写入。
|
||||
|
||||
## Design Rationale
|
||||
|
||||
从 v4.0 的 writer 内联 CLI 执行,改为 subagent 隔离调用。
|
||||
好处:CLI 调用的大量 token 消耗不污染 writer 主 agent 上下文,
|
||||
writer 只拿到压缩摘要,可在多任务间保持上下文连续。
|
||||
|
||||
## Invocation
|
||||
|
||||
```
|
||||
Task({
|
||||
subagent_type: "universal-executor",
|
||||
run_in_background: false,
|
||||
description: "Generate <doc-type>",
|
||||
prompt: `## Document Generation: <doc-type>
|
||||
|
||||
### Session
|
||||
- Folder: <session-folder>
|
||||
- Spec config: <spec-config-path>
|
||||
|
||||
### Document Config
|
||||
- Type: <product-brief | requirements | architecture | epics>
|
||||
- Template: <template-path>
|
||||
- Output: <output-path>
|
||||
- Prior discussion: <discussion-file or "none">
|
||||
|
||||
### Writer Accumulator (prior decisions)
|
||||
<JSON array of prior task summaries>
|
||||
|
||||
### Execution Strategy
|
||||
<从 generate-doc.md 对应 doc-type 段落加载>
|
||||
|
||||
### Output Requirements
|
||||
1. Write document to <output-path> (follow template + document-standards.md)
|
||||
2. Return JSON summary:
|
||||
{
|
||||
"artifact_path": "<path>",
|
||||
"summary": "<100-200字>",
|
||||
"key_decisions": [],
|
||||
"sections_generated": [],
|
||||
"cross_references": [],
|
||||
"warnings": []
|
||||
}`
|
||||
})
|
||||
```
|
||||
|
||||
## Doc Type Strategies
|
||||
|
||||
(直接引用 generate-doc.md 的 DRAFT-001/002/003/004 策略,不重复)
|
||||
See: roles/writer/commands/generate-doc.md
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Scenario | Resolution |
|
||||
|----------|------------|
|
||||
| CLI 工具失败 | fallback chain: gemini → codex → claude |
|
||||
| Template 不存在 | 返回错误 JSON |
|
||||
| Prior doc 不存在 | 返回错误 JSON,writer 决定是否继续 |
|
||||
@@ -53,10 +53,10 @@
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^2.5.0",
|
||||
"web-vitals": "^5.1.0",
|
||||
"zod": "^4.1.13",
|
||||
"zustand": "^5.0.0",
|
||||
"xterm": "^5.3.0",
|
||||
"xterm-addon-fit": "^0.8.0"
|
||||
"xterm-addon-fit": "^0.8.0",
|
||||
"zod": "^4.1.13",
|
||||
"zustand": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.57.0",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import {
|
||||
Zap,
|
||||
CheckCircle,
|
||||
@@ -17,11 +18,17 @@ import {
|
||||
FileText,
|
||||
Database,
|
||||
Activity,
|
||||
Copy,
|
||||
Check,
|
||||
} from 'lucide-react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import rehypeHighlight from 'rehype-highlight';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/Dialog';
|
||||
import 'highlight.js/styles/github-dark.css';
|
||||
import {
|
||||
useExtractionStatus,
|
||||
useConsolidationStatus,
|
||||
@@ -31,6 +38,24 @@ import {
|
||||
} from '@/hooks/useMemoryV2';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
// ========== Helper Functions ==========
|
||||
|
||||
/**
|
||||
* Format a timestamp to relative time string
|
||||
*/
|
||||
function formatRelativeTime(timestamp: number | string | undefined): string | null {
|
||||
if (!timestamp) return null;
|
||||
|
||||
try {
|
||||
const date = typeof timestamp === 'string' ? new Date(timestamp) : new Date(timestamp);
|
||||
if (isNaN(date.getTime())) return null;
|
||||
|
||||
return formatDistanceToNow(date, { addSuffix: true });
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Status Badge ==========
|
||||
|
||||
const STATUS_CONFIG: Record<string, { color: string; icon: React.ReactNode; label: string }> = {
|
||||
@@ -66,6 +91,7 @@ function ExtractionCard() {
|
||||
|
||||
// Check if any job is running
|
||||
const hasRunningJob = status?.jobs?.some(j => j.status === 'running');
|
||||
const lastRunText = formatRelativeTime(status?.lastRun);
|
||||
|
||||
return (
|
||||
<Card className="p-4">
|
||||
@@ -78,6 +104,11 @@ function ExtractionCard() {
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{intl.formatMessage({ id: 'memory.v2.extraction.description', defaultMessage: 'Extract structured memories from CLI sessions' })}
|
||||
</p>
|
||||
{lastRunText && (
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{intl.formatMessage({ id: 'memory.v2.extraction.lastRun', defaultMessage: 'Last run' })}: {lastRunText}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{status && (
|
||||
<div className="text-right">
|
||||
@@ -150,12 +181,24 @@ function ConsolidationCard() {
|
||||
const { data: status, isLoading, refetch } = useConsolidationStatus();
|
||||
const trigger = useTriggerConsolidation();
|
||||
const [showPreview, setShowPreview] = useState(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const handleTrigger = () => {
|
||||
trigger.mutate();
|
||||
};
|
||||
|
||||
const handleCopy = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(status?.memoryMdPreview || '');
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const isRunning = status?.status === 'running';
|
||||
const lastRunText = formatRelativeTime(status?.lastRun);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -169,6 +212,11 @@ function ConsolidationCard() {
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{intl.formatMessage({ id: 'memory.v2.consolidation.description', defaultMessage: 'Merge extracted results into MEMORY.md' })}
|
||||
</p>
|
||||
{lastRunText && (
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{intl.formatMessage({ id: 'memory.v2.consolidation.lastRun', defaultMessage: 'Last run' })}: {lastRunText}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{status && <StatusBadge status={status.status} />}
|
||||
</div>
|
||||
@@ -226,17 +274,43 @@ function ConsolidationCard() {
|
||||
|
||||
{/* MEMORY.md Preview Dialog */}
|
||||
<Dialog open={showPreview} onOpenChange={setShowPreview}>
|
||||
<DialogContent className="max-w-3xl max-h-[80vh] overflow-hidden flex flex-col">
|
||||
<DialogContent className="max-w-4xl max-h-[80vh] overflow-hidden flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<FileText className="w-5 h-5" />
|
||||
MEMORY.md
|
||||
<DialogTitle className="flex items-center justify-between pr-8">
|
||||
<span className="flex items-center gap-2">
|
||||
<FileText className="w-5 h-5" />
|
||||
MEMORY.md
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleCopy}
|
||||
className="h-8 px-2"
|
||||
title={copied
|
||||
? intl.formatMessage({ id: 'memory.v2.consolidation.copySuccess', defaultMessage: 'Copied' })
|
||||
: intl.formatMessage({ id: 'memory.actions.copy', defaultMessage: 'Copy' })
|
||||
}
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="w-4 h-4 text-green-500" />
|
||||
) : (
|
||||
<Copy className="w-4 h-4" />
|
||||
)}
|
||||
</Button>
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="overflow-auto flex-1">
|
||||
<pre className="text-sm whitespace-pre-wrap p-4 bg-muted rounded font-mono">
|
||||
{status?.memoryMdPreview || 'No content available'}
|
||||
</pre>
|
||||
<div className="overflow-auto flex-1 markdown-preview">
|
||||
<div className="prose prose-sm dark:prose-invert max-w-none">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
rehypePlugins={[rehypeHighlight]}
|
||||
>
|
||||
{status?.memoryMdPreview || intl.formatMessage({
|
||||
id: 'memory.v2.consolidation.noContent',
|
||||
defaultMessage: 'No content available'
|
||||
})}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
@@ -246,10 +320,55 @@ function ConsolidationCard() {
|
||||
|
||||
// ========== Jobs List ==========
|
||||
|
||||
interface V2Job {
|
||||
kind: string;
|
||||
job_key: string;
|
||||
status: 'pending' | 'running' | 'done' | 'error';
|
||||
last_error?: string;
|
||||
worker_id?: string;
|
||||
started_at?: number;
|
||||
finished_at?: number;
|
||||
retry_remaining?: number;
|
||||
}
|
||||
|
||||
function JobsList() {
|
||||
const intl = useIntl();
|
||||
const [kindFilter, setKindFilter] = useState<string>('');
|
||||
const { data, isLoading, refetch } = useV2Jobs(kindFilter ? { kind: kindFilter } : undefined);
|
||||
const [statusFilter, setStatusFilter] = useState<string>('');
|
||||
const [selectedJob, setSelectedJob] = useState<V2Job | null>(null);
|
||||
const [copiedError, setCopiedError] = useState(false);
|
||||
|
||||
// Build filters object
|
||||
const filters = {
|
||||
...(kindFilter && { kind: kindFilter }),
|
||||
...(statusFilter && { status_filter: statusFilter }),
|
||||
};
|
||||
|
||||
const { data, isLoading, refetch } = useV2Jobs(Object.keys(filters).length > 0 ? filters : undefined);
|
||||
|
||||
// Format timestamp to readable string
|
||||
const formatTimestamp = (timestamp: number | undefined): string => {
|
||||
if (!timestamp) return '-';
|
||||
try {
|
||||
const date = new Date(timestamp);
|
||||
return date.toLocaleString();
|
||||
} catch {
|
||||
return '-';
|
||||
}
|
||||
};
|
||||
|
||||
// Copy error to clipboard
|
||||
const handleCopyError = async () => {
|
||||
if (selectedJob?.last_error) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(selectedJob.last_error);
|
||||
setCopiedError(true);
|
||||
setTimeout(() => setCopiedError(false), 2000);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy:', err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="p-4">
|
||||
@@ -264,9 +383,36 @@ function JobsList() {
|
||||
onChange={(e) => setKindFilter(e.target.value)}
|
||||
className="px-2 py-1 text-sm border rounded bg-background"
|
||||
>
|
||||
<option value="">All Kinds</option>
|
||||
<option value="phase1_extraction">Extraction</option>
|
||||
<option value="memory_consolidate_global">Consolidation</option>
|
||||
<option value="">
|
||||
{intl.formatMessage({ id: 'memory.v2.jobs.allKinds', defaultMessage: 'All Kinds' })}
|
||||
</option>
|
||||
<option value="phase1_extraction">
|
||||
{intl.formatMessage({ id: 'memory.v2.jobs.extraction', defaultMessage: 'Extraction' })}
|
||||
</option>
|
||||
<option value="memory_consolidate_global">
|
||||
{intl.formatMessage({ id: 'memory.v2.jobs.consolidation', defaultMessage: 'Consolidation' })}
|
||||
</option>
|
||||
</select>
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value)}
|
||||
className="px-2 py-1 text-sm border rounded bg-background"
|
||||
>
|
||||
<option value="">
|
||||
{intl.formatMessage({ id: 'memory.v2.jobs.statusFilter.all', defaultMessage: 'All Status' })}
|
||||
</option>
|
||||
<option value="pending">
|
||||
{intl.formatMessage({ id: 'memory.v2.jobs.statusFilter.pending', defaultMessage: 'Pending' })}
|
||||
</option>
|
||||
<option value="running">
|
||||
{intl.formatMessage({ id: 'memory.v2.jobs.statusFilter.running', defaultMessage: 'Running' })}
|
||||
</option>
|
||||
<option value="done">
|
||||
{intl.formatMessage({ id: 'memory.v2.jobs.statusFilter.done', defaultMessage: 'Done' })}
|
||||
</option>
|
||||
<option value="error">
|
||||
{intl.formatMessage({ id: 'memory.v2.jobs.statusFilter.error', defaultMessage: 'Error' })}
|
||||
</option>
|
||||
</select>
|
||||
<Button variant="outline" size="sm" onClick={() => refetch()}>
|
||||
<RefreshCw className={cn('w-4 h-4', isLoading && 'animate-spin')} />
|
||||
@@ -295,7 +441,11 @@ function JobsList() {
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.jobs.map((job) => (
|
||||
<tr key={`${job.kind}-${job.job_key}`} className="border-b">
|
||||
<tr
|
||||
key={`${job.kind}-${job.job_key}`}
|
||||
className="border-b cursor-pointer hover:bg-muted/50 transition-colors"
|
||||
onClick={() => setSelectedJob(job)}
|
||||
>
|
||||
<td className="p-2">
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{job.kind === 'phase1_extraction' ? 'Extraction' :
|
||||
@@ -330,15 +480,164 @@ function JobsList() {
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Job Detail Dialog */}
|
||||
<Dialog open={!!selectedJob} onOpenChange={() => setSelectedJob(null)}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{intl.formatMessage({ id: 'memory.v2.jobs.detail.title', defaultMessage: 'Job Details' })}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-xs text-muted-foreground">
|
||||
{intl.formatMessage({ id: 'memory.v2.jobs.detail.kind', defaultMessage: 'Kind' })}
|
||||
</label>
|
||||
<p className="text-sm font-medium">
|
||||
{selectedJob?.kind === 'phase1_extraction' ? 'Extraction' :
|
||||
selectedJob?.kind === 'memory_consolidate_global' ? 'Consolidation' : selectedJob?.kind}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-muted-foreground">
|
||||
{intl.formatMessage({ id: 'memory.v2.jobs.detail.status', defaultMessage: 'Status' })}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
{selectedJob && <StatusBadge status={selectedJob.status} />}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<label className="text-xs text-muted-foreground">
|
||||
{intl.formatMessage({ id: 'memory.v2.jobs.detail.jobKey', defaultMessage: 'Job ID' })}
|
||||
</label>
|
||||
<p className="text-sm font-mono break-all">{selectedJob?.job_key}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-muted-foreground">
|
||||
{intl.formatMessage({ id: 'memory.v2.jobs.detail.startedAt', defaultMessage: 'Started At' })}
|
||||
</label>
|
||||
<p className="text-sm">{formatTimestamp(selectedJob?.started_at)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-muted-foreground">
|
||||
{intl.formatMessage({ id: 'memory.v2.jobs.detail.finishedAt', defaultMessage: 'Finished At' })}
|
||||
</label>
|
||||
<p className="text-sm">{formatTimestamp(selectedJob?.finished_at)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-muted-foreground">
|
||||
{intl.formatMessage({ id: 'memory.v2.jobs.detail.workerId', defaultMessage: 'Worker ID' })}
|
||||
</label>
|
||||
<p className="text-sm font-mono truncate">{selectedJob?.worker_id || '-'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-muted-foreground">
|
||||
{intl.formatMessage({ id: 'memory.v2.jobs.detail.retryRemaining', defaultMessage: 'Retry Remaining' })}
|
||||
</label>
|
||||
<p className="text-sm">{selectedJob?.retry_remaining ?? '-'}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error Section */}
|
||||
{selectedJob?.last_error && (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<label className="text-xs text-muted-foreground">
|
||||
{intl.formatMessage({ id: 'memory.v2.jobs.detail.error', defaultMessage: 'Error' })}
|
||||
</label>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleCopyError}
|
||||
className="h-6 px-2"
|
||||
title={copiedError
|
||||
? intl.formatMessage({ id: 'memory.actions.copySuccess', defaultMessage: 'Copied' })
|
||||
: intl.formatMessage({ id: 'memory.actions.copy', defaultMessage: 'Copy' })
|
||||
}
|
||||
>
|
||||
{copiedError ? (
|
||||
<Check className="w-3 h-3 text-green-500" />
|
||||
) : (
|
||||
<Copy className="w-3 h-3" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<pre className="text-xs bg-red-50 dark:bg-red-950/30 text-red-800 dark:text-red-300 p-3 rounded overflow-auto max-h-40 whitespace-pre-wrap break-all">
|
||||
{selectedJob.last_error}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!selectedJob?.last_error && (
|
||||
<div>
|
||||
<label className="text-xs text-muted-foreground">
|
||||
{intl.formatMessage({ id: 'memory.v2.jobs.detail.error', defaultMessage: 'Error' })}
|
||||
</label>
|
||||
<p className="text-sm text-muted-foreground italic">
|
||||
{intl.formatMessage({ id: 'memory.v2.jobs.detail.noError', defaultMessage: 'No error' })}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// ========== Pipeline Status Banner ==========
|
||||
|
||||
function PipelineStatusBanner() {
|
||||
const intl = useIntl();
|
||||
const { data } = useV2Jobs();
|
||||
|
||||
// Detect running and error jobs
|
||||
const jobs = data?.jobs || [];
|
||||
const runningJobs = jobs.filter(j => j.status === 'running');
|
||||
const errorJobs = jobs.filter(j => j.status === 'error');
|
||||
const hasRunningJobs = runningJobs.length > 0;
|
||||
const errorCount = errorJobs.length;
|
||||
const runningCount = runningJobs.length;
|
||||
|
||||
if (hasRunningJobs) {
|
||||
return (
|
||||
<div className="mb-4 p-3 bg-blue-50 dark:bg-blue-950/30 border border-blue-200 dark:border-blue-800 rounded-lg flex items-center gap-2">
|
||||
<Loader2 className="w-4 h-4 animate-spin text-blue-500" />
|
||||
<span className="text-sm text-blue-700 dark:text-blue-300">
|
||||
{intl.formatMessage(
|
||||
{ id: 'memory.v2.statusBanner.running', defaultMessage: 'Pipeline Running - {count} job(s) in progress' },
|
||||
{ count: runningCount }
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (errorCount > 0) {
|
||||
return (
|
||||
<div className="mb-4 p-3 bg-yellow-50 dark:bg-yellow-950/30 border border-yellow-200 dark:border-yellow-800 rounded-lg flex items-center gap-2">
|
||||
<AlertCircle className="w-4 h-4 text-yellow-500" />
|
||||
<span className="text-sm text-yellow-700 dark:text-yellow-300">
|
||||
{intl.formatMessage(
|
||||
{ id: 'memory.v2.statusBanner.hasErrors', defaultMessage: 'Pipeline Idle - {count} job(s) failed' },
|
||||
{ count: errorCount }
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ========== Main Component ==========
|
||||
|
||||
export function V2PipelineTab() {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<PipelineStatusBanner />
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<ExtractionCard />
|
||||
<ConsolidationCard />
|
||||
|
||||
@@ -744,3 +744,125 @@
|
||||
[data-reduced-motion="true"] .bg-image-layer {
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
/* ===========================
|
||||
Markdown Preview Styles
|
||||
=========================== */
|
||||
.markdown-preview {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.markdown-preview .prose {
|
||||
color: hsl(var(--text));
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.markdown-preview .prose h1,
|
||||
.markdown-preview .prose h2,
|
||||
.markdown-preview .prose h3,
|
||||
.markdown-preview .prose h4 {
|
||||
color: hsl(var(--text));
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: 0.5em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.markdown-preview .prose h1 { font-size: 1.5em; }
|
||||
.markdown-preview .prose h2 { font-size: 1.25em; }
|
||||
.markdown-preview .prose h3 { font-size: 1.125em; }
|
||||
.markdown-preview .prose h4 { font-size: 1em; }
|
||||
|
||||
.markdown-preview .prose p {
|
||||
margin-top: 0.75em;
|
||||
margin-bottom: 0.75em;
|
||||
}
|
||||
|
||||
.markdown-preview .prose ul,
|
||||
.markdown-preview .prose ol {
|
||||
margin-top: 0.75em;
|
||||
margin-bottom: 0.75em;
|
||||
padding-left: 1.5em;
|
||||
}
|
||||
|
||||
.markdown-preview .prose li {
|
||||
margin-top: 0.25em;
|
||||
margin-bottom: 0.25em;
|
||||
}
|
||||
|
||||
.markdown-preview .prose code {
|
||||
background-color: hsl(var(--muted));
|
||||
padding: 0.125em 0.375em;
|
||||
border-radius: 0.25em;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
.markdown-preview .prose pre {
|
||||
background-color: hsl(var(--muted));
|
||||
padding: 1em;
|
||||
border-radius: 0.5em;
|
||||
overflow-x: auto;
|
||||
margin-top: 0.75em;
|
||||
margin-bottom: 0.75em;
|
||||
}
|
||||
|
||||
.markdown-preview .prose pre code {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
.markdown-preview .prose blockquote {
|
||||
border-left: 3px solid hsl(var(--accent));
|
||||
padding-left: 1em;
|
||||
margin-left: 0;
|
||||
color: hsl(var(--text-secondary));
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.markdown-preview .prose a {
|
||||
color: hsl(var(--accent));
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.markdown-preview .prose a:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.markdown-preview .prose table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 0.75em;
|
||||
margin-bottom: 0.75em;
|
||||
}
|
||||
|
||||
.markdown-preview .prose th,
|
||||
.markdown-preview .prose td {
|
||||
border: 1px solid hsl(var(--border));
|
||||
padding: 0.5em 0.75em;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.markdown-preview .prose th {
|
||||
background-color: hsl(var(--muted));
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.markdown-preview .prose hr {
|
||||
border: none;
|
||||
border-top: 1px solid hsl(var(--border));
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
||||
/* Highlight.js overrides for dark theme */
|
||||
.markdown-preview .hljs {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
[data-theme^="dark"] .markdown-preview .prose pre {
|
||||
background-color: hsl(220 25% 12%);
|
||||
}
|
||||
|
||||
[data-theme^="dark"] .markdown-preview .prose code {
|
||||
background-color: hsl(220 25% 18%);
|
||||
}
|
||||
|
||||
@@ -1642,6 +1642,7 @@ export async function unarchiveMemory(memoryId: string, projectPath?: string): P
|
||||
|
||||
export interface ExtractionStatus {
|
||||
total_stage1: number;
|
||||
lastRun?: number;
|
||||
jobs: Array<{
|
||||
job_key: string;
|
||||
status: string;
|
||||
|
||||
@@ -119,6 +119,7 @@
|
||||
"extracting": "Extracting...",
|
||||
"extracted": "Extracted",
|
||||
"recentJobs": "Recent Jobs",
|
||||
"lastRun": "Last run",
|
||||
"triggered": "Extraction triggered",
|
||||
"triggerError": "Failed to trigger extraction"
|
||||
},
|
||||
@@ -132,8 +133,11 @@
|
||||
"exists": "Exists",
|
||||
"notExists": "Not Exists",
|
||||
"inputs": "Inputs",
|
||||
"lastRun": "Last run",
|
||||
"triggered": "Consolidation triggered",
|
||||
"triggerError": "Failed to trigger consolidation"
|
||||
"triggerError": "Failed to trigger consolidation",
|
||||
"copySuccess": "Copied",
|
||||
"noContent": "No content available"
|
||||
},
|
||||
"jobs": {
|
||||
"title": "Jobs",
|
||||
@@ -144,7 +148,26 @@
|
||||
"noJobs": "No jobs found",
|
||||
"allKinds": "All Kinds",
|
||||
"extraction": "Extraction",
|
||||
"consolidation": "Consolidation"
|
||||
"consolidation": "Consolidation",
|
||||
"statusFilter": {
|
||||
"all": "All Status",
|
||||
"pending": "Pending",
|
||||
"running": "Running",
|
||||
"done": "Done",
|
||||
"error": "Error"
|
||||
},
|
||||
"detail": {
|
||||
"title": "Job Details",
|
||||
"kind": "Kind",
|
||||
"jobKey": "Job ID",
|
||||
"status": "Status",
|
||||
"startedAt": "Started At",
|
||||
"finishedAt": "Finished At",
|
||||
"workerId": "Worker ID",
|
||||
"retryRemaining": "Retry Remaining",
|
||||
"error": "Error",
|
||||
"noError": "No error"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"idle": "Idle",
|
||||
@@ -153,6 +176,10 @@
|
||||
"done": "Done",
|
||||
"error": "Error",
|
||||
"pending": "Pending"
|
||||
},
|
||||
"statusBanner": {
|
||||
"running": "Pipeline Running - {count} job(s) in progress",
|
||||
"hasErrors": "Pipeline Idle - {count} job(s) failed"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,6 +119,7 @@
|
||||
"extracting": "提取中...",
|
||||
"extracted": "已提取",
|
||||
"recentJobs": "最近作业",
|
||||
"lastRun": "上次运行",
|
||||
"triggered": "提取已触发",
|
||||
"triggerError": "触发提取失败"
|
||||
},
|
||||
@@ -132,8 +133,11 @@
|
||||
"exists": "存在",
|
||||
"notExists": "不存在",
|
||||
"inputs": "输入",
|
||||
"lastRun": "上次运行",
|
||||
"triggered": "合并已触发",
|
||||
"triggerError": "触发合并失败"
|
||||
"triggerError": "触发合并失败",
|
||||
"copySuccess": "复制成功",
|
||||
"noContent": "暂无内容"
|
||||
},
|
||||
"jobs": {
|
||||
"title": "作业列表",
|
||||
@@ -144,7 +148,26 @@
|
||||
"noJobs": "暂无作业记录",
|
||||
"allKinds": "所有类型",
|
||||
"extraction": "提取",
|
||||
"consolidation": "合并"
|
||||
"consolidation": "合并",
|
||||
"statusFilter": {
|
||||
"all": "所有状态",
|
||||
"pending": "等待",
|
||||
"running": "运行中",
|
||||
"done": "完成",
|
||||
"error": "错误"
|
||||
},
|
||||
"detail": {
|
||||
"title": "作业详情",
|
||||
"kind": "类型",
|
||||
"jobKey": "作业 ID",
|
||||
"status": "状态",
|
||||
"startedAt": "开始时间",
|
||||
"finishedAt": "结束时间",
|
||||
"workerId": "Worker ID",
|
||||
"retryRemaining": "剩余重试次数",
|
||||
"error": "错误信息",
|
||||
"noError": "无错误"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"idle": "空闲",
|
||||
@@ -153,6 +176,10 @@
|
||||
"done": "完成",
|
||||
"error": "错误",
|
||||
"pending": "等待"
|
||||
},
|
||||
"statusBanner": {
|
||||
"running": "Pipeline 运行中 - {count} 个作业正在执行",
|
||||
"hasErrors": "Pipeline 空闲 - {count} 个作业失败"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user