mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-02 15:23:19 +08:00
feat(idaw): add resume, run, and status commands for task management
- Implemented /idaw:resume to resume interrupted sessions with task handling and auto mode. - Created /idaw:run for executing task skill chains with git checkpoints and session management. - Added /idaw:status for viewing task and session progress, including overview and specific session details. - Introduced helper functions for task type inference and skill argument assembly. - Enhanced task management with session tracking, progress reporting, and error handling.
This commit is contained in:
273
.claude/commands/idaw/add.md
Normal file
273
.claude/commands/idaw/add.md
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
---
|
||||||
|
name: add
|
||||||
|
description: Add IDAW tasks - manual creation or import from ccw issue
|
||||||
|
argument-hint: "[-y|--yes] [--from-issue <id>[,<id>,...]] \"description\" [--type <task_type>] [--priority <1-5>]"
|
||||||
|
allowed-tools: AskUserQuestion(*), Read(*), Bash(*), Write(*), Glob(*)
|
||||||
|
---
|
||||||
|
|
||||||
|
# IDAW Add Command (/idaw:add)
|
||||||
|
|
||||||
|
## Auto Mode
|
||||||
|
|
||||||
|
When `--yes` or `-y`: Skip clarification questions, create task with inferred details.
|
||||||
|
|
||||||
|
## IDAW Task Schema
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "IDAW-001",
|
||||||
|
"title": "string",
|
||||||
|
"description": "string",
|
||||||
|
"status": "pending",
|
||||||
|
"priority": 2,
|
||||||
|
"task_type": null,
|
||||||
|
"skill_chain": null,
|
||||||
|
"context": {
|
||||||
|
"affected_files": [],
|
||||||
|
"acceptance_criteria": [],
|
||||||
|
"constraints": [],
|
||||||
|
"references": []
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"type": "manual|import-issue",
|
||||||
|
"issue_id": null,
|
||||||
|
"issue_snapshot": null
|
||||||
|
},
|
||||||
|
"execution": {
|
||||||
|
"session_id": null,
|
||||||
|
"started_at": null,
|
||||||
|
"completed_at": null,
|
||||||
|
"skill_results": [],
|
||||||
|
"git_commit": null,
|
||||||
|
"error": null
|
||||||
|
},
|
||||||
|
"created_at": "ISO",
|
||||||
|
"updated_at": "ISO"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Valid task_type values**: `bugfix|bugfix-hotfix|feature|feature-complex|refactor|tdd|test|test-fix|review|docs`
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
### Phase 1: Parse Arguments
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const args = $ARGUMENTS;
|
||||||
|
const autoYes = /(-y|--yes)/.test(args);
|
||||||
|
const fromIssue = args.match(/--from-issue\s+([\w,-]+)/)?.[1];
|
||||||
|
const typeFlag = args.match(/--type\s+([\w-]+)/)?.[1];
|
||||||
|
const priorityFlag = args.match(/--priority\s+(\d)/)?.[1];
|
||||||
|
const description = args.replace(/(-y|--yes|--from-issue\s+[\w,-]+|--type\s+[\w-]+|--priority\s+\d)/g, '').trim().replace(/^["']|["']$/g, '');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 2: Route — Import or Manual
|
||||||
|
|
||||||
|
```
|
||||||
|
--from-issue present?
|
||||||
|
├─ YES → Import Mode (Phase 3A)
|
||||||
|
└─ NO → Manual Mode (Phase 3B)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 3A: Import Mode (from ccw issue)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const issueIds = fromIssue.split(',');
|
||||||
|
|
||||||
|
for (const issueId of issueIds) {
|
||||||
|
// 1. Fetch issue data
|
||||||
|
const issueJson = Bash(`ccw issue list --json`);
|
||||||
|
const issues = JSON.parse(issueJson).issues;
|
||||||
|
const issue = issues.find(i => i.id === issueId.trim());
|
||||||
|
if (!issue) {
|
||||||
|
console.log(`Warning: Issue ${issueId} not found, skipping`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Check duplicate (same issue_id already imported)
|
||||||
|
const existing = Glob('.workflow/.idaw/tasks/IDAW-*.json');
|
||||||
|
for (const f of existing) {
|
||||||
|
const data = JSON.parse(Read(f));
|
||||||
|
if (data.source?.issue_id === issueId.trim()) {
|
||||||
|
console.log(`Warning: Issue ${issueId} already imported as ${data.id}, skipping`);
|
||||||
|
continue; // skip to next issue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Generate next IDAW ID
|
||||||
|
const nextId = generateNextId();
|
||||||
|
|
||||||
|
// 4. Map issue → IDAW task
|
||||||
|
const task = {
|
||||||
|
id: nextId,
|
||||||
|
title: issue.title,
|
||||||
|
description: issue.context || issue.title,
|
||||||
|
status: 'pending',
|
||||||
|
priority: parseInt(priorityFlag) || issue.priority || 3,
|
||||||
|
task_type: typeFlag || inferTaskType(issue.title, issue.context || ''),
|
||||||
|
skill_chain: null,
|
||||||
|
context: {
|
||||||
|
affected_files: issue.affected_components || [],
|
||||||
|
acceptance_criteria: [],
|
||||||
|
constraints: [],
|
||||||
|
references: issue.source_url ? [issue.source_url] : []
|
||||||
|
},
|
||||||
|
source: {
|
||||||
|
type: 'import-issue',
|
||||||
|
issue_id: issue.id,
|
||||||
|
issue_snapshot: {
|
||||||
|
id: issue.id,
|
||||||
|
title: issue.title,
|
||||||
|
status: issue.status,
|
||||||
|
context: issue.context,
|
||||||
|
priority: issue.priority,
|
||||||
|
created_at: issue.created_at
|
||||||
|
}
|
||||||
|
},
|
||||||
|
execution: {
|
||||||
|
session_id: null,
|
||||||
|
started_at: null,
|
||||||
|
completed_at: null,
|
||||||
|
skill_results: [],
|
||||||
|
git_commit: null,
|
||||||
|
error: null
|
||||||
|
},
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
updated_at: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
// 5. Write task file
|
||||||
|
Bash('mkdir -p .workflow/.idaw/tasks');
|
||||||
|
Write(`.workflow/.idaw/tasks/${nextId}.json`, JSON.stringify(task, null, 2));
|
||||||
|
console.log(`Created ${nextId} from issue ${issueId}: ${issue.title}`);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 3B: Manual Mode
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 1. Validate description
|
||||||
|
if (!description && !autoYes) {
|
||||||
|
const answer = AskUserQuestion({
|
||||||
|
questions: [{
|
||||||
|
question: 'Please provide a task description:',
|
||||||
|
header: 'Task',
|
||||||
|
multiSelect: false,
|
||||||
|
options: [
|
||||||
|
{ label: 'Provide description', description: 'What needs to be done?' }
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
// Use custom text from "Other"
|
||||||
|
description = answer.customText || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!description) {
|
||||||
|
console.log('Error: No description provided. Usage: /idaw:add "task description"');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Generate next IDAW ID
|
||||||
|
const nextId = generateNextId();
|
||||||
|
|
||||||
|
// 3. Build title from first sentence
|
||||||
|
const title = description.split(/[.\n]/)[0].substring(0, 80).trim();
|
||||||
|
|
||||||
|
// 4. Determine task_type
|
||||||
|
const taskType = typeFlag || null; // null → inferred at run time
|
||||||
|
|
||||||
|
// 5. Create task
|
||||||
|
const task = {
|
||||||
|
id: nextId,
|
||||||
|
title: title,
|
||||||
|
description: description,
|
||||||
|
status: 'pending',
|
||||||
|
priority: parseInt(priorityFlag) || 3,
|
||||||
|
task_type: taskType,
|
||||||
|
skill_chain: null,
|
||||||
|
context: {
|
||||||
|
affected_files: [],
|
||||||
|
acceptance_criteria: [],
|
||||||
|
constraints: [],
|
||||||
|
references: []
|
||||||
|
},
|
||||||
|
source: {
|
||||||
|
type: 'manual',
|
||||||
|
issue_id: null,
|
||||||
|
issue_snapshot: null
|
||||||
|
},
|
||||||
|
execution: {
|
||||||
|
session_id: null,
|
||||||
|
started_at: null,
|
||||||
|
completed_at: null,
|
||||||
|
skill_results: [],
|
||||||
|
git_commit: null,
|
||||||
|
error: null
|
||||||
|
},
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
updated_at: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
Bash('mkdir -p .workflow/.idaw/tasks');
|
||||||
|
Write(`.workflow/.idaw/tasks/${nextId}.json`, JSON.stringify(task, null, 2));
|
||||||
|
console.log(`Created ${nextId}: ${title}`);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Helper Functions
|
||||||
|
|
||||||
|
### ID Generation
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function generateNextId() {
|
||||||
|
const files = Glob('.workflow/.idaw/tasks/IDAW-*.json') || [];
|
||||||
|
if (files.length === 0) return 'IDAW-001';
|
||||||
|
|
||||||
|
const maxNum = files
|
||||||
|
.map(f => parseInt(f.match(/IDAW-(\d+)/)?.[1] || '0'))
|
||||||
|
.reduce((max, n) => Math.max(max, n), 0);
|
||||||
|
|
||||||
|
return `IDAW-${String(maxNum + 1).padStart(3, '0')}`;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task Type Inference (deferred — used at run time if task_type is null)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function inferTaskType(title, description) {
|
||||||
|
const text = `${title} ${description}`.toLowerCase();
|
||||||
|
if (/urgent|production|critical/.test(text) && /fix|bug/.test(text)) return 'bugfix-hotfix';
|
||||||
|
if (/refactor|重构|tech.*debt/.test(text)) return 'refactor';
|
||||||
|
if (/tdd|test-driven|test first/.test(text)) return 'tdd';
|
||||||
|
if (/test fail|fix test|failing test/.test(text)) return 'test-fix';
|
||||||
|
if (/generate test|写测试|add test/.test(text)) return 'test';
|
||||||
|
if (/review|code review/.test(text)) return 'review';
|
||||||
|
if (/docs|documentation|readme/.test(text)) return 'docs';
|
||||||
|
if (/fix|bug|error|crash|fail/.test(text)) return 'bugfix';
|
||||||
|
if (/complex|multi-module|architecture/.test(text)) return 'feature-complex';
|
||||||
|
return 'feature';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Manual creation
|
||||||
|
/idaw:add "Fix login timeout bug" --type bugfix --priority 2
|
||||||
|
/idaw:add "Add rate limiting to API endpoints" --priority 1
|
||||||
|
/idaw:add "Refactor auth module to use strategy pattern"
|
||||||
|
|
||||||
|
# Import from ccw issue
|
||||||
|
/idaw:add --from-issue ISS-20260128-001
|
||||||
|
/idaw:add --from-issue ISS-20260128-001,ISS-20260128-002 --priority 1
|
||||||
|
|
||||||
|
# Auto mode (skip clarification)
|
||||||
|
/idaw:add -y "Quick fix for typo in header"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Output
|
||||||
|
|
||||||
|
```
|
||||||
|
Created IDAW-001: Fix login timeout bug
|
||||||
|
Type: bugfix | Priority: 2 | Source: manual
|
||||||
|
→ Next: /idaw:run or /idaw:status
|
||||||
|
```
|
||||||
398
.claude/commands/idaw/resume.md
Normal file
398
.claude/commands/idaw/resume.md
Normal file
@@ -0,0 +1,398 @@
|
|||||||
|
---
|
||||||
|
name: resume
|
||||||
|
description: Resume interrupted IDAW session from last checkpoint
|
||||||
|
argument-hint: "[-y|--yes] [session-id]"
|
||||||
|
allowed-tools: Skill(*), TodoWrite(*), AskUserQuestion(*), Read(*), Write(*), Bash(*), Glob(*)
|
||||||
|
---
|
||||||
|
|
||||||
|
# IDAW Resume Command (/idaw:resume)
|
||||||
|
|
||||||
|
## Auto Mode
|
||||||
|
|
||||||
|
When `--yes` or `-y`: Auto-skip interrupted task, continue with remaining.
|
||||||
|
|
||||||
|
## Skill Chain Mapping
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const SKILL_CHAIN_MAP = {
|
||||||
|
'bugfix': ['workflow-lite-plan', 'workflow-test-fix'],
|
||||||
|
'bugfix-hotfix': ['workflow-lite-plan'],
|
||||||
|
'feature': ['workflow-lite-plan', 'workflow-test-fix'],
|
||||||
|
'feature-complex': ['workflow-plan', 'workflow-execute', 'workflow-test-fix'],
|
||||||
|
'refactor': ['workflow:refactor-cycle'],
|
||||||
|
'tdd': ['workflow-tdd-plan', 'workflow-execute'],
|
||||||
|
'test': ['workflow-test-fix'],
|
||||||
|
'test-fix': ['workflow-test-fix'],
|
||||||
|
'review': ['review-cycle'],
|
||||||
|
'docs': ['workflow-lite-plan']
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Task Type Inference
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function inferTaskType(title, description) {
|
||||||
|
const text = `${title} ${description}`.toLowerCase();
|
||||||
|
if (/urgent|production|critical/.test(text) && /fix|bug/.test(text)) return 'bugfix-hotfix';
|
||||||
|
if (/refactor|重构|tech.*debt/.test(text)) return 'refactor';
|
||||||
|
if (/tdd|test-driven|test first/.test(text)) return 'tdd';
|
||||||
|
if (/test fail|fix test|failing test/.test(text)) return 'test-fix';
|
||||||
|
if (/generate test|写测试|add test/.test(text)) return 'test';
|
||||||
|
if (/review|code review/.test(text)) return 'review';
|
||||||
|
if (/docs|documentation|readme/.test(text)) return 'docs';
|
||||||
|
if (/fix|bug|error|crash|fail/.test(text)) return 'bugfix';
|
||||||
|
if (/complex|multi-module|architecture/.test(text)) return 'feature-complex';
|
||||||
|
return 'feature';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
### Phase 1: Find Resumable Session
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const args = $ARGUMENTS;
|
||||||
|
const autoYes = /(-y|--yes)/.test(args);
|
||||||
|
const targetSessionId = args.replace(/(-y|--yes)/g, '').trim();
|
||||||
|
|
||||||
|
let session = null;
|
||||||
|
let sessionDir = null;
|
||||||
|
|
||||||
|
if (targetSessionId) {
|
||||||
|
// Load specific session
|
||||||
|
sessionDir = `.workflow/.idaw/sessions/${targetSessionId}`;
|
||||||
|
try {
|
||||||
|
session = JSON.parse(Read(`${sessionDir}/session.json`));
|
||||||
|
} catch {
|
||||||
|
console.log(`Session "${targetSessionId}" not found.`);
|
||||||
|
console.log('Use /idaw:status to list sessions, or /idaw:run to start a new one.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Find most recent running session
|
||||||
|
const sessionFiles = Glob('.workflow/.idaw/sessions/IDA-*/session.json') || [];
|
||||||
|
|
||||||
|
for (const f of sessionFiles) {
|
||||||
|
try {
|
||||||
|
const s = JSON.parse(Read(f));
|
||||||
|
if (s.status === 'running') {
|
||||||
|
session = s;
|
||||||
|
sessionDir = f.replace(/\/session\.json$/, '').replace(/\\session\.json$/, '');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Skip malformed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
console.log('No running sessions found to resume.');
|
||||||
|
console.log('Use /idaw:run to start a new execution.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Resuming session: ${session.session_id}`);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 2: Handle Interrupted Task
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Find the task that was in_progress when interrupted
|
||||||
|
let currentTaskId = session.current_task;
|
||||||
|
let currentTask = null;
|
||||||
|
|
||||||
|
if (currentTaskId) {
|
||||||
|
try {
|
||||||
|
currentTask = JSON.parse(Read(`.workflow/.idaw/tasks/${currentTaskId}.json`));
|
||||||
|
} catch {
|
||||||
|
console.log(`Warning: Could not read task ${currentTaskId}`);
|
||||||
|
currentTaskId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentTask && currentTask.status === 'in_progress') {
|
||||||
|
if (autoYes) {
|
||||||
|
// Auto: skip interrupted task
|
||||||
|
currentTask.status = 'skipped';
|
||||||
|
currentTask.execution.error = 'Skipped on resume (auto mode)';
|
||||||
|
currentTask.execution.completed_at = new Date().toISOString();
|
||||||
|
currentTask.updated_at = new Date().toISOString();
|
||||||
|
Write(`.workflow/.idaw/tasks/${currentTaskId}.json`, JSON.stringify(currentTask, null, 2));
|
||||||
|
session.skipped.push(currentTaskId);
|
||||||
|
console.log(`Skipped interrupted task: ${currentTaskId}`);
|
||||||
|
} else {
|
||||||
|
const answer = AskUserQuestion({
|
||||||
|
questions: [{
|
||||||
|
question: `Task ${currentTaskId} was interrupted: "${currentTask.title}". How to proceed?`,
|
||||||
|
header: 'Resume',
|
||||||
|
multiSelect: false,
|
||||||
|
options: [
|
||||||
|
{ label: 'Retry', description: 'Reset to pending, re-execute from beginning' },
|
||||||
|
{ label: 'Skip', description: 'Mark as skipped, move to next task' }
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (answer.answers?.Resume === 'Skip') {
|
||||||
|
currentTask.status = 'skipped';
|
||||||
|
currentTask.execution.error = 'Skipped on resume (user choice)';
|
||||||
|
currentTask.execution.completed_at = new Date().toISOString();
|
||||||
|
currentTask.updated_at = new Date().toISOString();
|
||||||
|
Write(`.workflow/.idaw/tasks/${currentTaskId}.json`, JSON.stringify(currentTask, null, 2));
|
||||||
|
session.skipped.push(currentTaskId);
|
||||||
|
} else {
|
||||||
|
// Retry: reset to pending
|
||||||
|
currentTask.status = 'pending';
|
||||||
|
currentTask.execution.started_at = null;
|
||||||
|
currentTask.execution.completed_at = null;
|
||||||
|
currentTask.execution.skill_results = [];
|
||||||
|
currentTask.execution.error = null;
|
||||||
|
currentTask.updated_at = new Date().toISOString();
|
||||||
|
Write(`.workflow/.idaw/tasks/${currentTaskId}.json`, JSON.stringify(currentTask, null, 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 3: Build Remaining Task Queue
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Collect remaining tasks (pending, or the retried current task)
|
||||||
|
const allTaskIds = session.tasks;
|
||||||
|
const completedSet = new Set([...session.completed, ...session.failed, ...session.skipped]);
|
||||||
|
|
||||||
|
const remainingTasks = [];
|
||||||
|
for (const taskId of allTaskIds) {
|
||||||
|
if (completedSet.has(taskId)) continue;
|
||||||
|
try {
|
||||||
|
const task = JSON.parse(Read(`.workflow/.idaw/tasks/${taskId}.json`));
|
||||||
|
if (task.status === 'pending') {
|
||||||
|
remainingTasks.push(task);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
console.log(`Warning: Could not read task ${taskId}, skipping`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remainingTasks.length === 0) {
|
||||||
|
console.log('No remaining tasks to execute. Session complete.');
|
||||||
|
session.status = 'completed';
|
||||||
|
session.current_task = null;
|
||||||
|
session.updated_at = new Date().toISOString();
|
||||||
|
Write(`${sessionDir}/session.json`, JSON.stringify(session, null, 2));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort: priority ASC, then ID ASC
|
||||||
|
remainingTasks.sort((a, b) => {
|
||||||
|
if (a.priority !== b.priority) return a.priority - b.priority;
|
||||||
|
return a.id.localeCompare(b.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Remaining tasks: ${remainingTasks.length}`);
|
||||||
|
|
||||||
|
// Append resume marker to progress.md
|
||||||
|
const progressFile = `${sessionDir}/progress.md`;
|
||||||
|
try {
|
||||||
|
const currentProgress = Read(progressFile);
|
||||||
|
Write(progressFile, currentProgress + `\n---\n**Resumed**: ${new Date().toISOString()}\n\n`);
|
||||||
|
} catch {
|
||||||
|
Write(progressFile, `# IDAW Progress — ${session.session_id}\n\n---\n**Resumed**: ${new Date().toISOString()}\n\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update TodoWrite
|
||||||
|
TodoWrite({
|
||||||
|
todos: remainingTasks.map((t, i) => ({
|
||||||
|
content: `IDAW:[${i + 1}/${remainingTasks.length}] ${t.title}`,
|
||||||
|
status: i === 0 ? 'in_progress' : 'pending',
|
||||||
|
activeForm: `Executing ${t.title}`
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 4-6: Execute Remaining (reuse run.md main loop)
|
||||||
|
|
||||||
|
Execute remaining tasks using the same Phase 4-6 logic from `/idaw:run`:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Phase 4: Main Loop — identical to run.md Phase 4
|
||||||
|
for (let taskIdx = 0; taskIdx < remainingTasks.length; taskIdx++) {
|
||||||
|
const task = remainingTasks[taskIdx];
|
||||||
|
|
||||||
|
// Resolve skill chain
|
||||||
|
const resolvedType = task.task_type || inferTaskType(task.title, task.description);
|
||||||
|
const chain = task.skill_chain || SKILL_CHAIN_MAP[resolvedType] || SKILL_CHAIN_MAP['feature'];
|
||||||
|
|
||||||
|
// Update task → in_progress
|
||||||
|
task.status = 'in_progress';
|
||||||
|
task.task_type = resolvedType;
|
||||||
|
task.execution.started_at = new Date().toISOString();
|
||||||
|
Write(`.workflow/.idaw/tasks/${task.id}.json`, JSON.stringify(task, null, 2));
|
||||||
|
|
||||||
|
session.current_task = task.id;
|
||||||
|
session.updated_at = new Date().toISOString();
|
||||||
|
Write(`${sessionDir}/session.json`, JSON.stringify(session, null, 2));
|
||||||
|
|
||||||
|
console.log(`\n--- [${taskIdx + 1}/${remainingTasks.length}] ${task.id}: ${task.title} ---`);
|
||||||
|
console.log(`Chain: ${chain.join(' → ')}`);
|
||||||
|
|
||||||
|
// Execute skill chain
|
||||||
|
let previousResult = null;
|
||||||
|
let taskFailed = false;
|
||||||
|
|
||||||
|
for (let skillIdx = 0; skillIdx < chain.length; skillIdx++) {
|
||||||
|
const skillName = chain[skillIdx];
|
||||||
|
const skillArgs = assembleSkillArgs(skillName, task, previousResult, autoYes, skillIdx === 0);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = Skill({ skill: skillName, args: skillArgs });
|
||||||
|
previousResult = result;
|
||||||
|
task.execution.skill_results.push({
|
||||||
|
skill: skillName,
|
||||||
|
status: 'completed',
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// Retry once
|
||||||
|
try {
|
||||||
|
const retryResult = Skill({ skill: skillName, args: skillArgs });
|
||||||
|
previousResult = retryResult;
|
||||||
|
task.execution.skill_results.push({
|
||||||
|
skill: skillName,
|
||||||
|
status: 'completed-retry',
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
} catch (retryError) {
|
||||||
|
task.execution.skill_results.push({
|
||||||
|
skill: skillName,
|
||||||
|
status: 'failed',
|
||||||
|
error: String(retryError).substring(0, 200),
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
if (autoYes) {
|
||||||
|
taskFailed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const answer = AskUserQuestion({
|
||||||
|
questions: [{
|
||||||
|
question: `${skillName} failed: ${String(retryError).substring(0, 100)}`,
|
||||||
|
header: 'Error',
|
||||||
|
multiSelect: false,
|
||||||
|
options: [
|
||||||
|
{ label: 'Skip task', description: 'Mark as failed, continue' },
|
||||||
|
{ label: 'Abort', description: 'Stop run' }
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (answer.answers?.Error === 'Abort') {
|
||||||
|
task.status = 'failed';
|
||||||
|
task.execution.error = String(retryError).substring(0, 200);
|
||||||
|
Write(`.workflow/.idaw/tasks/${task.id}.json`, JSON.stringify(task, null, 2));
|
||||||
|
session.failed.push(task.id);
|
||||||
|
session.status = 'failed';
|
||||||
|
session.updated_at = new Date().toISOString();
|
||||||
|
Write(`${sessionDir}/session.json`, JSON.stringify(session, null, 2));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
taskFailed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 5: Checkpoint
|
||||||
|
if (taskFailed) {
|
||||||
|
task.status = 'failed';
|
||||||
|
task.execution.error = 'Skill chain failed after retry';
|
||||||
|
task.execution.completed_at = new Date().toISOString();
|
||||||
|
session.failed.push(task.id);
|
||||||
|
} else {
|
||||||
|
// Git commit
|
||||||
|
const commitMsg = `feat(idaw): ${task.title} [${task.id}]`;
|
||||||
|
const diffCheck = Bash('git diff --stat HEAD 2>/dev/null || echo ""');
|
||||||
|
const untrackedCheck = Bash('git ls-files --others --exclude-standard 2>/dev/null || echo ""');
|
||||||
|
|
||||||
|
if (diffCheck?.trim() || untrackedCheck?.trim()) {
|
||||||
|
Bash('git add -A');
|
||||||
|
Bash(`git commit -m "$(cat <<'EOF'\n${commitMsg}\nEOF\n)"`);
|
||||||
|
const commitHash = Bash('git rev-parse --short HEAD 2>/dev/null')?.trim();
|
||||||
|
task.execution.git_commit = commitHash;
|
||||||
|
} else {
|
||||||
|
task.execution.git_commit = 'no-commit';
|
||||||
|
}
|
||||||
|
|
||||||
|
task.status = 'completed';
|
||||||
|
task.execution.completed_at = new Date().toISOString();
|
||||||
|
session.completed.push(task.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
task.updated_at = new Date().toISOString();
|
||||||
|
Write(`.workflow/.idaw/tasks/${task.id}.json`, JSON.stringify(task, null, 2));
|
||||||
|
session.updated_at = new Date().toISOString();
|
||||||
|
Write(`${sessionDir}/session.json`, JSON.stringify(session, null, 2));
|
||||||
|
|
||||||
|
// Append progress
|
||||||
|
const chain_str = chain.join(' → ');
|
||||||
|
const progressEntry = `## ${task.id} — ${task.title}\n- Status: ${task.status}\n- Chain: ${chain_str}\n- Commit: ${task.execution.git_commit || '-'}\n\n`;
|
||||||
|
const currentProgress = Read(`${sessionDir}/progress.md`);
|
||||||
|
Write(`${sessionDir}/progress.md`, currentProgress + progressEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 6: Report
|
||||||
|
session.status = session.failed.length > 0 && session.completed.length === 0 ? 'failed' : 'completed';
|
||||||
|
session.current_task = null;
|
||||||
|
session.updated_at = new Date().toISOString();
|
||||||
|
Write(`${sessionDir}/session.json`, JSON.stringify(session, null, 2));
|
||||||
|
|
||||||
|
const summary = `\n---\n## Summary (Resumed)\n- Completed: ${session.completed.length}\n- Failed: ${session.failed.length}\n- Skipped: ${session.skipped.length}\n`;
|
||||||
|
const finalProgress = Read(`${sessionDir}/progress.md`);
|
||||||
|
Write(`${sessionDir}/progress.md`, finalProgress + summary);
|
||||||
|
|
||||||
|
console.log('\n=== IDAW Resume Complete ===');
|
||||||
|
console.log(`Session: ${session.session_id}`);
|
||||||
|
console.log(`Completed: ${session.completed.length} | Failed: ${session.failed.length} | Skipped: ${session.skipped.length}`);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Helper Functions
|
||||||
|
|
||||||
|
### assembleSkillArgs
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function assembleSkillArgs(skillName, task, previousResult, autoYes, isFirst) {
|
||||||
|
let args = '';
|
||||||
|
|
||||||
|
if (isFirst) {
|
||||||
|
const goal = `${task.title}\n${task.description}`;
|
||||||
|
args = `"${goal.replace(/"/g, '\\"')}"`;
|
||||||
|
if (task.task_type === 'bugfix-hotfix') args += ' --hotfix';
|
||||||
|
} else if (previousResult?.session_id) {
|
||||||
|
args = `--session="${previousResult.session_id}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (autoYes && !args.includes('-y') && !args.includes('--yes')) {
|
||||||
|
args = args ? `${args} -y` : '-y';
|
||||||
|
}
|
||||||
|
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Resume most recent running session (interactive)
|
||||||
|
/idaw:resume
|
||||||
|
|
||||||
|
# Resume specific session
|
||||||
|
/idaw:resume IDA-auth-fix-20260301
|
||||||
|
|
||||||
|
# Resume with auto mode (skip interrupted, continue)
|
||||||
|
/idaw:resume -y
|
||||||
|
|
||||||
|
# Resume specific session with auto mode
|
||||||
|
/idaw:resume -y IDA-auth-fix-20260301
|
||||||
|
```
|
||||||
456
.claude/commands/idaw/run.md
Normal file
456
.claude/commands/idaw/run.md
Normal file
@@ -0,0 +1,456 @@
|
|||||||
|
---
|
||||||
|
name: run
|
||||||
|
description: IDAW orchestrator - execute task skill chains serially with git checkpoints
|
||||||
|
argument-hint: "[-y|--yes] [--task <id>[,<id>,...]] [--dry-run]"
|
||||||
|
allowed-tools: Skill(*), TodoWrite(*), AskUserQuestion(*), Read(*), Write(*), Bash(*), Glob(*)
|
||||||
|
---
|
||||||
|
|
||||||
|
# IDAW Run Command (/idaw:run)
|
||||||
|
|
||||||
|
## Auto Mode
|
||||||
|
|
||||||
|
When `--yes` or `-y`: Skip all confirmations, auto-skip on failure, proceed with dirty git.
|
||||||
|
|
||||||
|
## Skill Chain Mapping
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const SKILL_CHAIN_MAP = {
|
||||||
|
'bugfix': ['workflow-lite-plan', 'workflow-test-fix'],
|
||||||
|
'bugfix-hotfix': ['workflow-lite-plan'],
|
||||||
|
'feature': ['workflow-lite-plan', 'workflow-test-fix'],
|
||||||
|
'feature-complex': ['workflow-plan', 'workflow-execute', 'workflow-test-fix'],
|
||||||
|
'refactor': ['workflow:refactor-cycle'],
|
||||||
|
'tdd': ['workflow-tdd-plan', 'workflow-execute'],
|
||||||
|
'test': ['workflow-test-fix'],
|
||||||
|
'test-fix': ['workflow-test-fix'],
|
||||||
|
'review': ['review-cycle'],
|
||||||
|
'docs': ['workflow-lite-plan']
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Task Type Inference
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function inferTaskType(title, description) {
|
||||||
|
const text = `${title} ${description}`.toLowerCase();
|
||||||
|
if (/urgent|production|critical/.test(text) && /fix|bug/.test(text)) return 'bugfix-hotfix';
|
||||||
|
if (/refactor|重构|tech.*debt/.test(text)) return 'refactor';
|
||||||
|
if (/tdd|test-driven|test first/.test(text)) return 'tdd';
|
||||||
|
if (/test fail|fix test|failing test/.test(text)) return 'test-fix';
|
||||||
|
if (/generate test|写测试|add test/.test(text)) return 'test';
|
||||||
|
if (/review|code review/.test(text)) return 'review';
|
||||||
|
if (/docs|documentation|readme/.test(text)) return 'docs';
|
||||||
|
if (/fix|bug|error|crash|fail/.test(text)) return 'bugfix';
|
||||||
|
if (/complex|multi-module|architecture/.test(text)) return 'feature-complex';
|
||||||
|
return 'feature';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6-Phase Execution
|
||||||
|
|
||||||
|
### Phase 1: Load Tasks
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const args = $ARGUMENTS;
|
||||||
|
const autoYes = /(-y|--yes)/.test(args);
|
||||||
|
const dryRun = /--dry-run/.test(args);
|
||||||
|
const taskFilter = args.match(/--task\s+([\w,-]+)/)?.[1]?.split(',') || null;
|
||||||
|
|
||||||
|
// Load task files
|
||||||
|
const taskFiles = Glob('.workflow/.idaw/tasks/IDAW-*.json') || [];
|
||||||
|
|
||||||
|
if (taskFiles.length === 0) {
|
||||||
|
console.log('No IDAW tasks found. Use /idaw:add to create tasks.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse and filter
|
||||||
|
let tasks = taskFiles.map(f => JSON.parse(Read(f)));
|
||||||
|
|
||||||
|
if (taskFilter) {
|
||||||
|
tasks = tasks.filter(t => taskFilter.includes(t.id));
|
||||||
|
} else {
|
||||||
|
tasks = tasks.filter(t => t.status === 'pending');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tasks.length === 0) {
|
||||||
|
console.log('No pending tasks to execute. Use /idaw:add to add tasks or --task to specify IDs.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort: priority ASC (1=critical first), then ID ASC
|
||||||
|
tasks.sort((a, b) => {
|
||||||
|
if (a.priority !== b.priority) return a.priority - b.priority;
|
||||||
|
return a.id.localeCompare(b.id);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 2: Session Setup
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Generate session ID: IDA-{slug}-YYYYMMDD
|
||||||
|
const slug = tasks[0].title
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]+/g, '-')
|
||||||
|
.substring(0, 20)
|
||||||
|
.replace(/-$/, '');
|
||||||
|
const dateStr = new Date().toISOString().slice(0, 10).replace(/-/g, '');
|
||||||
|
let sessionId = `IDA-${slug}-${dateStr}`;
|
||||||
|
|
||||||
|
// Check collision
|
||||||
|
const existingSession = Glob(`.workflow/.idaw/sessions/${sessionId}/session.json`);
|
||||||
|
if (existingSession?.length > 0) {
|
||||||
|
sessionId = `${sessionId}-2`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionDir = `.workflow/.idaw/sessions/${sessionId}`;
|
||||||
|
Bash(`mkdir -p "${sessionDir}"`);
|
||||||
|
|
||||||
|
const session = {
|
||||||
|
session_id: sessionId,
|
||||||
|
status: 'running',
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
tasks: tasks.map(t => t.id),
|
||||||
|
current_task: null,
|
||||||
|
completed: [],
|
||||||
|
failed: [],
|
||||||
|
skipped: []
|
||||||
|
};
|
||||||
|
|
||||||
|
Write(`${sessionDir}/session.json`, JSON.stringify(session, null, 2));
|
||||||
|
|
||||||
|
// Initialize progress.md
|
||||||
|
const progressHeader = `# IDAW Progress — ${sessionId}\nStarted: ${session.created_at}\n\n`;
|
||||||
|
Write(`${sessionDir}/progress.md`, progressHeader);
|
||||||
|
|
||||||
|
// TodoWrite
|
||||||
|
TodoWrite({
|
||||||
|
todos: tasks.map((t, i) => ({
|
||||||
|
content: `IDAW:[${i + 1}/${tasks.length}] ${t.title}`,
|
||||||
|
status: i === 0 ? 'in_progress' : 'pending',
|
||||||
|
activeForm: `Executing ${t.title}`
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 3: Startup Protocol
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Check for existing running sessions
|
||||||
|
const runningSessions = Glob('.workflow/.idaw/sessions/IDA-*/session.json')
|
||||||
|
?.map(f => JSON.parse(Read(f)))
|
||||||
|
.filter(s => s.status === 'running' && s.session_id !== sessionId) || [];
|
||||||
|
|
||||||
|
if (runningSessions.length > 0) {
|
||||||
|
if (!autoYes) {
|
||||||
|
const answer = AskUserQuestion({
|
||||||
|
questions: [{
|
||||||
|
question: `Found running session: ${runningSessions[0].session_id}. How to proceed?`,
|
||||||
|
header: 'Conflict',
|
||||||
|
multiSelect: false,
|
||||||
|
options: [
|
||||||
|
{ label: 'Resume existing', description: 'Use /idaw:resume instead' },
|
||||||
|
{ label: 'Start fresh', description: 'Continue with new session' },
|
||||||
|
{ label: 'Abort', description: 'Cancel this run' }
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
if (answer.answers?.Conflict === 'Resume existing') {
|
||||||
|
console.log(`Use: /idaw:resume ${runningSessions[0].session_id}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (answer.answers?.Conflict === 'Abort') return;
|
||||||
|
}
|
||||||
|
// autoYes or "Start fresh": proceed
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check git status
|
||||||
|
const gitStatus = Bash('git status --porcelain 2>/dev/null');
|
||||||
|
if (gitStatus?.trim()) {
|
||||||
|
if (!autoYes) {
|
||||||
|
const answer = AskUserQuestion({
|
||||||
|
questions: [{
|
||||||
|
question: 'Working tree has uncommitted changes. How to proceed?',
|
||||||
|
header: 'Git',
|
||||||
|
multiSelect: false,
|
||||||
|
options: [
|
||||||
|
{ label: 'Continue', description: 'Proceed with dirty tree' },
|
||||||
|
{ label: 'Stash', description: 'git stash before running' },
|
||||||
|
{ label: 'Abort', description: 'Stop and handle manually' }
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
if (answer.answers?.Git === 'Stash') {
|
||||||
|
Bash('git stash push -m "idaw-pre-run"');
|
||||||
|
}
|
||||||
|
if (answer.answers?.Git === 'Abort') return;
|
||||||
|
}
|
||||||
|
// autoYes: proceed silently
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dry run: show plan and exit
|
||||||
|
if (dryRun) {
|
||||||
|
console.log(`# Dry Run — ${sessionId}\n`);
|
||||||
|
for (const task of tasks) {
|
||||||
|
const taskType = task.task_type || inferTaskType(task.title, task.description);
|
||||||
|
const chain = task.skill_chain || SKILL_CHAIN_MAP[taskType] || SKILL_CHAIN_MAP['feature'];
|
||||||
|
console.log(`## ${task.id}: ${task.title}`);
|
||||||
|
console.log(` Type: ${taskType} | Priority: ${task.priority}`);
|
||||||
|
console.log(` Chain: ${chain.join(' → ')}\n`);
|
||||||
|
}
|
||||||
|
console.log(`Total: ${tasks.length} tasks`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 4: Main Loop (serial, one task at a time)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
for (let taskIdx = 0; taskIdx < tasks.length; taskIdx++) {
|
||||||
|
const task = tasks[taskIdx];
|
||||||
|
|
||||||
|
// Skip completed/failed/skipped
|
||||||
|
if (['completed', 'failed', 'skipped'].includes(task.status)) continue;
|
||||||
|
|
||||||
|
// Resolve skill chain
|
||||||
|
const resolvedType = task.task_type || inferTaskType(task.title, task.description);
|
||||||
|
const chain = task.skill_chain || SKILL_CHAIN_MAP[resolvedType] || SKILL_CHAIN_MAP['feature'];
|
||||||
|
|
||||||
|
// Update task status → in_progress
|
||||||
|
task.status = 'in_progress';
|
||||||
|
task.task_type = resolvedType; // persist inferred type
|
||||||
|
task.execution.started_at = new Date().toISOString();
|
||||||
|
Write(`.workflow/.idaw/tasks/${task.id}.json`, JSON.stringify(task, null, 2));
|
||||||
|
|
||||||
|
// Update session
|
||||||
|
session.current_task = task.id;
|
||||||
|
session.updated_at = new Date().toISOString();
|
||||||
|
Write(`${sessionDir}/session.json`, JSON.stringify(session, null, 2));
|
||||||
|
|
||||||
|
console.log(`\n--- [${taskIdx + 1}/${tasks.length}] ${task.id}: ${task.title} ---`);
|
||||||
|
console.log(`Chain: ${chain.join(' → ')}`);
|
||||||
|
|
||||||
|
// Execute each skill in chain
|
||||||
|
let previousResult = null;
|
||||||
|
let taskFailed = false;
|
||||||
|
|
||||||
|
for (let skillIdx = 0; skillIdx < chain.length; skillIdx++) {
|
||||||
|
const skillName = chain[skillIdx];
|
||||||
|
const skillArgs = assembleSkillArgs(skillName, task, previousResult, autoYes, skillIdx === 0);
|
||||||
|
|
||||||
|
console.log(` [${skillIdx + 1}/${chain.length}] ${skillName}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = Skill({ skill: skillName, args: skillArgs });
|
||||||
|
previousResult = result;
|
||||||
|
task.execution.skill_results.push({
|
||||||
|
skill: skillName,
|
||||||
|
status: 'completed',
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// Retry once
|
||||||
|
console.log(` Retry: ${skillName} (first attempt failed)`);
|
||||||
|
try {
|
||||||
|
const retryResult = Skill({ skill: skillName, args: skillArgs });
|
||||||
|
previousResult = retryResult;
|
||||||
|
task.execution.skill_results.push({
|
||||||
|
skill: skillName,
|
||||||
|
status: 'completed-retry',
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
} catch (retryError) {
|
||||||
|
// Failed after retry
|
||||||
|
task.execution.skill_results.push({
|
||||||
|
skill: skillName,
|
||||||
|
status: 'failed',
|
||||||
|
error: String(retryError).substring(0, 200),
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
if (autoYes) {
|
||||||
|
taskFailed = true;
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
const answer = AskUserQuestion({
|
||||||
|
questions: [{
|
||||||
|
question: `${skillName} failed: ${String(retryError).substring(0, 100)}. How to proceed?`,
|
||||||
|
header: 'Error',
|
||||||
|
multiSelect: false,
|
||||||
|
options: [
|
||||||
|
{ label: 'Skip task', description: 'Mark task as failed, continue to next' },
|
||||||
|
{ label: 'Abort', description: 'Stop entire run' }
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
if (answer.answers?.Error === 'Abort') {
|
||||||
|
task.status = 'failed';
|
||||||
|
task.execution.error = String(retryError).substring(0, 200);
|
||||||
|
Write(`.workflow/.idaw/tasks/${task.id}.json`, JSON.stringify(task, null, 2));
|
||||||
|
session.failed.push(task.id);
|
||||||
|
session.status = 'failed';
|
||||||
|
session.updated_at = new Date().toISOString();
|
||||||
|
Write(`${sessionDir}/session.json`, JSON.stringify(session, null, 2));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
taskFailed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 5: Checkpoint (per task) — inline
|
||||||
|
if (taskFailed) {
|
||||||
|
task.status = 'failed';
|
||||||
|
task.execution.error = 'Skill chain failed after retry';
|
||||||
|
task.execution.completed_at = new Date().toISOString();
|
||||||
|
session.failed.push(task.id);
|
||||||
|
} else {
|
||||||
|
// Git commit checkpoint
|
||||||
|
const commitMsg = `feat(idaw): ${task.title} [${task.id}]`;
|
||||||
|
const diffCheck = Bash('git diff --stat HEAD 2>/dev/null || echo ""');
|
||||||
|
const untrackedCheck = Bash('git ls-files --others --exclude-standard 2>/dev/null || echo ""');
|
||||||
|
|
||||||
|
if (diffCheck?.trim() || untrackedCheck?.trim()) {
|
||||||
|
Bash('git add -A');
|
||||||
|
const commitResult = Bash(`git commit -m "$(cat <<'EOF'\n${commitMsg}\nEOF\n)"`);
|
||||||
|
const commitHash = Bash('git rev-parse --short HEAD 2>/dev/null')?.trim();
|
||||||
|
task.execution.git_commit = commitHash;
|
||||||
|
} else {
|
||||||
|
task.execution.git_commit = 'no-commit';
|
||||||
|
}
|
||||||
|
|
||||||
|
task.status = 'completed';
|
||||||
|
task.execution.completed_at = new Date().toISOString();
|
||||||
|
session.completed.push(task.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write task + session state
|
||||||
|
task.updated_at = new Date().toISOString();
|
||||||
|
Write(`.workflow/.idaw/tasks/${task.id}.json`, JSON.stringify(task, null, 2));
|
||||||
|
|
||||||
|
session.updated_at = new Date().toISOString();
|
||||||
|
Write(`${sessionDir}/session.json`, JSON.stringify(session, null, 2));
|
||||||
|
|
||||||
|
// Append to progress.md
|
||||||
|
const duration = task.execution.started_at && task.execution.completed_at
|
||||||
|
? formatDuration(new Date(task.execution.completed_at) - new Date(task.execution.started_at))
|
||||||
|
: 'unknown';
|
||||||
|
|
||||||
|
const progressEntry = `## ${task.id} — ${task.title}\n` +
|
||||||
|
`- Status: ${task.status}\n` +
|
||||||
|
`- Type: ${task.task_type}\n` +
|
||||||
|
`- Chain: ${chain.join(' → ')}\n` +
|
||||||
|
`- Commit: ${task.execution.git_commit || '-'}\n` +
|
||||||
|
`- Duration: ${duration}\n\n`;
|
||||||
|
|
||||||
|
const currentProgress = Read(`${sessionDir}/progress.md`);
|
||||||
|
Write(`${sessionDir}/progress.md`, currentProgress + progressEntry);
|
||||||
|
|
||||||
|
// Update TodoWrite
|
||||||
|
if (taskIdx + 1 < tasks.length) {
|
||||||
|
TodoWrite({
|
||||||
|
todos: tasks.map((t, i) => ({
|
||||||
|
content: `IDAW:[${i + 1}/${tasks.length}] ${t.title}`,
|
||||||
|
status: i < taskIdx + 1 ? 'completed' : (i === taskIdx + 1 ? 'in_progress' : 'pending'),
|
||||||
|
activeForm: `Executing ${t.title}`
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 6: Report
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
session.status = session.failed.length > 0 && session.completed.length === 0 ? 'failed' : 'completed';
|
||||||
|
session.current_task = null;
|
||||||
|
session.updated_at = new Date().toISOString();
|
||||||
|
Write(`${sessionDir}/session.json`, JSON.stringify(session, null, 2));
|
||||||
|
|
||||||
|
// Final progress summary
|
||||||
|
const summary = `\n---\n## Summary\n` +
|
||||||
|
`- Completed: ${session.completed.length}\n` +
|
||||||
|
`- Failed: ${session.failed.length}\n` +
|
||||||
|
`- Skipped: ${session.skipped.length}\n` +
|
||||||
|
`- Total: ${tasks.length}\n`;
|
||||||
|
|
||||||
|
const finalProgress = Read(`${sessionDir}/progress.md`);
|
||||||
|
Write(`${sessionDir}/progress.md`, finalProgress + summary);
|
||||||
|
|
||||||
|
// Display report
|
||||||
|
console.log('\n=== IDAW Run Complete ===');
|
||||||
|
console.log(`Session: ${sessionId}`);
|
||||||
|
console.log(`Completed: ${session.completed.length}/${tasks.length}`);
|
||||||
|
if (session.failed.length > 0) console.log(`Failed: ${session.failed.join(', ')}`);
|
||||||
|
if (session.skipped.length > 0) console.log(`Skipped: ${session.skipped.join(', ')}`);
|
||||||
|
|
||||||
|
// List git commits
|
||||||
|
for (const taskId of session.completed) {
|
||||||
|
const t = JSON.parse(Read(`.workflow/.idaw/tasks/${taskId}.json`));
|
||||||
|
if (t.execution.git_commit && t.execution.git_commit !== 'no-commit') {
|
||||||
|
console.log(` ${t.execution.git_commit} ${t.title}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Helper Functions
|
||||||
|
|
||||||
|
### assembleSkillArgs
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function assembleSkillArgs(skillName, task, previousResult, autoYes, isFirst) {
|
||||||
|
let args = '';
|
||||||
|
|
||||||
|
if (isFirst) {
|
||||||
|
// First skill: pass task goal
|
||||||
|
const goal = `${task.title}\n${task.description}`;
|
||||||
|
args = `"${goal.replace(/"/g, '\\"')}"`;
|
||||||
|
|
||||||
|
// bugfix-hotfix: add --hotfix
|
||||||
|
if (task.task_type === 'bugfix-hotfix') {
|
||||||
|
args += ' --hotfix';
|
||||||
|
}
|
||||||
|
} else if (previousResult?.session_id) {
|
||||||
|
// Subsequent skills: chain session
|
||||||
|
args = `--session="${previousResult.session_id}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Propagate -y
|
||||||
|
if (autoYes && !args.includes('-y') && !args.includes('--yes')) {
|
||||||
|
args = args ? `${args} -y` : '-y';
|
||||||
|
}
|
||||||
|
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### formatDuration
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function formatDuration(ms) {
|
||||||
|
const seconds = Math.floor(ms / 1000);
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
const remainingSeconds = seconds % 60;
|
||||||
|
if (minutes > 0) return `${minutes}m ${remainingSeconds}s`;
|
||||||
|
return `${seconds}s`;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Execute all pending tasks
|
||||||
|
/idaw:run -y
|
||||||
|
|
||||||
|
# Execute specific tasks
|
||||||
|
/idaw:run --task IDAW-001,IDAW-003
|
||||||
|
|
||||||
|
# Dry run (show plan without executing)
|
||||||
|
/idaw:run --dry-run
|
||||||
|
|
||||||
|
# Interactive mode (confirm at each step)
|
||||||
|
/idaw:run
|
||||||
|
```
|
||||||
182
.claude/commands/idaw/status.md
Normal file
182
.claude/commands/idaw/status.md
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
---
|
||||||
|
name: status
|
||||||
|
description: View IDAW task and session progress
|
||||||
|
argument-hint: "[session-id]"
|
||||||
|
allowed-tools: Read(*), Glob(*), Bash(*)
|
||||||
|
---
|
||||||
|
|
||||||
|
# IDAW Status Command (/idaw:status)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Read-only command to view IDAW task queue and execution session progress.
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
### Phase 1: Determine View Mode
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const sessionId = $ARGUMENTS?.trim();
|
||||||
|
|
||||||
|
if (sessionId) {
|
||||||
|
// Specific session view
|
||||||
|
showSession(sessionId);
|
||||||
|
} else {
|
||||||
|
// Overview: pending tasks + latest session
|
||||||
|
showOverview();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 2: Show Overview
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function showOverview() {
|
||||||
|
// 1. Load all tasks
|
||||||
|
const taskFiles = Glob('.workflow/.idaw/tasks/IDAW-*.json') || [];
|
||||||
|
|
||||||
|
if (taskFiles.length === 0) {
|
||||||
|
console.log('No IDAW tasks found. Use /idaw:add to create tasks.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tasks = taskFiles.map(f => JSON.parse(Read(f)));
|
||||||
|
|
||||||
|
// 2. Group by status
|
||||||
|
const byStatus = {
|
||||||
|
pending: tasks.filter(t => t.status === 'pending'),
|
||||||
|
in_progress: tasks.filter(t => t.status === 'in_progress'),
|
||||||
|
completed: tasks.filter(t => t.status === 'completed'),
|
||||||
|
failed: tasks.filter(t => t.status === 'failed'),
|
||||||
|
skipped: tasks.filter(t => t.status === 'skipped')
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3. Display task summary table
|
||||||
|
console.log('# IDAW Tasks\n');
|
||||||
|
console.log('| ID | Title | Type | Priority | Status |');
|
||||||
|
console.log('|----|-------|------|----------|--------|');
|
||||||
|
|
||||||
|
// Sort: priority ASC, then ID ASC
|
||||||
|
const sorted = [...tasks].sort((a, b) => {
|
||||||
|
if (a.priority !== b.priority) return a.priority - b.priority;
|
||||||
|
return a.id.localeCompare(b.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const t of sorted) {
|
||||||
|
const type = t.task_type || '(infer)';
|
||||||
|
console.log(`| ${t.id} | ${t.title.substring(0, 40)} | ${type} | ${t.priority} | ${t.status} |`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\nTotal: ${tasks.length} | Pending: ${byStatus.pending.length} | Completed: ${byStatus.completed.length} | Failed: ${byStatus.failed.length}`);
|
||||||
|
|
||||||
|
// 4. Show latest session (if any)
|
||||||
|
const sessionDirs = Glob('.workflow/.idaw/sessions/IDA-*/session.json') || [];
|
||||||
|
if (sessionDirs.length > 0) {
|
||||||
|
// Sort by modification time (newest first) — Glob returns sorted by mtime
|
||||||
|
const latestSessionFile = sessionDirs[0];
|
||||||
|
const session = JSON.parse(Read(latestSessionFile));
|
||||||
|
console.log(`\n## Latest Session: ${session.session_id}`);
|
||||||
|
console.log(`Status: ${session.status} | Tasks: ${session.tasks?.length || 0}`);
|
||||||
|
console.log(`Completed: ${session.completed?.length || 0} | Failed: ${session.failed?.length || 0} | Skipped: ${session.skipped?.length || 0}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 3: Show Specific Session
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function showSession(sessionId) {
|
||||||
|
const sessionFile = `.workflow/.idaw/sessions/${sessionId}/session.json`;
|
||||||
|
const progressFile = `.workflow/.idaw/sessions/${sessionId}/progress.md`;
|
||||||
|
|
||||||
|
// Try reading session
|
||||||
|
try {
|
||||||
|
const session = JSON.parse(Read(sessionFile));
|
||||||
|
|
||||||
|
console.log(`# IDAW Session: ${session.session_id}\n`);
|
||||||
|
console.log(`Status: ${session.status}`);
|
||||||
|
console.log(`Created: ${session.created_at}`);
|
||||||
|
console.log(`Updated: ${session.updated_at}`);
|
||||||
|
console.log(`Current Task: ${session.current_task || 'none'}\n`);
|
||||||
|
|
||||||
|
// Task detail table
|
||||||
|
console.log('| ID | Title | Status | Commit |');
|
||||||
|
console.log('|----|-------|--------|--------|');
|
||||||
|
|
||||||
|
for (const taskId of session.tasks) {
|
||||||
|
const taskFile = `.workflow/.idaw/tasks/${taskId}.json`;
|
||||||
|
try {
|
||||||
|
const task = JSON.parse(Read(taskFile));
|
||||||
|
const commit = task.execution?.git_commit?.substring(0, 7) || '-';
|
||||||
|
console.log(`| ${task.id} | ${task.title.substring(0, 40)} | ${task.status} | ${commit} |`);
|
||||||
|
} catch {
|
||||||
|
console.log(`| ${taskId} | (file not found) | unknown | - |`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\nCompleted: ${session.completed?.length || 0} | Failed: ${session.failed?.length || 0} | Skipped: ${session.skipped?.length || 0}`);
|
||||||
|
|
||||||
|
// Show progress.md if exists
|
||||||
|
try {
|
||||||
|
const progress = Read(progressFile);
|
||||||
|
console.log('\n---\n');
|
||||||
|
console.log(progress);
|
||||||
|
} catch {
|
||||||
|
// No progress file yet
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
// Session not found — try listing all sessions
|
||||||
|
console.log(`Session "${sessionId}" not found.\n`);
|
||||||
|
listSessions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 4: List All Sessions
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function listSessions() {
|
||||||
|
const sessionFiles = Glob('.workflow/.idaw/sessions/IDA-*/session.json') || [];
|
||||||
|
|
||||||
|
if (sessionFiles.length === 0) {
|
||||||
|
console.log('No IDAW sessions found. Use /idaw:run to start execution.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('# IDAW Sessions\n');
|
||||||
|
console.log('| Session ID | Status | Tasks | Completed | Failed |');
|
||||||
|
console.log('|------------|--------|-------|-----------|--------|');
|
||||||
|
|
||||||
|
for (const f of sessionFiles) {
|
||||||
|
try {
|
||||||
|
const session = JSON.parse(Read(f));
|
||||||
|
console.log(`| ${session.session_id} | ${session.status} | ${session.tasks?.length || 0} | ${session.completed?.length || 0} | ${session.failed?.length || 0} |`);
|
||||||
|
} catch {
|
||||||
|
// Skip malformed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\nUse /idaw:status <session-id> for details.');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Show overview (pending tasks + latest session)
|
||||||
|
/idaw:status
|
||||||
|
|
||||||
|
# Show specific session details
|
||||||
|
/idaw:status IDA-auth-fix-20260301
|
||||||
|
|
||||||
|
# Output example:
|
||||||
|
# IDAW Tasks
|
||||||
|
#
|
||||||
|
# | ID | Title | Type | Priority | Status |
|
||||||
|
# |----------|------------------------------------|--------|----------|-----------|
|
||||||
|
# | IDAW-001 | Fix auth token refresh | bugfix | 1 | completed |
|
||||||
|
# | IDAW-002 | Add rate limiting | feature| 2 | pending |
|
||||||
|
# | IDAW-003 | Refactor payment module | refact | 3 | pending |
|
||||||
|
#
|
||||||
|
# Total: 3 | Pending: 2 | Completed: 1 | Failed: 0
|
||||||
|
```
|
||||||
@@ -2800,22 +2800,22 @@
|
|||||||
<path d="M16.9 4.5 A8 3 -30 0 1 7.1 19.5" stroke-width="1.2" opacity="0.7"/>
|
<path d="M16.9 4.5 A8 3 -30 0 1 7.1 19.5" stroke-width="1.2" opacity="0.7"/>
|
||||||
<!-- Agent icons with brand colors -->
|
<!-- Agent icons with brand colors -->
|
||||||
<g>
|
<g>
|
||||||
<circle cx="17" cy="9.7" r="2.2" fill="#D97757" opacity="0.15"/>
|
<circle cx="17" cy="9.7" r="2.2" fill="currentColor" opacity="0.12"/>
|
||||||
<circle cx="17" cy="9.7" r="1.6" fill="#D97757" opacity="0.45"/>
|
<circle cx="17" cy="9.7" r="1.6" fill="currentColor" opacity="0.5"/>
|
||||||
<svg x="15.2" y="7.9" width="3.6" height="3.6" viewBox="0 0 16 16" fill="#D97757" opacity="0.95">
|
<svg x="15.2" y="7.9" width="3.6" height="3.6" viewBox="0 0 16 16" fill="#D97757" opacity="0.95">
|
||||||
<path d="m3.127 10.604 3.135-1.76.053-.153-.053-.085H6.11l-.525-.032-1.791-.048-1.554-.065-1.505-.08-.38-.081L0 7.832l.036-.234.32-.214.455.04 1.009.069 1.513.105 1.097.064 1.626.17h.259l.036-.105-.089-.065-.068-.064-1.566-1.062-1.695-1.121-.887-.646-.48-.327-.243-.306-.104-.67.435-.48.585.04.15.04.593.456 1.267.981 1.654 1.218.242.202.097-.068.012-.049-.109-.181-.9-1.626-.96-1.655-.428-.686-.113-.411a2 2 0 0 1-.068-.484l.496-.674L4.446 0l.662.089.279.242.411.94.666 1.48 1.033 2.014.302.597.162.553.06.17h.105v-.097l.085-1.134.157-1.392.154-1.792.052-.504.25-.605.497-.327.387.186.319.456-.045.294-.19 1.23-.37 1.93-.243 1.29h.142l.161-.16.654-.868 1.097-1.372.484-.545.565-.601.363-.287h.686l.505.751-.226.775-.707.895-.585.759-.839 1.13-.524.904.048.072.125-.012 1.897-.403 1.024-.186 1.223-.21.553.258.06.263-.218.536-1.307.323-1.533.307-2.284.54-.028.02.032.04 1.029.098.44.024h1.077l2.005.15.525.346.315.424-.053.323-.807.411-3.631-.863-.872-.218h-.12v.073l.726.71 1.331 1.202 1.667 1.55.084.383-.214.302-.226-.032-1.464-1.101-.565-.497-1.28-1.077h-.084v.113l.295.432 1.557 2.34.08.718-.112.234-.404.141-.444-.08-.911-1.28-.94-1.44-.759-1.291-.093.053-.448 4.821-.21.246-.484.186-.403-.307-.214-.496.214-.98.258-1.28.21-1.016.19-1.263.112-.42-.008-.028-.092.012-.953 1.307-1.448 1.957-1.146 1.227-.274.109-.477-.247.045-.44.266-.39 1.586-2.018.956-1.25.617-.723-.004-.105h-.036l-4.212 2.736-.75.096-.324-.302.04-.496.154-.162 1.267-.871z"/>
|
<path d="m3.127 10.604 3.135-1.76.053-.153-.053-.085H6.11l-.525-.032-1.791-.048-1.554-.065-1.505-.08-.38-.081L0 7.832l.036-.234.32-.214.455.04 1.009.069 1.513.105 1.097.064 1.626.17h.259l.036-.105-.089-.065-.068-.064-1.566-1.062-1.695-1.121-.887-.646-.48-.327-.243-.306-.104-.67.435-.48.585.04.15.04.593.456 1.267.981 1.654 1.218.242.202.097-.068.012-.049-.109-.181-.9-1.626-.96-1.655-.428-.686-.113-.411a2 2 0 0 1-.068-.484l.496-.674L4.446 0l.662.089.279.242.411.94.666 1.48 1.033 2.014.302.597.162.553.06.17h.105v-.097l.085-1.134.157-1.392.154-1.792.052-.504.25-.605.497-.327.387.186.319.456-.045.294-.19 1.23-.37 1.93-.243 1.29h.142l.161-.16.654-.868 1.097-1.372.484-.545.565-.601.363-.287h.686l.505.751-.226.775-.707.895-.585.759-.839 1.13-.524.904.048.072.125-.012 1.897-.403 1.024-.186 1.223-.21.553.258.06.263-.218.536-1.307.323-1.533.307-2.284.54-.028.02.032.04 1.029.098.44.024h1.077l2.005.15.525.346.315.424-.053.323-.807.411-3.631-.863-.872-.218h-.12v.073l.726.71 1.331 1.202 1.667 1.55.084.383-.214.302-.226-.032-1.464-1.101-.565-.497-1.28-1.077h-.084v.113l.295.432 1.557 2.34.08.718-.112.234-.404.141-.444-.08-.911-1.28-.94-1.44-.759-1.291-.093.053-.448 4.821-.21.246-.484.186-.403-.307-.214-.496.214-.98.258-1.28.21-1.016.19-1.263.112-.42-.008-.028-.092.012-.953 1.307-1.448 1.957-1.146 1.227-.274.109-.477-.247.045-.44.266-.39 1.586-2.018.956-1.25.617-.723-.004-.105h-.036l-4.212 2.736-.75.096-.324-.302.04-.496.154-.162 1.267-.871z"/>
|
||||||
</svg>
|
</svg>
|
||||||
</g>
|
</g>
|
||||||
<g>
|
<g>
|
||||||
<circle cx="8" cy="16" r="2.2" fill="#10A37F" opacity="0.15"/>
|
<circle cx="8" cy="16" r="2.2" fill="currentColor" opacity="0.12"/>
|
||||||
<circle cx="8" cy="16" r="1.6" fill="#10A37F" opacity="0.45"/>
|
<circle cx="8" cy="16" r="1.6" fill="currentColor" opacity="0.5"/>
|
||||||
<svg x="6.2" y="14.2" width="3.6" height="3.6" viewBox="0 0 16 16" fill="#10A37F" opacity="0.95">
|
<svg x="6.2" y="14.2" width="3.6" height="3.6" viewBox="0 0 16 16" fill="#10A37F" opacity="0.95">
|
||||||
<path d="M14.949 6.547a3.94 3.94 0 0 0-.348-3.273 4.11 4.11 0 0 0-4.4-1.934A4.1 4.1 0 0 0 8.423.2 4.15 4.15 0 0 0 6.305.086a4.1 4.1 0 0 0-1.891.948 4.04 4.04 0 0 0-1.158 1.753 4.1 4.1 0 0 0-1.563.679A4 4 0 0 0 .554 4.72a3.99 3.99 0 0 0 .502 4.731 3.94 3.94 0 0 0 .346 3.274 4.11 4.11 0 0 0 4.402 1.933c.382.425.852.764 1.377.995.526.231 1.095.35 1.67.346 1.78.002 3.358-1.132 3.901-2.804a4.1 4.1 0 0 0 1.563-.68 4 4 0 0 0 1.14-1.253 3.99 3.99 0 0 0-.506-4.716m-6.097 8.406a3.05 3.05 0 0 1-1.945-.694l.096-.054 3.23-1.838a.53.53 0 0 0 .265-.455v-4.49l1.366.778q.02.011.025.035v3.722c-.003 1.653-1.361 2.992-3.037 2.996m-6.53-2.75a2.95 2.95 0 0 1-.36-2.01l.095.057L5.29 12.09a.53.53 0 0 0 .527 0l3.949-2.246v1.555a.05.05 0 0 1-.022.041L6.473 13.3c-1.454.826-3.311.335-4.15-1.098m-.85-6.94A3.02 3.02 0 0 1 3.07 3.949v3.785a.51.51 0 0 0 .262.451l3.93 2.237-1.366.779a.05.05 0 0 1-.048 0L2.585 9.342a2.98 2.98 0 0 1-1.113-4.094zm11.216 2.571L8.747 5.576l1.362-.776a.05.05 0 0 1 .048 0l3.265 1.86a3 3 0 0 1 1.173 1.207 2.96 2.96 0 0 1-.27 3.2 3.05 3.05 0 0 1-1.36.997V8.279a.52.52 0 0 0-.276-.445m1.36-2.015-.097-.057-3.226-1.855a.53.53 0 0 0-.53 0L6.249 6.153V4.598a.04.04 0 0 1 .019-.04L9.533 2.7a3.07 3.07 0 0 1 3.257.139c.474.325.843.778 1.066 1.303.223.526.289 1.103.191 1.664zM5.503 8.575 4.139 7.8a.05.05 0 0 1-.026-.037V4.049c0-.57.166-1.127.476-1.607s.752-.864 1.275-1.105a3.08 3.08 0 0 1 3.234.41l-.096.054-3.23 1.838a.53.53 0 0 0-.265.455zm.742-1.577 1.758-1 1.762 1v2l-1.755 1-1.762-1z"/>
|
<path d="M14.949 6.547a3.94 3.94 0 0 0-.348-3.273 4.11 4.11 0 0 0-4.4-1.934A4.1 4.1 0 0 0 8.423.2 4.15 4.15 0 0 0 6.305.086a4.1 4.1 0 0 0-1.891.948 4.04 4.04 0 0 0-1.158 1.753 4.1 4.1 0 0 0-1.563.679A4 4 0 0 0 .554 4.72a3.99 3.99 0 0 0 .502 4.731 3.94 3.94 0 0 0 .346 3.274 4.11 4.11 0 0 0 4.402 1.933c.382.425.852.764 1.377.995.526.231 1.095.35 1.67.346 1.78.002 3.358-1.132 3.901-2.804a4.1 4.1 0 0 0 1.563-.68 4 4 0 0 0 1.14-1.253 3.99 3.99 0 0 0-.506-4.716m-6.097 8.406a3.05 3.05 0 0 1-1.945-.694l.096-.054 3.23-1.838a.53.53 0 0 0 .265-.455v-4.49l1.366.778q.02.011.025.035v3.722c-.003 1.653-1.361 2.992-3.037 2.996m-6.53-2.75a2.95 2.95 0 0 1-.36-2.01l.095.057L5.29 12.09a.53.53 0 0 0 .527 0l3.949-2.246v1.555a.05.05 0 0 1-.022.041L6.473 13.3c-1.454.826-3.311.335-4.15-1.098m-.85-6.94A3.02 3.02 0 0 1 3.07 3.949v3.785a.51.51 0 0 0 .262.451l3.93 2.237-1.366.779a.05.05 0 0 1-.048 0L2.585 9.342a2.98 2.98 0 0 1-1.113-4.094zm11.216 2.571L8.747 5.576l1.362-.776a.05.05 0 0 1 .048 0l3.265 1.86a3 3 0 0 1 1.173 1.207 2.96 2.96 0 0 1-.27 3.2 3.05 3.05 0 0 1-1.36.997V8.279a.52.52 0 0 0-.276-.445m1.36-2.015-.097-.057-3.226-1.855a.53.53 0 0 0-.53 0L6.249 6.153V4.598a.04.04 0 0 1 .019-.04L9.533 2.7a3.07 3.07 0 0 1 3.257.139c.474.325.843.778 1.066 1.303.223.526.289 1.103.191 1.664zM5.503 8.575 4.139 7.8a.05.05 0 0 1-.026-.037V4.049c0-.57.166-1.127.476-1.607s.752-.864 1.275-1.105a3.08 3.08 0 0 1 3.234.41l-.096.054-3.23 1.838a.53.53 0 0 0-.265.455zm.742-1.577 1.758-1 1.762 1v2l-1.755 1-1.762-1z"/>
|
||||||
</svg>
|
</svg>
|
||||||
</g>
|
</g>
|
||||||
<g>
|
<g>
|
||||||
<circle cx="14" cy="5.5" r="2.2" fill="#4285F4" opacity="0.15"/>
|
<circle cx="14" cy="5.5" r="2.2" fill="currentColor" opacity="0.12"/>
|
||||||
<circle cx="14" cy="5.5" r="1.6" fill="#4285F4" opacity="0.45"/>
|
<circle cx="14" cy="5.5" r="1.6" fill="currentColor" opacity="0.5"/>
|
||||||
<svg x="12.5" y="4" width="3" height="3" viewBox="0 0 24 24" fill="none">
|
<svg x="12.5" y="4" width="3" height="3" viewBox="0 0 24 24" fill="none">
|
||||||
<path d="M3 12a9 9 0 0 0 9-9a9 9 0 0 0 9 9a9 9 0 0 0-9 9a9 9 0 0 0-9-9" stroke="#4285F4" stroke-width="2" fill="#4285F4" fill-opacity="0.3" opacity="0.95"/>
|
<path d="M3 12a9 9 0 0 0 9-9a9 9 0 0 0 9 9a9 9 0 0 0-9 9a9 9 0 0 0-9-9" stroke="#4285F4" stroke-width="2" fill="#4285F4" fill-opacity="0.3" opacity="0.95"/>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -2840,22 +2840,22 @@
|
|||||||
<path d="M16.9 4.5 A8 3 -30 0 1 7.1 19.5" stroke-width="1.3" opacity="0.7"/>
|
<path d="M16.9 4.5 A8 3 -30 0 1 7.1 19.5" stroke-width="1.3" opacity="0.7"/>
|
||||||
<!-- Agent icons with brand colors -->
|
<!-- Agent icons with brand colors -->
|
||||||
<g>
|
<g>
|
||||||
<circle cx="17" cy="9.7" r="2" fill="#D97757" opacity="0.15"/>
|
<circle cx="17" cy="9.7" r="2" fill="currentColor" opacity="0.12"/>
|
||||||
<circle cx="17" cy="9.7" r="1.4" fill="#D97757" opacity="0.45"/>
|
<circle cx="17" cy="9.7" r="1.4" fill="currentColor" opacity="0.5"/>
|
||||||
<svg x="15.5" y="8.2" width="3" height="3" viewBox="0 0 16 16" fill="#D97757" opacity="0.95">
|
<svg x="15.5" y="8.2" width="3" height="3" viewBox="0 0 16 16" fill="#D97757" opacity="0.95">
|
||||||
<path d="m3.127 10.604 3.135-1.76.053-.153-.053-.085H6.11l-.525-.032-1.791-.048-1.554-.065-1.505-.08-.38-.081L0 7.832l.036-.234.32-.214.455.04 1.009.069 1.513.105 1.097.064 1.626.17h.259l.036-.105-.089-.065-.068-.064-1.566-1.062-1.695-1.121-.887-.646-.48-.327-.243-.306-.104-.67.435-.48.585.04.15.04.593.456 1.267.981 1.654 1.218.242.202.097-.068.012-.049-.109-.181-.9-1.626-.96-1.655-.428-.686-.113-.411a2 2 0 0 1-.068-.484l.496-.674L4.446 0l.662.089.279.242.411.94.666 1.48 1.033 2.014.302.597.162.553.06.17h.105v-.097l.085-1.134.157-1.392.154-1.792.052-.504.25-.605.497-.327.387.186.319.456-.045.294-.19 1.23-.37 1.93-.243 1.29h.142l.161-.16.654-.868 1.097-1.372.484-.545.565-.601.363-.287h.686l.505.751-.226.775-.707.895-.585.759-.839 1.13-.524.904.048.072.125-.012 1.897-.403 1.024-.186 1.223-.21.553.258.06.263-.218.536-1.307.323-1.533.307-2.284.54-.028.02.032.04 1.029.098.44.024h1.077l2.005.15.525.346.315.424-.053.323-.807.411-3.631-.863-.872-.218h-.12v.073l.726.71 1.331 1.202 1.667 1.55.084.383-.214.302-.226-.032-1.464-1.101-.565-.497-1.28-1.077h-.084v.113l.295.432 1.557 2.34.08.718-.112.234-.404.141-.444-.08-.911-1.28-.94-1.44-.759-1.291-.093.053-.448 4.821-.21.246-.484.186-.403-.307-.214-.496.214-.98.258-1.28.21-1.016.19-1.263.112-.42-.008-.028-.092.012-.953 1.307-1.448 1.957-1.146 1.227-.274.109-.477-.247.045-.44.266-.39 1.586-2.018.956-1.25.617-.723-.004-.105h-.036l-4.212 2.736-.75.096-.324-.302.04-.496.154-.162 1.267-.871z"/>
|
<path d="m3.127 10.604 3.135-1.76.053-.153-.053-.085H6.11l-.525-.032-1.791-.048-1.554-.065-1.505-.08-.38-.081L0 7.832l.036-.234.32-.214.455.04 1.009.069 1.513.105 1.097.064 1.626.17h.259l.036-.105-.089-.065-.068-.064-1.566-1.062-1.695-1.121-.887-.646-.48-.327-.243-.306-.104-.67.435-.48.585.04.15.04.593.456 1.267.981 1.654 1.218.242.202.097-.068.012-.049-.109-.181-.9-1.626-.96-1.655-.428-.686-.113-.411a2 2 0 0 1-.068-.484l.496-.674L4.446 0l.662.089.279.242.411.94.666 1.48 1.033 2.014.302.597.162.553.06.17h.105v-.097l.085-1.134.157-1.392.154-1.792.052-.504.25-.605.497-.327.387.186.319.456-.045.294-.19 1.23-.37 1.93-.243 1.29h.142l.161-.16.654-.868 1.097-1.372.484-.545.565-.601.363-.287h.686l.505.751-.226.775-.707.895-.585.759-.839 1.13-.524.904.048.072.125-.012 1.897-.403 1.024-.186 1.223-.21.553.258.06.263-.218.536-1.307.323-1.533.307-2.284.54-.028.02.032.04 1.029.098.44.024h1.077l2.005.15.525.346.315.424-.053.323-.807.411-3.631-.863-.872-.218h-.12v.073l.726.71 1.331 1.202 1.667 1.55.084.383-.214.302-.226-.032-1.464-1.101-.565-.497-1.28-1.077h-.084v.113l.295.432 1.557 2.34.08.718-.112.234-.404.141-.444-.08-.911-1.28-.94-1.44-.759-1.291-.093.053-.448 4.821-.21.246-.484.186-.403-.307-.214-.496.214-.98.258-1.28.21-1.016.19-1.263.112-.42-.008-.028-.092.012-.953 1.307-1.448 1.957-1.146 1.227-.274.109-.477-.247.045-.44.266-.39 1.586-2.018.956-1.25.617-.723-.004-.105h-.036l-4.212 2.736-.75.096-.324-.302.04-.496.154-.162 1.267-.871z"/>
|
||||||
</svg>
|
</svg>
|
||||||
</g>
|
</g>
|
||||||
<g>
|
<g>
|
||||||
<circle cx="8" cy="16" r="2" fill="#10A37F" opacity="0.15"/>
|
<circle cx="8" cy="16" r="2" fill="currentColor" opacity="0.12"/>
|
||||||
<circle cx="8" cy="16" r="1.4" fill="#10A37F" opacity="0.45"/>
|
<circle cx="8" cy="16" r="1.4" fill="currentColor" opacity="0.5"/>
|
||||||
<svg x="6.5" y="14.5" width="3" height="3" viewBox="0 0 16 16" fill="#10A37F" opacity="0.95">
|
<svg x="6.5" y="14.5" width="3" height="3" viewBox="0 0 16 16" fill="#10A37F" opacity="0.95">
|
||||||
<path d="M14.949 6.547a3.94 3.94 0 0 0-.348-3.273 4.11 4.11 0 0 0-4.4-1.934A4.1 4.1 0 0 0 8.423.2 4.15 4.15 0 0 0 6.305.086a4.1 4.1 0 0 0-1.891.948 4.04 4.04 0 0 0-1.158 1.753 4.1 4.1 0 0 0-1.563.679A4 4 0 0 0 .554 4.72a3.99 3.99 0 0 0 .502 4.731 3.94 3.94 0 0 0 .346 3.274 4.11 4.11 0 0 0 4.402 1.933c.382.425.852.764 1.377.995.526.231 1.095.35 1.67.346 1.78.002 3.358-1.132 3.901-2.804a4.1 4.1 0 0 0 1.563-.68 4 4 0 0 0 1.14-1.253 3.99 3.99 0 0 0-.506-4.716m-6.097 8.406a3.05 3.05 0 0 1-1.945-.694l.096-.054 3.23-1.838a.53.53 0 0 0 .265-.455v-4.49l1.366.778q.02.011.025.035v3.722c-.003 1.653-1.361 2.992-3.037 2.996m-6.53-2.75a2.95 2.95 0 0 1-.36-2.01l.095.057L5.29 12.09a.53.53 0 0 0 .527 0l3.949-2.246v1.555a.05.05 0 0 1-.022.041L6.473 13.3c-1.454.826-3.311.335-4.15-1.098m-.85-6.94A3.02 3.02 0 0 1 3.07 3.949v3.785a.51.51 0 0 0 .262.451l3.93 2.237-1.366.779a.05.05 0 0 1-.048 0L2.585 9.342a2.98 2.98 0 0 1-1.113-4.094zm11.216 2.571L8.747 5.576l1.362-.776a.05.05 0 0 1 .048 0l3.265 1.86a3 3 0 0 1 1.173 1.207 2.96 2.96 0 0 1-.27 3.2 3.05 3.05 0 0 1-1.36.997V8.279a.52.52 0 0 0-.276-.445m1.36-2.015-.097-.057-3.226-1.855a.53.53 0 0 0-.53 0L6.249 6.153V4.598a.04.04 0 0 1 .019-.04L9.533 2.7a3.07 3.07 0 0 1 3.257.139c.474.325.843.778 1.066 1.303.223.526.289 1.103.191 1.664zM5.503 8.575 4.139 7.8a.05.05 0 0 1-.026-.037V4.049c0-.57.166-1.127.476-1.607s.752-.864 1.275-1.105a3.08 3.08 0 0 1 3.234.41l-.096.054-3.23 1.838a.53.53 0 0 0-.265.455zm.742-1.577 1.758-1 1.762 1v2l-1.755 1-1.762-1z"/>
|
<path d="M14.949 6.547a3.94 3.94 0 0 0-.348-3.273 4.11 4.11 0 0 0-4.4-1.934A4.1 4.1 0 0 0 8.423.2 4.15 4.15 0 0 0 6.305.086a4.1 4.1 0 0 0-1.891.948 4.04 4.04 0 0 0-1.158 1.753 4.1 4.1 0 0 0-1.563.679A4 4 0 0 0 .554 4.72a3.99 3.99 0 0 0 .502 4.731 3.94 3.94 0 0 0 .346 3.274 4.11 4.11 0 0 0 4.402 1.933c.382.425.852.764 1.377.995.526.231 1.095.35 1.67.346 1.78.002 3.358-1.132 3.901-2.804a4.1 4.1 0 0 0 1.563-.68 4 4 0 0 0 1.14-1.253 3.99 3.99 0 0 0-.506-4.716m-6.097 8.406a3.05 3.05 0 0 1-1.945-.694l.096-.054 3.23-1.838a.53.53 0 0 0 .265-.455v-4.49l1.366.778q.02.011.025.035v3.722c-.003 1.653-1.361 2.992-3.037 2.996m-6.53-2.75a2.95 2.95 0 0 1-.36-2.01l.095.057L5.29 12.09a.53.53 0 0 0 .527 0l3.949-2.246v1.555a.05.05 0 0 1-.022.041L6.473 13.3c-1.454.826-3.311.335-4.15-1.098m-.85-6.94A3.02 3.02 0 0 1 3.07 3.949v3.785a.51.51 0 0 0 .262.451l3.93 2.237-1.366.779a.05.05 0 0 1-.048 0L2.585 9.342a2.98 2.98 0 0 1-1.113-4.094zm11.216 2.571L8.747 5.576l1.362-.776a.05.05 0 0 1 .048 0l3.265 1.86a3 3 0 0 1 1.173 1.207 2.96 2.96 0 0 1-.27 3.2 3.05 3.05 0 0 1-1.36.997V8.279a.52.52 0 0 0-.276-.445m1.36-2.015-.097-.057-3.226-1.855a.53.53 0 0 0-.53 0L6.249 6.153V4.598a.04.04 0 0 1 .019-.04L9.533 2.7a3.07 3.07 0 0 1 3.257.139c.474.325.843.778 1.066 1.303.223.526.289 1.103.191 1.664zM5.503 8.575 4.139 7.8a.05.05 0 0 1-.026-.037V4.049c0-.57.166-1.127.476-1.607s.752-.864 1.275-1.105a3.08 3.08 0 0 1 3.234.41l-.096.054-3.23 1.838a.53.53 0 0 0-.265.455zm.742-1.577 1.758-1 1.762 1v2l-1.755 1-1.762-1z"/>
|
||||||
</svg>
|
</svg>
|
||||||
</g>
|
</g>
|
||||||
<g>
|
<g>
|
||||||
<circle cx="14" cy="5.5" r="2" fill="#4285F4" opacity="0.15"/>
|
<circle cx="14" cy="5.5" r="2" fill="currentColor" opacity="0.12"/>
|
||||||
<circle cx="14" cy="5.5" r="1.4" fill="#4285F4" opacity="0.45"/>
|
<circle cx="14" cy="5.5" r="1.4" fill="currentColor" opacity="0.5"/>
|
||||||
<svg x="12.7" y="4.2" width="2.6" height="2.6" viewBox="0 0 24 24" fill="none">
|
<svg x="12.7" y="4.2" width="2.6" height="2.6" viewBox="0 0 24 24" fill="none">
|
||||||
<path d="M3 12a9 9 0 0 0 9-9a9 9 0 0 0 9 9a9 9 0 0 0-9 9a9 9 0 0 0-9-9" stroke="#4285F4" stroke-width="2" fill="#4285F4" fill-opacity="0.3" opacity="0.95"/>
|
<path d="M3 12a9 9 0 0 0 9-9a9 9 0 0 0 9 9a9 9 0 0 0-9 9a9 9 0 0 0-9-9" stroke="#4285F4" stroke-width="2" fill="#4285F4" fill-opacity="0.3" opacity="0.95"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
Reference in New Issue
Block a user