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:
catlog22
2026-02-27 14:45:38 +08:00
parent b449b225fe
commit 3db74cc7b0
15 changed files with 1110 additions and 48 deletions

View File

@@ -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 | | 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 | | 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 ### Dispatch
@@ -105,6 +106,10 @@ User provides task description
|---------|--------| |---------|--------|
| `check` / `status` | Output execution status graph, no advancement | | `check` / `status` | Output execution status graph, no advancement |
| `resume` / `continue` | Check worker states, advance next step | | `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 | | 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) | | Multiple ready tasks (parallel window) | SendMessage to coordinator (needs orchestration) |
| No ready tasks + others running | SendMessage to coordinator (status update) | | No ready tasks + others running | SendMessage to coordinator (status update) |
| No ready tasks + nothing running | SendMessage to coordinator (pipeline may be complete) | | 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. **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) ### Inline Discuss Protocol (produce roles: analyst, writer, reviewer)
After completing their primary output, produce roles call the discuss subagent inline: After completing their primary output, produce roles call the discuss subagent inline:
@@ -354,10 +446,41 @@ Beat 1 2 3 4
| Trigger | Position | Behavior | | 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 | | 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 | | 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`): **Stall detection** (coordinator `handleCheck`):
| Check | Condition | Resolution | | Check | Condition | Resolution |
@@ -385,6 +508,8 @@ Beat 1 2 3 4
## Coordinator Spawn Template ## Coordinator Spawn Template
### 标准 Worker (单任务角色: analyst, tester, reviewer, architect)
When coordinator spawns workers, use background mode (Spawn-and-Stop): 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 ## Session Directory
``` ```

View File

@@ -106,12 +106,21 @@ Every task description includes session, scope, and inline discuss metadata:
TaskCreate({ TaskCreate({
subject: "<TASK-ID>", subject: "<TASK-ID>",
owner: "<role>", 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>], blockedBy: [<dependency-list>],
status: "pending" 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 ### Execution Method
| Method | Behavior | | Method | Behavior |
@@ -129,6 +138,56 @@ TaskCreate({
| Session reference | Every task description contains `Session: <session-folder>` | | Session reference | Every task description contains `Session: <session-folder>` |
| Inline discuss | Spec tasks have InlineDiscuss field matching round config | | 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 ## Error Handling
| Scenario | Resolution | | Scenario | Resolution |

View File

@@ -33,6 +33,10 @@ Parse `$ARGUMENTS` to determine handler:
| 2 | Contains "check" or "status" | handleCheck | | 2 | Contains "check" or "status" | handleCheck |
| 3 | Contains "resume", "continue", or "next" | handleResume | | 3 | Contains "resume", "continue", or "next" | handleResume |
| 4 | None of the above (initial spawn after dispatch) | handleSpawnNext | | 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. 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>] Receive callback from [<role>]
+- Find matching active worker by 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? +- Task status = completed?
| +- YES -> remove from active_workers -> update session | +- YES -> remove from active_workers -> update session
| | +- Handle checkpoints (see below) | | +- Handle checkpoints (see below)
@@ -87,7 +93,7 @@ Read-only status report. No pipeline advancement.
done=completed >>>=running o=pending .=not created done=completed >>>=running o=pending .=not created
[coordinator] Active Workers: [coordinator] Active Workers:
> <subject> (<role>) - running <elapsed> > <subject> (<role>) - running <elapsed> [inner-loop: N/M tasks done]
[coordinator] Ready to spawn: <subjects> [coordinator] Ready to spawn: <subjects>
[coordinator] Commands: 'resume' to advance | 'check' to refresh [coordinator] Commands: 'resume' to advance | 'check' to refresh
@@ -132,6 +138,9 @@ Ready tasks found?
+- NONE + work in progress -> report waiting -> STOP +- NONE + work in progress -> report waiting -> STOP
+- NONE + nothing in progress -> PIPELINE_COMPLETE -> Phase 5 +- NONE + nothing in progress -> PIPELINE_COMPLETE -> Phase 5
+- HAS ready tasks -> for each: +- 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 +- TaskUpdate -> in_progress
+- team_msg log -> task_unblocked +- team_msg log -> task_unblocked
+- Spawn worker (see tool call below) +- 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 ### Checkpoints
| Completed Task | Mode Condition | Action | | 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 |
--- ---

View File

@@ -151,6 +151,25 @@ Delegate to `commands/dispatch.md` which creates the full task chain:
- User "check" -> handleCheck (status only) - User "check" -> handleCheck (status only)
- User "resume" -> handleResume (advance) - 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 ## Phase 5: Report + Next Steps

View File

@@ -1,10 +1,12 @@
# Role: planner # 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 ## Identity
- **Name**: `planner` | **Prefix**: `PLAN-*` | **Tag**: `[planner]` - **Name**: `planner` | **Prefix**: `PLAN-*` | **Tag**: `[planner]`
- **Mode**: Inner Loop
- **Responsibility**: Complexity assessment -> Code exploration (shared cache) -> Plan generation -> Approval - **Responsibility**: Complexity assessment -> Code exploration (shared cache) -> Plan generation -> Approval
## Boundaries ## 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 ## Error Handling
| Scenario | Resolution | | Scenario | Resolution |

View File

@@ -2,7 +2,12 @@
## Purpose ## 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 ## Phase 2: Context Loading

View File

@@ -1,27 +1,29 @@
# Role: writer # 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 ## Identity
- **Name**: `writer` | **Prefix**: `DRAFT-*` | **Tag**: `[writer]` - **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 ## Boundaries
### MUST ### MUST
- Only process DRAFT-* tasks - Only process DRAFT-* tasks
- Read templates before generating (from `../../templates/`) - Use subagent for document generation (不在主 agent 内执行 CLI)
- Follow document-standards.md (from `../../specs/`) - Maintain context_accumulator across tasks
- Integrate prior discussion feedback when available - Call discuss subagent after each document output
- Generate proper YAML frontmatter - Loop through all DRAFT-* tasks before reporting to coordinator
- Call discuss subagent after document output (round from InlineDiscuss field)
### MUST NOT ### MUST NOT
- Create tasks for other roles - Create tasks for other roles
- Skip template loading - Skip template loading
- Modify discussion records from prior rounds - Execute CLI document generation in main agent (delegate to subagent)
- Skip inline discuss - SendMessage to coordinator mid-loop (除非 consensus_blocked HIGH)
## Message Types ## Message Types
@@ -35,13 +37,22 @@ Product Brief, Requirements/PRD, Architecture, and Epics & Stories document gene
| Tool | Purpose | | Tool | Purpose |
|------|---------| |------|---------|
| commands/generate-doc.md | Multi-CLI document generation | | subagents/doc-generation-subagent.md | Document generation (per task) |
| gemini, codex, claude CLI | Multi-perspective content generation |
| discuss subagent | Inline discuss critique | | 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. **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 | | architecture | + requirements/_index.md |
| epics | + architecture/_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 ## Error Handling
| Scenario | Resolution | | Scenario | Resolution |
|----------|------------| |----------|------------|
| Subagent 失败 | 重试 1 次,换 subagent_type仍失败则记录错误继续下一任务 |
| Discuss subagent 失败 | 跳过 discuss记录 warning |
| 累计 3 个任务失败 | SendMessage 报告 coordinatorSTOP |
| Agent crash mid-loop | Coordinator resume 检测 orphan → 重新 spawn → 从断点恢复 |
| Prior doc not found | Notify coordinator, request prerequisite | | Prior doc not found | Notify coordinator, request prerequisite |
| CLI failure | Retry with fallback tool |
| Discussion contradicts prior docs | Note conflict, flag for coordinator | | Discussion contradicts prior docs | Note conflict, flag for coordinator |
| Discuss subagent fails | Proceed without discuss, log warning in report |

View File

@@ -22,17 +22,21 @@
"writer": { "writer": {
"task_prefix": "DRAFT", "task_prefix": "DRAFT",
"responsibility": "Product Brief / PRD / Architecture / Epics document generation + inline discuss", "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"], "inline_discuss": ["DISCUSS-002", "DISCUSS-003", "DISCUSS-004", "DISCUSS-005"],
"message_types": ["draft_ready", "draft_revision", "error"] "message_types": ["draft_ready", "draft_revision", "error"]
}, },
"planner": { "planner": {
"task_prefix": "PLAN", "task_prefix": "PLAN",
"responsibility": "Multi-angle code exploration (via shared explore), structured implementation planning", "responsibility": "Multi-angle code exploration (via shared explore), structured implementation planning",
"execution_mode": "inner_loop",
"message_types": ["plan_ready", "plan_revision", "error"] "message_types": ["plan_ready", "plan_revision", "error"]
}, },
"executor": { "executor": {
"task_prefix": "IMPL", "task_prefix": "IMPL",
"responsibility": "Code implementation following approved plans", "responsibility": "Code implementation following approved plans",
"execution_mode": "inner_loop",
"message_types": ["impl_complete", "impl_progress", "error"] "message_types": ["impl_complete", "impl_progress", "error"]
}, },
"tester": { "tester": {
@@ -42,10 +46,10 @@
}, },
"reviewer": { "reviewer": {
"task_prefix": "REVIEW", "task_prefix": "REVIEW",
"additional_prefixes": ["QUALITY"], "additional_prefixes": ["QUALITY", "IMPROVE"],
"responsibility": "Code review (REVIEW-*) + Spec quality validation (QUALITY-*) + inline discuss for sign-off", "responsibility": "Code review (REVIEW-*) + Spec quality validation (QUALITY-*) + Quality improvement recheck (IMPROVE-*) + inline discuss for sign-off",
"inline_discuss": "DISCUSS-006", "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": { "architect": {
"task_prefix": "ARCH", "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": { "pipelines": {
"spec-only": { "spec-only": {
"description": "Specification pipeline: research+discuss -> draft+discuss x4 -> quality+discuss", "description": "Specification pipeline: research+discuss -> draft+discuss x4 -> quality+discuss",

View File

@@ -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 不存在 | 返回错误 JSONwriter 决定是否继续 |

View File

@@ -53,10 +53,10 @@
"sonner": "^2.0.7", "sonner": "^2.0.7",
"tailwind-merge": "^2.5.0", "tailwind-merge": "^2.5.0",
"web-vitals": "^5.1.0", "web-vitals": "^5.1.0",
"zod": "^4.1.13",
"zustand": "^5.0.0",
"xterm": "^5.3.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": { "devDependencies": {
"@playwright/test": "^1.57.0", "@playwright/test": "^1.57.0",

View File

@@ -5,6 +5,7 @@
import { useState } from 'react'; import { useState } from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { formatDistanceToNow } from 'date-fns';
import { import {
Zap, Zap,
CheckCircle, CheckCircle,
@@ -17,11 +18,17 @@ import {
FileText, FileText,
Database, Database,
Activity, Activity,
Copy,
Check,
} from 'lucide-react'; } 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 { Card } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge'; import { Badge } from '@/components/ui/Badge';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/Dialog'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/Dialog';
import 'highlight.js/styles/github-dark.css';
import { import {
useExtractionStatus, useExtractionStatus,
useConsolidationStatus, useConsolidationStatus,
@@ -31,6 +38,24 @@ import {
} from '@/hooks/useMemoryV2'; } from '@/hooks/useMemoryV2';
import { cn } from '@/lib/utils'; 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 ========== // ========== Status Badge ==========
const STATUS_CONFIG: Record<string, { color: string; icon: React.ReactNode; label: string }> = { const STATUS_CONFIG: Record<string, { color: string; icon: React.ReactNode; label: string }> = {
@@ -66,6 +91,7 @@ function ExtractionCard() {
// Check if any job is running // Check if any job is running
const hasRunningJob = status?.jobs?.some(j => j.status === 'running'); const hasRunningJob = status?.jobs?.some(j => j.status === 'running');
const lastRunText = formatRelativeTime(status?.lastRun);
return ( return (
<Card className="p-4"> <Card className="p-4">
@@ -78,6 +104,11 @@ function ExtractionCard() {
<p className="text-sm text-muted-foreground mt-1"> <p className="text-sm text-muted-foreground mt-1">
{intl.formatMessage({ id: 'memory.v2.extraction.description', defaultMessage: 'Extract structured memories from CLI sessions' })} {intl.formatMessage({ id: 'memory.v2.extraction.description', defaultMessage: 'Extract structured memories from CLI sessions' })}
</p> </p>
{lastRunText && (
<p className="text-xs text-muted-foreground mt-1">
{intl.formatMessage({ id: 'memory.v2.extraction.lastRun', defaultMessage: 'Last run' })}: {lastRunText}
</p>
)}
</div> </div>
{status && ( {status && (
<div className="text-right"> <div className="text-right">
@@ -150,12 +181,24 @@ function ConsolidationCard() {
const { data: status, isLoading, refetch } = useConsolidationStatus(); const { data: status, isLoading, refetch } = useConsolidationStatus();
const trigger = useTriggerConsolidation(); const trigger = useTriggerConsolidation();
const [showPreview, setShowPreview] = useState(false); const [showPreview, setShowPreview] = useState(false);
const [copied, setCopied] = useState(false);
const handleTrigger = () => { const handleTrigger = () => {
trigger.mutate(); 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 isRunning = status?.status === 'running';
const lastRunText = formatRelativeTime(status?.lastRun);
return ( return (
<> <>
@@ -169,6 +212,11 @@ function ConsolidationCard() {
<p className="text-sm text-muted-foreground mt-1"> <p className="text-sm text-muted-foreground mt-1">
{intl.formatMessage({ id: 'memory.v2.consolidation.description', defaultMessage: 'Merge extracted results into MEMORY.md' })} {intl.formatMessage({ id: 'memory.v2.consolidation.description', defaultMessage: 'Merge extracted results into MEMORY.md' })}
</p> </p>
{lastRunText && (
<p className="text-xs text-muted-foreground mt-1">
{intl.formatMessage({ id: 'memory.v2.consolidation.lastRun', defaultMessage: 'Last run' })}: {lastRunText}
</p>
)}
</div> </div>
{status && <StatusBadge status={status.status} />} {status && <StatusBadge status={status.status} />}
</div> </div>
@@ -226,17 +274,43 @@ function ConsolidationCard() {
{/* MEMORY.md Preview Dialog */} {/* MEMORY.md Preview Dialog */}
<Dialog open={showPreview} onOpenChange={setShowPreview}> <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> <DialogHeader>
<DialogTitle className="flex items-center gap-2"> <DialogTitle className="flex items-center justify-between pr-8">
<span className="flex items-center gap-2">
<FileText className="w-5 h-5" /> <FileText className="w-5 h-5" />
MEMORY.md 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> </DialogTitle>
</DialogHeader> </DialogHeader>
<div className="overflow-auto flex-1"> <div className="overflow-auto flex-1 markdown-preview">
<pre className="text-sm whitespace-pre-wrap p-4 bg-muted rounded font-mono"> <div className="prose prose-sm dark:prose-invert max-w-none">
{status?.memoryMdPreview || 'No content available'} <ReactMarkdown
</pre> remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeHighlight]}
>
{status?.memoryMdPreview || intl.formatMessage({
id: 'memory.v2.consolidation.noContent',
defaultMessage: 'No content available'
})}
</ReactMarkdown>
</div>
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
@@ -246,10 +320,55 @@ function ConsolidationCard() {
// ========== Jobs List ========== // ========== 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() { function JobsList() {
const intl = useIntl(); const intl = useIntl();
const [kindFilter, setKindFilter] = useState<string>(''); 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 ( return (
<Card className="p-4"> <Card className="p-4">
@@ -264,9 +383,36 @@ function JobsList() {
onChange={(e) => setKindFilter(e.target.value)} onChange={(e) => setKindFilter(e.target.value)}
className="px-2 py-1 text-sm border rounded bg-background" className="px-2 py-1 text-sm border rounded bg-background"
> >
<option value="">All Kinds</option> <option value="">
<option value="phase1_extraction">Extraction</option> {intl.formatMessage({ id: 'memory.v2.jobs.allKinds', defaultMessage: 'All Kinds' })}
<option value="memory_consolidate_global">Consolidation</option> </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> </select>
<Button variant="outline" size="sm" onClick={() => refetch()}> <Button variant="outline" size="sm" onClick={() => refetch()}>
<RefreshCw className={cn('w-4 h-4', isLoading && 'animate-spin')} /> <RefreshCw className={cn('w-4 h-4', isLoading && 'animate-spin')} />
@@ -295,7 +441,11 @@ function JobsList() {
</thead> </thead>
<tbody> <tbody>
{data.jobs.map((job) => ( {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"> <td className="p-2">
<Badge variant="outline" className="text-xs"> <Badge variant="outline" className="text-xs">
{job.kind === 'phase1_extraction' ? 'Extraction' : {job.kind === 'phase1_extraction' ? 'Extraction' :
@@ -330,15 +480,164 @@ function JobsList() {
</span> </span>
</div> </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> </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 ========== // ========== Main Component ==========
export function V2PipelineTab() { export function V2PipelineTab() {
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<PipelineStatusBanner />
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<ExtractionCard /> <ExtractionCard />
<ConsolidationCard /> <ConsolidationCard />

View File

@@ -744,3 +744,125 @@
[data-reduced-motion="true"] .bg-image-layer { [data-reduced-motion="true"] .bg-image-layer {
transition: none !important; 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%);
}

View File

@@ -1642,6 +1642,7 @@ export async function unarchiveMemory(memoryId: string, projectPath?: string): P
export interface ExtractionStatus { export interface ExtractionStatus {
total_stage1: number; total_stage1: number;
lastRun?: number;
jobs: Array<{ jobs: Array<{
job_key: string; job_key: string;
status: string; status: string;

View File

@@ -119,6 +119,7 @@
"extracting": "Extracting...", "extracting": "Extracting...",
"extracted": "Extracted", "extracted": "Extracted",
"recentJobs": "Recent Jobs", "recentJobs": "Recent Jobs",
"lastRun": "Last run",
"triggered": "Extraction triggered", "triggered": "Extraction triggered",
"triggerError": "Failed to trigger extraction" "triggerError": "Failed to trigger extraction"
}, },
@@ -132,8 +133,11 @@
"exists": "Exists", "exists": "Exists",
"notExists": "Not Exists", "notExists": "Not Exists",
"inputs": "Inputs", "inputs": "Inputs",
"lastRun": "Last run",
"triggered": "Consolidation triggered", "triggered": "Consolidation triggered",
"triggerError": "Failed to trigger consolidation" "triggerError": "Failed to trigger consolidation",
"copySuccess": "Copied",
"noContent": "No content available"
}, },
"jobs": { "jobs": {
"title": "Jobs", "title": "Jobs",
@@ -144,7 +148,26 @@
"noJobs": "No jobs found", "noJobs": "No jobs found",
"allKinds": "All Kinds", "allKinds": "All Kinds",
"extraction": "Extraction", "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": { "status": {
"idle": "Idle", "idle": "Idle",
@@ -153,6 +176,10 @@
"done": "Done", "done": "Done",
"error": "Error", "error": "Error",
"pending": "Pending" "pending": "Pending"
},
"statusBanner": {
"running": "Pipeline Running - {count} job(s) in progress",
"hasErrors": "Pipeline Idle - {count} job(s) failed"
} }
} }
} }

View File

@@ -119,6 +119,7 @@
"extracting": "提取中...", "extracting": "提取中...",
"extracted": "已提取", "extracted": "已提取",
"recentJobs": "最近作业", "recentJobs": "最近作业",
"lastRun": "上次运行",
"triggered": "提取已触发", "triggered": "提取已触发",
"triggerError": "触发提取失败" "triggerError": "触发提取失败"
}, },
@@ -132,8 +133,11 @@
"exists": "存在", "exists": "存在",
"notExists": "不存在", "notExists": "不存在",
"inputs": "输入", "inputs": "输入",
"lastRun": "上次运行",
"triggered": "合并已触发", "triggered": "合并已触发",
"triggerError": "触发合并失败" "triggerError": "触发合并失败",
"copySuccess": "复制成功",
"noContent": "暂无内容"
}, },
"jobs": { "jobs": {
"title": "作业列表", "title": "作业列表",
@@ -144,7 +148,26 @@
"noJobs": "暂无作业记录", "noJobs": "暂无作业记录",
"allKinds": "所有类型", "allKinds": "所有类型",
"extraction": "提取", "extraction": "提取",
"consolidation": "合并" "consolidation": "合并",
"statusFilter": {
"all": "所有状态",
"pending": "等待",
"running": "运行中",
"done": "完成",
"error": "错误"
},
"detail": {
"title": "作业详情",
"kind": "类型",
"jobKey": "作业 ID",
"status": "状态",
"startedAt": "开始时间",
"finishedAt": "结束时间",
"workerId": "Worker ID",
"retryRemaining": "剩余重试次数",
"error": "错误信息",
"noError": "无错误"
}
}, },
"status": { "status": {
"idle": "空闲", "idle": "空闲",
@@ -153,6 +176,10 @@
"done": "完成", "done": "完成",
"error": "错误", "error": "错误",
"pending": "等待" "pending": "等待"
},
"statusBanner": {
"running": "Pipeline 运行中 - {count} 个作业正在执行",
"hasErrors": "Pipeline 空闲 - {count} 个作业失败"
} }
} }
} }