Spaces:
Sleeping
Sleeping
cccmmd commited on
Commit ·
76b5743
0
Parent(s):
init: TokenTrace - LLM interpretability toolbox
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .cursorindexingignore +3 -0
- .dockerignore +51 -0
- .gitattributes +36 -0
- .gitignore +45 -0
- .kilocode/skills/openspec-apply-change/SKILL.md +156 -0
- .kilocode/skills/openspec-archive-change/SKILL.md +114 -0
- .kilocode/skills/openspec-explore/SKILL.md +288 -0
- .kilocode/skills/openspec-propose/SKILL.md +110 -0
- .kilocode/workflows/opsx-apply.md +145 -0
- .kilocode/workflows/opsx-archive.md +150 -0
- .kilocode/workflows/opsx-explore.md +166 -0
- .kilocode/workflows/opsx-propose.md +99 -0
- .kiro/prompts/opsx-apply.prompt.md +149 -0
- .kiro/prompts/opsx-archive.prompt.md +154 -0
- .kiro/prompts/opsx-explore.prompt.md +170 -0
- .kiro/prompts/opsx-propose.prompt.md +103 -0
- .kiro/skills/openspec-apply-change/SKILL.md +156 -0
- .kiro/skills/openspec-archive-change/SKILL.md +114 -0
- .kiro/skills/openspec-explore/SKILL.md +288 -0
- .kiro/skills/openspec-propose/SKILL.md +110 -0
- .opencode/commands/opsx-apply.md +149 -0
- .opencode/commands/opsx-archive.md +154 -0
- .opencode/commands/opsx-explore.md +170 -0
- .opencode/commands/opsx-propose.md +103 -0
- .opencode/skills/openspec-apply-change/SKILL.md +156 -0
- .opencode/skills/openspec-archive-change/SKILL.md +114 -0
- .opencode/skills/openspec-explore/SKILL.md +288 -0
- .opencode/skills/openspec-propose/SKILL.md +110 -0
- AGENTS.md +13 -0
- CONTEXT.md +29 -0
- Dockerfile +67 -0
- LICENSE +201 -0
- NOTICE +8 -0
- README.md +148 -0
- backend/__init__.py +5 -0
- backend/api/__init__.py +2 -0
- backend/api/ablation_attribute.py +135 -0
- backend/api/analyze.py +412 -0
- backend/api/analyze_semantic.py +212 -0
- backend/api/branch_next.py +81 -0
- backend/api/client_activity.py +60 -0
- backend/api/demo.py +174 -0
- backend/api/fetch_url.py +221 -0
- backend/api/folder.py +102 -0
- backend/api/logit_lens.py +104 -0
- backend/api/model_switch.py +229 -0
- backend/api/openai_completions.py +571 -0
- backend/api/prediction_attribute.py +151 -0
- backend/api/sse_utils.py +181 -0
- backend/api/static.py +67 -0
.cursorindexingignore
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
# Don't index SpecStory auto-save files, but allow explicit context inclusion via @ references
|
| 3 |
+
.specstory/**
|
.dockerignore
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# --- 核心语言与依赖 ---
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
.venv/
|
| 5 |
+
venv/
|
| 6 |
+
env/
|
| 7 |
+
node_modules/
|
| 8 |
+
client/src/node_modules/
|
| 9 |
+
client/src/.cache-loader/
|
| 10 |
+
|
| 11 |
+
# --- 构建产物与缓存 ---
|
| 12 |
+
client/dist/
|
| 13 |
+
build/
|
| 14 |
+
dist/
|
| 15 |
+
*.egg-info/
|
| 16 |
+
.cache_huggingface/
|
| 17 |
+
*.tsbuildinfo
|
| 18 |
+
|
| 19 |
+
# --- 项目特定配置 ---
|
| 20 |
+
# 忽略所有数据,防止误传大文件
|
| 21 |
+
data/*
|
| 22 |
+
# 白名单:只保留public文件夹
|
| 23 |
+
!data/demo/
|
| 24 |
+
data/demo/*
|
| 25 |
+
!data/demo/public/
|
| 26 |
+
|
| 27 |
+
# 忽略临时文件和日志
|
| 28 |
+
notes.md
|
| 29 |
+
.env
|
| 30 |
+
*.log
|
| 31 |
+
npm-debug.log*
|
| 32 |
+
yarn-debug.log*
|
| 33 |
+
yarn-error.log*
|
| 34 |
+
|
| 35 |
+
# --- 系统与 IDE ---
|
| 36 |
+
.DS_Store
|
| 37 |
+
.DS_Store?
|
| 38 |
+
._*
|
| 39 |
+
.Spotlight-V100
|
| 40 |
+
.Trashes
|
| 41 |
+
ehthumbs.db
|
| 42 |
+
Thumbs.db
|
| 43 |
+
.vscode/
|
| 44 |
+
.idea/
|
| 45 |
+
*.swp
|
| 46 |
+
*.swo
|
| 47 |
+
*~
|
| 48 |
+
|
| 49 |
+
# --- Git ---
|
| 50 |
+
.git
|
| 51 |
+
.gitignore
|
.gitattributes
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
artifacts/tiny_nla/checkpoints/av/tokenizer.json filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.cursor/
|
| 2 |
+
.agents/
|
| 3 |
+
.scratch/
|
| 4 |
+
skills-lock.json
|
| 5 |
+
.cache_huggingface
|
| 6 |
+
# --- 核心语言与依赖 ---
|
| 7 |
+
__pycache__/
|
| 8 |
+
*.py[cod]
|
| 9 |
+
.venv/
|
| 10 |
+
venv/
|
| 11 |
+
env/
|
| 12 |
+
node_modules/
|
| 13 |
+
client/src/node_modules/
|
| 14 |
+
client/src/.cache-loader/
|
| 15 |
+
|
| 16 |
+
# --- 构建产物 ---
|
| 17 |
+
client/dist/
|
| 18 |
+
build/
|
| 19 |
+
dist/
|
| 20 |
+
*.egg-info/
|
| 21 |
+
|
| 22 |
+
# --- 项目特定配置 ---
|
| 23 |
+
# 忽略所有数据,防止误传大文件
|
| 24 |
+
data/*
|
| 25 |
+
# 白名单:只保留 GLTR 演示数据
|
| 26 |
+
!data/demo/
|
| 27 |
+
data/demo/*
|
| 28 |
+
!data/demo/public/
|
| 29 |
+
data/demo/public/.deleted/
|
| 30 |
+
|
| 31 |
+
# 忽略临时笔记和 HuggingFace 缓存
|
| 32 |
+
notes/*
|
| 33 |
+
user_dialog_history/*
|
| 34 |
+
.cache_huggingface/
|
| 35 |
+
.env
|
| 36 |
+
|
| 37 |
+
# --- 系统与 IDE ---
|
| 38 |
+
.DS_Store
|
| 39 |
+
.idea/
|
| 40 |
+
.vscode/
|
| 41 |
+
*.swp
|
| 42 |
+
*.log
|
| 43 |
+
.specstory
|
| 44 |
+
scripts/log.py
|
| 45 |
+
scripts/results/
|
.kilocode/skills/openspec-apply-change/SKILL.md
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: openspec-apply-change
|
| 3 |
+
description: Implement tasks from an OpenSpec change. Use when the user wants to start implementing, continue implementation, or work through tasks.
|
| 4 |
+
license: MIT
|
| 5 |
+
compatibility: Requires openspec CLI.
|
| 6 |
+
metadata:
|
| 7 |
+
author: openspec
|
| 8 |
+
version: "1.0"
|
| 9 |
+
generatedBy: "1.3.1"
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
Implement tasks from an OpenSpec change.
|
| 13 |
+
|
| 14 |
+
**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
| 15 |
+
|
| 16 |
+
**Steps**
|
| 17 |
+
|
| 18 |
+
1. **Select the change**
|
| 19 |
+
|
| 20 |
+
If a name is provided, use it. Otherwise:
|
| 21 |
+
- Infer from conversation context if the user mentioned a change
|
| 22 |
+
- Auto-select if only one active change exists
|
| 23 |
+
- If ambiguous, run `openspec list --json` to get available changes and use the **AskUserQuestion tool** to let the user select
|
| 24 |
+
|
| 25 |
+
Always announce: "Using change: <name>" and how to override (e.g., `/opsx:apply <other>`).
|
| 26 |
+
|
| 27 |
+
2. **Check status to understand the schema**
|
| 28 |
+
```bash
|
| 29 |
+
openspec status --change "<name>" --json
|
| 30 |
+
```
|
| 31 |
+
Parse the JSON to understand:
|
| 32 |
+
- `schemaName`: The workflow being used (e.g., "spec-driven")
|
| 33 |
+
- Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
|
| 34 |
+
|
| 35 |
+
3. **Get apply instructions**
|
| 36 |
+
|
| 37 |
+
```bash
|
| 38 |
+
openspec instructions apply --change "<name>" --json
|
| 39 |
+
```
|
| 40 |
+
|
| 41 |
+
This returns:
|
| 42 |
+
- `contextFiles`: artifact ID -> array of concrete file paths (varies by schema - could be proposal/specs/design/tasks or spec/tests/implementation/docs)
|
| 43 |
+
- Progress (total, complete, remaining)
|
| 44 |
+
- Task list with status
|
| 45 |
+
- Dynamic instruction based on current state
|
| 46 |
+
|
| 47 |
+
**Handle states:**
|
| 48 |
+
- If `state: "blocked"` (missing artifacts): show message, suggest using openspec-continue-change
|
| 49 |
+
- If `state: "all_done"`: congratulate, suggest archive
|
| 50 |
+
- Otherwise: proceed to implementation
|
| 51 |
+
|
| 52 |
+
4. **Read context files**
|
| 53 |
+
|
| 54 |
+
Read every file path listed under `contextFiles` from the apply instructions output.
|
| 55 |
+
The files depend on the schema being used:
|
| 56 |
+
- **spec-driven**: proposal, specs, design, tasks
|
| 57 |
+
- Other schemas: follow the contextFiles from CLI output
|
| 58 |
+
|
| 59 |
+
5. **Show current progress**
|
| 60 |
+
|
| 61 |
+
Display:
|
| 62 |
+
- Schema being used
|
| 63 |
+
- Progress: "N/M tasks complete"
|
| 64 |
+
- Remaining tasks overview
|
| 65 |
+
- Dynamic instruction from CLI
|
| 66 |
+
|
| 67 |
+
6. **Implement tasks (loop until done or blocked)**
|
| 68 |
+
|
| 69 |
+
For each pending task:
|
| 70 |
+
- Show which task is being worked on
|
| 71 |
+
- Make the code changes required
|
| 72 |
+
- Keep changes minimal and focused
|
| 73 |
+
- Mark task complete in the tasks file: `- [ ]` → `- [x]`
|
| 74 |
+
- Continue to next task
|
| 75 |
+
|
| 76 |
+
**Pause if:**
|
| 77 |
+
- Task is unclear → ask for clarification
|
| 78 |
+
- Implementation reveals a design issue → suggest updating artifacts
|
| 79 |
+
- Error or blocker encountered → report and wait for guidance
|
| 80 |
+
- User interrupts
|
| 81 |
+
|
| 82 |
+
7. **On completion or pause, show status**
|
| 83 |
+
|
| 84 |
+
Display:
|
| 85 |
+
- Tasks completed this session
|
| 86 |
+
- Overall progress: "N/M tasks complete"
|
| 87 |
+
- If all done: suggest archive
|
| 88 |
+
- If paused: explain why and wait for guidance
|
| 89 |
+
|
| 90 |
+
**Output During Implementation**
|
| 91 |
+
|
| 92 |
+
```
|
| 93 |
+
## Implementing: <change-name> (schema: <schema-name>)
|
| 94 |
+
|
| 95 |
+
Working on task 3/7: <task description>
|
| 96 |
+
[...implementation happening...]
|
| 97 |
+
✓ Task complete
|
| 98 |
+
|
| 99 |
+
Working on task 4/7: <task description>
|
| 100 |
+
[...implementation happening...]
|
| 101 |
+
✓ Task complete
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
**Output On Completion**
|
| 105 |
+
|
| 106 |
+
```
|
| 107 |
+
## Implementation Complete
|
| 108 |
+
|
| 109 |
+
**Change:** <change-name>
|
| 110 |
+
**Schema:** <schema-name>
|
| 111 |
+
**Progress:** 7/7 tasks complete ✓
|
| 112 |
+
|
| 113 |
+
### Completed This Session
|
| 114 |
+
- [x] Task 1
|
| 115 |
+
- [x] Task 2
|
| 116 |
+
...
|
| 117 |
+
|
| 118 |
+
All tasks complete! Ready to archive this change.
|
| 119 |
+
```
|
| 120 |
+
|
| 121 |
+
**Output On Pause (Issue Encountered)**
|
| 122 |
+
|
| 123 |
+
```
|
| 124 |
+
## Implementation Paused
|
| 125 |
+
|
| 126 |
+
**Change:** <change-name>
|
| 127 |
+
**Schema:** <schema-name>
|
| 128 |
+
**Progress:** 4/7 tasks complete
|
| 129 |
+
|
| 130 |
+
### Issue Encountered
|
| 131 |
+
<description of the issue>
|
| 132 |
+
|
| 133 |
+
**Options:**
|
| 134 |
+
1. <option 1>
|
| 135 |
+
2. <option 2>
|
| 136 |
+
3. Other approach
|
| 137 |
+
|
| 138 |
+
What would you like to do?
|
| 139 |
+
```
|
| 140 |
+
|
| 141 |
+
**Guardrails**
|
| 142 |
+
- Keep going through tasks until done or blocked
|
| 143 |
+
- Always read context files before starting (from the apply instructions output)
|
| 144 |
+
- If task is ambiguous, pause and ask before implementing
|
| 145 |
+
- If implementation reveals issues, pause and suggest artifact updates
|
| 146 |
+
- Keep code changes minimal and scoped to each task
|
| 147 |
+
- Update task checkbox immediately after completing each task
|
| 148 |
+
- Pause on errors, blockers, or unclear requirements - don't guess
|
| 149 |
+
- Use contextFiles from CLI output, don't assume specific file names
|
| 150 |
+
|
| 151 |
+
**Fluid Workflow Integration**
|
| 152 |
+
|
| 153 |
+
This skill supports the "actions on a change" model:
|
| 154 |
+
|
| 155 |
+
- **Can be invoked anytime**: Before all artifacts are done (if tasks exist), after partial implementation, interleaved with other actions
|
| 156 |
+
- **Allows artifact updates**: If implementation reveals design issues, suggest updating artifacts - not phase-locked, work fluidly
|
.kilocode/skills/openspec-archive-change/SKILL.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: openspec-archive-change
|
| 3 |
+
description: Archive a completed change in the experimental workflow. Use when the user wants to finalize and archive a change after implementation is complete.
|
| 4 |
+
license: MIT
|
| 5 |
+
compatibility: Requires openspec CLI.
|
| 6 |
+
metadata:
|
| 7 |
+
author: openspec
|
| 8 |
+
version: "1.0"
|
| 9 |
+
generatedBy: "1.3.1"
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
Archive a completed change in the experimental workflow.
|
| 13 |
+
|
| 14 |
+
**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
| 15 |
+
|
| 16 |
+
**Steps**
|
| 17 |
+
|
| 18 |
+
1. **If no change name provided, prompt for selection**
|
| 19 |
+
|
| 20 |
+
Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
|
| 21 |
+
|
| 22 |
+
Show only active changes (not already archived).
|
| 23 |
+
Include the schema used for each change if available.
|
| 24 |
+
|
| 25 |
+
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
| 26 |
+
|
| 27 |
+
2. **Check artifact completion status**
|
| 28 |
+
|
| 29 |
+
Run `openspec status --change "<name>" --json` to check artifact completion.
|
| 30 |
+
|
| 31 |
+
Parse the JSON to understand:
|
| 32 |
+
- `schemaName`: The workflow being used
|
| 33 |
+
- `artifacts`: List of artifacts with their status (`done` or other)
|
| 34 |
+
|
| 35 |
+
**If any artifacts are not `done`:**
|
| 36 |
+
- Display warning listing incomplete artifacts
|
| 37 |
+
- Use **AskUserQuestion tool** to confirm user wants to proceed
|
| 38 |
+
- Proceed if user confirms
|
| 39 |
+
|
| 40 |
+
3. **Check task completion status**
|
| 41 |
+
|
| 42 |
+
Read the tasks file (typically `tasks.md`) to check for incomplete tasks.
|
| 43 |
+
|
| 44 |
+
Count tasks marked with `- [ ]` (incomplete) vs `- [x]` (complete).
|
| 45 |
+
|
| 46 |
+
**If incomplete tasks found:**
|
| 47 |
+
- Display warning showing count of incomplete tasks
|
| 48 |
+
- Use **AskUserQuestion tool** to confirm user wants to proceed
|
| 49 |
+
- Proceed if user confirms
|
| 50 |
+
|
| 51 |
+
**If no tasks file exists:** Proceed without task-related warning.
|
| 52 |
+
|
| 53 |
+
4. **Assess delta spec sync state**
|
| 54 |
+
|
| 55 |
+
Check for delta specs at `openspec/changes/<name>/specs/`. If none exist, proceed without sync prompt.
|
| 56 |
+
|
| 57 |
+
**If delta specs exist:**
|
| 58 |
+
- Compare each delta spec with its corresponding main spec at `openspec/specs/<capability>/spec.md`
|
| 59 |
+
- Determine what changes would be applied (adds, modifications, removals, renames)
|
| 60 |
+
- Show a combined summary before prompting
|
| 61 |
+
|
| 62 |
+
**Prompt options:**
|
| 63 |
+
- If changes needed: "Sync now (recommended)", "Archive without syncing"
|
| 64 |
+
- If already synced: "Archive now", "Sync anyway", "Cancel"
|
| 65 |
+
|
| 66 |
+
If user chooses sync, use Task tool (subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change '<name>'. Delta spec analysis: <include the analyzed delta spec summary>"). Proceed to archive regardless of choice.
|
| 67 |
+
|
| 68 |
+
5. **Perform the archive**
|
| 69 |
+
|
| 70 |
+
Create the archive directory if it doesn't exist:
|
| 71 |
+
```bash
|
| 72 |
+
mkdir -p openspec/changes/archive
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
Generate target name using current date: `YYYY-MM-DD-<change-name>`
|
| 76 |
+
|
| 77 |
+
**Check if target already exists:**
|
| 78 |
+
- If yes: Fail with error, suggest renaming existing archive or using different date
|
| 79 |
+
- If no: Move the change directory to archive
|
| 80 |
+
|
| 81 |
+
```bash
|
| 82 |
+
mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name>
|
| 83 |
+
```
|
| 84 |
+
|
| 85 |
+
6. **Display summary**
|
| 86 |
+
|
| 87 |
+
Show archive completion summary including:
|
| 88 |
+
- Change name
|
| 89 |
+
- Schema that was used
|
| 90 |
+
- Archive location
|
| 91 |
+
- Whether specs were synced (if applicable)
|
| 92 |
+
- Note about any warnings (incomplete artifacts/tasks)
|
| 93 |
+
|
| 94 |
+
**Output On Success**
|
| 95 |
+
|
| 96 |
+
```
|
| 97 |
+
## Archive Complete
|
| 98 |
+
|
| 99 |
+
**Change:** <change-name>
|
| 100 |
+
**Schema:** <schema-name>
|
| 101 |
+
**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
| 102 |
+
**Specs:** ✓ Synced to main specs (or "No delta specs" or "Sync skipped")
|
| 103 |
+
|
| 104 |
+
All artifacts complete. All tasks complete.
|
| 105 |
+
```
|
| 106 |
+
|
| 107 |
+
**Guardrails**
|
| 108 |
+
- Always prompt for change selection if not provided
|
| 109 |
+
- Use artifact graph (openspec status --json) for completion checking
|
| 110 |
+
- Don't block archive on warnings - just inform and confirm
|
| 111 |
+
- Preserve .openspec.yaml when moving to archive (it moves with the directory)
|
| 112 |
+
- Show clear summary of what happened
|
| 113 |
+
- If sync is requested, use openspec-sync-specs approach (agent-driven)
|
| 114 |
+
- If delta specs exist, always run the sync assessment and show the combined summary before prompting
|
.kilocode/skills/openspec-explore/SKILL.md
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: openspec-explore
|
| 3 |
+
description: Enter explore mode - a thinking partner for exploring ideas, investigating problems, and clarifying requirements. Use when the user wants to think through something before or during a change.
|
| 4 |
+
license: MIT
|
| 5 |
+
compatibility: Requires openspec CLI.
|
| 6 |
+
metadata:
|
| 7 |
+
author: openspec
|
| 8 |
+
version: "1.0"
|
| 9 |
+
generatedBy: "1.3.1"
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes.
|
| 13 |
+
|
| 14 |
+
**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first and create a change proposal. You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing.
|
| 15 |
+
|
| 16 |
+
**This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore.
|
| 17 |
+
|
| 18 |
+
---
|
| 19 |
+
|
| 20 |
+
## The Stance
|
| 21 |
+
|
| 22 |
+
- **Curious, not prescriptive** - Ask questions that emerge naturally, don't follow a script
|
| 23 |
+
- **Open threads, not interrogations** - Surface multiple interesting directions and let the user follow what resonates. Don't funnel them through a single path of questions.
|
| 24 |
+
- **Visual** - Use ASCII diagrams liberally when they'd help clarify thinking
|
| 25 |
+
- **Adaptive** - Follow interesting threads, pivot when new information emerges
|
| 26 |
+
- **Patient** - Don't rush to conclusions, let the shape of the problem emerge
|
| 27 |
+
- **Grounded** - Explore the actual codebase when relevant, don't just theorize
|
| 28 |
+
|
| 29 |
+
---
|
| 30 |
+
|
| 31 |
+
## What You Might Do
|
| 32 |
+
|
| 33 |
+
Depending on what the user brings, you might:
|
| 34 |
+
|
| 35 |
+
**Explore the problem space**
|
| 36 |
+
- Ask clarifying questions that emerge from what they said
|
| 37 |
+
- Challenge assumptions
|
| 38 |
+
- Reframe the problem
|
| 39 |
+
- Find analogies
|
| 40 |
+
|
| 41 |
+
**Investigate the codebase**
|
| 42 |
+
- Map existing architecture relevant to the discussion
|
| 43 |
+
- Find integration points
|
| 44 |
+
- Identify patterns already in use
|
| 45 |
+
- Surface hidden complexity
|
| 46 |
+
|
| 47 |
+
**Compare options**
|
| 48 |
+
- Brainstorm multiple approaches
|
| 49 |
+
- Build comparison tables
|
| 50 |
+
- Sketch tradeoffs
|
| 51 |
+
- Recommend a path (if asked)
|
| 52 |
+
|
| 53 |
+
**Visualize**
|
| 54 |
+
```
|
| 55 |
+
┌─────────────────────────────────────────┐
|
| 56 |
+
│ Use ASCII diagrams liberally │
|
| 57 |
+
├─────────────────────────────────────────┤
|
| 58 |
+
│ │
|
| 59 |
+
│ ┌────────┐ ┌────────┐ │
|
| 60 |
+
│ │ State │────────▶│ State │ │
|
| 61 |
+
│ │ A │ │ B │ │
|
| 62 |
+
│ └────────┘ └────────┘ │
|
| 63 |
+
│ │
|
| 64 |
+
│ System diagrams, state machines, │
|
| 65 |
+
│ data flows, architecture sketches, │
|
| 66 |
+
│ dependency graphs, comparison tables │
|
| 67 |
+
│ │
|
| 68 |
+
└─────────────────────────────────────────┘
|
| 69 |
+
```
|
| 70 |
+
|
| 71 |
+
**Surface risks and unknowns**
|
| 72 |
+
- Identify what could go wrong
|
| 73 |
+
- Find gaps in understanding
|
| 74 |
+
- Suggest spikes or investigations
|
| 75 |
+
|
| 76 |
+
---
|
| 77 |
+
|
| 78 |
+
## OpenSpec Awareness
|
| 79 |
+
|
| 80 |
+
You have full context of the OpenSpec system. Use it naturally, don't force it.
|
| 81 |
+
|
| 82 |
+
### Check for context
|
| 83 |
+
|
| 84 |
+
At the start, quickly check what exists:
|
| 85 |
+
```bash
|
| 86 |
+
openspec list --json
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
This tells you:
|
| 90 |
+
- If there are active changes
|
| 91 |
+
- Their names, schemas, and status
|
| 92 |
+
- What the user might be working on
|
| 93 |
+
|
| 94 |
+
### When no change exists
|
| 95 |
+
|
| 96 |
+
Think freely. When insights crystallize, you might offer:
|
| 97 |
+
|
| 98 |
+
- "This feels solid enough to start a change. Want me to create a proposal?"
|
| 99 |
+
- Or keep exploring - no pressure to formalize
|
| 100 |
+
|
| 101 |
+
### When a change exists
|
| 102 |
+
|
| 103 |
+
If the user mentions a change or you detect one is relevant:
|
| 104 |
+
|
| 105 |
+
1. **Read existing artifacts for context**
|
| 106 |
+
- `openspec/changes/<name>/proposal.md`
|
| 107 |
+
- `openspec/changes/<name>/design.md`
|
| 108 |
+
- `openspec/changes/<name>/tasks.md`
|
| 109 |
+
- etc.
|
| 110 |
+
|
| 111 |
+
2. **Reference them naturally in conversation**
|
| 112 |
+
- "Your design mentions using Redis, but we just realized SQLite fits better..."
|
| 113 |
+
- "The proposal scopes this to premium users, but we're now thinking everyone..."
|
| 114 |
+
|
| 115 |
+
3. **Offer to capture when decisions are made**
|
| 116 |
+
|
| 117 |
+
| Insight Type | Where to Capture |
|
| 118 |
+
|----------------------------|--------------------------------|
|
| 119 |
+
| New requirement discovered | `specs/<capability>/spec.md` |
|
| 120 |
+
| Requirement changed | `specs/<capability>/spec.md` |
|
| 121 |
+
| Design decision made | `design.md` |
|
| 122 |
+
| Scope changed | `proposal.md` |
|
| 123 |
+
| New work identified | `tasks.md` |
|
| 124 |
+
| Assumption invalidated | Relevant artifact |
|
| 125 |
+
|
| 126 |
+
Example offers:
|
| 127 |
+
- "That's a design decision. Capture it in design.md?"
|
| 128 |
+
- "This is a new requirement. Add it to specs?"
|
| 129 |
+
- "This changes scope. Update the proposal?"
|
| 130 |
+
|
| 131 |
+
4. **The user decides** - Offer and move on. Don't pressure. Don't auto-capture.
|
| 132 |
+
|
| 133 |
+
---
|
| 134 |
+
|
| 135 |
+
## What You Don't Have To Do
|
| 136 |
+
|
| 137 |
+
- Follow a script
|
| 138 |
+
- Ask the same questions every time
|
| 139 |
+
- Produce a specific artifact
|
| 140 |
+
- Reach a conclusion
|
| 141 |
+
- Stay on topic if a tangent is valuable
|
| 142 |
+
- Be brief (this is thinking time)
|
| 143 |
+
|
| 144 |
+
---
|
| 145 |
+
|
| 146 |
+
## Handling Different Entry Points
|
| 147 |
+
|
| 148 |
+
**User brings a vague idea:**
|
| 149 |
+
```
|
| 150 |
+
User: I'm thinking about adding real-time collaboration
|
| 151 |
+
|
| 152 |
+
You: Real-time collab is a big space. Let me think about this...
|
| 153 |
+
|
| 154 |
+
COLLABORATION SPECTRUM
|
| 155 |
+
════════════════════════════════════════════
|
| 156 |
+
|
| 157 |
+
Awareness Coordination Sync
|
| 158 |
+
│ │ │
|
| 159 |
+
▼ ▼ ▼
|
| 160 |
+
┌────────┐ ┌────────┐ ┌────────┐
|
| 161 |
+
│Presence│ │Cursors │ │ CRDT │
|
| 162 |
+
│ "3 │ │ Multi │ │Conflict│
|
| 163 |
+
│online" │ │ select │ │ free │
|
| 164 |
+
└────────┘ └────────┘ └────────┘
|
| 165 |
+
│ │ │
|
| 166 |
+
trivial moderate complex
|
| 167 |
+
|
| 168 |
+
Where's your head at?
|
| 169 |
+
```
|
| 170 |
+
|
| 171 |
+
**User brings a specific problem:**
|
| 172 |
+
```
|
| 173 |
+
User: The auth system is a mess
|
| 174 |
+
|
| 175 |
+
You: [reads codebase]
|
| 176 |
+
|
| 177 |
+
┌─────────────────────────────────────────────┐
|
| 178 |
+
│ CURRENT AUTH FLOW │
|
| 179 |
+
└─────────────────────────────────────────────┘
|
| 180 |
+
│
|
| 181 |
+
┌─────────────┼─────────────┐
|
| 182 |
+
▼ ▼ ▼
|
| 183 |
+
┌─────────┐ ┌─────────┐ ┌─────────┐
|
| 184 |
+
│ Google │ │ GitHub │ │ Email │
|
| 185 |
+
│ OAuth │ │ OAuth │ │ Magic │
|
| 186 |
+
└────┬────┘ └────┬────┘ └────┬────┘
|
| 187 |
+
│ │ │
|
| 188 |
+
└─────────────┼─────────────┘
|
| 189 |
+
▼
|
| 190 |
+
┌───────────┐
|
| 191 |
+
│ Session │
|
| 192 |
+
└─────┬─────┘
|
| 193 |
+
│
|
| 194 |
+
▼
|
| 195 |
+
┌───────────┐
|
| 196 |
+
│ Perms │
|
| 197 |
+
└───────────┘
|
| 198 |
+
|
| 199 |
+
I see three tangles. Which one's burning?
|
| 200 |
+
```
|
| 201 |
+
|
| 202 |
+
**User is stuck mid-implementation:**
|
| 203 |
+
```
|
| 204 |
+
User: /opsx:explore add-auth-system
|
| 205 |
+
The OAuth integration is more complex than expected
|
| 206 |
+
|
| 207 |
+
You: [reads change artifacts]
|
| 208 |
+
|
| 209 |
+
You're on task 4: "Implement OAuth flow"
|
| 210 |
+
|
| 211 |
+
Let me trace what's involved...
|
| 212 |
+
|
| 213 |
+
[draws diagram, explores options, suggests paths]
|
| 214 |
+
|
| 215 |
+
Want to update the design to reflect this?
|
| 216 |
+
Or add a spike task to investigate?
|
| 217 |
+
```
|
| 218 |
+
|
| 219 |
+
**User wants to compare options:**
|
| 220 |
+
```
|
| 221 |
+
User: Should we use Postgres or SQLite?
|
| 222 |
+
|
| 223 |
+
You: Generic answer is boring. What's the context?
|
| 224 |
+
|
| 225 |
+
User: A CLI tool that tracks local dev environments
|
| 226 |
+
|
| 227 |
+
You: That changes everything.
|
| 228 |
+
|
| 229 |
+
┌─────────────────────────────────────────────────┐
|
| 230 |
+
│ CLI TOOL DATA STORAGE │
|
| 231 |
+
└─────────────────────────────────────────────────┘
|
| 232 |
+
|
| 233 |
+
Key constraints:
|
| 234 |
+
• No daemon running
|
| 235 |
+
• Must work offline
|
| 236 |
+
• Single user
|
| 237 |
+
|
| 238 |
+
SQLite Postgres
|
| 239 |
+
Deployment embedded ✓ needs server ✗
|
| 240 |
+
Offline yes ✓ no ✗
|
| 241 |
+
Single file yes ✓ no ✗
|
| 242 |
+
|
| 243 |
+
SQLite. Not even close.
|
| 244 |
+
|
| 245 |
+
Unless... is there a sync component?
|
| 246 |
+
```
|
| 247 |
+
|
| 248 |
+
---
|
| 249 |
+
|
| 250 |
+
## Ending Discovery
|
| 251 |
+
|
| 252 |
+
There's no required ending. Discovery might:
|
| 253 |
+
|
| 254 |
+
- **Flow into a proposal**: "Ready to start? I can create a change proposal."
|
| 255 |
+
- **Result in artifact updates**: "Updated design.md with these decisions"
|
| 256 |
+
- **Just provide clarity**: User has what they need, moves on
|
| 257 |
+
- **Continue later**: "We can pick this up anytime"
|
| 258 |
+
|
| 259 |
+
When it feels like things are crystallizing, you might summarize:
|
| 260 |
+
|
| 261 |
+
```
|
| 262 |
+
## What We Figured Out
|
| 263 |
+
|
| 264 |
+
**The problem**: [crystallized understanding]
|
| 265 |
+
|
| 266 |
+
**The approach**: [if one emerged]
|
| 267 |
+
|
| 268 |
+
**Open questions**: [if any remain]
|
| 269 |
+
|
| 270 |
+
**Next steps** (if ready):
|
| 271 |
+
- Create a change proposal
|
| 272 |
+
- Keep exploring: just keep talking
|
| 273 |
+
```
|
| 274 |
+
|
| 275 |
+
But this summary is optional. Sometimes the thinking IS the value.
|
| 276 |
+
|
| 277 |
+
---
|
| 278 |
+
|
| 279 |
+
## Guardrails
|
| 280 |
+
|
| 281 |
+
- **Don't implement** - Never write code or implement features. Creating OpenSpec artifacts is fine, writing application code is not.
|
| 282 |
+
- **Don't fake understanding** - If something is unclear, dig deeper
|
| 283 |
+
- **Don't rush** - Discovery is thinking time, not task time
|
| 284 |
+
- **Don't force structure** - Let patterns emerge naturally
|
| 285 |
+
- **Don't auto-capture** - Offer to save insights, don't just do it
|
| 286 |
+
- **Do visualize** - A good diagram is worth many paragraphs
|
| 287 |
+
- **Do explore the codebase** - Ground discussions in reality
|
| 288 |
+
- **Do question assumptions** - Including the user's and your own
|
.kilocode/skills/openspec-propose/SKILL.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: openspec-propose
|
| 3 |
+
description: Propose a new change with all artifacts generated in one step. Use when the user wants to quickly describe what they want to build and get a complete proposal with design, specs, and tasks ready for implementation.
|
| 4 |
+
license: MIT
|
| 5 |
+
compatibility: Requires openspec CLI.
|
| 6 |
+
metadata:
|
| 7 |
+
author: openspec
|
| 8 |
+
version: "1.0"
|
| 9 |
+
generatedBy: "1.3.1"
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
Propose a new change - create the change and generate all artifacts in one step.
|
| 13 |
+
|
| 14 |
+
I'll create a change with artifacts:
|
| 15 |
+
- proposal.md (what & why)
|
| 16 |
+
- design.md (how)
|
| 17 |
+
- tasks.md (implementation steps)
|
| 18 |
+
|
| 19 |
+
When ready to implement, run /opsx:apply
|
| 20 |
+
|
| 21 |
+
---
|
| 22 |
+
|
| 23 |
+
**Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build.
|
| 24 |
+
|
| 25 |
+
**Steps**
|
| 26 |
+
|
| 27 |
+
1. **If no clear input provided, ask what they want to build**
|
| 28 |
+
|
| 29 |
+
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
|
| 30 |
+
> "What change do you want to work on? Describe what you want to build or fix."
|
| 31 |
+
|
| 32 |
+
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
|
| 33 |
+
|
| 34 |
+
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
|
| 35 |
+
|
| 36 |
+
2. **Create the change directory**
|
| 37 |
+
```bash
|
| 38 |
+
openspec new change "<name>"
|
| 39 |
+
```
|
| 40 |
+
This creates a scaffolded change at `openspec/changes/<name>/` with `.openspec.yaml`.
|
| 41 |
+
|
| 42 |
+
3. **Get the artifact build order**
|
| 43 |
+
```bash
|
| 44 |
+
openspec status --change "<name>" --json
|
| 45 |
+
```
|
| 46 |
+
Parse the JSON to get:
|
| 47 |
+
- `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`)
|
| 48 |
+
- `artifacts`: list of all artifacts with their status and dependencies
|
| 49 |
+
|
| 50 |
+
4. **Create artifacts in sequence until apply-ready**
|
| 51 |
+
|
| 52 |
+
Use the **TodoWrite tool** to track progress through the artifacts.
|
| 53 |
+
|
| 54 |
+
Loop through artifacts in dependency order (artifacts with no pending dependencies first):
|
| 55 |
+
|
| 56 |
+
a. **For each artifact that is `ready` (dependencies satisfied)**:
|
| 57 |
+
- Get instructions:
|
| 58 |
+
```bash
|
| 59 |
+
openspec instructions <artifact-id> --change "<name>" --json
|
| 60 |
+
```
|
| 61 |
+
- The instructions JSON includes:
|
| 62 |
+
- `context`: Project background (constraints for you - do NOT include in output)
|
| 63 |
+
- `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
|
| 64 |
+
- `template`: The structure to use for your output file
|
| 65 |
+
- `instruction`: Schema-specific guidance for this artifact type
|
| 66 |
+
- `outputPath`: Where to write the artifact
|
| 67 |
+
- `dependencies`: Completed artifacts to read for context
|
| 68 |
+
- Read any completed dependency files for context
|
| 69 |
+
- Create the artifact file using `template` as the structure
|
| 70 |
+
- Apply `context` and `rules` as constraints - but do NOT copy them into the file
|
| 71 |
+
- Show brief progress: "Created <artifact-id>"
|
| 72 |
+
|
| 73 |
+
b. **Continue until all `applyRequires` artifacts are complete**
|
| 74 |
+
- After creating each artifact, re-run `openspec status --change "<name>" --json`
|
| 75 |
+
- Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array
|
| 76 |
+
- Stop when all `applyRequires` artifacts are done
|
| 77 |
+
|
| 78 |
+
c. **If an artifact requires user input** (unclear context):
|
| 79 |
+
- Use **AskUserQuestion tool** to clarify
|
| 80 |
+
- Then continue with creation
|
| 81 |
+
|
| 82 |
+
5. **Show final status**
|
| 83 |
+
```bash
|
| 84 |
+
openspec status --change "<name>"
|
| 85 |
+
```
|
| 86 |
+
|
| 87 |
+
**Output**
|
| 88 |
+
|
| 89 |
+
After completing all artifacts, summarize:
|
| 90 |
+
- Change name and location
|
| 91 |
+
- List of artifacts created with brief descriptions
|
| 92 |
+
- What's ready: "All artifacts created! Ready for implementation."
|
| 93 |
+
- Prompt: "Run `/opsx:apply` or ask me to implement to start working on the tasks."
|
| 94 |
+
|
| 95 |
+
**Artifact Creation Guidelines**
|
| 96 |
+
|
| 97 |
+
- Follow the `instruction` field from `openspec instructions` for each artifact type
|
| 98 |
+
- The schema defines what each artifact should contain - follow it
|
| 99 |
+
- Read dependency artifacts for context before creating new ones
|
| 100 |
+
- Use `template` as the structure for your output file - fill in its sections
|
| 101 |
+
- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file
|
| 102 |
+
- Do NOT copy `<context>`, `<rules>`, `<project_context>` blocks into the artifact
|
| 103 |
+
- These guide what you write, but should never appear in the output
|
| 104 |
+
|
| 105 |
+
**Guardrails**
|
| 106 |
+
- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`)
|
| 107 |
+
- Always read dependency artifacts before creating a new one
|
| 108 |
+
- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
|
| 109 |
+
- If a change with that name already exists, ask if user wants to continue it or create a new one
|
| 110 |
+
- Verify each artifact file exists after writing before proceeding to next
|
.kilocode/workflows/opsx-apply.md
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Implement tasks from an OpenSpec change.
|
| 2 |
+
|
| 3 |
+
**Input**: Optionally specify a change name (e.g., `/opsx:apply add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
| 4 |
+
|
| 5 |
+
**Steps**
|
| 6 |
+
|
| 7 |
+
1. **Select the change**
|
| 8 |
+
|
| 9 |
+
If a name is provided, use it. Otherwise:
|
| 10 |
+
- Infer from conversation context if the user mentioned a change
|
| 11 |
+
- Auto-select if only one active change exists
|
| 12 |
+
- If ambiguous, run `openspec list --json` to get available changes and use the **AskUserQuestion tool** to let the user select
|
| 13 |
+
|
| 14 |
+
Always announce: "Using change: <name>" and how to override (e.g., `/opsx:apply <other>`).
|
| 15 |
+
|
| 16 |
+
2. **Check status to understand the schema**
|
| 17 |
+
```bash
|
| 18 |
+
openspec status --change "<name>" --json
|
| 19 |
+
```
|
| 20 |
+
Parse the JSON to understand:
|
| 21 |
+
- `schemaName`: The workflow being used (e.g., "spec-driven")
|
| 22 |
+
- Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
|
| 23 |
+
|
| 24 |
+
3. **Get apply instructions**
|
| 25 |
+
|
| 26 |
+
```bash
|
| 27 |
+
openspec instructions apply --change "<name>" --json
|
| 28 |
+
```
|
| 29 |
+
|
| 30 |
+
This returns:
|
| 31 |
+
- `contextFiles`: artifact ID -> array of concrete file paths (varies by schema)
|
| 32 |
+
- Progress (total, complete, remaining)
|
| 33 |
+
- Task list with status
|
| 34 |
+
- Dynamic instruction based on current state
|
| 35 |
+
|
| 36 |
+
**Handle states:**
|
| 37 |
+
- If `state: "blocked"` (missing artifacts): show message, suggest using `/opsx:continue`
|
| 38 |
+
- If `state: "all_done"`: congratulate, suggest archive
|
| 39 |
+
- Otherwise: proceed to implementation
|
| 40 |
+
|
| 41 |
+
4. **Read context files**
|
| 42 |
+
|
| 43 |
+
Read every file path listed under `contextFiles` from the apply instructions output.
|
| 44 |
+
The files depend on the schema being used:
|
| 45 |
+
- **spec-driven**: proposal, specs, design, tasks
|
| 46 |
+
- Other schemas: follow the contextFiles from CLI output
|
| 47 |
+
|
| 48 |
+
5. **Show current progress**
|
| 49 |
+
|
| 50 |
+
Display:
|
| 51 |
+
- Schema being used
|
| 52 |
+
- Progress: "N/M tasks complete"
|
| 53 |
+
- Remaining tasks overview
|
| 54 |
+
- Dynamic instruction from CLI
|
| 55 |
+
|
| 56 |
+
6. **Implement tasks (loop until done or blocked)**
|
| 57 |
+
|
| 58 |
+
For each pending task:
|
| 59 |
+
- Show which task is being worked on
|
| 60 |
+
- Make the code changes required
|
| 61 |
+
- Keep changes minimal and focused
|
| 62 |
+
- Mark task complete in the tasks file: `- [ ]` → `- [x]`
|
| 63 |
+
- Continue to next task
|
| 64 |
+
|
| 65 |
+
**Pause if:**
|
| 66 |
+
- Task is unclear → ask for clarification
|
| 67 |
+
- Implementation reveals a design issue → suggest updating artifacts
|
| 68 |
+
- Error or blocker encountered → report and wait for guidance
|
| 69 |
+
- User interrupts
|
| 70 |
+
|
| 71 |
+
7. **On completion or pause, show status**
|
| 72 |
+
|
| 73 |
+
Display:
|
| 74 |
+
- Tasks completed this session
|
| 75 |
+
- Overall progress: "N/M tasks complete"
|
| 76 |
+
- If all done: suggest archive
|
| 77 |
+
- If paused: explain why and wait for guidance
|
| 78 |
+
|
| 79 |
+
**Output During Implementation**
|
| 80 |
+
|
| 81 |
+
```
|
| 82 |
+
## Implementing: <change-name> (schema: <schema-name>)
|
| 83 |
+
|
| 84 |
+
Working on task 3/7: <task description>
|
| 85 |
+
[...implementation happening...]
|
| 86 |
+
✓ Task complete
|
| 87 |
+
|
| 88 |
+
Working on task 4/7: <task description>
|
| 89 |
+
[...implementation happening...]
|
| 90 |
+
✓ Task complete
|
| 91 |
+
```
|
| 92 |
+
|
| 93 |
+
**Output On Completion**
|
| 94 |
+
|
| 95 |
+
```
|
| 96 |
+
## Implementation Complete
|
| 97 |
+
|
| 98 |
+
**Change:** <change-name>
|
| 99 |
+
**Schema:** <schema-name>
|
| 100 |
+
**Progress:** 7/7 tasks complete ✓
|
| 101 |
+
|
| 102 |
+
### Completed This Session
|
| 103 |
+
- [x] Task 1
|
| 104 |
+
- [x] Task 2
|
| 105 |
+
...
|
| 106 |
+
|
| 107 |
+
All tasks complete! You can archive this change with `/opsx:archive`.
|
| 108 |
+
```
|
| 109 |
+
|
| 110 |
+
**Output On Pause (Issue Encountered)**
|
| 111 |
+
|
| 112 |
+
```
|
| 113 |
+
## Implementation Paused
|
| 114 |
+
|
| 115 |
+
**Change:** <change-name>
|
| 116 |
+
**Schema:** <schema-name>
|
| 117 |
+
**Progress:** 4/7 tasks complete
|
| 118 |
+
|
| 119 |
+
### Issue Encountered
|
| 120 |
+
<description of the issue>
|
| 121 |
+
|
| 122 |
+
**Options:**
|
| 123 |
+
1. <option 1>
|
| 124 |
+
2. <option 2>
|
| 125 |
+
3. Other approach
|
| 126 |
+
|
| 127 |
+
What would you like to do?
|
| 128 |
+
```
|
| 129 |
+
|
| 130 |
+
**Guardrails**
|
| 131 |
+
- Keep going through tasks until done or blocked
|
| 132 |
+
- Always read context files before starting (from the apply instructions output)
|
| 133 |
+
- If task is ambiguous, pause and ask before implementing
|
| 134 |
+
- If implementation reveals issues, pause and suggest artifact updates
|
| 135 |
+
- Keep code changes minimal and scoped to each task
|
| 136 |
+
- Update task checkbox immediately after completing each task
|
| 137 |
+
- Pause on errors, blockers, or unclear requirements - don't guess
|
| 138 |
+
- Use contextFiles from CLI output, don't assume specific file names
|
| 139 |
+
|
| 140 |
+
**Fluid Workflow Integration**
|
| 141 |
+
|
| 142 |
+
This skill supports the "actions on a change" model:
|
| 143 |
+
|
| 144 |
+
- **Can be invoked anytime**: Before all artifacts are done (if tasks exist), after partial implementation, interleaved with other actions
|
| 145 |
+
- **Allows artifact updates**: If implementation reveals design issues, suggest updating artifacts - not phase-locked, work fluidly
|
.kilocode/workflows/opsx-archive.md
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Archive a completed change in the experimental workflow.
|
| 2 |
+
|
| 3 |
+
**Input**: Optionally specify a change name after `/opsx:archive` (e.g., `/opsx:archive add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
| 4 |
+
|
| 5 |
+
**Steps**
|
| 6 |
+
|
| 7 |
+
1. **If no change name provided, prompt for selection**
|
| 8 |
+
|
| 9 |
+
Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
|
| 10 |
+
|
| 11 |
+
Show only active changes (not already archived).
|
| 12 |
+
Include the schema used for each change if available.
|
| 13 |
+
|
| 14 |
+
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
| 15 |
+
|
| 16 |
+
2. **Check artifact completion status**
|
| 17 |
+
|
| 18 |
+
Run `openspec status --change "<name>" --json` to check artifact completion.
|
| 19 |
+
|
| 20 |
+
Parse the JSON to understand:
|
| 21 |
+
- `schemaName`: The workflow being used
|
| 22 |
+
- `artifacts`: List of artifacts with their status (`done` or other)
|
| 23 |
+
|
| 24 |
+
**If any artifacts are not `done`:**
|
| 25 |
+
- Display warning listing incomplete artifacts
|
| 26 |
+
- Prompt user for confirmation to continue
|
| 27 |
+
- Proceed if user confirms
|
| 28 |
+
|
| 29 |
+
3. **Check task completion status**
|
| 30 |
+
|
| 31 |
+
Read the tasks file (typically `tasks.md`) to check for incomplete tasks.
|
| 32 |
+
|
| 33 |
+
Count tasks marked with `- [ ]` (incomplete) vs `- [x]` (complete).
|
| 34 |
+
|
| 35 |
+
**If incomplete tasks found:**
|
| 36 |
+
- Display warning showing count of incomplete tasks
|
| 37 |
+
- Prompt user for confirmation to continue
|
| 38 |
+
- Proceed if user confirms
|
| 39 |
+
|
| 40 |
+
**If no tasks file exists:** Proceed without task-related warning.
|
| 41 |
+
|
| 42 |
+
4. **Assess delta spec sync state**
|
| 43 |
+
|
| 44 |
+
Check for delta specs at `openspec/changes/<name>/specs/`. If none exist, proceed without sync prompt.
|
| 45 |
+
|
| 46 |
+
**If delta specs exist:**
|
| 47 |
+
- Compare each delta spec with its corresponding main spec at `openspec/specs/<capability>/spec.md`
|
| 48 |
+
- Determine what changes would be applied (adds, modifications, removals, renames)
|
| 49 |
+
- Show a combined summary before prompting
|
| 50 |
+
|
| 51 |
+
**Prompt options:**
|
| 52 |
+
- If changes needed: "Sync now (recommended)", "Archive without syncing"
|
| 53 |
+
- If already synced: "Archive now", "Sync anyway", "Cancel"
|
| 54 |
+
|
| 55 |
+
If user chooses sync, use Task tool (subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change '<name>'. Delta spec analysis: <include the analyzed delta spec summary>"). Proceed to archive regardless of choice.
|
| 56 |
+
|
| 57 |
+
5. **Perform the archive**
|
| 58 |
+
|
| 59 |
+
Create the archive directory if it doesn't exist:
|
| 60 |
+
```bash
|
| 61 |
+
mkdir -p openspec/changes/archive
|
| 62 |
+
```
|
| 63 |
+
|
| 64 |
+
Generate target name using current date: `YYYY-MM-DD-<change-name>`
|
| 65 |
+
|
| 66 |
+
**Check if target already exists:**
|
| 67 |
+
- If yes: Fail with error, suggest renaming existing archive or using different date
|
| 68 |
+
- If no: Move the change directory to archive
|
| 69 |
+
|
| 70 |
+
```bash
|
| 71 |
+
mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name>
|
| 72 |
+
```
|
| 73 |
+
|
| 74 |
+
6. **Display summary**
|
| 75 |
+
|
| 76 |
+
Show archive completion summary including:
|
| 77 |
+
- Change name
|
| 78 |
+
- Schema that was used
|
| 79 |
+
- Archive location
|
| 80 |
+
- Spec sync status (synced / sync skipped / no delta specs)
|
| 81 |
+
- Note about any warnings (incomplete artifacts/tasks)
|
| 82 |
+
|
| 83 |
+
**Output On Success**
|
| 84 |
+
|
| 85 |
+
```
|
| 86 |
+
## Archive Complete
|
| 87 |
+
|
| 88 |
+
**Change:** <change-name>
|
| 89 |
+
**Schema:** <schema-name>
|
| 90 |
+
**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
| 91 |
+
**Specs:** ✓ Synced to main specs
|
| 92 |
+
|
| 93 |
+
All artifacts complete. All tasks complete.
|
| 94 |
+
```
|
| 95 |
+
|
| 96 |
+
**Output On Success (No Delta Specs)**
|
| 97 |
+
|
| 98 |
+
```
|
| 99 |
+
## Archive Complete
|
| 100 |
+
|
| 101 |
+
**Change:** <change-name>
|
| 102 |
+
**Schema:** <schema-name>
|
| 103 |
+
**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
| 104 |
+
**Specs:** No delta specs
|
| 105 |
+
|
| 106 |
+
All artifacts complete. All tasks complete.
|
| 107 |
+
```
|
| 108 |
+
|
| 109 |
+
**Output On Success With Warnings**
|
| 110 |
+
|
| 111 |
+
```
|
| 112 |
+
## Archive Complete (with warnings)
|
| 113 |
+
|
| 114 |
+
**Change:** <change-name>
|
| 115 |
+
**Schema:** <schema-name>
|
| 116 |
+
**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
| 117 |
+
**Specs:** Sync skipped (user chose to skip)
|
| 118 |
+
|
| 119 |
+
**Warnings:**
|
| 120 |
+
- Archived with 2 incomplete artifacts
|
| 121 |
+
- Archived with 3 incomplete tasks
|
| 122 |
+
- Delta spec sync was skipped (user chose to skip)
|
| 123 |
+
|
| 124 |
+
Review the archive if this was not intentional.
|
| 125 |
+
```
|
| 126 |
+
|
| 127 |
+
**Output On Error (Archive Exists)**
|
| 128 |
+
|
| 129 |
+
```
|
| 130 |
+
## Archive Failed
|
| 131 |
+
|
| 132 |
+
**Change:** <change-name>
|
| 133 |
+
**Target:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
| 134 |
+
|
| 135 |
+
Target archive directory already exists.
|
| 136 |
+
|
| 137 |
+
**Options:**
|
| 138 |
+
1. Rename the existing archive
|
| 139 |
+
2. Delete the existing archive if it's a duplicate
|
| 140 |
+
3. Wait until a different date to archive
|
| 141 |
+
```
|
| 142 |
+
|
| 143 |
+
**Guardrails**
|
| 144 |
+
- Always prompt for change selection if not provided
|
| 145 |
+
- Use artifact graph (openspec status --json) for completion checking
|
| 146 |
+
- Don't block archive on warnings - just inform and confirm
|
| 147 |
+
- Preserve .openspec.yaml when moving to archive (it moves with the directory)
|
| 148 |
+
- Show clear summary of what happened
|
| 149 |
+
- If sync is requested, use the Skill tool to invoke `openspec-sync-specs` (agent-driven)
|
| 150 |
+
- If delta specs exist, always run the sync assessment and show the combined summary before prompting
|
.kilocode/workflows/opsx-explore.md
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes.
|
| 2 |
+
|
| 3 |
+
**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first and create a change proposal. You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing.
|
| 4 |
+
|
| 5 |
+
**This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore.
|
| 6 |
+
|
| 7 |
+
**Input**: The argument after `/opsx:explore` is whatever the user wants to think about. Could be:
|
| 8 |
+
- A vague idea: "real-time collaboration"
|
| 9 |
+
- A specific problem: "the auth system is getting unwieldy"
|
| 10 |
+
- A change name: "add-dark-mode" (to explore in context of that change)
|
| 11 |
+
- A comparison: "postgres vs sqlite for this"
|
| 12 |
+
- Nothing (just enter explore mode)
|
| 13 |
+
|
| 14 |
+
---
|
| 15 |
+
|
| 16 |
+
## The Stance
|
| 17 |
+
|
| 18 |
+
- **Curious, not prescriptive** - Ask questions that emerge naturally, don't follow a script
|
| 19 |
+
- **Open threads, not interrogations** - Surface multiple interesting directions and let the user follow what resonates. Don't funnel them through a single path of questions.
|
| 20 |
+
- **Visual** - Use ASCII diagrams liberally when they'd help clarify thinking
|
| 21 |
+
- **Adaptive** - Follow interesting threads, pivot when new information emerges
|
| 22 |
+
- **Patient** - Don't rush to conclusions, let the shape of the problem emerge
|
| 23 |
+
- **Grounded** - Explore the actual codebase when relevant, don't just theorize
|
| 24 |
+
|
| 25 |
+
---
|
| 26 |
+
|
| 27 |
+
## What You Might Do
|
| 28 |
+
|
| 29 |
+
Depending on what the user brings, you might:
|
| 30 |
+
|
| 31 |
+
**Explore the problem space**
|
| 32 |
+
- Ask clarifying questions that emerge from what they said
|
| 33 |
+
- Challenge assumptions
|
| 34 |
+
- Reframe the problem
|
| 35 |
+
- Find analogies
|
| 36 |
+
|
| 37 |
+
**Investigate the codebase**
|
| 38 |
+
- Map existing architecture relevant to the discussion
|
| 39 |
+
- Find integration points
|
| 40 |
+
- Identify patterns already in use
|
| 41 |
+
- Surface hidden complexity
|
| 42 |
+
|
| 43 |
+
**Compare options**
|
| 44 |
+
- Brainstorm multiple approaches
|
| 45 |
+
- Build comparison tables
|
| 46 |
+
- Sketch tradeoffs
|
| 47 |
+
- Recommend a path (if asked)
|
| 48 |
+
|
| 49 |
+
**Visualize**
|
| 50 |
+
```
|
| 51 |
+
┌─────────────────────────────────────────┐
|
| 52 |
+
│ Use ASCII diagrams liberally │
|
| 53 |
+
├─────────────────────────────────────────┤
|
| 54 |
+
│ │
|
| 55 |
+
│ ┌────────┐ ┌────────┐ │
|
| 56 |
+
│ │ State │────────▶│ State │ │
|
| 57 |
+
│ │ A │ │ B │ │
|
| 58 |
+
│ └────────┘ └────────┘ │
|
| 59 |
+
│ │
|
| 60 |
+
│ System diagrams, state machines, │
|
| 61 |
+
│ data flows, architecture sketches, │
|
| 62 |
+
│ dependency graphs, comparison tables │
|
| 63 |
+
│ │
|
| 64 |
+
└─────────────────────────────────────────┘
|
| 65 |
+
```
|
| 66 |
+
|
| 67 |
+
**Surface risks and unknowns**
|
| 68 |
+
- Identify what could go wrong
|
| 69 |
+
- Find gaps in understanding
|
| 70 |
+
- Suggest spikes or investigations
|
| 71 |
+
|
| 72 |
+
---
|
| 73 |
+
|
| 74 |
+
## OpenSpec Awareness
|
| 75 |
+
|
| 76 |
+
You have full context of the OpenSpec system. Use it naturally, don't force it.
|
| 77 |
+
|
| 78 |
+
### Check for context
|
| 79 |
+
|
| 80 |
+
At the start, quickly check what exists:
|
| 81 |
+
```bash
|
| 82 |
+
openspec list --json
|
| 83 |
+
```
|
| 84 |
+
|
| 85 |
+
This tells you:
|
| 86 |
+
- If there are active changes
|
| 87 |
+
- Their names, schemas, and status
|
| 88 |
+
- What the user might be working on
|
| 89 |
+
|
| 90 |
+
If the user mentioned a specific change name, read its artifacts for context.
|
| 91 |
+
|
| 92 |
+
### When no change exists
|
| 93 |
+
|
| 94 |
+
Think freely. When insights crystallize, you might offer:
|
| 95 |
+
|
| 96 |
+
- "This feels solid enough to start a change. Want me to create a proposal?"
|
| 97 |
+
- Or keep exploring - no pressure to formalize
|
| 98 |
+
|
| 99 |
+
### When a change exists
|
| 100 |
+
|
| 101 |
+
If the user mentions a change or you detect one is relevant:
|
| 102 |
+
|
| 103 |
+
1. **Read existing artifacts for context**
|
| 104 |
+
- `openspec/changes/<name>/proposal.md`
|
| 105 |
+
- `openspec/changes/<name>/design.md`
|
| 106 |
+
- `openspec/changes/<name>/tasks.md`
|
| 107 |
+
- etc.
|
| 108 |
+
|
| 109 |
+
2. **Reference them naturally in conversation**
|
| 110 |
+
- "Your design mentions using Redis, but we just realized SQLite fits better..."
|
| 111 |
+
- "The proposal scopes this to premium users, but we're now thinking everyone..."
|
| 112 |
+
|
| 113 |
+
3. **Offer to capture when decisions are made**
|
| 114 |
+
|
| 115 |
+
| Insight Type | Where to Capture |
|
| 116 |
+
|----------------------------|--------------------------------|
|
| 117 |
+
| New requirement discovered | `specs/<capability>/spec.md` |
|
| 118 |
+
| Requirement changed | `specs/<capability>/spec.md` |
|
| 119 |
+
| Design decision made | `design.md` |
|
| 120 |
+
| Scope changed | `proposal.md` |
|
| 121 |
+
| New work identified | `tasks.md` |
|
| 122 |
+
| Assumption invalidated | Relevant artifact |
|
| 123 |
+
|
| 124 |
+
Example offers:
|
| 125 |
+
- "That's a design decision. Capture it in design.md?"
|
| 126 |
+
- "This is a new requirement. Add it to specs?"
|
| 127 |
+
- "This changes scope. Update the proposal?"
|
| 128 |
+
|
| 129 |
+
4. **The user decides** - Offer and move on. Don't pressure. Don't auto-capture.
|
| 130 |
+
|
| 131 |
+
---
|
| 132 |
+
|
| 133 |
+
## What You Don't Have To Do
|
| 134 |
+
|
| 135 |
+
- Follow a script
|
| 136 |
+
- Ask the same questions every time
|
| 137 |
+
- Produce a specific artifact
|
| 138 |
+
- Reach a conclusion
|
| 139 |
+
- Stay on topic if a tangent is valuable
|
| 140 |
+
- Be brief (this is thinking time)
|
| 141 |
+
|
| 142 |
+
---
|
| 143 |
+
|
| 144 |
+
## Ending Discovery
|
| 145 |
+
|
| 146 |
+
There's no required ending. Discovery might:
|
| 147 |
+
|
| 148 |
+
- **Flow into a proposal**: "Ready to start? I can create a change proposal."
|
| 149 |
+
- **Result in artifact updates**: "Updated design.md with these decisions"
|
| 150 |
+
- **Just provide clarity**: User has what they need, moves on
|
| 151 |
+
- **Continue later**: "We can pick this up anytime"
|
| 152 |
+
|
| 153 |
+
When things crystallize, you might offer a summary - but it's optional. Sometimes the thinking IS the value.
|
| 154 |
+
|
| 155 |
+
---
|
| 156 |
+
|
| 157 |
+
## Guardrails
|
| 158 |
+
|
| 159 |
+
- **Don't implement** - Never write code or implement features. Creating OpenSpec artifacts is fine, writing application code is not.
|
| 160 |
+
- **Don't fake understanding** - If something is unclear, dig deeper
|
| 161 |
+
- **Don't rush** - Discovery is thinking time, not task time
|
| 162 |
+
- **Don't force structure** - Let patterns emerge naturally
|
| 163 |
+
- **Don't auto-capture** - Offer to save insights, don't just do it
|
| 164 |
+
- **Do visualize** - A good diagram is worth many paragraphs
|
| 165 |
+
- **Do explore the codebase** - Ground discussions in reality
|
| 166 |
+
- **Do question assumptions** - Including the user's and your own
|
.kilocode/workflows/opsx-propose.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Propose a new change - create the change and generate all artifacts in one step.
|
| 2 |
+
|
| 3 |
+
I'll create a change with artifacts:
|
| 4 |
+
- proposal.md (what & why)
|
| 5 |
+
- design.md (how)
|
| 6 |
+
- tasks.md (implementation steps)
|
| 7 |
+
|
| 8 |
+
When ready to implement, run /opsx:apply
|
| 9 |
+
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
**Input**: The argument after `/opsx:propose` is the change name (kebab-case), OR a description of what the user wants to build.
|
| 13 |
+
|
| 14 |
+
**Steps**
|
| 15 |
+
|
| 16 |
+
1. **If no input provided, ask what they want to build**
|
| 17 |
+
|
| 18 |
+
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
|
| 19 |
+
> "What change do you want to work on? Describe what you want to build or fix."
|
| 20 |
+
|
| 21 |
+
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
|
| 22 |
+
|
| 23 |
+
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
|
| 24 |
+
|
| 25 |
+
2. **Create the change directory**
|
| 26 |
+
```bash
|
| 27 |
+
openspec new change "<name>"
|
| 28 |
+
```
|
| 29 |
+
This creates a scaffolded change at `openspec/changes/<name>/` with `.openspec.yaml`.
|
| 30 |
+
|
| 31 |
+
3. **Get the artifact build order**
|
| 32 |
+
```bash
|
| 33 |
+
openspec status --change "<name>" --json
|
| 34 |
+
```
|
| 35 |
+
Parse the JSON to get:
|
| 36 |
+
- `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`)
|
| 37 |
+
- `artifacts`: list of all artifacts with their status and dependencies
|
| 38 |
+
|
| 39 |
+
4. **Create artifacts in sequence until apply-ready**
|
| 40 |
+
|
| 41 |
+
Use the **TodoWrite tool** to track progress through the artifacts.
|
| 42 |
+
|
| 43 |
+
Loop through artifacts in dependency order (artifacts with no pending dependencies first):
|
| 44 |
+
|
| 45 |
+
a. **For each artifact that is `ready` (dependencies satisfied)**:
|
| 46 |
+
- Get instructions:
|
| 47 |
+
```bash
|
| 48 |
+
openspec instructions <artifact-id> --change "<name>" --json
|
| 49 |
+
```
|
| 50 |
+
- The instructions JSON includes:
|
| 51 |
+
- `context`: Project background (constraints for you - do NOT include in output)
|
| 52 |
+
- `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
|
| 53 |
+
- `template`: The structure to use for your output file
|
| 54 |
+
- `instruction`: Schema-specific guidance for this artifact type
|
| 55 |
+
- `outputPath`: Where to write the artifact
|
| 56 |
+
- `dependencies`: Completed artifacts to read for context
|
| 57 |
+
- Read any completed dependency files for context
|
| 58 |
+
- Create the artifact file using `template` as the structure
|
| 59 |
+
- Apply `context` and `rules` as constraints - but do NOT copy them into the file
|
| 60 |
+
- Show brief progress: "Created <artifact-id>"
|
| 61 |
+
|
| 62 |
+
b. **Continue until all `applyRequires` artifacts are complete**
|
| 63 |
+
- After creating each artifact, re-run `openspec status --change "<name>" --json`
|
| 64 |
+
- Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array
|
| 65 |
+
- Stop when all `applyRequires` artifacts are done
|
| 66 |
+
|
| 67 |
+
c. **If an artifact requires user input** (unclear context):
|
| 68 |
+
- Use **AskUserQuestion tool** to clarify
|
| 69 |
+
- Then continue with creation
|
| 70 |
+
|
| 71 |
+
5. **Show final status**
|
| 72 |
+
```bash
|
| 73 |
+
openspec status --change "<name>"
|
| 74 |
+
```
|
| 75 |
+
|
| 76 |
+
**Output**
|
| 77 |
+
|
| 78 |
+
After completing all artifacts, summarize:
|
| 79 |
+
- Change name and location
|
| 80 |
+
- List of artifacts created with brief descriptions
|
| 81 |
+
- What's ready: "All artifacts created! Ready for implementation."
|
| 82 |
+
- Prompt: "Run `/opsx:apply` to start implementing."
|
| 83 |
+
|
| 84 |
+
**Artifact Creation Guidelines**
|
| 85 |
+
|
| 86 |
+
- Follow the `instruction` field from `openspec instructions` for each artifact type
|
| 87 |
+
- The schema defines what each artifact should contain - follow it
|
| 88 |
+
- Read dependency artifacts for context before creating new ones
|
| 89 |
+
- Use `template` as the structure for your output file - fill in its sections
|
| 90 |
+
- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file
|
| 91 |
+
- Do NOT copy `<context>`, `<rules>`, `<project_context>` blocks into the artifact
|
| 92 |
+
- These guide what you write, but should never appear in the output
|
| 93 |
+
|
| 94 |
+
**Guardrails**
|
| 95 |
+
- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`)
|
| 96 |
+
- Always read dependency artifacts before creating a new one
|
| 97 |
+
- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
|
| 98 |
+
- If a change with that name already exists, ask if user wants to continue it or create a new one
|
| 99 |
+
- Verify each artifact file exists after writing before proceeding to next
|
.kiro/prompts/opsx-apply.prompt.md
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
description: Implement tasks from an OpenSpec change (Experimental)
|
| 3 |
+
---
|
| 4 |
+
|
| 5 |
+
Implement tasks from an OpenSpec change.
|
| 6 |
+
|
| 7 |
+
**Input**: Optionally specify a change name (e.g., `/opsx:apply add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
| 8 |
+
|
| 9 |
+
**Steps**
|
| 10 |
+
|
| 11 |
+
1. **Select the change**
|
| 12 |
+
|
| 13 |
+
If a name is provided, use it. Otherwise:
|
| 14 |
+
- Infer from conversation context if the user mentioned a change
|
| 15 |
+
- Auto-select if only one active change exists
|
| 16 |
+
- If ambiguous, run `openspec list --json` to get available changes and use the **AskUserQuestion tool** to let the user select
|
| 17 |
+
|
| 18 |
+
Always announce: "Using change: <name>" and how to override (e.g., `/opsx:apply <other>`).
|
| 19 |
+
|
| 20 |
+
2. **Check status to understand the schema**
|
| 21 |
+
```bash
|
| 22 |
+
openspec status --change "<name>" --json
|
| 23 |
+
```
|
| 24 |
+
Parse the JSON to understand:
|
| 25 |
+
- `schemaName`: The workflow being used (e.g., "spec-driven")
|
| 26 |
+
- Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
|
| 27 |
+
|
| 28 |
+
3. **Get apply instructions**
|
| 29 |
+
|
| 30 |
+
```bash
|
| 31 |
+
openspec instructions apply --change "<name>" --json
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
This returns:
|
| 35 |
+
- `contextFiles`: artifact ID -> array of concrete file paths (varies by schema)
|
| 36 |
+
- Progress (total, complete, remaining)
|
| 37 |
+
- Task list with status
|
| 38 |
+
- Dynamic instruction based on current state
|
| 39 |
+
|
| 40 |
+
**Handle states:**
|
| 41 |
+
- If `state: "blocked"` (missing artifacts): show message, suggest using `/opsx:continue`
|
| 42 |
+
- If `state: "all_done"`: congratulate, suggest archive
|
| 43 |
+
- Otherwise: proceed to implementation
|
| 44 |
+
|
| 45 |
+
4. **Read context files**
|
| 46 |
+
|
| 47 |
+
Read every file path listed under `contextFiles` from the apply instructions output.
|
| 48 |
+
The files depend on the schema being used:
|
| 49 |
+
- **spec-driven**: proposal, specs, design, tasks
|
| 50 |
+
- Other schemas: follow the contextFiles from CLI output
|
| 51 |
+
|
| 52 |
+
5. **Show current progress**
|
| 53 |
+
|
| 54 |
+
Display:
|
| 55 |
+
- Schema being used
|
| 56 |
+
- Progress: "N/M tasks complete"
|
| 57 |
+
- Remaining tasks overview
|
| 58 |
+
- Dynamic instruction from CLI
|
| 59 |
+
|
| 60 |
+
6. **Implement tasks (loop until done or blocked)**
|
| 61 |
+
|
| 62 |
+
For each pending task:
|
| 63 |
+
- Show which task is being worked on
|
| 64 |
+
- Make the code changes required
|
| 65 |
+
- Keep changes minimal and focused
|
| 66 |
+
- Mark task complete in the tasks file: `- [ ]` → `- [x]`
|
| 67 |
+
- Continue to next task
|
| 68 |
+
|
| 69 |
+
**Pause if:**
|
| 70 |
+
- Task is unclear → ask for clarification
|
| 71 |
+
- Implementation reveals a design issue → suggest updating artifacts
|
| 72 |
+
- Error or blocker encountered → report and wait for guidance
|
| 73 |
+
- User interrupts
|
| 74 |
+
|
| 75 |
+
7. **On completion or pause, show status**
|
| 76 |
+
|
| 77 |
+
Display:
|
| 78 |
+
- Tasks completed this session
|
| 79 |
+
- Overall progress: "N/M tasks complete"
|
| 80 |
+
- If all done: suggest archive
|
| 81 |
+
- If paused: explain why and wait for guidance
|
| 82 |
+
|
| 83 |
+
**Output During Implementation**
|
| 84 |
+
|
| 85 |
+
```
|
| 86 |
+
## Implementing: <change-name> (schema: <schema-name>)
|
| 87 |
+
|
| 88 |
+
Working on task 3/7: <task description>
|
| 89 |
+
[...implementation happening...]
|
| 90 |
+
✓ Task complete
|
| 91 |
+
|
| 92 |
+
Working on task 4/7: <task description>
|
| 93 |
+
[...implementation happening...]
|
| 94 |
+
✓ Task complete
|
| 95 |
+
```
|
| 96 |
+
|
| 97 |
+
**Output On Completion**
|
| 98 |
+
|
| 99 |
+
```
|
| 100 |
+
## Implementation Complete
|
| 101 |
+
|
| 102 |
+
**Change:** <change-name>
|
| 103 |
+
**Schema:** <schema-name>
|
| 104 |
+
**Progress:** 7/7 tasks complete ✓
|
| 105 |
+
|
| 106 |
+
### Completed This Session
|
| 107 |
+
- [x] Task 1
|
| 108 |
+
- [x] Task 2
|
| 109 |
+
...
|
| 110 |
+
|
| 111 |
+
All tasks complete! You can archive this change with `/opsx:archive`.
|
| 112 |
+
```
|
| 113 |
+
|
| 114 |
+
**Output On Pause (Issue Encountered)**
|
| 115 |
+
|
| 116 |
+
```
|
| 117 |
+
## Implementation Paused
|
| 118 |
+
|
| 119 |
+
**Change:** <change-name>
|
| 120 |
+
**Schema:** <schema-name>
|
| 121 |
+
**Progress:** 4/7 tasks complete
|
| 122 |
+
|
| 123 |
+
### Issue Encountered
|
| 124 |
+
<description of the issue>
|
| 125 |
+
|
| 126 |
+
**Options:**
|
| 127 |
+
1. <option 1>
|
| 128 |
+
2. <option 2>
|
| 129 |
+
3. Other approach
|
| 130 |
+
|
| 131 |
+
What would you like to do?
|
| 132 |
+
```
|
| 133 |
+
|
| 134 |
+
**Guardrails**
|
| 135 |
+
- Keep going through tasks until done or blocked
|
| 136 |
+
- Always read context files before starting (from the apply instructions output)
|
| 137 |
+
- If task is ambiguous, pause and ask before implementing
|
| 138 |
+
- If implementation reveals issues, pause and suggest artifact updates
|
| 139 |
+
- Keep code changes minimal and scoped to each task
|
| 140 |
+
- Update task checkbox immediately after completing each task
|
| 141 |
+
- Pause on errors, blockers, or unclear requirements - don't guess
|
| 142 |
+
- Use contextFiles from CLI output, don't assume specific file names
|
| 143 |
+
|
| 144 |
+
**Fluid Workflow Integration**
|
| 145 |
+
|
| 146 |
+
This skill supports the "actions on a change" model:
|
| 147 |
+
|
| 148 |
+
- **Can be invoked anytime**: Before all artifacts are done (if tasks exist), after partial implementation, interleaved with other actions
|
| 149 |
+
- **Allows artifact updates**: If implementation reveals design issues, suggest updating artifacts - not phase-locked, work fluidly
|
.kiro/prompts/opsx-archive.prompt.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
description: Archive a completed change in the experimental workflow
|
| 3 |
+
---
|
| 4 |
+
|
| 5 |
+
Archive a completed change in the experimental workflow.
|
| 6 |
+
|
| 7 |
+
**Input**: Optionally specify a change name after `/opsx:archive` (e.g., `/opsx:archive add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
| 8 |
+
|
| 9 |
+
**Steps**
|
| 10 |
+
|
| 11 |
+
1. **If no change name provided, prompt for selection**
|
| 12 |
+
|
| 13 |
+
Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
|
| 14 |
+
|
| 15 |
+
Show only active changes (not already archived).
|
| 16 |
+
Include the schema used for each change if available.
|
| 17 |
+
|
| 18 |
+
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
| 19 |
+
|
| 20 |
+
2. **Check artifact completion status**
|
| 21 |
+
|
| 22 |
+
Run `openspec status --change "<name>" --json` to check artifact completion.
|
| 23 |
+
|
| 24 |
+
Parse the JSON to understand:
|
| 25 |
+
- `schemaName`: The workflow being used
|
| 26 |
+
- `artifacts`: List of artifacts with their status (`done` or other)
|
| 27 |
+
|
| 28 |
+
**If any artifacts are not `done`:**
|
| 29 |
+
- Display warning listing incomplete artifacts
|
| 30 |
+
- Prompt user for confirmation to continue
|
| 31 |
+
- Proceed if user confirms
|
| 32 |
+
|
| 33 |
+
3. **Check task completion status**
|
| 34 |
+
|
| 35 |
+
Read the tasks file (typically `tasks.md`) to check for incomplete tasks.
|
| 36 |
+
|
| 37 |
+
Count tasks marked with `- [ ]` (incomplete) vs `- [x]` (complete).
|
| 38 |
+
|
| 39 |
+
**If incomplete tasks found:**
|
| 40 |
+
- Display warning showing count of incomplete tasks
|
| 41 |
+
- Prompt user for confirmation to continue
|
| 42 |
+
- Proceed if user confirms
|
| 43 |
+
|
| 44 |
+
**If no tasks file exists:** Proceed without task-related warning.
|
| 45 |
+
|
| 46 |
+
4. **Assess delta spec sync state**
|
| 47 |
+
|
| 48 |
+
Check for delta specs at `openspec/changes/<name>/specs/`. If none exist, proceed without sync prompt.
|
| 49 |
+
|
| 50 |
+
**If delta specs exist:**
|
| 51 |
+
- Compare each delta spec with its corresponding main spec at `openspec/specs/<capability>/spec.md`
|
| 52 |
+
- Determine what changes would be applied (adds, modifications, removals, renames)
|
| 53 |
+
- Show a combined summary before prompting
|
| 54 |
+
|
| 55 |
+
**Prompt options:**
|
| 56 |
+
- If changes needed: "Sync now (recommended)", "Archive without syncing"
|
| 57 |
+
- If already synced: "Archive now", "Sync anyway", "Cancel"
|
| 58 |
+
|
| 59 |
+
If user chooses sync, use Task tool (subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change '<name>'. Delta spec analysis: <include the analyzed delta spec summary>"). Proceed to archive regardless of choice.
|
| 60 |
+
|
| 61 |
+
5. **Perform the archive**
|
| 62 |
+
|
| 63 |
+
Create the archive directory if it doesn't exist:
|
| 64 |
+
```bash
|
| 65 |
+
mkdir -p openspec/changes/archive
|
| 66 |
+
```
|
| 67 |
+
|
| 68 |
+
Generate target name using current date: `YYYY-MM-DD-<change-name>`
|
| 69 |
+
|
| 70 |
+
**Check if target already exists:**
|
| 71 |
+
- If yes: Fail with error, suggest renaming existing archive or using different date
|
| 72 |
+
- If no: Move the change directory to archive
|
| 73 |
+
|
| 74 |
+
```bash
|
| 75 |
+
mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name>
|
| 76 |
+
```
|
| 77 |
+
|
| 78 |
+
6. **Display summary**
|
| 79 |
+
|
| 80 |
+
Show archive completion summary including:
|
| 81 |
+
- Change name
|
| 82 |
+
- Schema that was used
|
| 83 |
+
- Archive location
|
| 84 |
+
- Spec sync status (synced / sync skipped / no delta specs)
|
| 85 |
+
- Note about any warnings (incomplete artifacts/tasks)
|
| 86 |
+
|
| 87 |
+
**Output On Success**
|
| 88 |
+
|
| 89 |
+
```
|
| 90 |
+
## Archive Complete
|
| 91 |
+
|
| 92 |
+
**Change:** <change-name>
|
| 93 |
+
**Schema:** <schema-name>
|
| 94 |
+
**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
| 95 |
+
**Specs:** ✓ Synced to main specs
|
| 96 |
+
|
| 97 |
+
All artifacts complete. All tasks complete.
|
| 98 |
+
```
|
| 99 |
+
|
| 100 |
+
**Output On Success (No Delta Specs)**
|
| 101 |
+
|
| 102 |
+
```
|
| 103 |
+
## Archive Complete
|
| 104 |
+
|
| 105 |
+
**Change:** <change-name>
|
| 106 |
+
**Schema:** <schema-name>
|
| 107 |
+
**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
| 108 |
+
**Specs:** No delta specs
|
| 109 |
+
|
| 110 |
+
All artifacts complete. All tasks complete.
|
| 111 |
+
```
|
| 112 |
+
|
| 113 |
+
**Output On Success With Warnings**
|
| 114 |
+
|
| 115 |
+
```
|
| 116 |
+
## Archive Complete (with warnings)
|
| 117 |
+
|
| 118 |
+
**Change:** <change-name>
|
| 119 |
+
**Schema:** <schema-name>
|
| 120 |
+
**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
| 121 |
+
**Specs:** Sync skipped (user chose to skip)
|
| 122 |
+
|
| 123 |
+
**Warnings:**
|
| 124 |
+
- Archived with 2 incomplete artifacts
|
| 125 |
+
- Archived with 3 incomplete tasks
|
| 126 |
+
- Delta spec sync was skipped (user chose to skip)
|
| 127 |
+
|
| 128 |
+
Review the archive if this was not intentional.
|
| 129 |
+
```
|
| 130 |
+
|
| 131 |
+
**Output On Error (Archive Exists)**
|
| 132 |
+
|
| 133 |
+
```
|
| 134 |
+
## Archive Failed
|
| 135 |
+
|
| 136 |
+
**Change:** <change-name>
|
| 137 |
+
**Target:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
| 138 |
+
|
| 139 |
+
Target archive directory already exists.
|
| 140 |
+
|
| 141 |
+
**Options:**
|
| 142 |
+
1. Rename the existing archive
|
| 143 |
+
2. Delete the existing archive if it's a duplicate
|
| 144 |
+
3. Wait until a different date to archive
|
| 145 |
+
```
|
| 146 |
+
|
| 147 |
+
**Guardrails**
|
| 148 |
+
- Always prompt for change selection if not provided
|
| 149 |
+
- Use artifact graph (openspec status --json) for completion checking
|
| 150 |
+
- Don't block archive on warnings - just inform and confirm
|
| 151 |
+
- Preserve .openspec.yaml when moving to archive (it moves with the directory)
|
| 152 |
+
- Show clear summary of what happened
|
| 153 |
+
- If sync is requested, use the Skill tool to invoke `openspec-sync-specs` (agent-driven)
|
| 154 |
+
- If delta specs exist, always run the sync assessment and show the combined summary before prompting
|
.kiro/prompts/opsx-explore.prompt.md
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
description: Enter explore mode - think through ideas, investigate problems, clarify requirements
|
| 3 |
+
---
|
| 4 |
+
|
| 5 |
+
Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes.
|
| 6 |
+
|
| 7 |
+
**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first and create a change proposal. You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing.
|
| 8 |
+
|
| 9 |
+
**This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore.
|
| 10 |
+
|
| 11 |
+
**Input**: The argument after `/opsx:explore` is whatever the user wants to think about. Could be:
|
| 12 |
+
- A vague idea: "real-time collaboration"
|
| 13 |
+
- A specific problem: "the auth system is getting unwieldy"
|
| 14 |
+
- A change name: "add-dark-mode" (to explore in context of that change)
|
| 15 |
+
- A comparison: "postgres vs sqlite for this"
|
| 16 |
+
- Nothing (just enter explore mode)
|
| 17 |
+
|
| 18 |
+
---
|
| 19 |
+
|
| 20 |
+
## The Stance
|
| 21 |
+
|
| 22 |
+
- **Curious, not prescriptive** - Ask questions that emerge naturally, don't follow a script
|
| 23 |
+
- **Open threads, not interrogations** - Surface multiple interesting directions and let the user follow what resonates. Don't funnel them through a single path of questions.
|
| 24 |
+
- **Visual** - Use ASCII diagrams liberally when they'd help clarify thinking
|
| 25 |
+
- **Adaptive** - Follow interesting threads, pivot when new information emerges
|
| 26 |
+
- **Patient** - Don't rush to conclusions, let the shape of the problem emerge
|
| 27 |
+
- **Grounded** - Explore the actual codebase when relevant, don't just theorize
|
| 28 |
+
|
| 29 |
+
---
|
| 30 |
+
|
| 31 |
+
## What You Might Do
|
| 32 |
+
|
| 33 |
+
Depending on what the user brings, you might:
|
| 34 |
+
|
| 35 |
+
**Explore the problem space**
|
| 36 |
+
- Ask clarifying questions that emerge from what they said
|
| 37 |
+
- Challenge assumptions
|
| 38 |
+
- Reframe the problem
|
| 39 |
+
- Find analogies
|
| 40 |
+
|
| 41 |
+
**Investigate the codebase**
|
| 42 |
+
- Map existing architecture relevant to the discussion
|
| 43 |
+
- Find integration points
|
| 44 |
+
- Identify patterns already in use
|
| 45 |
+
- Surface hidden complexity
|
| 46 |
+
|
| 47 |
+
**Compare options**
|
| 48 |
+
- Brainstorm multiple approaches
|
| 49 |
+
- Build comparison tables
|
| 50 |
+
- Sketch tradeoffs
|
| 51 |
+
- Recommend a path (if asked)
|
| 52 |
+
|
| 53 |
+
**Visualize**
|
| 54 |
+
```
|
| 55 |
+
┌─────────────────────────────────────────┐
|
| 56 |
+
│ Use ASCII diagrams liberally │
|
| 57 |
+
├─────────────────────────────────────────┤
|
| 58 |
+
│ │
|
| 59 |
+
│ ┌────────┐ ┌────────┐ │
|
| 60 |
+
│ │ State │────────▶│ State │ │
|
| 61 |
+
│ │ A │ │ B │ │
|
| 62 |
+
│ └────────┘ └────────┘ │
|
| 63 |
+
│ │
|
| 64 |
+
│ System diagrams, state machines, │
|
| 65 |
+
│ data flows, architecture sketches, │
|
| 66 |
+
│ dependency graphs, comparison tables │
|
| 67 |
+
│ │
|
| 68 |
+
└─────────────────────────────────────────┘
|
| 69 |
+
```
|
| 70 |
+
|
| 71 |
+
**Surface risks and unknowns**
|
| 72 |
+
- Identify what could go wrong
|
| 73 |
+
- Find gaps in understanding
|
| 74 |
+
- Suggest spikes or investigations
|
| 75 |
+
|
| 76 |
+
---
|
| 77 |
+
|
| 78 |
+
## OpenSpec Awareness
|
| 79 |
+
|
| 80 |
+
You have full context of the OpenSpec system. Use it naturally, don't force it.
|
| 81 |
+
|
| 82 |
+
### Check for context
|
| 83 |
+
|
| 84 |
+
At the start, quickly check what exists:
|
| 85 |
+
```bash
|
| 86 |
+
openspec list --json
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
This tells you:
|
| 90 |
+
- If there are active changes
|
| 91 |
+
- Their names, schemas, and status
|
| 92 |
+
- What the user might be working on
|
| 93 |
+
|
| 94 |
+
If the user mentioned a specific change name, read its artifacts for context.
|
| 95 |
+
|
| 96 |
+
### When no change exists
|
| 97 |
+
|
| 98 |
+
Think freely. When insights crystallize, you might offer:
|
| 99 |
+
|
| 100 |
+
- "This feels solid enough to start a change. Want me to create a proposal?"
|
| 101 |
+
- Or keep exploring - no pressure to formalize
|
| 102 |
+
|
| 103 |
+
### When a change exists
|
| 104 |
+
|
| 105 |
+
If the user mentions a change or you detect one is relevant:
|
| 106 |
+
|
| 107 |
+
1. **Read existing artifacts for context**
|
| 108 |
+
- `openspec/changes/<name>/proposal.md`
|
| 109 |
+
- `openspec/changes/<name>/design.md`
|
| 110 |
+
- `openspec/changes/<name>/tasks.md`
|
| 111 |
+
- etc.
|
| 112 |
+
|
| 113 |
+
2. **Reference them naturally in conversation**
|
| 114 |
+
- "Your design mentions using Redis, but we just realized SQLite fits better..."
|
| 115 |
+
- "The proposal scopes this to premium users, but we're now thinking everyone..."
|
| 116 |
+
|
| 117 |
+
3. **Offer to capture when decisions are made**
|
| 118 |
+
|
| 119 |
+
| Insight Type | Where to Capture |
|
| 120 |
+
|----------------------------|--------------------------------|
|
| 121 |
+
| New requirement discovered | `specs/<capability>/spec.md` |
|
| 122 |
+
| Requirement changed | `specs/<capability>/spec.md` |
|
| 123 |
+
| Design decision made | `design.md` |
|
| 124 |
+
| Scope changed | `proposal.md` |
|
| 125 |
+
| New work identified | `tasks.md` |
|
| 126 |
+
| Assumption invalidated | Relevant artifact |
|
| 127 |
+
|
| 128 |
+
Example offers:
|
| 129 |
+
- "That's a design decision. Capture it in design.md?"
|
| 130 |
+
- "This is a new requirement. Add it to specs?"
|
| 131 |
+
- "This changes scope. Update the proposal?"
|
| 132 |
+
|
| 133 |
+
4. **The user decides** - Offer and move on. Don't pressure. Don't auto-capture.
|
| 134 |
+
|
| 135 |
+
---
|
| 136 |
+
|
| 137 |
+
## What You Don't Have To Do
|
| 138 |
+
|
| 139 |
+
- Follow a script
|
| 140 |
+
- Ask the same questions every time
|
| 141 |
+
- Produce a specific artifact
|
| 142 |
+
- Reach a conclusion
|
| 143 |
+
- Stay on topic if a tangent is valuable
|
| 144 |
+
- Be brief (this is thinking time)
|
| 145 |
+
|
| 146 |
+
---
|
| 147 |
+
|
| 148 |
+
## Ending Discovery
|
| 149 |
+
|
| 150 |
+
There's no required ending. Discovery might:
|
| 151 |
+
|
| 152 |
+
- **Flow into a proposal**: "Ready to start? I can create a change proposal."
|
| 153 |
+
- **Result in artifact updates**: "Updated design.md with these decisions"
|
| 154 |
+
- **Just provide clarity**: User has what they need, moves on
|
| 155 |
+
- **Continue later**: "We can pick this up anytime"
|
| 156 |
+
|
| 157 |
+
When things crystallize, you might offer a summary - but it's optional. Sometimes the thinking IS the value.
|
| 158 |
+
|
| 159 |
+
---
|
| 160 |
+
|
| 161 |
+
## Guardrails
|
| 162 |
+
|
| 163 |
+
- **Don't implement** - Never write code or implement features. Creating OpenSpec artifacts is fine, writing application code is not.
|
| 164 |
+
- **Don't fake understanding** - If something is unclear, dig deeper
|
| 165 |
+
- **Don't rush** - Discovery is thinking time, not task time
|
| 166 |
+
- **Don't force structure** - Let patterns emerge naturally
|
| 167 |
+
- **Don't auto-capture** - Offer to save insights, don't just do it
|
| 168 |
+
- **Do visualize** - A good diagram is worth many paragraphs
|
| 169 |
+
- **Do explore the codebase** - Ground discussions in reality
|
| 170 |
+
- **Do question assumptions** - Including the user's and your own
|
.kiro/prompts/opsx-propose.prompt.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
description: Propose a new change - create it and generate all artifacts in one step
|
| 3 |
+
---
|
| 4 |
+
|
| 5 |
+
Propose a new change - create the change and generate all artifacts in one step.
|
| 6 |
+
|
| 7 |
+
I'll create a change with artifacts:
|
| 8 |
+
- proposal.md (what & why)
|
| 9 |
+
- design.md (how)
|
| 10 |
+
- tasks.md (implementation steps)
|
| 11 |
+
|
| 12 |
+
When ready to implement, run /opsx:apply
|
| 13 |
+
|
| 14 |
+
---
|
| 15 |
+
|
| 16 |
+
**Input**: The argument after `/opsx:propose` is the change name (kebab-case), OR a description of what the user wants to build.
|
| 17 |
+
|
| 18 |
+
**Steps**
|
| 19 |
+
|
| 20 |
+
1. **If no input provided, ask what they want to build**
|
| 21 |
+
|
| 22 |
+
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
|
| 23 |
+
> "What change do you want to work on? Describe what you want to build or fix."
|
| 24 |
+
|
| 25 |
+
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
|
| 26 |
+
|
| 27 |
+
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
|
| 28 |
+
|
| 29 |
+
2. **Create the change directory**
|
| 30 |
+
```bash
|
| 31 |
+
openspec new change "<name>"
|
| 32 |
+
```
|
| 33 |
+
This creates a scaffolded change at `openspec/changes/<name>/` with `.openspec.yaml`.
|
| 34 |
+
|
| 35 |
+
3. **Get the artifact build order**
|
| 36 |
+
```bash
|
| 37 |
+
openspec status --change "<name>" --json
|
| 38 |
+
```
|
| 39 |
+
Parse the JSON to get:
|
| 40 |
+
- `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`)
|
| 41 |
+
- `artifacts`: list of all artifacts with their status and dependencies
|
| 42 |
+
|
| 43 |
+
4. **Create artifacts in sequence until apply-ready**
|
| 44 |
+
|
| 45 |
+
Use the **TodoWrite tool** to track progress through the artifacts.
|
| 46 |
+
|
| 47 |
+
Loop through artifacts in dependency order (artifacts with no pending dependencies first):
|
| 48 |
+
|
| 49 |
+
a. **For each artifact that is `ready` (dependencies satisfied)**:
|
| 50 |
+
- Get instructions:
|
| 51 |
+
```bash
|
| 52 |
+
openspec instructions <artifact-id> --change "<name>" --json
|
| 53 |
+
```
|
| 54 |
+
- The instructions JSON includes:
|
| 55 |
+
- `context`: Project background (constraints for you - do NOT include in output)
|
| 56 |
+
- `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
|
| 57 |
+
- `template`: The structure to use for your output file
|
| 58 |
+
- `instruction`: Schema-specific guidance for this artifact type
|
| 59 |
+
- `outputPath`: Where to write the artifact
|
| 60 |
+
- `dependencies`: Completed artifacts to read for context
|
| 61 |
+
- Read any completed dependency files for context
|
| 62 |
+
- Create the artifact file using `template` as the structure
|
| 63 |
+
- Apply `context` and `rules` as constraints - but do NOT copy them into the file
|
| 64 |
+
- Show brief progress: "Created <artifact-id>"
|
| 65 |
+
|
| 66 |
+
b. **Continue until all `applyRequires` artifacts are complete**
|
| 67 |
+
- After creating each artifact, re-run `openspec status --change "<name>" --json`
|
| 68 |
+
- Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array
|
| 69 |
+
- Stop when all `applyRequires` artifacts are done
|
| 70 |
+
|
| 71 |
+
c. **If an artifact requires user input** (unclear context):
|
| 72 |
+
- Use **AskUserQuestion tool** to clarify
|
| 73 |
+
- Then continue with creation
|
| 74 |
+
|
| 75 |
+
5. **Show final status**
|
| 76 |
+
```bash
|
| 77 |
+
openspec status --change "<name>"
|
| 78 |
+
```
|
| 79 |
+
|
| 80 |
+
**Output**
|
| 81 |
+
|
| 82 |
+
After completing all artifacts, summarize:
|
| 83 |
+
- Change name and location
|
| 84 |
+
- List of artifacts created with brief descriptions
|
| 85 |
+
- What's ready: "All artifacts created! Ready for implementation."
|
| 86 |
+
- Prompt: "Run `/opsx:apply` to start implementing."
|
| 87 |
+
|
| 88 |
+
**Artifact Creation Guidelines**
|
| 89 |
+
|
| 90 |
+
- Follow the `instruction` field from `openspec instructions` for each artifact type
|
| 91 |
+
- The schema defines what each artifact should contain - follow it
|
| 92 |
+
- Read dependency artifacts for context before creating new ones
|
| 93 |
+
- Use `template` as the structure for your output file - fill in its sections
|
| 94 |
+
- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file
|
| 95 |
+
- Do NOT copy `<context>`, `<rules>`, `<project_context>` blocks into the artifact
|
| 96 |
+
- These guide what you write, but should never appear in the output
|
| 97 |
+
|
| 98 |
+
**Guardrails**
|
| 99 |
+
- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`)
|
| 100 |
+
- Always read dependency artifacts before creating a new one
|
| 101 |
+
- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
|
| 102 |
+
- If a change with that name already exists, ask if user wants to continue it or create a new one
|
| 103 |
+
- Verify each artifact file exists after writing before proceeding to next
|
.kiro/skills/openspec-apply-change/SKILL.md
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: openspec-apply-change
|
| 3 |
+
description: Implement tasks from an OpenSpec change. Use when the user wants to start implementing, continue implementation, or work through tasks.
|
| 4 |
+
license: MIT
|
| 5 |
+
compatibility: Requires openspec CLI.
|
| 6 |
+
metadata:
|
| 7 |
+
author: openspec
|
| 8 |
+
version: "1.0"
|
| 9 |
+
generatedBy: "1.3.1"
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
Implement tasks from an OpenSpec change.
|
| 13 |
+
|
| 14 |
+
**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
| 15 |
+
|
| 16 |
+
**Steps**
|
| 17 |
+
|
| 18 |
+
1. **Select the change**
|
| 19 |
+
|
| 20 |
+
If a name is provided, use it. Otherwise:
|
| 21 |
+
- Infer from conversation context if the user mentioned a change
|
| 22 |
+
- Auto-select if only one active change exists
|
| 23 |
+
- If ambiguous, run `openspec list --json` to get available changes and use the **AskUserQuestion tool** to let the user select
|
| 24 |
+
|
| 25 |
+
Always announce: "Using change: <name>" and how to override (e.g., `/opsx:apply <other>`).
|
| 26 |
+
|
| 27 |
+
2. **Check status to understand the schema**
|
| 28 |
+
```bash
|
| 29 |
+
openspec status --change "<name>" --json
|
| 30 |
+
```
|
| 31 |
+
Parse the JSON to understand:
|
| 32 |
+
- `schemaName`: The workflow being used (e.g., "spec-driven")
|
| 33 |
+
- Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
|
| 34 |
+
|
| 35 |
+
3. **Get apply instructions**
|
| 36 |
+
|
| 37 |
+
```bash
|
| 38 |
+
openspec instructions apply --change "<name>" --json
|
| 39 |
+
```
|
| 40 |
+
|
| 41 |
+
This returns:
|
| 42 |
+
- `contextFiles`: artifact ID -> array of concrete file paths (varies by schema - could be proposal/specs/design/tasks or spec/tests/implementation/docs)
|
| 43 |
+
- Progress (total, complete, remaining)
|
| 44 |
+
- Task list with status
|
| 45 |
+
- Dynamic instruction based on current state
|
| 46 |
+
|
| 47 |
+
**Handle states:**
|
| 48 |
+
- If `state: "blocked"` (missing artifacts): show message, suggest using openspec-continue-change
|
| 49 |
+
- If `state: "all_done"`: congratulate, suggest archive
|
| 50 |
+
- Otherwise: proceed to implementation
|
| 51 |
+
|
| 52 |
+
4. **Read context files**
|
| 53 |
+
|
| 54 |
+
Read every file path listed under `contextFiles` from the apply instructions output.
|
| 55 |
+
The files depend on the schema being used:
|
| 56 |
+
- **spec-driven**: proposal, specs, design, tasks
|
| 57 |
+
- Other schemas: follow the contextFiles from CLI output
|
| 58 |
+
|
| 59 |
+
5. **Show current progress**
|
| 60 |
+
|
| 61 |
+
Display:
|
| 62 |
+
- Schema being used
|
| 63 |
+
- Progress: "N/M tasks complete"
|
| 64 |
+
- Remaining tasks overview
|
| 65 |
+
- Dynamic instruction from CLI
|
| 66 |
+
|
| 67 |
+
6. **Implement tasks (loop until done or blocked)**
|
| 68 |
+
|
| 69 |
+
For each pending task:
|
| 70 |
+
- Show which task is being worked on
|
| 71 |
+
- Make the code changes required
|
| 72 |
+
- Keep changes minimal and focused
|
| 73 |
+
- Mark task complete in the tasks file: `- [ ]` → `- [x]`
|
| 74 |
+
- Continue to next task
|
| 75 |
+
|
| 76 |
+
**Pause if:**
|
| 77 |
+
- Task is unclear → ask for clarification
|
| 78 |
+
- Implementation reveals a design issue → suggest updating artifacts
|
| 79 |
+
- Error or blocker encountered → report and wait for guidance
|
| 80 |
+
- User interrupts
|
| 81 |
+
|
| 82 |
+
7. **On completion or pause, show status**
|
| 83 |
+
|
| 84 |
+
Display:
|
| 85 |
+
- Tasks completed this session
|
| 86 |
+
- Overall progress: "N/M tasks complete"
|
| 87 |
+
- If all done: suggest archive
|
| 88 |
+
- If paused: explain why and wait for guidance
|
| 89 |
+
|
| 90 |
+
**Output During Implementation**
|
| 91 |
+
|
| 92 |
+
```
|
| 93 |
+
## Implementing: <change-name> (schema: <schema-name>)
|
| 94 |
+
|
| 95 |
+
Working on task 3/7: <task description>
|
| 96 |
+
[...implementation happening...]
|
| 97 |
+
✓ Task complete
|
| 98 |
+
|
| 99 |
+
Working on task 4/7: <task description>
|
| 100 |
+
[...implementation happening...]
|
| 101 |
+
✓ Task complete
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
**Output On Completion**
|
| 105 |
+
|
| 106 |
+
```
|
| 107 |
+
## Implementation Complete
|
| 108 |
+
|
| 109 |
+
**Change:** <change-name>
|
| 110 |
+
**Schema:** <schema-name>
|
| 111 |
+
**Progress:** 7/7 tasks complete ✓
|
| 112 |
+
|
| 113 |
+
### Completed This Session
|
| 114 |
+
- [x] Task 1
|
| 115 |
+
- [x] Task 2
|
| 116 |
+
...
|
| 117 |
+
|
| 118 |
+
All tasks complete! Ready to archive this change.
|
| 119 |
+
```
|
| 120 |
+
|
| 121 |
+
**Output On Pause (Issue Encountered)**
|
| 122 |
+
|
| 123 |
+
```
|
| 124 |
+
## Implementation Paused
|
| 125 |
+
|
| 126 |
+
**Change:** <change-name>
|
| 127 |
+
**Schema:** <schema-name>
|
| 128 |
+
**Progress:** 4/7 tasks complete
|
| 129 |
+
|
| 130 |
+
### Issue Encountered
|
| 131 |
+
<description of the issue>
|
| 132 |
+
|
| 133 |
+
**Options:**
|
| 134 |
+
1. <option 1>
|
| 135 |
+
2. <option 2>
|
| 136 |
+
3. Other approach
|
| 137 |
+
|
| 138 |
+
What would you like to do?
|
| 139 |
+
```
|
| 140 |
+
|
| 141 |
+
**Guardrails**
|
| 142 |
+
- Keep going through tasks until done or blocked
|
| 143 |
+
- Always read context files before starting (from the apply instructions output)
|
| 144 |
+
- If task is ambiguous, pause and ask before implementing
|
| 145 |
+
- If implementation reveals issues, pause and suggest artifact updates
|
| 146 |
+
- Keep code changes minimal and scoped to each task
|
| 147 |
+
- Update task checkbox immediately after completing each task
|
| 148 |
+
- Pause on errors, blockers, or unclear requirements - don't guess
|
| 149 |
+
- Use contextFiles from CLI output, don't assume specific file names
|
| 150 |
+
|
| 151 |
+
**Fluid Workflow Integration**
|
| 152 |
+
|
| 153 |
+
This skill supports the "actions on a change" model:
|
| 154 |
+
|
| 155 |
+
- **Can be invoked anytime**: Before all artifacts are done (if tasks exist), after partial implementation, interleaved with other actions
|
| 156 |
+
- **Allows artifact updates**: If implementation reveals design issues, suggest updating artifacts - not phase-locked, work fluidly
|
.kiro/skills/openspec-archive-change/SKILL.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: openspec-archive-change
|
| 3 |
+
description: Archive a completed change in the experimental workflow. Use when the user wants to finalize and archive a change after implementation is complete.
|
| 4 |
+
license: MIT
|
| 5 |
+
compatibility: Requires openspec CLI.
|
| 6 |
+
metadata:
|
| 7 |
+
author: openspec
|
| 8 |
+
version: "1.0"
|
| 9 |
+
generatedBy: "1.3.1"
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
Archive a completed change in the experimental workflow.
|
| 13 |
+
|
| 14 |
+
**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
| 15 |
+
|
| 16 |
+
**Steps**
|
| 17 |
+
|
| 18 |
+
1. **If no change name provided, prompt for selection**
|
| 19 |
+
|
| 20 |
+
Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
|
| 21 |
+
|
| 22 |
+
Show only active changes (not already archived).
|
| 23 |
+
Include the schema used for each change if available.
|
| 24 |
+
|
| 25 |
+
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
| 26 |
+
|
| 27 |
+
2. **Check artifact completion status**
|
| 28 |
+
|
| 29 |
+
Run `openspec status --change "<name>" --json` to check artifact completion.
|
| 30 |
+
|
| 31 |
+
Parse the JSON to understand:
|
| 32 |
+
- `schemaName`: The workflow being used
|
| 33 |
+
- `artifacts`: List of artifacts with their status (`done` or other)
|
| 34 |
+
|
| 35 |
+
**If any artifacts are not `done`:**
|
| 36 |
+
- Display warning listing incomplete artifacts
|
| 37 |
+
- Use **AskUserQuestion tool** to confirm user wants to proceed
|
| 38 |
+
- Proceed if user confirms
|
| 39 |
+
|
| 40 |
+
3. **Check task completion status**
|
| 41 |
+
|
| 42 |
+
Read the tasks file (typically `tasks.md`) to check for incomplete tasks.
|
| 43 |
+
|
| 44 |
+
Count tasks marked with `- [ ]` (incomplete) vs `- [x]` (complete).
|
| 45 |
+
|
| 46 |
+
**If incomplete tasks found:**
|
| 47 |
+
- Display warning showing count of incomplete tasks
|
| 48 |
+
- Use **AskUserQuestion tool** to confirm user wants to proceed
|
| 49 |
+
- Proceed if user confirms
|
| 50 |
+
|
| 51 |
+
**If no tasks file exists:** Proceed without task-related warning.
|
| 52 |
+
|
| 53 |
+
4. **Assess delta spec sync state**
|
| 54 |
+
|
| 55 |
+
Check for delta specs at `openspec/changes/<name>/specs/`. If none exist, proceed without sync prompt.
|
| 56 |
+
|
| 57 |
+
**If delta specs exist:**
|
| 58 |
+
- Compare each delta spec with its corresponding main spec at `openspec/specs/<capability>/spec.md`
|
| 59 |
+
- Determine what changes would be applied (adds, modifications, removals, renames)
|
| 60 |
+
- Show a combined summary before prompting
|
| 61 |
+
|
| 62 |
+
**Prompt options:**
|
| 63 |
+
- If changes needed: "Sync now (recommended)", "Archive without syncing"
|
| 64 |
+
- If already synced: "Archive now", "Sync anyway", "Cancel"
|
| 65 |
+
|
| 66 |
+
If user chooses sync, use Task tool (subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change '<name>'. Delta spec analysis: <include the analyzed delta spec summary>"). Proceed to archive regardless of choice.
|
| 67 |
+
|
| 68 |
+
5. **Perform the archive**
|
| 69 |
+
|
| 70 |
+
Create the archive directory if it doesn't exist:
|
| 71 |
+
```bash
|
| 72 |
+
mkdir -p openspec/changes/archive
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
Generate target name using current date: `YYYY-MM-DD-<change-name>`
|
| 76 |
+
|
| 77 |
+
**Check if target already exists:**
|
| 78 |
+
- If yes: Fail with error, suggest renaming existing archive or using different date
|
| 79 |
+
- If no: Move the change directory to archive
|
| 80 |
+
|
| 81 |
+
```bash
|
| 82 |
+
mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name>
|
| 83 |
+
```
|
| 84 |
+
|
| 85 |
+
6. **Display summary**
|
| 86 |
+
|
| 87 |
+
Show archive completion summary including:
|
| 88 |
+
- Change name
|
| 89 |
+
- Schema that was used
|
| 90 |
+
- Archive location
|
| 91 |
+
- Whether specs were synced (if applicable)
|
| 92 |
+
- Note about any warnings (incomplete artifacts/tasks)
|
| 93 |
+
|
| 94 |
+
**Output On Success**
|
| 95 |
+
|
| 96 |
+
```
|
| 97 |
+
## Archive Complete
|
| 98 |
+
|
| 99 |
+
**Change:** <change-name>
|
| 100 |
+
**Schema:** <schema-name>
|
| 101 |
+
**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
| 102 |
+
**Specs:** ✓ Synced to main specs (or "No delta specs" or "Sync skipped")
|
| 103 |
+
|
| 104 |
+
All artifacts complete. All tasks complete.
|
| 105 |
+
```
|
| 106 |
+
|
| 107 |
+
**Guardrails**
|
| 108 |
+
- Always prompt for change selection if not provided
|
| 109 |
+
- Use artifact graph (openspec status --json) for completion checking
|
| 110 |
+
- Don't block archive on warnings - just inform and confirm
|
| 111 |
+
- Preserve .openspec.yaml when moving to archive (it moves with the directory)
|
| 112 |
+
- Show clear summary of what happened
|
| 113 |
+
- If sync is requested, use openspec-sync-specs approach (agent-driven)
|
| 114 |
+
- If delta specs exist, always run the sync assessment and show the combined summary before prompting
|
.kiro/skills/openspec-explore/SKILL.md
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: openspec-explore
|
| 3 |
+
description: Enter explore mode - a thinking partner for exploring ideas, investigating problems, and clarifying requirements. Use when the user wants to think through something before or during a change.
|
| 4 |
+
license: MIT
|
| 5 |
+
compatibility: Requires openspec CLI.
|
| 6 |
+
metadata:
|
| 7 |
+
author: openspec
|
| 8 |
+
version: "1.0"
|
| 9 |
+
generatedBy: "1.3.1"
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes.
|
| 13 |
+
|
| 14 |
+
**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first and create a change proposal. You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing.
|
| 15 |
+
|
| 16 |
+
**This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore.
|
| 17 |
+
|
| 18 |
+
---
|
| 19 |
+
|
| 20 |
+
## The Stance
|
| 21 |
+
|
| 22 |
+
- **Curious, not prescriptive** - Ask questions that emerge naturally, don't follow a script
|
| 23 |
+
- **Open threads, not interrogations** - Surface multiple interesting directions and let the user follow what resonates. Don't funnel them through a single path of questions.
|
| 24 |
+
- **Visual** - Use ASCII diagrams liberally when they'd help clarify thinking
|
| 25 |
+
- **Adaptive** - Follow interesting threads, pivot when new information emerges
|
| 26 |
+
- **Patient** - Don't rush to conclusions, let the shape of the problem emerge
|
| 27 |
+
- **Grounded** - Explore the actual codebase when relevant, don't just theorize
|
| 28 |
+
|
| 29 |
+
---
|
| 30 |
+
|
| 31 |
+
## What You Might Do
|
| 32 |
+
|
| 33 |
+
Depending on what the user brings, you might:
|
| 34 |
+
|
| 35 |
+
**Explore the problem space**
|
| 36 |
+
- Ask clarifying questions that emerge from what they said
|
| 37 |
+
- Challenge assumptions
|
| 38 |
+
- Reframe the problem
|
| 39 |
+
- Find analogies
|
| 40 |
+
|
| 41 |
+
**Investigate the codebase**
|
| 42 |
+
- Map existing architecture relevant to the discussion
|
| 43 |
+
- Find integration points
|
| 44 |
+
- Identify patterns already in use
|
| 45 |
+
- Surface hidden complexity
|
| 46 |
+
|
| 47 |
+
**Compare options**
|
| 48 |
+
- Brainstorm multiple approaches
|
| 49 |
+
- Build comparison tables
|
| 50 |
+
- Sketch tradeoffs
|
| 51 |
+
- Recommend a path (if asked)
|
| 52 |
+
|
| 53 |
+
**Visualize**
|
| 54 |
+
```
|
| 55 |
+
┌─────────────────────────────────────────┐
|
| 56 |
+
│ Use ASCII diagrams liberally │
|
| 57 |
+
├─────────────────────────────────────────┤
|
| 58 |
+
│ │
|
| 59 |
+
│ ┌────────┐ ┌────────┐ │
|
| 60 |
+
│ │ State │────────▶│ State │ │
|
| 61 |
+
│ │ A │ │ B │ │
|
| 62 |
+
│ └────────┘ └────────┘ │
|
| 63 |
+
│ │
|
| 64 |
+
│ System diagrams, state machines, │
|
| 65 |
+
│ data flows, architecture sketches, │
|
| 66 |
+
│ dependency graphs, comparison tables │
|
| 67 |
+
│ │
|
| 68 |
+
└─────────────────────────────────────────┘
|
| 69 |
+
```
|
| 70 |
+
|
| 71 |
+
**Surface risks and unknowns**
|
| 72 |
+
- Identify what could go wrong
|
| 73 |
+
- Find gaps in understanding
|
| 74 |
+
- Suggest spikes or investigations
|
| 75 |
+
|
| 76 |
+
---
|
| 77 |
+
|
| 78 |
+
## OpenSpec Awareness
|
| 79 |
+
|
| 80 |
+
You have full context of the OpenSpec system. Use it naturally, don't force it.
|
| 81 |
+
|
| 82 |
+
### Check for context
|
| 83 |
+
|
| 84 |
+
At the start, quickly check what exists:
|
| 85 |
+
```bash
|
| 86 |
+
openspec list --json
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
This tells you:
|
| 90 |
+
- If there are active changes
|
| 91 |
+
- Their names, schemas, and status
|
| 92 |
+
- What the user might be working on
|
| 93 |
+
|
| 94 |
+
### When no change exists
|
| 95 |
+
|
| 96 |
+
Think freely. When insights crystallize, you might offer:
|
| 97 |
+
|
| 98 |
+
- "This feels solid enough to start a change. Want me to create a proposal?"
|
| 99 |
+
- Or keep exploring - no pressure to formalize
|
| 100 |
+
|
| 101 |
+
### When a change exists
|
| 102 |
+
|
| 103 |
+
If the user mentions a change or you detect one is relevant:
|
| 104 |
+
|
| 105 |
+
1. **Read existing artifacts for context**
|
| 106 |
+
- `openspec/changes/<name>/proposal.md`
|
| 107 |
+
- `openspec/changes/<name>/design.md`
|
| 108 |
+
- `openspec/changes/<name>/tasks.md`
|
| 109 |
+
- etc.
|
| 110 |
+
|
| 111 |
+
2. **Reference them naturally in conversation**
|
| 112 |
+
- "Your design mentions using Redis, but we just realized SQLite fits better..."
|
| 113 |
+
- "The proposal scopes this to premium users, but we're now thinking everyone..."
|
| 114 |
+
|
| 115 |
+
3. **Offer to capture when decisions are made**
|
| 116 |
+
|
| 117 |
+
| Insight Type | Where to Capture |
|
| 118 |
+
|----------------------------|--------------------------------|
|
| 119 |
+
| New requirement discovered | `specs/<capability>/spec.md` |
|
| 120 |
+
| Requirement changed | `specs/<capability>/spec.md` |
|
| 121 |
+
| Design decision made | `design.md` |
|
| 122 |
+
| Scope changed | `proposal.md` |
|
| 123 |
+
| New work identified | `tasks.md` |
|
| 124 |
+
| Assumption invalidated | Relevant artifact |
|
| 125 |
+
|
| 126 |
+
Example offers:
|
| 127 |
+
- "That's a design decision. Capture it in design.md?"
|
| 128 |
+
- "This is a new requirement. Add it to specs?"
|
| 129 |
+
- "This changes scope. Update the proposal?"
|
| 130 |
+
|
| 131 |
+
4. **The user decides** - Offer and move on. Don't pressure. Don't auto-capture.
|
| 132 |
+
|
| 133 |
+
---
|
| 134 |
+
|
| 135 |
+
## What You Don't Have To Do
|
| 136 |
+
|
| 137 |
+
- Follow a script
|
| 138 |
+
- Ask the same questions every time
|
| 139 |
+
- Produce a specific artifact
|
| 140 |
+
- Reach a conclusion
|
| 141 |
+
- Stay on topic if a tangent is valuable
|
| 142 |
+
- Be brief (this is thinking time)
|
| 143 |
+
|
| 144 |
+
---
|
| 145 |
+
|
| 146 |
+
## Handling Different Entry Points
|
| 147 |
+
|
| 148 |
+
**User brings a vague idea:**
|
| 149 |
+
```
|
| 150 |
+
User: I'm thinking about adding real-time collaboration
|
| 151 |
+
|
| 152 |
+
You: Real-time collab is a big space. Let me think about this...
|
| 153 |
+
|
| 154 |
+
COLLABORATION SPECTRUM
|
| 155 |
+
════════════════════════════════════════════
|
| 156 |
+
|
| 157 |
+
Awareness Coordination Sync
|
| 158 |
+
│ │ │
|
| 159 |
+
▼ ▼ ▼
|
| 160 |
+
┌────────┐ ┌────────┐ ┌────────┐
|
| 161 |
+
│Presence│ │Cursors │ │ CRDT │
|
| 162 |
+
│ "3 │ │ Multi │ │Conflict│
|
| 163 |
+
│online" │ │ select │ │ free │
|
| 164 |
+
└────────┘ └────────┘ └────────┘
|
| 165 |
+
│ │ │
|
| 166 |
+
trivial moderate complex
|
| 167 |
+
|
| 168 |
+
Where's your head at?
|
| 169 |
+
```
|
| 170 |
+
|
| 171 |
+
**User brings a specific problem:**
|
| 172 |
+
```
|
| 173 |
+
User: The auth system is a mess
|
| 174 |
+
|
| 175 |
+
You: [reads codebase]
|
| 176 |
+
|
| 177 |
+
┌─────────────────────────────────────────────┐
|
| 178 |
+
│ CURRENT AUTH FLOW │
|
| 179 |
+
└─────────────────────────────────────────────┘
|
| 180 |
+
│
|
| 181 |
+
┌─────────────┼─────────────┐
|
| 182 |
+
▼ ▼ ▼
|
| 183 |
+
┌─────────┐ ┌─────────┐ ┌─────────┐
|
| 184 |
+
│ Google │ │ GitHub │ │ Email │
|
| 185 |
+
│ OAuth │ │ OAuth │ │ Magic │
|
| 186 |
+
└────┬────┘ └────┬────┘ └────┬────┘
|
| 187 |
+
│ │ │
|
| 188 |
+
└─────────────┼─────────────┘
|
| 189 |
+
▼
|
| 190 |
+
┌───────────┐
|
| 191 |
+
│ Session │
|
| 192 |
+
└─────┬─────┘
|
| 193 |
+
│
|
| 194 |
+
▼
|
| 195 |
+
┌───────────┐
|
| 196 |
+
│ Perms │
|
| 197 |
+
└───────────┘
|
| 198 |
+
|
| 199 |
+
I see three tangles. Which one's burning?
|
| 200 |
+
```
|
| 201 |
+
|
| 202 |
+
**User is stuck mid-implementation:**
|
| 203 |
+
```
|
| 204 |
+
User: /opsx:explore add-auth-system
|
| 205 |
+
The OAuth integration is more complex than expected
|
| 206 |
+
|
| 207 |
+
You: [reads change artifacts]
|
| 208 |
+
|
| 209 |
+
You're on task 4: "Implement OAuth flow"
|
| 210 |
+
|
| 211 |
+
Let me trace what's involved...
|
| 212 |
+
|
| 213 |
+
[draws diagram, explores options, suggests paths]
|
| 214 |
+
|
| 215 |
+
Want to update the design to reflect this?
|
| 216 |
+
Or add a spike task to investigate?
|
| 217 |
+
```
|
| 218 |
+
|
| 219 |
+
**User wants to compare options:**
|
| 220 |
+
```
|
| 221 |
+
User: Should we use Postgres or SQLite?
|
| 222 |
+
|
| 223 |
+
You: Generic answer is boring. What's the context?
|
| 224 |
+
|
| 225 |
+
User: A CLI tool that tracks local dev environments
|
| 226 |
+
|
| 227 |
+
You: That changes everything.
|
| 228 |
+
|
| 229 |
+
┌─────────────────────────────────────────────────┐
|
| 230 |
+
│ CLI TOOL DATA STORAGE │
|
| 231 |
+
└─────────────────────────────────────────────────┘
|
| 232 |
+
|
| 233 |
+
Key constraints:
|
| 234 |
+
• No daemon running
|
| 235 |
+
• Must work offline
|
| 236 |
+
• Single user
|
| 237 |
+
|
| 238 |
+
SQLite Postgres
|
| 239 |
+
Deployment embedded ✓ needs server ✗
|
| 240 |
+
Offline yes ✓ no ✗
|
| 241 |
+
Single file yes ✓ no ✗
|
| 242 |
+
|
| 243 |
+
SQLite. Not even close.
|
| 244 |
+
|
| 245 |
+
Unless... is there a sync component?
|
| 246 |
+
```
|
| 247 |
+
|
| 248 |
+
---
|
| 249 |
+
|
| 250 |
+
## Ending Discovery
|
| 251 |
+
|
| 252 |
+
There's no required ending. Discovery might:
|
| 253 |
+
|
| 254 |
+
- **Flow into a proposal**: "Ready to start? I can create a change proposal."
|
| 255 |
+
- **Result in artifact updates**: "Updated design.md with these decisions"
|
| 256 |
+
- **Just provide clarity**: User has what they need, moves on
|
| 257 |
+
- **Continue later**: "We can pick this up anytime"
|
| 258 |
+
|
| 259 |
+
When it feels like things are crystallizing, you might summarize:
|
| 260 |
+
|
| 261 |
+
```
|
| 262 |
+
## What We Figured Out
|
| 263 |
+
|
| 264 |
+
**The problem**: [crystallized understanding]
|
| 265 |
+
|
| 266 |
+
**The approach**: [if one emerged]
|
| 267 |
+
|
| 268 |
+
**Open questions**: [if any remain]
|
| 269 |
+
|
| 270 |
+
**Next steps** (if ready):
|
| 271 |
+
- Create a change proposal
|
| 272 |
+
- Keep exploring: just keep talking
|
| 273 |
+
```
|
| 274 |
+
|
| 275 |
+
But this summary is optional. Sometimes the thinking IS the value.
|
| 276 |
+
|
| 277 |
+
---
|
| 278 |
+
|
| 279 |
+
## Guardrails
|
| 280 |
+
|
| 281 |
+
- **Don't implement** - Never write code or implement features. Creating OpenSpec artifacts is fine, writing application code is not.
|
| 282 |
+
- **Don't fake understanding** - If something is unclear, dig deeper
|
| 283 |
+
- **Don't rush** - Discovery is thinking time, not task time
|
| 284 |
+
- **Don't force structure** - Let patterns emerge naturally
|
| 285 |
+
- **Don't auto-capture** - Offer to save insights, don't just do it
|
| 286 |
+
- **Do visualize** - A good diagram is worth many paragraphs
|
| 287 |
+
- **Do explore the codebase** - Ground discussions in reality
|
| 288 |
+
- **Do question assumptions** - Including the user's and your own
|
.kiro/skills/openspec-propose/SKILL.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: openspec-propose
|
| 3 |
+
description: Propose a new change with all artifacts generated in one step. Use when the user wants to quickly describe what they want to build and get a complete proposal with design, specs, and tasks ready for implementation.
|
| 4 |
+
license: MIT
|
| 5 |
+
compatibility: Requires openspec CLI.
|
| 6 |
+
metadata:
|
| 7 |
+
author: openspec
|
| 8 |
+
version: "1.0"
|
| 9 |
+
generatedBy: "1.3.1"
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
Propose a new change - create the change and generate all artifacts in one step.
|
| 13 |
+
|
| 14 |
+
I'll create a change with artifacts:
|
| 15 |
+
- proposal.md (what & why)
|
| 16 |
+
- design.md (how)
|
| 17 |
+
- tasks.md (implementation steps)
|
| 18 |
+
|
| 19 |
+
When ready to implement, run /opsx:apply
|
| 20 |
+
|
| 21 |
+
---
|
| 22 |
+
|
| 23 |
+
**Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build.
|
| 24 |
+
|
| 25 |
+
**Steps**
|
| 26 |
+
|
| 27 |
+
1. **If no clear input provided, ask what they want to build**
|
| 28 |
+
|
| 29 |
+
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
|
| 30 |
+
> "What change do you want to work on? Describe what you want to build or fix."
|
| 31 |
+
|
| 32 |
+
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
|
| 33 |
+
|
| 34 |
+
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
|
| 35 |
+
|
| 36 |
+
2. **Create the change directory**
|
| 37 |
+
```bash
|
| 38 |
+
openspec new change "<name>"
|
| 39 |
+
```
|
| 40 |
+
This creates a scaffolded change at `openspec/changes/<name>/` with `.openspec.yaml`.
|
| 41 |
+
|
| 42 |
+
3. **Get the artifact build order**
|
| 43 |
+
```bash
|
| 44 |
+
openspec status --change "<name>" --json
|
| 45 |
+
```
|
| 46 |
+
Parse the JSON to get:
|
| 47 |
+
- `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`)
|
| 48 |
+
- `artifacts`: list of all artifacts with their status and dependencies
|
| 49 |
+
|
| 50 |
+
4. **Create artifacts in sequence until apply-ready**
|
| 51 |
+
|
| 52 |
+
Use the **TodoWrite tool** to track progress through the artifacts.
|
| 53 |
+
|
| 54 |
+
Loop through artifacts in dependency order (artifacts with no pending dependencies first):
|
| 55 |
+
|
| 56 |
+
a. **For each artifact that is `ready` (dependencies satisfied)**:
|
| 57 |
+
- Get instructions:
|
| 58 |
+
```bash
|
| 59 |
+
openspec instructions <artifact-id> --change "<name>" --json
|
| 60 |
+
```
|
| 61 |
+
- The instructions JSON includes:
|
| 62 |
+
- `context`: Project background (constraints for you - do NOT include in output)
|
| 63 |
+
- `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
|
| 64 |
+
- `template`: The structure to use for your output file
|
| 65 |
+
- `instruction`: Schema-specific guidance for this artifact type
|
| 66 |
+
- `outputPath`: Where to write the artifact
|
| 67 |
+
- `dependencies`: Completed artifacts to read for context
|
| 68 |
+
- Read any completed dependency files for context
|
| 69 |
+
- Create the artifact file using `template` as the structure
|
| 70 |
+
- Apply `context` and `rules` as constraints - but do NOT copy them into the file
|
| 71 |
+
- Show brief progress: "Created <artifact-id>"
|
| 72 |
+
|
| 73 |
+
b. **Continue until all `applyRequires` artifacts are complete**
|
| 74 |
+
- After creating each artifact, re-run `openspec status --change "<name>" --json`
|
| 75 |
+
- Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array
|
| 76 |
+
- Stop when all `applyRequires` artifacts are done
|
| 77 |
+
|
| 78 |
+
c. **If an artifact requires user input** (unclear context):
|
| 79 |
+
- Use **AskUserQuestion tool** to clarify
|
| 80 |
+
- Then continue with creation
|
| 81 |
+
|
| 82 |
+
5. **Show final status**
|
| 83 |
+
```bash
|
| 84 |
+
openspec status --change "<name>"
|
| 85 |
+
```
|
| 86 |
+
|
| 87 |
+
**Output**
|
| 88 |
+
|
| 89 |
+
After completing all artifacts, summarize:
|
| 90 |
+
- Change name and location
|
| 91 |
+
- List of artifacts created with brief descriptions
|
| 92 |
+
- What's ready: "All artifacts created! Ready for implementation."
|
| 93 |
+
- Prompt: "Run `/opsx:apply` or ask me to implement to start working on the tasks."
|
| 94 |
+
|
| 95 |
+
**Artifact Creation Guidelines**
|
| 96 |
+
|
| 97 |
+
- Follow the `instruction` field from `openspec instructions` for each artifact type
|
| 98 |
+
- The schema defines what each artifact should contain - follow it
|
| 99 |
+
- Read dependency artifacts for context before creating new ones
|
| 100 |
+
- Use `template` as the structure for your output file - fill in its sections
|
| 101 |
+
- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file
|
| 102 |
+
- Do NOT copy `<context>`, `<rules>`, `<project_context>` blocks into the artifact
|
| 103 |
+
- These guide what you write, but should never appear in the output
|
| 104 |
+
|
| 105 |
+
**Guardrails**
|
| 106 |
+
- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`)
|
| 107 |
+
- Always read dependency artifacts before creating a new one
|
| 108 |
+
- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
|
| 109 |
+
- If a change with that name already exists, ask if user wants to continue it or create a new one
|
| 110 |
+
- Verify each artifact file exists after writing before proceeding to next
|
.opencode/commands/opsx-apply.md
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
description: Implement tasks from an OpenSpec change (Experimental)
|
| 3 |
+
---
|
| 4 |
+
|
| 5 |
+
Implement tasks from an OpenSpec change.
|
| 6 |
+
|
| 7 |
+
**Input**: Optionally specify a change name (e.g., `/opsx-apply add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
| 8 |
+
|
| 9 |
+
**Steps**
|
| 10 |
+
|
| 11 |
+
1. **Select the change**
|
| 12 |
+
|
| 13 |
+
If a name is provided, use it. Otherwise:
|
| 14 |
+
- Infer from conversation context if the user mentioned a change
|
| 15 |
+
- Auto-select if only one active change exists
|
| 16 |
+
- If ambiguous, run `openspec list --json` to get available changes and use the **AskUserQuestion tool** to let the user select
|
| 17 |
+
|
| 18 |
+
Always announce: "Using change: <name>" and how to override (e.g., `/opsx-apply <other>`).
|
| 19 |
+
|
| 20 |
+
2. **Check status to understand the schema**
|
| 21 |
+
```bash
|
| 22 |
+
openspec status --change "<name>" --json
|
| 23 |
+
```
|
| 24 |
+
Parse the JSON to understand:
|
| 25 |
+
- `schemaName`: The workflow being used (e.g., "spec-driven")
|
| 26 |
+
- Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
|
| 27 |
+
|
| 28 |
+
3. **Get apply instructions**
|
| 29 |
+
|
| 30 |
+
```bash
|
| 31 |
+
openspec instructions apply --change "<name>" --json
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
This returns:
|
| 35 |
+
- `contextFiles`: artifact ID -> array of concrete file paths (varies by schema)
|
| 36 |
+
- Progress (total, complete, remaining)
|
| 37 |
+
- Task list with status
|
| 38 |
+
- Dynamic instruction based on current state
|
| 39 |
+
|
| 40 |
+
**Handle states:**
|
| 41 |
+
- If `state: "blocked"` (missing artifacts): show message, suggest using `/opsx-continue`
|
| 42 |
+
- If `state: "all_done"`: congratulate, suggest archive
|
| 43 |
+
- Otherwise: proceed to implementation
|
| 44 |
+
|
| 45 |
+
4. **Read context files**
|
| 46 |
+
|
| 47 |
+
Read every file path listed under `contextFiles` from the apply instructions output.
|
| 48 |
+
The files depend on the schema being used:
|
| 49 |
+
- **spec-driven**: proposal, specs, design, tasks
|
| 50 |
+
- Other schemas: follow the contextFiles from CLI output
|
| 51 |
+
|
| 52 |
+
5. **Show current progress**
|
| 53 |
+
|
| 54 |
+
Display:
|
| 55 |
+
- Schema being used
|
| 56 |
+
- Progress: "N/M tasks complete"
|
| 57 |
+
- Remaining tasks overview
|
| 58 |
+
- Dynamic instruction from CLI
|
| 59 |
+
|
| 60 |
+
6. **Implement tasks (loop until done or blocked)**
|
| 61 |
+
|
| 62 |
+
For each pending task:
|
| 63 |
+
- Show which task is being worked on
|
| 64 |
+
- Make the code changes required
|
| 65 |
+
- Keep changes minimal and focused
|
| 66 |
+
- Mark task complete in the tasks file: `- [ ]` → `- [x]`
|
| 67 |
+
- Continue to next task
|
| 68 |
+
|
| 69 |
+
**Pause if:**
|
| 70 |
+
- Task is unclear → ask for clarification
|
| 71 |
+
- Implementation reveals a design issue → suggest updating artifacts
|
| 72 |
+
- Error or blocker encountered → report and wait for guidance
|
| 73 |
+
- User interrupts
|
| 74 |
+
|
| 75 |
+
7. **On completion or pause, show status**
|
| 76 |
+
|
| 77 |
+
Display:
|
| 78 |
+
- Tasks completed this session
|
| 79 |
+
- Overall progress: "N/M tasks complete"
|
| 80 |
+
- If all done: suggest archive
|
| 81 |
+
- If paused: explain why and wait for guidance
|
| 82 |
+
|
| 83 |
+
**Output During Implementation**
|
| 84 |
+
|
| 85 |
+
```
|
| 86 |
+
## Implementing: <change-name> (schema: <schema-name>)
|
| 87 |
+
|
| 88 |
+
Working on task 3/7: <task description>
|
| 89 |
+
[...implementation happening...]
|
| 90 |
+
✓ Task complete
|
| 91 |
+
|
| 92 |
+
Working on task 4/7: <task description>
|
| 93 |
+
[...implementation happening...]
|
| 94 |
+
✓ Task complete
|
| 95 |
+
```
|
| 96 |
+
|
| 97 |
+
**Output On Completion**
|
| 98 |
+
|
| 99 |
+
```
|
| 100 |
+
## Implementation Complete
|
| 101 |
+
|
| 102 |
+
**Change:** <change-name>
|
| 103 |
+
**Schema:** <schema-name>
|
| 104 |
+
**Progress:** 7/7 tasks complete ✓
|
| 105 |
+
|
| 106 |
+
### Completed This Session
|
| 107 |
+
- [x] Task 1
|
| 108 |
+
- [x] Task 2
|
| 109 |
+
...
|
| 110 |
+
|
| 111 |
+
All tasks complete! You can archive this change with `/opsx-archive`.
|
| 112 |
+
```
|
| 113 |
+
|
| 114 |
+
**Output On Pause (Issue Encountered)**
|
| 115 |
+
|
| 116 |
+
```
|
| 117 |
+
## Implementation Paused
|
| 118 |
+
|
| 119 |
+
**Change:** <change-name>
|
| 120 |
+
**Schema:** <schema-name>
|
| 121 |
+
**Progress:** 4/7 tasks complete
|
| 122 |
+
|
| 123 |
+
### Issue Encountered
|
| 124 |
+
<description of the issue>
|
| 125 |
+
|
| 126 |
+
**Options:**
|
| 127 |
+
1. <option 1>
|
| 128 |
+
2. <option 2>
|
| 129 |
+
3. Other approach
|
| 130 |
+
|
| 131 |
+
What would you like to do?
|
| 132 |
+
```
|
| 133 |
+
|
| 134 |
+
**Guardrails**
|
| 135 |
+
- Keep going through tasks until done or blocked
|
| 136 |
+
- Always read context files before starting (from the apply instructions output)
|
| 137 |
+
- If task is ambiguous, pause and ask before implementing
|
| 138 |
+
- If implementation reveals issues, pause and suggest artifact updates
|
| 139 |
+
- Keep code changes minimal and scoped to each task
|
| 140 |
+
- Update task checkbox immediately after completing each task
|
| 141 |
+
- Pause on errors, blockers, or unclear requirements - don't guess
|
| 142 |
+
- Use contextFiles from CLI output, don't assume specific file names
|
| 143 |
+
|
| 144 |
+
**Fluid Workflow Integration**
|
| 145 |
+
|
| 146 |
+
This skill supports the "actions on a change" model:
|
| 147 |
+
|
| 148 |
+
- **Can be invoked anytime**: Before all artifacts are done (if tasks exist), after partial implementation, interleaved with other actions
|
| 149 |
+
- **Allows artifact updates**: If implementation reveals design issues, suggest updating artifacts - not phase-locked, work fluidly
|
.opencode/commands/opsx-archive.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
description: Archive a completed change in the experimental workflow
|
| 3 |
+
---
|
| 4 |
+
|
| 5 |
+
Archive a completed change in the experimental workflow.
|
| 6 |
+
|
| 7 |
+
**Input**: Optionally specify a change name after `/opsx-archive` (e.g., `/opsx-archive add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
| 8 |
+
|
| 9 |
+
**Steps**
|
| 10 |
+
|
| 11 |
+
1. **If no change name provided, prompt for selection**
|
| 12 |
+
|
| 13 |
+
Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
|
| 14 |
+
|
| 15 |
+
Show only active changes (not already archived).
|
| 16 |
+
Include the schema used for each change if available.
|
| 17 |
+
|
| 18 |
+
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
| 19 |
+
|
| 20 |
+
2. **Check artifact completion status**
|
| 21 |
+
|
| 22 |
+
Run `openspec status --change "<name>" --json` to check artifact completion.
|
| 23 |
+
|
| 24 |
+
Parse the JSON to understand:
|
| 25 |
+
- `schemaName`: The workflow being used
|
| 26 |
+
- `artifacts`: List of artifacts with their status (`done` or other)
|
| 27 |
+
|
| 28 |
+
**If any artifacts are not `done`:**
|
| 29 |
+
- Display warning listing incomplete artifacts
|
| 30 |
+
- Prompt user for confirmation to continue
|
| 31 |
+
- Proceed if user confirms
|
| 32 |
+
|
| 33 |
+
3. **Check task completion status**
|
| 34 |
+
|
| 35 |
+
Read the tasks file (typically `tasks.md`) to check for incomplete tasks.
|
| 36 |
+
|
| 37 |
+
Count tasks marked with `- [ ]` (incomplete) vs `- [x]` (complete).
|
| 38 |
+
|
| 39 |
+
**If incomplete tasks found:**
|
| 40 |
+
- Display warning showing count of incomplete tasks
|
| 41 |
+
- Prompt user for confirmation to continue
|
| 42 |
+
- Proceed if user confirms
|
| 43 |
+
|
| 44 |
+
**If no tasks file exists:** Proceed without task-related warning.
|
| 45 |
+
|
| 46 |
+
4. **Assess delta spec sync state**
|
| 47 |
+
|
| 48 |
+
Check for delta specs at `openspec/changes/<name>/specs/`. If none exist, proceed without sync prompt.
|
| 49 |
+
|
| 50 |
+
**If delta specs exist:**
|
| 51 |
+
- Compare each delta spec with its corresponding main spec at `openspec/specs/<capability>/spec.md`
|
| 52 |
+
- Determine what changes would be applied (adds, modifications, removals, renames)
|
| 53 |
+
- Show a combined summary before prompting
|
| 54 |
+
|
| 55 |
+
**Prompt options:**
|
| 56 |
+
- If changes needed: "Sync now (recommended)", "Archive without syncing"
|
| 57 |
+
- If already synced: "Archive now", "Sync anyway", "Cancel"
|
| 58 |
+
|
| 59 |
+
If user chooses sync, use Task tool (subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change '<name>'. Delta spec analysis: <include the analyzed delta spec summary>"). Proceed to archive regardless of choice.
|
| 60 |
+
|
| 61 |
+
5. **Perform the archive**
|
| 62 |
+
|
| 63 |
+
Create the archive directory if it doesn't exist:
|
| 64 |
+
```bash
|
| 65 |
+
mkdir -p openspec/changes/archive
|
| 66 |
+
```
|
| 67 |
+
|
| 68 |
+
Generate target name using current date: `YYYY-MM-DD-<change-name>`
|
| 69 |
+
|
| 70 |
+
**Check if target already exists:**
|
| 71 |
+
- If yes: Fail with error, suggest renaming existing archive or using different date
|
| 72 |
+
- If no: Move the change directory to archive
|
| 73 |
+
|
| 74 |
+
```bash
|
| 75 |
+
mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name>
|
| 76 |
+
```
|
| 77 |
+
|
| 78 |
+
6. **Display summary**
|
| 79 |
+
|
| 80 |
+
Show archive completion summary including:
|
| 81 |
+
- Change name
|
| 82 |
+
- Schema that was used
|
| 83 |
+
- Archive location
|
| 84 |
+
- Spec sync status (synced / sync skipped / no delta specs)
|
| 85 |
+
- Note about any warnings (incomplete artifacts/tasks)
|
| 86 |
+
|
| 87 |
+
**Output On Success**
|
| 88 |
+
|
| 89 |
+
```
|
| 90 |
+
## Archive Complete
|
| 91 |
+
|
| 92 |
+
**Change:** <change-name>
|
| 93 |
+
**Schema:** <schema-name>
|
| 94 |
+
**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
| 95 |
+
**Specs:** ✓ Synced to main specs
|
| 96 |
+
|
| 97 |
+
All artifacts complete. All tasks complete.
|
| 98 |
+
```
|
| 99 |
+
|
| 100 |
+
**Output On Success (No Delta Specs)**
|
| 101 |
+
|
| 102 |
+
```
|
| 103 |
+
## Archive Complete
|
| 104 |
+
|
| 105 |
+
**Change:** <change-name>
|
| 106 |
+
**Schema:** <schema-name>
|
| 107 |
+
**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
| 108 |
+
**Specs:** No delta specs
|
| 109 |
+
|
| 110 |
+
All artifacts complete. All tasks complete.
|
| 111 |
+
```
|
| 112 |
+
|
| 113 |
+
**Output On Success With Warnings**
|
| 114 |
+
|
| 115 |
+
```
|
| 116 |
+
## Archive Complete (with warnings)
|
| 117 |
+
|
| 118 |
+
**Change:** <change-name>
|
| 119 |
+
**Schema:** <schema-name>
|
| 120 |
+
**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
| 121 |
+
**Specs:** Sync skipped (user chose to skip)
|
| 122 |
+
|
| 123 |
+
**Warnings:**
|
| 124 |
+
- Archived with 2 incomplete artifacts
|
| 125 |
+
- Archived with 3 incomplete tasks
|
| 126 |
+
- Delta spec sync was skipped (user chose to skip)
|
| 127 |
+
|
| 128 |
+
Review the archive if this was not intentional.
|
| 129 |
+
```
|
| 130 |
+
|
| 131 |
+
**Output On Error (Archive Exists)**
|
| 132 |
+
|
| 133 |
+
```
|
| 134 |
+
## Archive Failed
|
| 135 |
+
|
| 136 |
+
**Change:** <change-name>
|
| 137 |
+
**Target:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
| 138 |
+
|
| 139 |
+
Target archive directory already exists.
|
| 140 |
+
|
| 141 |
+
**Options:**
|
| 142 |
+
1. Rename the existing archive
|
| 143 |
+
2. Delete the existing archive if it's a duplicate
|
| 144 |
+
3. Wait until a different date to archive
|
| 145 |
+
```
|
| 146 |
+
|
| 147 |
+
**Guardrails**
|
| 148 |
+
- Always prompt for change selection if not provided
|
| 149 |
+
- Use artifact graph (openspec status --json) for completion checking
|
| 150 |
+
- Don't block archive on warnings - just inform and confirm
|
| 151 |
+
- Preserve .openspec.yaml when moving to archive (it moves with the directory)
|
| 152 |
+
- Show clear summary of what happened
|
| 153 |
+
- If sync is requested, use the Skill tool to invoke `openspec-sync-specs` (agent-driven)
|
| 154 |
+
- If delta specs exist, always run the sync assessment and show the combined summary before prompting
|
.opencode/commands/opsx-explore.md
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
description: Enter explore mode - think through ideas, investigate problems, clarify requirements
|
| 3 |
+
---
|
| 4 |
+
|
| 5 |
+
Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes.
|
| 6 |
+
|
| 7 |
+
**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first and create a change proposal. You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing.
|
| 8 |
+
|
| 9 |
+
**This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore.
|
| 10 |
+
|
| 11 |
+
**Input**: The argument after `/opsx-explore` is whatever the user wants to think about. Could be:
|
| 12 |
+
- A vague idea: "real-time collaboration"
|
| 13 |
+
- A specific problem: "the auth system is getting unwieldy"
|
| 14 |
+
- A change name: "add-dark-mode" (to explore in context of that change)
|
| 15 |
+
- A comparison: "postgres vs sqlite for this"
|
| 16 |
+
- Nothing (just enter explore mode)
|
| 17 |
+
|
| 18 |
+
---
|
| 19 |
+
|
| 20 |
+
## The Stance
|
| 21 |
+
|
| 22 |
+
- **Curious, not prescriptive** - Ask questions that emerge naturally, don't follow a script
|
| 23 |
+
- **Open threads, not interrogations** - Surface multiple interesting directions and let the user follow what resonates. Don't funnel them through a single path of questions.
|
| 24 |
+
- **Visual** - Use ASCII diagrams liberally when they'd help clarify thinking
|
| 25 |
+
- **Adaptive** - Follow interesting threads, pivot when new information emerges
|
| 26 |
+
- **Patient** - Don't rush to conclusions, let the shape of the problem emerge
|
| 27 |
+
- **Grounded** - Explore the actual codebase when relevant, don't just theorize
|
| 28 |
+
|
| 29 |
+
---
|
| 30 |
+
|
| 31 |
+
## What You Might Do
|
| 32 |
+
|
| 33 |
+
Depending on what the user brings, you might:
|
| 34 |
+
|
| 35 |
+
**Explore the problem space**
|
| 36 |
+
- Ask clarifying questions that emerge from what they said
|
| 37 |
+
- Challenge assumptions
|
| 38 |
+
- Reframe the problem
|
| 39 |
+
- Find analogies
|
| 40 |
+
|
| 41 |
+
**Investigate the codebase**
|
| 42 |
+
- Map existing architecture relevant to the discussion
|
| 43 |
+
- Find integration points
|
| 44 |
+
- Identify patterns already in use
|
| 45 |
+
- Surface hidden complexity
|
| 46 |
+
|
| 47 |
+
**Compare options**
|
| 48 |
+
- Brainstorm multiple approaches
|
| 49 |
+
- Build comparison tables
|
| 50 |
+
- Sketch tradeoffs
|
| 51 |
+
- Recommend a path (if asked)
|
| 52 |
+
|
| 53 |
+
**Visualize**
|
| 54 |
+
```
|
| 55 |
+
┌─────────────────────────────────────────┐
|
| 56 |
+
│ Use ASCII diagrams liberally │
|
| 57 |
+
├─────────────────────────────────────────┤
|
| 58 |
+
│ │
|
| 59 |
+
│ ┌────────┐ ┌────────┐ │
|
| 60 |
+
│ │ State │────────▶│ State │ │
|
| 61 |
+
│ │ A │ │ B │ │
|
| 62 |
+
│ └────────┘ └────────┘ │
|
| 63 |
+
│ │
|
| 64 |
+
│ System diagrams, state machines, │
|
| 65 |
+
│ data flows, architecture sketches, │
|
| 66 |
+
│ dependency graphs, comparison tables │
|
| 67 |
+
│ │
|
| 68 |
+
└─────────────────────────────────────────┘
|
| 69 |
+
```
|
| 70 |
+
|
| 71 |
+
**Surface risks and unknowns**
|
| 72 |
+
- Identify what could go wrong
|
| 73 |
+
- Find gaps in understanding
|
| 74 |
+
- Suggest spikes or investigations
|
| 75 |
+
|
| 76 |
+
---
|
| 77 |
+
|
| 78 |
+
## OpenSpec Awareness
|
| 79 |
+
|
| 80 |
+
You have full context of the OpenSpec system. Use it naturally, don't force it.
|
| 81 |
+
|
| 82 |
+
### Check for context
|
| 83 |
+
|
| 84 |
+
At the start, quickly check what exists:
|
| 85 |
+
```bash
|
| 86 |
+
openspec list --json
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
This tells you:
|
| 90 |
+
- If there are active changes
|
| 91 |
+
- Their names, schemas, and status
|
| 92 |
+
- What the user might be working on
|
| 93 |
+
|
| 94 |
+
If the user mentioned a specific change name, read its artifacts for context.
|
| 95 |
+
|
| 96 |
+
### When no change exists
|
| 97 |
+
|
| 98 |
+
Think freely. When insights crystallize, you might offer:
|
| 99 |
+
|
| 100 |
+
- "This feels solid enough to start a change. Want me to create a proposal?"
|
| 101 |
+
- Or keep exploring - no pressure to formalize
|
| 102 |
+
|
| 103 |
+
### When a change exists
|
| 104 |
+
|
| 105 |
+
If the user mentions a change or you detect one is relevant:
|
| 106 |
+
|
| 107 |
+
1. **Read existing artifacts for context**
|
| 108 |
+
- `openspec/changes/<name>/proposal.md`
|
| 109 |
+
- `openspec/changes/<name>/design.md`
|
| 110 |
+
- `openspec/changes/<name>/tasks.md`
|
| 111 |
+
- etc.
|
| 112 |
+
|
| 113 |
+
2. **Reference them naturally in conversation**
|
| 114 |
+
- "Your design mentions using Redis, but we just realized SQLite fits better..."
|
| 115 |
+
- "The proposal scopes this to premium users, but we're now thinking everyone..."
|
| 116 |
+
|
| 117 |
+
3. **Offer to capture when decisions are made**
|
| 118 |
+
|
| 119 |
+
| Insight Type | Where to Capture |
|
| 120 |
+
|----------------------------|--------------------------------|
|
| 121 |
+
| New requirement discovered | `specs/<capability>/spec.md` |
|
| 122 |
+
| Requirement changed | `specs/<capability>/spec.md` |
|
| 123 |
+
| Design decision made | `design.md` |
|
| 124 |
+
| Scope changed | `proposal.md` |
|
| 125 |
+
| New work identified | `tasks.md` |
|
| 126 |
+
| Assumption invalidated | Relevant artifact |
|
| 127 |
+
|
| 128 |
+
Example offers:
|
| 129 |
+
- "That's a design decision. Capture it in design.md?"
|
| 130 |
+
- "This is a new requirement. Add it to specs?"
|
| 131 |
+
- "This changes scope. Update the proposal?"
|
| 132 |
+
|
| 133 |
+
4. **The user decides** - Offer and move on. Don't pressure. Don't auto-capture.
|
| 134 |
+
|
| 135 |
+
---
|
| 136 |
+
|
| 137 |
+
## What You Don't Have To Do
|
| 138 |
+
|
| 139 |
+
- Follow a script
|
| 140 |
+
- Ask the same questions every time
|
| 141 |
+
- Produce a specific artifact
|
| 142 |
+
- Reach a conclusion
|
| 143 |
+
- Stay on topic if a tangent is valuable
|
| 144 |
+
- Be brief (this is thinking time)
|
| 145 |
+
|
| 146 |
+
---
|
| 147 |
+
|
| 148 |
+
## Ending Discovery
|
| 149 |
+
|
| 150 |
+
There's no required ending. Discovery might:
|
| 151 |
+
|
| 152 |
+
- **Flow into a proposal**: "Ready to start? I can create a change proposal."
|
| 153 |
+
- **Result in artifact updates**: "Updated design.md with these decisions"
|
| 154 |
+
- **Just provide clarity**: User has what they need, moves on
|
| 155 |
+
- **Continue later**: "We can pick this up anytime"
|
| 156 |
+
|
| 157 |
+
When things crystallize, you might offer a summary - but it's optional. Sometimes the thinking IS the value.
|
| 158 |
+
|
| 159 |
+
---
|
| 160 |
+
|
| 161 |
+
## Guardrails
|
| 162 |
+
|
| 163 |
+
- **Don't implement** - Never write code or implement features. Creating OpenSpec artifacts is fine, writing application code is not.
|
| 164 |
+
- **Don't fake understanding** - If something is unclear, dig deeper
|
| 165 |
+
- **Don't rush** - Discovery is thinking time, not task time
|
| 166 |
+
- **Don't force structure** - Let patterns emerge naturally
|
| 167 |
+
- **Don't auto-capture** - Offer to save insights, don't just do it
|
| 168 |
+
- **Do visualize** - A good diagram is worth many paragraphs
|
| 169 |
+
- **Do explore the codebase** - Ground discussions in reality
|
| 170 |
+
- **Do question assumptions** - Including the user's and your own
|
.opencode/commands/opsx-propose.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
description: Propose a new change - create it and generate all artifacts in one step
|
| 3 |
+
---
|
| 4 |
+
|
| 5 |
+
Propose a new change - create the change and generate all artifacts in one step.
|
| 6 |
+
|
| 7 |
+
I'll create a change with artifacts:
|
| 8 |
+
- proposal.md (what & why)
|
| 9 |
+
- design.md (how)
|
| 10 |
+
- tasks.md (implementation steps)
|
| 11 |
+
|
| 12 |
+
When ready to implement, run /opsx-apply
|
| 13 |
+
|
| 14 |
+
---
|
| 15 |
+
|
| 16 |
+
**Input**: The argument after `/opsx-propose` is the change name (kebab-case), OR a description of what the user wants to build.
|
| 17 |
+
|
| 18 |
+
**Steps**
|
| 19 |
+
|
| 20 |
+
1. **If no input provided, ask what they want to build**
|
| 21 |
+
|
| 22 |
+
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
|
| 23 |
+
> "What change do you want to work on? Describe what you want to build or fix."
|
| 24 |
+
|
| 25 |
+
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
|
| 26 |
+
|
| 27 |
+
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
|
| 28 |
+
|
| 29 |
+
2. **Create the change directory**
|
| 30 |
+
```bash
|
| 31 |
+
openspec new change "<name>"
|
| 32 |
+
```
|
| 33 |
+
This creates a scaffolded change at `openspec/changes/<name>/` with `.openspec.yaml`.
|
| 34 |
+
|
| 35 |
+
3. **Get the artifact build order**
|
| 36 |
+
```bash
|
| 37 |
+
openspec status --change "<name>" --json
|
| 38 |
+
```
|
| 39 |
+
Parse the JSON to get:
|
| 40 |
+
- `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`)
|
| 41 |
+
- `artifacts`: list of all artifacts with their status and dependencies
|
| 42 |
+
|
| 43 |
+
4. **Create artifacts in sequence until apply-ready**
|
| 44 |
+
|
| 45 |
+
Use the **TodoWrite tool** to track progress through the artifacts.
|
| 46 |
+
|
| 47 |
+
Loop through artifacts in dependency order (artifacts with no pending dependencies first):
|
| 48 |
+
|
| 49 |
+
a. **For each artifact that is `ready` (dependencies satisfied)**:
|
| 50 |
+
- Get instructions:
|
| 51 |
+
```bash
|
| 52 |
+
openspec instructions <artifact-id> --change "<name>" --json
|
| 53 |
+
```
|
| 54 |
+
- The instructions JSON includes:
|
| 55 |
+
- `context`: Project background (constraints for you - do NOT include in output)
|
| 56 |
+
- `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
|
| 57 |
+
- `template`: The structure to use for your output file
|
| 58 |
+
- `instruction`: Schema-specific guidance for this artifact type
|
| 59 |
+
- `outputPath`: Where to write the artifact
|
| 60 |
+
- `dependencies`: Completed artifacts to read for context
|
| 61 |
+
- Read any completed dependency files for context
|
| 62 |
+
- Create the artifact file using `template` as the structure
|
| 63 |
+
- Apply `context` and `rules` as constraints - but do NOT copy them into the file
|
| 64 |
+
- Show brief progress: "Created <artifact-id>"
|
| 65 |
+
|
| 66 |
+
b. **Continue until all `applyRequires` artifacts are complete**
|
| 67 |
+
- After creating each artifact, re-run `openspec status --change "<name>" --json`
|
| 68 |
+
- Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array
|
| 69 |
+
- Stop when all `applyRequires` artifacts are done
|
| 70 |
+
|
| 71 |
+
c. **If an artifact requires user input** (unclear context):
|
| 72 |
+
- Use **AskUserQuestion tool** to clarify
|
| 73 |
+
- Then continue with creation
|
| 74 |
+
|
| 75 |
+
5. **Show final status**
|
| 76 |
+
```bash
|
| 77 |
+
openspec status --change "<name>"
|
| 78 |
+
```
|
| 79 |
+
|
| 80 |
+
**Output**
|
| 81 |
+
|
| 82 |
+
After completing all artifacts, summarize:
|
| 83 |
+
- Change name and location
|
| 84 |
+
- List of artifacts created with brief descriptions
|
| 85 |
+
- What's ready: "All artifacts created! Ready for implementation."
|
| 86 |
+
- Prompt: "Run `/opsx-apply` to start implementing."
|
| 87 |
+
|
| 88 |
+
**Artifact Creation Guidelines**
|
| 89 |
+
|
| 90 |
+
- Follow the `instruction` field from `openspec instructions` for each artifact type
|
| 91 |
+
- The schema defines what each artifact should contain - follow it
|
| 92 |
+
- Read dependency artifacts for context before creating new ones
|
| 93 |
+
- Use `template` as the structure for your output file - fill in its sections
|
| 94 |
+
- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file
|
| 95 |
+
- Do NOT copy `<context>`, `<rules>`, `<project_context>` blocks into the artifact
|
| 96 |
+
- These guide what you write, but should never appear in the output
|
| 97 |
+
|
| 98 |
+
**Guardrails**
|
| 99 |
+
- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`)
|
| 100 |
+
- Always read dependency artifacts before creating a new one
|
| 101 |
+
- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
|
| 102 |
+
- If a change with that name already exists, ask if user wants to continue it or create a new one
|
| 103 |
+
- Verify each artifact file exists after writing before proceeding to next
|
.opencode/skills/openspec-apply-change/SKILL.md
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: openspec-apply-change
|
| 3 |
+
description: Implement tasks from an OpenSpec change. Use when the user wants to start implementing, continue implementation, or work through tasks.
|
| 4 |
+
license: MIT
|
| 5 |
+
compatibility: Requires openspec CLI.
|
| 6 |
+
metadata:
|
| 7 |
+
author: openspec
|
| 8 |
+
version: "1.0"
|
| 9 |
+
generatedBy: "1.3.1"
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
Implement tasks from an OpenSpec change.
|
| 13 |
+
|
| 14 |
+
**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
| 15 |
+
|
| 16 |
+
**Steps**
|
| 17 |
+
|
| 18 |
+
1. **Select the change**
|
| 19 |
+
|
| 20 |
+
If a name is provided, use it. Otherwise:
|
| 21 |
+
- Infer from conversation context if the user mentioned a change
|
| 22 |
+
- Auto-select if only one active change exists
|
| 23 |
+
- If ambiguous, run `openspec list --json` to get available changes and use the **AskUserQuestion tool** to let the user select
|
| 24 |
+
|
| 25 |
+
Always announce: "Using change: <name>" and how to override (e.g., `/opsx-apply <other>`).
|
| 26 |
+
|
| 27 |
+
2. **Check status to understand the schema**
|
| 28 |
+
```bash
|
| 29 |
+
openspec status --change "<name>" --json
|
| 30 |
+
```
|
| 31 |
+
Parse the JSON to understand:
|
| 32 |
+
- `schemaName`: The workflow being used (e.g., "spec-driven")
|
| 33 |
+
- Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
|
| 34 |
+
|
| 35 |
+
3. **Get apply instructions**
|
| 36 |
+
|
| 37 |
+
```bash
|
| 38 |
+
openspec instructions apply --change "<name>" --json
|
| 39 |
+
```
|
| 40 |
+
|
| 41 |
+
This returns:
|
| 42 |
+
- `contextFiles`: artifact ID -> array of concrete file paths (varies by schema - could be proposal/specs/design/tasks or spec/tests/implementation/docs)
|
| 43 |
+
- Progress (total, complete, remaining)
|
| 44 |
+
- Task list with status
|
| 45 |
+
- Dynamic instruction based on current state
|
| 46 |
+
|
| 47 |
+
**Handle states:**
|
| 48 |
+
- If `state: "blocked"` (missing artifacts): show message, suggest using openspec-continue-change
|
| 49 |
+
- If `state: "all_done"`: congratulate, suggest archive
|
| 50 |
+
- Otherwise: proceed to implementation
|
| 51 |
+
|
| 52 |
+
4. **Read context files**
|
| 53 |
+
|
| 54 |
+
Read every file path listed under `contextFiles` from the apply instructions output.
|
| 55 |
+
The files depend on the schema being used:
|
| 56 |
+
- **spec-driven**: proposal, specs, design, tasks
|
| 57 |
+
- Other schemas: follow the contextFiles from CLI output
|
| 58 |
+
|
| 59 |
+
5. **Show current progress**
|
| 60 |
+
|
| 61 |
+
Display:
|
| 62 |
+
- Schema being used
|
| 63 |
+
- Progress: "N/M tasks complete"
|
| 64 |
+
- Remaining tasks overview
|
| 65 |
+
- Dynamic instruction from CLI
|
| 66 |
+
|
| 67 |
+
6. **Implement tasks (loop until done or blocked)**
|
| 68 |
+
|
| 69 |
+
For each pending task:
|
| 70 |
+
- Show which task is being worked on
|
| 71 |
+
- Make the code changes required
|
| 72 |
+
- Keep changes minimal and focused
|
| 73 |
+
- Mark task complete in the tasks file: `- [ ]` → `- [x]`
|
| 74 |
+
- Continue to next task
|
| 75 |
+
|
| 76 |
+
**Pause if:**
|
| 77 |
+
- Task is unclear → ask for clarification
|
| 78 |
+
- Implementation reveals a design issue → suggest updating artifacts
|
| 79 |
+
- Error or blocker encountered → report and wait for guidance
|
| 80 |
+
- User interrupts
|
| 81 |
+
|
| 82 |
+
7. **On completion or pause, show status**
|
| 83 |
+
|
| 84 |
+
Display:
|
| 85 |
+
- Tasks completed this session
|
| 86 |
+
- Overall progress: "N/M tasks complete"
|
| 87 |
+
- If all done: suggest archive
|
| 88 |
+
- If paused: explain why and wait for guidance
|
| 89 |
+
|
| 90 |
+
**Output During Implementation**
|
| 91 |
+
|
| 92 |
+
```
|
| 93 |
+
## Implementing: <change-name> (schema: <schema-name>)
|
| 94 |
+
|
| 95 |
+
Working on task 3/7: <task description>
|
| 96 |
+
[...implementation happening...]
|
| 97 |
+
✓ Task complete
|
| 98 |
+
|
| 99 |
+
Working on task 4/7: <task description>
|
| 100 |
+
[...implementation happening...]
|
| 101 |
+
✓ Task complete
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
**Output On Completion**
|
| 105 |
+
|
| 106 |
+
```
|
| 107 |
+
## Implementation Complete
|
| 108 |
+
|
| 109 |
+
**Change:** <change-name>
|
| 110 |
+
**Schema:** <schema-name>
|
| 111 |
+
**Progress:** 7/7 tasks complete ✓
|
| 112 |
+
|
| 113 |
+
### Completed This Session
|
| 114 |
+
- [x] Task 1
|
| 115 |
+
- [x] Task 2
|
| 116 |
+
...
|
| 117 |
+
|
| 118 |
+
All tasks complete! Ready to archive this change.
|
| 119 |
+
```
|
| 120 |
+
|
| 121 |
+
**Output On Pause (Issue Encountered)**
|
| 122 |
+
|
| 123 |
+
```
|
| 124 |
+
## Implementation Paused
|
| 125 |
+
|
| 126 |
+
**Change:** <change-name>
|
| 127 |
+
**Schema:** <schema-name>
|
| 128 |
+
**Progress:** 4/7 tasks complete
|
| 129 |
+
|
| 130 |
+
### Issue Encountered
|
| 131 |
+
<description of the issue>
|
| 132 |
+
|
| 133 |
+
**Options:**
|
| 134 |
+
1. <option 1>
|
| 135 |
+
2. <option 2>
|
| 136 |
+
3. Other approach
|
| 137 |
+
|
| 138 |
+
What would you like to do?
|
| 139 |
+
```
|
| 140 |
+
|
| 141 |
+
**Guardrails**
|
| 142 |
+
- Keep going through tasks until done or blocked
|
| 143 |
+
- Always read context files before starting (from the apply instructions output)
|
| 144 |
+
- If task is ambiguous, pause and ask before implementing
|
| 145 |
+
- If implementation reveals issues, pause and suggest artifact updates
|
| 146 |
+
- Keep code changes minimal and scoped to each task
|
| 147 |
+
- Update task checkbox immediately after completing each task
|
| 148 |
+
- Pause on errors, blockers, or unclear requirements - don't guess
|
| 149 |
+
- Use contextFiles from CLI output, don't assume specific file names
|
| 150 |
+
|
| 151 |
+
**Fluid Workflow Integration**
|
| 152 |
+
|
| 153 |
+
This skill supports the "actions on a change" model:
|
| 154 |
+
|
| 155 |
+
- **Can be invoked anytime**: Before all artifacts are done (if tasks exist), after partial implementation, interleaved with other actions
|
| 156 |
+
- **Allows artifact updates**: If implementation reveals design issues, suggest updating artifacts - not phase-locked, work fluidly
|
.opencode/skills/openspec-archive-change/SKILL.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: openspec-archive-change
|
| 3 |
+
description: Archive a completed change in the experimental workflow. Use when the user wants to finalize and archive a change after implementation is complete.
|
| 4 |
+
license: MIT
|
| 5 |
+
compatibility: Requires openspec CLI.
|
| 6 |
+
metadata:
|
| 7 |
+
author: openspec
|
| 8 |
+
version: "1.0"
|
| 9 |
+
generatedBy: "1.3.1"
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
Archive a completed change in the experimental workflow.
|
| 13 |
+
|
| 14 |
+
**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
| 15 |
+
|
| 16 |
+
**Steps**
|
| 17 |
+
|
| 18 |
+
1. **If no change name provided, prompt for selection**
|
| 19 |
+
|
| 20 |
+
Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
|
| 21 |
+
|
| 22 |
+
Show only active changes (not already archived).
|
| 23 |
+
Include the schema used for each change if available.
|
| 24 |
+
|
| 25 |
+
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
| 26 |
+
|
| 27 |
+
2. **Check artifact completion status**
|
| 28 |
+
|
| 29 |
+
Run `openspec status --change "<name>" --json` to check artifact completion.
|
| 30 |
+
|
| 31 |
+
Parse the JSON to understand:
|
| 32 |
+
- `schemaName`: The workflow being used
|
| 33 |
+
- `artifacts`: List of artifacts with their status (`done` or other)
|
| 34 |
+
|
| 35 |
+
**If any artifacts are not `done`:**
|
| 36 |
+
- Display warning listing incomplete artifacts
|
| 37 |
+
- Use **AskUserQuestion tool** to confirm user wants to proceed
|
| 38 |
+
- Proceed if user confirms
|
| 39 |
+
|
| 40 |
+
3. **Check task completion status**
|
| 41 |
+
|
| 42 |
+
Read the tasks file (typically `tasks.md`) to check for incomplete tasks.
|
| 43 |
+
|
| 44 |
+
Count tasks marked with `- [ ]` (incomplete) vs `- [x]` (complete).
|
| 45 |
+
|
| 46 |
+
**If incomplete tasks found:**
|
| 47 |
+
- Display warning showing count of incomplete tasks
|
| 48 |
+
- Use **AskUserQuestion tool** to confirm user wants to proceed
|
| 49 |
+
- Proceed if user confirms
|
| 50 |
+
|
| 51 |
+
**If no tasks file exists:** Proceed without task-related warning.
|
| 52 |
+
|
| 53 |
+
4. **Assess delta spec sync state**
|
| 54 |
+
|
| 55 |
+
Check for delta specs at `openspec/changes/<name>/specs/`. If none exist, proceed without sync prompt.
|
| 56 |
+
|
| 57 |
+
**If delta specs exist:**
|
| 58 |
+
- Compare each delta spec with its corresponding main spec at `openspec/specs/<capability>/spec.md`
|
| 59 |
+
- Determine what changes would be applied (adds, modifications, removals, renames)
|
| 60 |
+
- Show a combined summary before prompting
|
| 61 |
+
|
| 62 |
+
**Prompt options:**
|
| 63 |
+
- If changes needed: "Sync now (recommended)", "Archive without syncing"
|
| 64 |
+
- If already synced: "Archive now", "Sync anyway", "Cancel"
|
| 65 |
+
|
| 66 |
+
If user chooses sync, use Task tool (subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change '<name>'. Delta spec analysis: <include the analyzed delta spec summary>"). Proceed to archive regardless of choice.
|
| 67 |
+
|
| 68 |
+
5. **Perform the archive**
|
| 69 |
+
|
| 70 |
+
Create the archive directory if it doesn't exist:
|
| 71 |
+
```bash
|
| 72 |
+
mkdir -p openspec/changes/archive
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
Generate target name using current date: `YYYY-MM-DD-<change-name>`
|
| 76 |
+
|
| 77 |
+
**Check if target already exists:**
|
| 78 |
+
- If yes: Fail with error, suggest renaming existing archive or using different date
|
| 79 |
+
- If no: Move the change directory to archive
|
| 80 |
+
|
| 81 |
+
```bash
|
| 82 |
+
mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name>
|
| 83 |
+
```
|
| 84 |
+
|
| 85 |
+
6. **Display summary**
|
| 86 |
+
|
| 87 |
+
Show archive completion summary including:
|
| 88 |
+
- Change name
|
| 89 |
+
- Schema that was used
|
| 90 |
+
- Archive location
|
| 91 |
+
- Whether specs were synced (if applicable)
|
| 92 |
+
- Note about any warnings (incomplete artifacts/tasks)
|
| 93 |
+
|
| 94 |
+
**Output On Success**
|
| 95 |
+
|
| 96 |
+
```
|
| 97 |
+
## Archive Complete
|
| 98 |
+
|
| 99 |
+
**Change:** <change-name>
|
| 100 |
+
**Schema:** <schema-name>
|
| 101 |
+
**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
| 102 |
+
**Specs:** ✓ Synced to main specs (or "No delta specs" or "Sync skipped")
|
| 103 |
+
|
| 104 |
+
All artifacts complete. All tasks complete.
|
| 105 |
+
```
|
| 106 |
+
|
| 107 |
+
**Guardrails**
|
| 108 |
+
- Always prompt for change selection if not provided
|
| 109 |
+
- Use artifact graph (openspec status --json) for completion checking
|
| 110 |
+
- Don't block archive on warnings - just inform and confirm
|
| 111 |
+
- Preserve .openspec.yaml when moving to archive (it moves with the directory)
|
| 112 |
+
- Show clear summary of what happened
|
| 113 |
+
- If sync is requested, use openspec-sync-specs approach (agent-driven)
|
| 114 |
+
- If delta specs exist, always run the sync assessment and show the combined summary before prompting
|
.opencode/skills/openspec-explore/SKILL.md
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: openspec-explore
|
| 3 |
+
description: Enter explore mode - a thinking partner for exploring ideas, investigating problems, and clarifying requirements. Use when the user wants to think through something before or during a change.
|
| 4 |
+
license: MIT
|
| 5 |
+
compatibility: Requires openspec CLI.
|
| 6 |
+
metadata:
|
| 7 |
+
author: openspec
|
| 8 |
+
version: "1.0"
|
| 9 |
+
generatedBy: "1.3.1"
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes.
|
| 13 |
+
|
| 14 |
+
**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first and create a change proposal. You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing.
|
| 15 |
+
|
| 16 |
+
**This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore.
|
| 17 |
+
|
| 18 |
+
---
|
| 19 |
+
|
| 20 |
+
## The Stance
|
| 21 |
+
|
| 22 |
+
- **Curious, not prescriptive** - Ask questions that emerge naturally, don't follow a script
|
| 23 |
+
- **Open threads, not interrogations** - Surface multiple interesting directions and let the user follow what resonates. Don't funnel them through a single path of questions.
|
| 24 |
+
- **Visual** - Use ASCII diagrams liberally when they'd help clarify thinking
|
| 25 |
+
- **Adaptive** - Follow interesting threads, pivot when new information emerges
|
| 26 |
+
- **Patient** - Don't rush to conclusions, let the shape of the problem emerge
|
| 27 |
+
- **Grounded** - Explore the actual codebase when relevant, don't just theorize
|
| 28 |
+
|
| 29 |
+
---
|
| 30 |
+
|
| 31 |
+
## What You Might Do
|
| 32 |
+
|
| 33 |
+
Depending on what the user brings, you might:
|
| 34 |
+
|
| 35 |
+
**Explore the problem space**
|
| 36 |
+
- Ask clarifying questions that emerge from what they said
|
| 37 |
+
- Challenge assumptions
|
| 38 |
+
- Reframe the problem
|
| 39 |
+
- Find analogies
|
| 40 |
+
|
| 41 |
+
**Investigate the codebase**
|
| 42 |
+
- Map existing architecture relevant to the discussion
|
| 43 |
+
- Find integration points
|
| 44 |
+
- Identify patterns already in use
|
| 45 |
+
- Surface hidden complexity
|
| 46 |
+
|
| 47 |
+
**Compare options**
|
| 48 |
+
- Brainstorm multiple approaches
|
| 49 |
+
- Build comparison tables
|
| 50 |
+
- Sketch tradeoffs
|
| 51 |
+
- Recommend a path (if asked)
|
| 52 |
+
|
| 53 |
+
**Visualize**
|
| 54 |
+
```
|
| 55 |
+
┌─────────────────────────────────────────┐
|
| 56 |
+
│ Use ASCII diagrams liberally │
|
| 57 |
+
├─────────────────────────────────────────┤
|
| 58 |
+
│ │
|
| 59 |
+
│ ┌────────┐ ┌────────┐ │
|
| 60 |
+
│ │ State │────────▶│ State │ │
|
| 61 |
+
│ │ A │ │ B │ │
|
| 62 |
+
│ └────────┘ └────────┘ │
|
| 63 |
+
│ │
|
| 64 |
+
│ System diagrams, state machines, │
|
| 65 |
+
│ data flows, architecture sketches, │
|
| 66 |
+
│ dependency graphs, comparison tables │
|
| 67 |
+
│ │
|
| 68 |
+
└─────────────────────────────────────────┘
|
| 69 |
+
```
|
| 70 |
+
|
| 71 |
+
**Surface risks and unknowns**
|
| 72 |
+
- Identify what could go wrong
|
| 73 |
+
- Find gaps in understanding
|
| 74 |
+
- Suggest spikes or investigations
|
| 75 |
+
|
| 76 |
+
---
|
| 77 |
+
|
| 78 |
+
## OpenSpec Awareness
|
| 79 |
+
|
| 80 |
+
You have full context of the OpenSpec system. Use it naturally, don't force it.
|
| 81 |
+
|
| 82 |
+
### Check for context
|
| 83 |
+
|
| 84 |
+
At the start, quickly check what exists:
|
| 85 |
+
```bash
|
| 86 |
+
openspec list --json
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
This tells you:
|
| 90 |
+
- If there are active changes
|
| 91 |
+
- Their names, schemas, and status
|
| 92 |
+
- What the user might be working on
|
| 93 |
+
|
| 94 |
+
### When no change exists
|
| 95 |
+
|
| 96 |
+
Think freely. When insights crystallize, you might offer:
|
| 97 |
+
|
| 98 |
+
- "This feels solid enough to start a change. Want me to create a proposal?"
|
| 99 |
+
- Or keep exploring - no pressure to formalize
|
| 100 |
+
|
| 101 |
+
### When a change exists
|
| 102 |
+
|
| 103 |
+
If the user mentions a change or you detect one is relevant:
|
| 104 |
+
|
| 105 |
+
1. **Read existing artifacts for context**
|
| 106 |
+
- `openspec/changes/<name>/proposal.md`
|
| 107 |
+
- `openspec/changes/<name>/design.md`
|
| 108 |
+
- `openspec/changes/<name>/tasks.md`
|
| 109 |
+
- etc.
|
| 110 |
+
|
| 111 |
+
2. **Reference them naturally in conversation**
|
| 112 |
+
- "Your design mentions using Redis, but we just realized SQLite fits better..."
|
| 113 |
+
- "The proposal scopes this to premium users, but we're now thinking everyone..."
|
| 114 |
+
|
| 115 |
+
3. **Offer to capture when decisions are made**
|
| 116 |
+
|
| 117 |
+
| Insight Type | Where to Capture |
|
| 118 |
+
|----------------------------|--------------------------------|
|
| 119 |
+
| New requirement discovered | `specs/<capability>/spec.md` |
|
| 120 |
+
| Requirement changed | `specs/<capability>/spec.md` |
|
| 121 |
+
| Design decision made | `design.md` |
|
| 122 |
+
| Scope changed | `proposal.md` |
|
| 123 |
+
| New work identified | `tasks.md` |
|
| 124 |
+
| Assumption invalidated | Relevant artifact |
|
| 125 |
+
|
| 126 |
+
Example offers:
|
| 127 |
+
- "That's a design decision. Capture it in design.md?"
|
| 128 |
+
- "This is a new requirement. Add it to specs?"
|
| 129 |
+
- "This changes scope. Update the proposal?"
|
| 130 |
+
|
| 131 |
+
4. **The user decides** - Offer and move on. Don't pressure. Don't auto-capture.
|
| 132 |
+
|
| 133 |
+
---
|
| 134 |
+
|
| 135 |
+
## What You Don't Have To Do
|
| 136 |
+
|
| 137 |
+
- Follow a script
|
| 138 |
+
- Ask the same questions every time
|
| 139 |
+
- Produce a specific artifact
|
| 140 |
+
- Reach a conclusion
|
| 141 |
+
- Stay on topic if a tangent is valuable
|
| 142 |
+
- Be brief (this is thinking time)
|
| 143 |
+
|
| 144 |
+
---
|
| 145 |
+
|
| 146 |
+
## Handling Different Entry Points
|
| 147 |
+
|
| 148 |
+
**User brings a vague idea:**
|
| 149 |
+
```
|
| 150 |
+
User: I'm thinking about adding real-time collaboration
|
| 151 |
+
|
| 152 |
+
You: Real-time collab is a big space. Let me think about this...
|
| 153 |
+
|
| 154 |
+
COLLABORATION SPECTRUM
|
| 155 |
+
════════════════════════════════════════════
|
| 156 |
+
|
| 157 |
+
Awareness Coordination Sync
|
| 158 |
+
│ │ │
|
| 159 |
+
▼ ▼ ▼
|
| 160 |
+
┌────────┐ ┌────────┐ ┌────────┐
|
| 161 |
+
│Presence│ │Cursors │ │ CRDT │
|
| 162 |
+
│ "3 │ │ Multi │ │Conflict│
|
| 163 |
+
│online" │ │ select │ │ free │
|
| 164 |
+
└────────┘ └────────┘ └────────┘
|
| 165 |
+
│ │ │
|
| 166 |
+
trivial moderate complex
|
| 167 |
+
|
| 168 |
+
Where's your head at?
|
| 169 |
+
```
|
| 170 |
+
|
| 171 |
+
**User brings a specific problem:**
|
| 172 |
+
```
|
| 173 |
+
User: The auth system is a mess
|
| 174 |
+
|
| 175 |
+
You: [reads codebase]
|
| 176 |
+
|
| 177 |
+
┌─────────────────────────────────────────────┐
|
| 178 |
+
│ CURRENT AUTH FLOW │
|
| 179 |
+
└─────────────────────────────────────────────┘
|
| 180 |
+
│
|
| 181 |
+
┌─────────────┼─────────────┐
|
| 182 |
+
▼ ▼ ▼
|
| 183 |
+
┌─────────┐ ┌─────────┐ ┌─────────┐
|
| 184 |
+
│ Google │ │ GitHub │ │ Email │
|
| 185 |
+
│ OAuth │ │ OAuth │ │ Magic │
|
| 186 |
+
└────┬────┘ └────┬────┘ └────┬────┘
|
| 187 |
+
│ │ │
|
| 188 |
+
└─────────────┼─────────────┘
|
| 189 |
+
▼
|
| 190 |
+
┌───────────┐
|
| 191 |
+
│ Session │
|
| 192 |
+
└─────┬─────┘
|
| 193 |
+
│
|
| 194 |
+
▼
|
| 195 |
+
┌───────────┐
|
| 196 |
+
│ Perms │
|
| 197 |
+
└───────────┘
|
| 198 |
+
|
| 199 |
+
I see three tangles. Which one's burning?
|
| 200 |
+
```
|
| 201 |
+
|
| 202 |
+
**User is stuck mid-implementation:**
|
| 203 |
+
```
|
| 204 |
+
User: /opsx-explore add-auth-system
|
| 205 |
+
The OAuth integration is more complex than expected
|
| 206 |
+
|
| 207 |
+
You: [reads change artifacts]
|
| 208 |
+
|
| 209 |
+
You're on task 4: "Implement OAuth flow"
|
| 210 |
+
|
| 211 |
+
Let me trace what's involved...
|
| 212 |
+
|
| 213 |
+
[draws diagram, explores options, suggests paths]
|
| 214 |
+
|
| 215 |
+
Want to update the design to reflect this?
|
| 216 |
+
Or add a spike task to investigate?
|
| 217 |
+
```
|
| 218 |
+
|
| 219 |
+
**User wants to compare options:**
|
| 220 |
+
```
|
| 221 |
+
User: Should we use Postgres or SQLite?
|
| 222 |
+
|
| 223 |
+
You: Generic answer is boring. What's the context?
|
| 224 |
+
|
| 225 |
+
User: A CLI tool that tracks local dev environments
|
| 226 |
+
|
| 227 |
+
You: That changes everything.
|
| 228 |
+
|
| 229 |
+
┌─────────────────────────────────────────────────┐
|
| 230 |
+
│ CLI TOOL DATA STORAGE │
|
| 231 |
+
└─────────────────────────────────────────────────┘
|
| 232 |
+
|
| 233 |
+
Key constraints:
|
| 234 |
+
• No daemon running
|
| 235 |
+
• Must work offline
|
| 236 |
+
• Single user
|
| 237 |
+
|
| 238 |
+
SQLite Postgres
|
| 239 |
+
Deployment embedded ✓ needs server ✗
|
| 240 |
+
Offline yes ✓ no ✗
|
| 241 |
+
Single file yes ✓ no ✗
|
| 242 |
+
|
| 243 |
+
SQLite. Not even close.
|
| 244 |
+
|
| 245 |
+
Unless... is there a sync component?
|
| 246 |
+
```
|
| 247 |
+
|
| 248 |
+
---
|
| 249 |
+
|
| 250 |
+
## Ending Discovery
|
| 251 |
+
|
| 252 |
+
There's no required ending. Discovery might:
|
| 253 |
+
|
| 254 |
+
- **Flow into a proposal**: "Ready to start? I can create a change proposal."
|
| 255 |
+
- **Result in artifact updates**: "Updated design.md with these decisions"
|
| 256 |
+
- **Just provide clarity**: User has what they need, moves on
|
| 257 |
+
- **Continue later**: "We can pick this up anytime"
|
| 258 |
+
|
| 259 |
+
When it feels like things are crystallizing, you might summarize:
|
| 260 |
+
|
| 261 |
+
```
|
| 262 |
+
## What We Figured Out
|
| 263 |
+
|
| 264 |
+
**The problem**: [crystallized understanding]
|
| 265 |
+
|
| 266 |
+
**The approach**: [if one emerged]
|
| 267 |
+
|
| 268 |
+
**Open questions**: [if any remain]
|
| 269 |
+
|
| 270 |
+
**Next steps** (if ready):
|
| 271 |
+
- Create a change proposal
|
| 272 |
+
- Keep exploring: just keep talking
|
| 273 |
+
```
|
| 274 |
+
|
| 275 |
+
But this summary is optional. Sometimes the thinking IS the value.
|
| 276 |
+
|
| 277 |
+
---
|
| 278 |
+
|
| 279 |
+
## Guardrails
|
| 280 |
+
|
| 281 |
+
- **Don't implement** - Never write code or implement features. Creating OpenSpec artifacts is fine, writing application code is not.
|
| 282 |
+
- **Don't fake understanding** - If something is unclear, dig deeper
|
| 283 |
+
- **Don't rush** - Discovery is thinking time, not task time
|
| 284 |
+
- **Don't force structure** - Let patterns emerge naturally
|
| 285 |
+
- **Don't auto-capture** - Offer to save insights, don't just do it
|
| 286 |
+
- **Do visualize** - A good diagram is worth many paragraphs
|
| 287 |
+
- **Do explore the codebase** - Ground discussions in reality
|
| 288 |
+
- **Do question assumptions** - Including the user's and your own
|
.opencode/skills/openspec-propose/SKILL.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: openspec-propose
|
| 3 |
+
description: Propose a new change with all artifacts generated in one step. Use when the user wants to quickly describe what they want to build and get a complete proposal with design, specs, and tasks ready for implementation.
|
| 4 |
+
license: MIT
|
| 5 |
+
compatibility: Requires openspec CLI.
|
| 6 |
+
metadata:
|
| 7 |
+
author: openspec
|
| 8 |
+
version: "1.0"
|
| 9 |
+
generatedBy: "1.3.1"
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
Propose a new change - create the change and generate all artifacts in one step.
|
| 13 |
+
|
| 14 |
+
I'll create a change with artifacts:
|
| 15 |
+
- proposal.md (what & why)
|
| 16 |
+
- design.md (how)
|
| 17 |
+
- tasks.md (implementation steps)
|
| 18 |
+
|
| 19 |
+
When ready to implement, run /opsx-apply
|
| 20 |
+
|
| 21 |
+
---
|
| 22 |
+
|
| 23 |
+
**Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build.
|
| 24 |
+
|
| 25 |
+
**Steps**
|
| 26 |
+
|
| 27 |
+
1. **If no clear input provided, ask what they want to build**
|
| 28 |
+
|
| 29 |
+
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
|
| 30 |
+
> "What change do you want to work on? Describe what you want to build or fix."
|
| 31 |
+
|
| 32 |
+
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
|
| 33 |
+
|
| 34 |
+
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
|
| 35 |
+
|
| 36 |
+
2. **Create the change directory**
|
| 37 |
+
```bash
|
| 38 |
+
openspec new change "<name>"
|
| 39 |
+
```
|
| 40 |
+
This creates a scaffolded change at `openspec/changes/<name>/` with `.openspec.yaml`.
|
| 41 |
+
|
| 42 |
+
3. **Get the artifact build order**
|
| 43 |
+
```bash
|
| 44 |
+
openspec status --change "<name>" --json
|
| 45 |
+
```
|
| 46 |
+
Parse the JSON to get:
|
| 47 |
+
- `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`)
|
| 48 |
+
- `artifacts`: list of all artifacts with their status and dependencies
|
| 49 |
+
|
| 50 |
+
4. **Create artifacts in sequence until apply-ready**
|
| 51 |
+
|
| 52 |
+
Use the **TodoWrite tool** to track progress through the artifacts.
|
| 53 |
+
|
| 54 |
+
Loop through artifacts in dependency order (artifacts with no pending dependencies first):
|
| 55 |
+
|
| 56 |
+
a. **For each artifact that is `ready` (dependencies satisfied)**:
|
| 57 |
+
- Get instructions:
|
| 58 |
+
```bash
|
| 59 |
+
openspec instructions <artifact-id> --change "<name>" --json
|
| 60 |
+
```
|
| 61 |
+
- The instructions JSON includes:
|
| 62 |
+
- `context`: Project background (constraints for you - do NOT include in output)
|
| 63 |
+
- `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
|
| 64 |
+
- `template`: The structure to use for your output file
|
| 65 |
+
- `instruction`: Schema-specific guidance for this artifact type
|
| 66 |
+
- `outputPath`: Where to write the artifact
|
| 67 |
+
- `dependencies`: Completed artifacts to read for context
|
| 68 |
+
- Read any completed dependency files for context
|
| 69 |
+
- Create the artifact file using `template` as the structure
|
| 70 |
+
- Apply `context` and `rules` as constraints - but do NOT copy them into the file
|
| 71 |
+
- Show brief progress: "Created <artifact-id>"
|
| 72 |
+
|
| 73 |
+
b. **Continue until all `applyRequires` artifacts are complete**
|
| 74 |
+
- After creating each artifact, re-run `openspec status --change "<name>" --json`
|
| 75 |
+
- Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array
|
| 76 |
+
- Stop when all `applyRequires` artifacts are done
|
| 77 |
+
|
| 78 |
+
c. **If an artifact requires user input** (unclear context):
|
| 79 |
+
- Use **AskUserQuestion tool** to clarify
|
| 80 |
+
- Then continue with creation
|
| 81 |
+
|
| 82 |
+
5. **Show final status**
|
| 83 |
+
```bash
|
| 84 |
+
openspec status --change "<name>"
|
| 85 |
+
```
|
| 86 |
+
|
| 87 |
+
**Output**
|
| 88 |
+
|
| 89 |
+
After completing all artifacts, summarize:
|
| 90 |
+
- Change name and location
|
| 91 |
+
- List of artifacts created with brief descriptions
|
| 92 |
+
- What's ready: "All artifacts created! Ready for implementation."
|
| 93 |
+
- Prompt: "Run `/opsx-apply` or ask me to implement to start working on the tasks."
|
| 94 |
+
|
| 95 |
+
**Artifact Creation Guidelines**
|
| 96 |
+
|
| 97 |
+
- Follow the `instruction` field from `openspec instructions` for each artifact type
|
| 98 |
+
- The schema defines what each artifact should contain - follow it
|
| 99 |
+
- Read dependency artifacts for context before creating new ones
|
| 100 |
+
- Use `template` as the structure for your output file - fill in its sections
|
| 101 |
+
- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file
|
| 102 |
+
- Do NOT copy `<context>`, `<rules>`, `<project_context>` blocks into the artifact
|
| 103 |
+
- These guide what you write, but should never appear in the output
|
| 104 |
+
|
| 105 |
+
**Guardrails**
|
| 106 |
+
- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`)
|
| 107 |
+
- Always read dependency artifacts before creating a new one
|
| 108 |
+
- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
|
| 109 |
+
- If a change with that name already exists, ask if user wants to continue it or create a new one
|
| 110 |
+
- Verify each artifact file exists after writing before proceeding to next
|
AGENTS.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
## Agent skills
|
| 2 |
+
|
| 3 |
+
### Issue tracker
|
| 4 |
+
|
| 5 |
+
Issues live as markdown under `.scratch/<feature-slug>/`. See `docs/agents/issue-tracker.md`.
|
| 6 |
+
|
| 7 |
+
### Triage labels
|
| 8 |
+
|
| 9 |
+
Five canonical roles with default label strings (no overrides). See `docs/agents/triage-labels.md`.
|
| 10 |
+
|
| 11 |
+
### Domain docs
|
| 12 |
+
|
| 13 |
+
Single-context: `CONTEXT.md` at repo root and `docs/adr/` for ADRs. See `docs/agents/domain.md`.
|
CONTEXT.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Info Lens
|
| 2 |
+
|
| 3 |
+
探索 LLM 与语言的信息结构;可视化预测、归因与生成过程。
|
| 4 |
+
|
| 5 |
+
## Language
|
| 6 |
+
|
| 7 |
+
**Tool call**:
|
| 8 |
+
模型在生成中发出的函数调用片段,通常包在 `<tool_call>…</tool_call>` 内,含函数名与参数 JSON。本项目中默认指展示与分析用途,不执行真实工具。
|
| 9 |
+
_Avoid_: function call(在与 OpenAI API 字段混用时可用,但领域讨论优先用 tool call)
|
| 10 |
+
|
| 11 |
+
**Tool config**:
|
| 12 |
+
用户可配置的工具集合,包含两个字段:`tools_schema`(注入 chat template 的 JSON schema 数组)和 `mock_results`(tool name → 假返回值的映射)。二者作为整体单元一起切换,避免 schema 与 mock 不一致。存储在前端 Run Draft 中,随生成参数一起持久化。
|
| 13 |
+
_Avoid_: tools preset(旧称,指后端硬编码的静态配置,已废弃)、tool list
|
| 14 |
+
|
| 15 |
+
**Tool calling 开关**:
|
| 16 |
+
chat 模板模式下、User 输入区上方的勾选框;默认关闭。开启后显示「多轮 & mock」子开关。旧缓存与 demo 无此字段时视为关闭,不做迁移。
|
| 17 |
+
_Avoid_: tool call mode、agent mode
|
| 18 |
+
|
| 19 |
+
**Tool calling(单轮)**:
|
| 20 |
+
在 chat template 中注入 tool config 的 schema,让模型产出 tool call 文本;不执行工具、不回灌结果。生成结果中的 tool call 与普通 token 同等对待。「多轮 & mock」关闭时的形态。
|
| 21 |
+
_Avoid_: observational、agent
|
| 22 |
+
|
| 23 |
+
**Tool calling(多轮)**:
|
| 24 |
+
在 chat template 中注入 tool config 的 schema,解析模型产出的 tool call,用 tool config 的 mock_results 回灌假数据,再驱动下一轮生成;只要解析成功且 mock 表中有对应 tool name 就继续下一轮,否则自然结束(无 tool call、或未配置 mock 均不报错)。`<tool_call>` 存在但 JSON 无效时报错。每轮生成各自保留 bpe_strings;**wire 模型**:前端维护一条单调增长的字符串 `wire`,首轮为 `apply_chat_template` 完整 prompt(含 teacher forcing),后续每轮将模型输出与后端返回的 `incremental_suffix` 依次追加;UI 首轮 input 展示完整 `wire`,后续轮次 input 展示本轮的 `incremental_suffix`(即 tool response + generation scaffold),output 展示当轮续写全文。`incremental_suffix` 由后端 `POST /v1/completions/prompt-incremental` 通过 placeholder 技术从 chat template 中提取,与前序历史内容无关。「多轮 & mock」开启时的形态。
|
| 25 |
+
_Avoid_: closed-loop、agent loop(指真实工具执行的完整代理流程)、mock agent
|
| 26 |
+
|
| 27 |
+
**Causal Flow 多轮 tool calling**:
|
| 28 |
+
Causal Flow 页的多轮 tool calling 形态。与 chat 页共享 wire 模型和 mock 编排逻辑,区别在于每轮用 `/api/prediction-attribute` 逐 token 生成并归因(而非 `/v1/completions` 续写)。每轮是独立的 attribution session(`startTokenGenAttribution`),由 `runMultiTurnAttribution` 编排层协调:检测 tool call 完成(`onComplete` 时)、注入 `incremental_suffix`、启动下一轮 session。所有轮次的 `TokenGenStep` 扁平合并进同一 `steps[]`,全局坐标一致(每轮 session 的 `initialContext` = 完整 wire 前缀)。**Token 语义**:只有 input(原始 prompt + 每次 tool response 注入)和 output(所有轮次模型生成的 token)两类;DAG 用 `inputRanges: [number,number][]`(`TokenGenStep` 新字段)描述 context 中哪些区间是 input,DAG 节点着色与单轮一致(input=teal,output=orange)。Cache key 在多轮开启时追加 `toolConfigFingerprint`(含 mock_results)以避免不同 mock 的 run 互相覆盖。`maxTokens` 为全局生成 token 上限(跨轮共享,非每轮独立)。Stop 打断保留已完成 steps(含当轮已归因部分),completionReason='abort'。
|
| 29 |
+
_Avoid_: agent loop、closed-loop
|
Dockerfile
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# syntax=docker/dockerfile:1
|
| 2 |
+
|
| 3 |
+
# -----------------------------------------------------------------------------
|
| 4 |
+
# Frontend build stage (stable Node toolchain for webpack/TS)
|
| 5 |
+
# -----------------------------------------------------------------------------
|
| 6 |
+
FROM node:20-bookworm-slim AS frontend
|
| 7 |
+
WORKDIR /app/client/src
|
| 8 |
+
|
| 9 |
+
COPY client/src/package.json client/src/package-lock.json ./
|
| 10 |
+
RUN npm ci
|
| 11 |
+
|
| 12 |
+
COPY client/src/ ./
|
| 13 |
+
# prebuild 需要读取的 JSON,否则 updateIntroHTML.js 会 ENOENT
|
| 14 |
+
COPY data/demo/public/ /app/data/demo/public/
|
| 15 |
+
RUN npm run build
|
| 16 |
+
|
| 17 |
+
# -----------------------------------------------------------------------------
|
| 18 |
+
# Runtime stage (Hugging Face Spaces runs container as UID 1000)
|
| 19 |
+
# Reference: https://huggingface.co/docs/hub/spaces-sdks-docker
|
| 20 |
+
# -----------------------------------------------------------------------------
|
| 21 |
+
FROM python:3.10-slim
|
| 22 |
+
|
| 23 |
+
# System deps (git for Hugging Face Hub downloads, build-essential for triton/AWQ CUDA kernel compilation)
|
| 24 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 25 |
+
git \
|
| 26 |
+
build-essential \
|
| 27 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 28 |
+
|
| 29 |
+
# Create a non-root user with UID 1000 (mandatory in Spaces)
|
| 30 |
+
RUN useradd -m -u 1000 user
|
| 31 |
+
USER user
|
| 32 |
+
|
| 33 |
+
# 只设置构建时需要的环境变量(pip install 需要这些路径)
|
| 34 |
+
ENV HOME=/home/user \
|
| 35 |
+
PATH=/home/user/.local/bin:$PATH
|
| 36 |
+
|
| 37 |
+
WORKDIR $HOME/app
|
| 38 |
+
|
| 39 |
+
# pip 依赖是本镜像体积与 HF 冷启动(拉取/解压 layer)的主要瓶颈:site-packages 可达数 GB,
|
| 40 |
+
# 其中 torch 依赖的 site-packages/nvidia & triton 占了大部分。
|
| 41 |
+
COPY --chown=user:users requirements.txt ./
|
| 42 |
+
RUN pip install --no-cache-dir --upgrade pip && \
|
| 43 |
+
pip install --no-cache-dir -r requirements.txt
|
| 44 |
+
|
| 45 |
+
# 运行时环境变量移到依赖安装之后(这些变量不影响依赖安装)
|
| 46 |
+
ENV PYTHONUNBUFFERED=1
|
| 47 |
+
|
| 48 |
+
# 启用 hf-transfer 加速下载
|
| 49 |
+
ENV HF_HUB_ENABLE_HF_TRANSFER=1
|
| 50 |
+
|
| 51 |
+
# App source(仅复制运行时需要的路径)
|
| 52 |
+
COPY --chown=user:users LICENSE NOTICE *.py *.yaml ./
|
| 53 |
+
COPY --chown=user:users backend/ ./backend/
|
| 54 |
+
COPY --chown=user:users data/demo/public/ ./data/demo/public/
|
| 55 |
+
|
| 56 |
+
# Frontend build artifacts
|
| 57 |
+
COPY --chown=user:users --from=frontend /app/client/dist ./client/dist
|
| 58 |
+
|
| 59 |
+
# ENV FORCE_INT8=1
|
| 60 |
+
|
| 61 |
+
EXPOSE 7860
|
| 62 |
+
# 硬件的模型适配:
|
| 63 |
+
# 在CPU basic 上使用0.6b模型能达到及格的速度
|
| 64 |
+
# 在CPU upgrade 上使用1.7b模型能达到及格的速度
|
| 65 |
+
# 在本地M5 16G芯片上使用4b模型能达到及格的速度(瓶颈是内存大小);M5 16G内存仅能同时支持一种分析模型(信息密度分析或语义分析)
|
| 66 |
+
CMD ["python", "run.py", "--no_auto_load", "--port", "7860", "--base_model", "qwen3-1.7b", "--instruct_model", "qwen3-1.7b-instruct"]
|
| 67 |
+
# CMD ["python", "run.py", "--no_auto_load", "--port", "7860", "--base_model", "qwen3-0.6b", "--instruct_model", "qwen3-0.6b-instruct"]
|
LICENSE
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Apache License
|
| 2 |
+
Version 2.0, January 2004
|
| 3 |
+
http://www.apache.org/licenses/
|
| 4 |
+
|
| 5 |
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
| 6 |
+
|
| 7 |
+
1. Definitions.
|
| 8 |
+
|
| 9 |
+
"License" shall mean the terms and conditions for use, reproduction,
|
| 10 |
+
and distribution as defined by Sections 1 through 9 of this document.
|
| 11 |
+
|
| 12 |
+
"Licensor" shall mean the copyright owner or entity authorized by
|
| 13 |
+
the copyright owner that is granting the License.
|
| 14 |
+
|
| 15 |
+
"Legal Entity" shall mean the union of the acting entity and all
|
| 16 |
+
other entities that control, are controlled by, or are under common
|
| 17 |
+
control with that entity. For the purposes of this definition,
|
| 18 |
+
"control" means (i) the power, direct or indirect, to cause the
|
| 19 |
+
direction or management of such entity, whether by contract or
|
| 20 |
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
| 21 |
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
| 22 |
+
|
| 23 |
+
"You" (or "Your") shall mean an individual or Legal Entity
|
| 24 |
+
exercising permissions granted by this License.
|
| 25 |
+
|
| 26 |
+
"Source" form shall mean the preferred form for making modifications,
|
| 27 |
+
including but not limited to software source code, documentation
|
| 28 |
+
source, and configuration files.
|
| 29 |
+
|
| 30 |
+
"Object" form shall mean any form resulting from mechanical
|
| 31 |
+
transformation or translation of a Source form, including but
|
| 32 |
+
not limited to compiled object code, generated documentation,
|
| 33 |
+
and conversions to other media types.
|
| 34 |
+
|
| 35 |
+
"Work" shall mean the work of authorship, whether in Source or
|
| 36 |
+
Object form, made available under the License, as indicated by a
|
| 37 |
+
copyright notice that is included in or attached to the work
|
| 38 |
+
(an example is provided in the Appendix below).
|
| 39 |
+
|
| 40 |
+
"Derivative Works" shall mean any work, whether in Source or Object
|
| 41 |
+
form, that is based on (or derived from) the Work and for which the
|
| 42 |
+
editorial revisions, annotations, elaborations, or other modifications
|
| 43 |
+
represent, as a whole, an original work of authorship. For the purposes
|
| 44 |
+
of this License, Derivative Works shall not include works that remain
|
| 45 |
+
separable from, or merely link (or bind by name) to the interfaces of,
|
| 46 |
+
the Work and Derivative Works thereof.
|
| 47 |
+
|
| 48 |
+
"Contribution" shall mean any work of authorship, including
|
| 49 |
+
the original version of the Work and any modifications or additions
|
| 50 |
+
to that Work or Derivative Works thereof, that is intentionally
|
| 51 |
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
| 52 |
+
or by an individual or Legal Entity authorized to submit on behalf of
|
| 53 |
+
the copyright owner. For the purposes of this definition, "submitted"
|
| 54 |
+
means any form of electronic, verbal, or written communication sent
|
| 55 |
+
to the Licensor or its representatives, including but not limited to
|
| 56 |
+
communication on electronic mailing lists, source code control systems,
|
| 57 |
+
and issue tracking systems that are managed by, or on behalf of, the
|
| 58 |
+
Licensor for the purpose of discussing and improving the Work, but
|
| 59 |
+
excluding communication that is conspicuously marked or otherwise
|
| 60 |
+
designated in writing by the copyright owner as "Not a Contribution."
|
| 61 |
+
|
| 62 |
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
| 63 |
+
on behalf of whom a Contribution has been received by Licensor and
|
| 64 |
+
subsequently incorporated within the Work.
|
| 65 |
+
|
| 66 |
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
| 67 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 68 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 69 |
+
copyright license to reproduce, prepare Derivative Works of,
|
| 70 |
+
publicly display, publicly perform, sublicense, and distribute the
|
| 71 |
+
Work and such Derivative Works in Source or Object form.
|
| 72 |
+
|
| 73 |
+
3. Grant of Patent License. Subject to the terms and conditions of
|
| 74 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 75 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 76 |
+
(except as stated in this section) patent license to make, have made,
|
| 77 |
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
| 78 |
+
where such license applies only to those patent claims licensable
|
| 79 |
+
by such Contributor that are necessarily infringed by their
|
| 80 |
+
Contribution(s) alone or by combination of their Contribution(s)
|
| 81 |
+
with the Work to which such Contribution(s) was submitted. If You
|
| 82 |
+
institute patent litigation against any entity (including a
|
| 83 |
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
| 84 |
+
or a Contribution incorporated within the Work constitutes direct
|
| 85 |
+
or contributory patent infringement, then any patent licenses
|
| 86 |
+
granted to You under this License for that Work shall terminate
|
| 87 |
+
as of the date such litigation is filed.
|
| 88 |
+
|
| 89 |
+
4. Redistribution. You may reproduce and distribute copies of the
|
| 90 |
+
Work or Derivative Works thereof in any medium, with or without
|
| 91 |
+
modifications, and in Source or Object form, provided that You
|
| 92 |
+
meet the following conditions:
|
| 93 |
+
|
| 94 |
+
(a) You must give any other recipients of the Work or
|
| 95 |
+
Derivative Works a copy of this License; and
|
| 96 |
+
|
| 97 |
+
(b) You must cause any modified files to carry prominent notices
|
| 98 |
+
stating that You changed the files; and
|
| 99 |
+
|
| 100 |
+
(c) You must retain, in the Source form of any Derivative Works
|
| 101 |
+
that You distribute, all copyright, patent, trademark, and
|
| 102 |
+
attribution notices from the Source form of the Work,
|
| 103 |
+
excluding those notices that do not pertain to any part of
|
| 104 |
+
the Derivative Works; and
|
| 105 |
+
|
| 106 |
+
(d) If the Work includes a "NOTICE" text file as part of its
|
| 107 |
+
distribution, then any Derivative Works that You distribute must
|
| 108 |
+
include a readable copy of the attribution notices contained
|
| 109 |
+
within such NOTICE file, excluding those notices that do not
|
| 110 |
+
pertain to any part of the Derivative Works, in at least one
|
| 111 |
+
of the following places: within a NOTICE text file distributed
|
| 112 |
+
as part of the Derivative Works; within the Source form or
|
| 113 |
+
documentation, if provided along with the Derivative Works; or,
|
| 114 |
+
within a display generated by the Derivative Works, if and
|
| 115 |
+
wherever such third-party notices normally appear. The contents
|
| 116 |
+
of the NOTICE file are for informational purposes only and
|
| 117 |
+
do not modify the License. You may add Your own attribution
|
| 118 |
+
notices within Derivative Works that You distribute, alongside
|
| 119 |
+
or as an addendum to the NOTICE text from the Work, provided
|
| 120 |
+
that such additional attribution notices cannot be construed
|
| 121 |
+
as modifying the License.
|
| 122 |
+
|
| 123 |
+
You may add Your own copyright statement to Your modifications and
|
| 124 |
+
may provide additional or different license terms and conditions
|
| 125 |
+
for use, reproduction, or distribution of Your modifications, or
|
| 126 |
+
for any such Derivative Works as a whole, provided Your use,
|
| 127 |
+
reproduction, and distribution of the Work otherwise complies with
|
| 128 |
+
the conditions stated in this License.
|
| 129 |
+
|
| 130 |
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
| 131 |
+
any Contribution intentionally submitted for inclusion in the Work
|
| 132 |
+
by You to the Licensor shall be under the terms and conditions of
|
| 133 |
+
this License, without any additional terms or conditions.
|
| 134 |
+
Notwithstanding the above, nothing herein shall supersede or modify
|
| 135 |
+
the terms of any separate license agreement you may have executed
|
| 136 |
+
with Licensor regarding such Contributions.
|
| 137 |
+
|
| 138 |
+
6. Trademarks. This License does not grant permission to use the trade
|
| 139 |
+
names, trademarks, service marks, or product names of the Licensor,
|
| 140 |
+
except as required for reasonable and customary use in describing the
|
| 141 |
+
origin of the Work and reproducing the content of the NOTICE file.
|
| 142 |
+
|
| 143 |
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
| 144 |
+
agreed to in writing, Licensor provides the Work (and each
|
| 145 |
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
| 146 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
| 147 |
+
implied, including, without limitation, any warranties or conditions
|
| 148 |
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
| 149 |
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
| 150 |
+
appropriateness of using or redistributing the Work and assume any
|
| 151 |
+
risks associated with Your exercise of permissions under this License.
|
| 152 |
+
|
| 153 |
+
8. Limitation of Liability. In no event and under no legal theory,
|
| 154 |
+
whether in tort (including negligence), contract, or otherwise,
|
| 155 |
+
unless required by applicable law (such as deliberate and grossly
|
| 156 |
+
negligent acts) or agreed to in writing, shall any Contributor be
|
| 157 |
+
liable to You for damages, including any direct, indirect, special,
|
| 158 |
+
incidental, or consequential damages of any character arising as a
|
| 159 |
+
result of this License or out of the use or inability to use the
|
| 160 |
+
Work (including but not limited to damages for loss of goodwill,
|
| 161 |
+
work stoppage, computer failure or malfunction, or any and all
|
| 162 |
+
other commercial damages or losses), even if such Contributor
|
| 163 |
+
has been advised of the possibility of such damages.
|
| 164 |
+
|
| 165 |
+
9. Accepting Warranty or Additional Liability. While redistributing
|
| 166 |
+
the Work or Derivative Works thereof, You may choose to offer,
|
| 167 |
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
| 168 |
+
or other liability obligations and/or rights consistent with this
|
| 169 |
+
License. However, in accepting such obligations, You may act only
|
| 170 |
+
on Your own behalf and on Your sole responsibility, not on behalf
|
| 171 |
+
of any other Contributor, and only if You agree to indemnify,
|
| 172 |
+
defend, and hold each Contributor harmless for any liability
|
| 173 |
+
incurred by, or claims asserted against, such Contributor by reason
|
| 174 |
+
of your accepting any such warranty or additional liability.
|
| 175 |
+
|
| 176 |
+
END OF TERMS AND CONDITIONS
|
| 177 |
+
|
| 178 |
+
APPENDIX: How to apply the Apache License to your work.
|
| 179 |
+
|
| 180 |
+
To apply the Apache License to your work, attach the following
|
| 181 |
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
| 182 |
+
replaced with your own identifying information. (Don't include
|
| 183 |
+
the brackets!) The text should be enclosed in the appropriate
|
| 184 |
+
comment syntax for the file format. We also recommend that a
|
| 185 |
+
file or class name and description of purpose be included on the
|
| 186 |
+
same "printed page" as the copyright notice for easier
|
| 187 |
+
identification within third-party archives.
|
| 188 |
+
|
| 189 |
+
Copyright [yyyy] [name of copyright owner]
|
| 190 |
+
|
| 191 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
| 192 |
+
you may not use this file except in compliance with the License.
|
| 193 |
+
You may obtain a copy of the License at
|
| 194 |
+
|
| 195 |
+
http://www.apache.org/licenses/LICENSE-2.0
|
| 196 |
+
|
| 197 |
+
Unless required by applicable law or agreed to in writing, software
|
| 198 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
| 199 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 200 |
+
See the License for the specific language governing permissions and
|
| 201 |
+
limitations under the License.
|
NOTICE
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Info Lens (Info Highlight)
|
| 2 |
+
Copyright 2025-2026 dqy <xiaoyundqy@qq.com>
|
| 3 |
+
|
| 4 |
+
Project: https://huggingface.co/spaces/dqy08/InfoLens
|
| 5 |
+
|
| 6 |
+
This product includes software from the GLTR project (Apache-2.0):
|
| 7 |
+
Copyright 2019 Hendrik Strobelt, Sebastian Gehrmann, Alexander M. Rush
|
| 8 |
+
https://github.com/HendrikStrobelt/detecting-fake-text
|
README.md
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: TokenTrace
|
| 3 |
+
emoji: 🔬
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
+
sdk: docker
|
| 7 |
+
short_description: Trace how LLMs think — token attribution, logit lens, semantic analysis & more
|
| 8 |
+
tags:
|
| 9 |
+
- nlp
|
| 10 |
+
- text-analysis
|
| 11 |
+
- llm-interpretability
|
| 12 |
+
- visualization
|
| 13 |
+
- mechanistic-interpretability
|
| 14 |
+
app_port: 7860
|
| 15 |
+
pinned: false
|
| 16 |
+
license: apache-2.0
|
| 17 |
+
---
|
| 18 |
+
|
| 19 |
+
# TokenTrace 🔬
|
| 20 |
+
|
| 21 |
+
> *Trace how every token shapes an LLM's predictions.*
|
| 22 |
+
|
| 23 |
+
**🌐 Live Demo**: [huggingface.co/spaces/Girlz/TokenTrace](https://huggingface.co/spaces/Girlz/TokenTrace)
|
| 24 |
+
|
| 25 |
+
**TokenTrace** is an interactive toolbox for exploring **how and why LLMs predict what they do**. It visualizes token-level attributions, layer-by-layer information flow, semantic relevance, and generation branching — all through a beautiful web UI.
|
| 26 |
+
|
| 27 |
+
---
|
| 28 |
+
|
| 29 |
+
## ✨ Features
|
| 30 |
+
|
| 31 |
+
### 🎯 Prediction Attribution
|
| 32 |
+
See which tokens in your input most influenced the model's next-token prediction. Uses gradient-based saliency (L2 norm of embedding gradients) to rank each token's contribution.
|
| 33 |
+
|
| 34 |
+
### 🔪 Ablation Attribution
|
| 35 |
+
Measure the impact of each token by occluding it and observing the probability change (ΔP = baseline − occluded). A counterfactual approach to understanding token importance.
|
| 36 |
+
|
| 37 |
+
### 📊 Logit Lens
|
| 38 |
+
Watch how the model's prediction "crystallizes" layer by layer. Each Transformer layer's hidden state is projected back to vocabulary space, revealing how information accumulates through the network depth.
|
| 39 |
+
|
| 40 |
+
### 🌳 Branch Tree
|
| 41 |
+
Visualize the top-k candidate tokens at every step of generation. Explore alternative paths the model could have taken — a probability tree that reveals the model's uncertainty and decision landscape.
|
| 42 |
+
|
| 43 |
+
### 📝 Information Density Analysis
|
| 44 |
+
Analyze which tokens carry the most "information content" using gradient-based methods. Distinguish high-information tokens (nouns, verbs) from low-information ones (stop words, punctuation).
|
| 45 |
+
|
| 46 |
+
### 🔍 Semantic Relevance Analysis
|
| 47 |
+
Given a query, score every token in a text by its relevance to that query. Uses logits gradient with a fill-in-the-blank prompt strategy to extract fine-grained relevance scores.
|
| 48 |
+
|
| 49 |
+
### 💬 Chat & Generation
|
| 50 |
+
OpenAI-compatible completions endpoint with SSE streaming. Supports chat templates, tool calling visualization, and multi-turn causal flow tracing.
|
| 51 |
+
|
| 52 |
+
### 🔄 Causal Flow
|
| 53 |
+
Multi-turn generation with per-token attribution at every step. Trace the full causal chain from input to output across multiple rounds of tool calling and generation.
|
| 54 |
+
|
| 55 |
+
---
|
| 56 |
+
|
| 57 |
+
## 🚀 Quick Start
|
| 58 |
+
|
| 59 |
+
### Run Locally with Docker
|
| 60 |
+
|
| 61 |
+
```bash
|
| 62 |
+
# 1. Build the image
|
| 63 |
+
docker build -t tokentrace .
|
| 64 |
+
|
| 65 |
+
# 2. Run the container
|
| 66 |
+
docker run -p 7860:7860 tokentrace
|
| 67 |
+
|
| 68 |
+
# 3. Visit http://localhost:7860
|
| 69 |
+
```
|
| 70 |
+
|
| 71 |
+
### Local Development
|
| 72 |
+
|
| 73 |
+
```bash
|
| 74 |
+
# Backend
|
| 75 |
+
pip install -r requirements.txt
|
| 76 |
+
python run.py --no_auto_load --base_model qwen3-0.6b --instruct_model qwen3-0.6b-instruct
|
| 77 |
+
|
| 78 |
+
# Frontend (separate terminal)
|
| 79 |
+
cd client/src && npm install && npm run build
|
| 80 |
+
```
|
| 81 |
+
|
| 82 |
+
---
|
| 83 |
+
|
| 84 |
+
## 🧠 How It Works
|
| 85 |
+
|
| 86 |
+
```mermaid
|
| 87 |
+
flowchart LR
|
| 88 |
+
U[User Input] --> A[TokenTrace Backend]
|
| 89 |
+
A --> M1[Base Model Slot<br/>Qwen3-0.6B-Base]
|
| 90 |
+
A --> M2[Instruct Model Slot<br/>Qwen3-0.6B]
|
| 91 |
+
M1 -->|forward + backward| G[Gradient Attribution]
|
| 92 |
+
M1 -->|forward pass| L[Logit Lens]
|
| 93 |
+
M1 -->|forward + occlusion| AB[Ablation Attribution]
|
| 94 |
+
M1 -->|forward + softmax| BT[Branch Tree]
|
| 95 |
+
M2 -->|chat template + generate| C[Chat / Completion]
|
| 96 |
+
M2 -->|gradient + prompt| S[Semantic Analysis]
|
| 97 |
+
G --> V[JSON Result → Frontend Visualization]
|
| 98 |
+
L --> V
|
| 99 |
+
AB --> V
|
| 100 |
+
BT --> V
|
| 101 |
+
C --> V
|
| 102 |
+
S --> V
|
| 103 |
+
```
|
| 104 |
+
|
| 105 |
+
The backend runs two model slots in PyTorch:
|
| 106 |
+
- **Base slot**: For analysis tasks (attribution, logit lens, branching) — Qwen3-0.6B-Base
|
| 107 |
+
- **Instruct slot**: For chat and semantic analysis — Qwen3-0.6B
|
| 108 |
+
|
| 109 |
+
All analysis is done locally on CPU (free tier) or GPU (if available), with gradient checkpointing to minimize memory usage.
|
| 110 |
+
|
| 111 |
+
---
|
| 112 |
+
|
| 113 |
+
## 🗺️ Feature Map
|
| 114 |
+
|
| 115 |
+
| Page | Path | Purpose | Requires |
|
| 116 |
+
|------|------|---------|----------|
|
| 117 |
+
| Home | `/` | Overview & navigation | Static |
|
| 118 |
+
| Analysis | `/client/analysis.html` | Information density & semantic analysis | Base slot |
|
| 119 |
+
| Attribution | `/client/attribution.html` | Prediction & ablation attribution | Base slot |
|
| 120 |
+
| Chat | `/client/chat.html` | Chat completion with tool calling | Instruct slot |
|
| 121 |
+
| Logit Lens | `/client/logit_lens.html` | Layer-by-layer prediction projection | Base slot |
|
| 122 |
+
| Branch Tree | `/client/branch_tree.html` | Next-token probability tree | Base slot |
|
| 123 |
+
| Causal Flow | `/client/causal_flow.html` | Multi-turn generation + attribution | Both slots |
|
| 124 |
+
|
| 125 |
+
---
|
| 126 |
+
|
| 127 |
+
## ⚙️ Configuration
|
| 128 |
+
|
| 129 |
+
Key environment variables:
|
| 130 |
+
|
| 131 |
+
| Variable | Purpose |
|
| 132 |
+
|----------|---------|
|
| 133 |
+
| `FORCE_INT8=1` | Enable INT8 quantization (CPU, saves ~50% memory) |
|
| 134 |
+
| `FORCE_CPU=1` | Force CPU mode |
|
| 135 |
+
| `HF_HUB_ENABLE_HF_TRANSFER=1` | Accelerate model downloads |
|
| 136 |
+
| `INFORADAR_ADMIN_TOKEN` | Admin token for model switching & demo management |
|
| 137 |
+
|
| 138 |
+
---
|
| 139 |
+
|
| 140 |
+
## 📜 License
|
| 141 |
+
|
| 142 |
+
[Apache 2.0](LICENSE). Copyright and attribution notices remain in [NOTICE](NOTICE).
|
| 143 |
+
|
| 144 |
+
---
|
| 145 |
+
|
| 146 |
+
## 🙏 Fork Attribution
|
| 147 |
+
|
| 148 |
+
This project is forked from [InfoLens](https://huggingface.co/spaces/dqy08/InfoLens) by [dqy08](https://huggingface.co/dqy08), with enhancements and adaptations for the Hugging Face Spaces free tier.
|
backend/__init__.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from backend.models import REGISTERED_MODELS
|
| 2 |
+
|
| 3 |
+
import backend.core.language_checker # noqa: F401
|
| 4 |
+
|
| 5 |
+
__all__ = ["REGISTERED_MODELS"]
|
backend/api/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""API 路由模块"""
|
| 2 |
+
|
backend/api/ablation_attribute.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""消融归因 API"""
|
| 2 |
+
import gc
|
| 3 |
+
import time
|
| 4 |
+
|
| 5 |
+
from backend.models.model_manager import inference_lock
|
| 6 |
+
from backend.platform.oom import exit_if_oom
|
| 7 |
+
from backend.core.ablation_attributor import analyze_ablation_attribution
|
| 8 |
+
from backend.api.analyze import LOCK_WAIT_TIMEOUT
|
| 9 |
+
from backend.platform.access_log import get_client_ip, log_prediction_attribute_request
|
| 10 |
+
from backend.platform.source_page import ALLOWED_SOURCE_PAGES, normalize_source_page
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def ablation_attribute(ablation_request):
|
| 14 |
+
"""
|
| 15 |
+
对上下文文本的下一 token 预测做消融归因。
|
| 16 |
+
|
| 17 |
+
Args:
|
| 18 |
+
ablation_request: 须含 ``context``、``model``、``source_page``。
|
| 19 |
+
归因目标三选一:省略两者时为 top-1;
|
| 20 |
+
``target_prediction``(首 token);``target_token_id``(非负整数);二者互斥。
|
| 21 |
+
|
| 22 |
+
Returns:
|
| 23 |
+
(响应字典, 状态码) 元组
|
| 24 |
+
"""
|
| 25 |
+
context = ablation_request.get("context")
|
| 26 |
+
target_prediction = ablation_request.get("target_prediction")
|
| 27 |
+
target_token_id = ablation_request.get("target_token_id")
|
| 28 |
+
model = ablation_request.get("model")
|
| 29 |
+
source_page = ablation_request.get("source_page")
|
| 30 |
+
flow_id = ablation_request.get("flow_id")
|
| 31 |
+
flow_step = ablation_request.get("flow_step")
|
| 32 |
+
|
| 33 |
+
if context is None or context == "":
|
| 34 |
+
return {"success": False, "message": "Missing required field: context"}, 400
|
| 35 |
+
if not isinstance(context, str):
|
| 36 |
+
return {"success": False, "message": "context must be a string"}, 400
|
| 37 |
+
|
| 38 |
+
if target_prediction is not None and not isinstance(target_prediction, str):
|
| 39 |
+
return {"success": False, "message": "target_prediction must be a string"}, 400
|
| 40 |
+
if target_prediction == "":
|
| 41 |
+
return {"success": False, "message": "target_prediction must not be empty"}, 400
|
| 42 |
+
if target_token_id is not None and not isinstance(target_token_id, int):
|
| 43 |
+
return {"success": False, "message": "target_token_id must be an integer"}, 400
|
| 44 |
+
if target_token_id is not None and target_token_id < 0:
|
| 45 |
+
return {"success": False, "message": "target_token_id must be >= 0"}, 400
|
| 46 |
+
if target_prediction is not None and target_token_id is not None:
|
| 47 |
+
return {"success": False, "message": "target_prediction and target_token_id are mutually exclusive"}, 400
|
| 48 |
+
|
| 49 |
+
if model is None:
|
| 50 |
+
return {"success": False, "message": "Missing required field: model"}, 400
|
| 51 |
+
if model not in ("base", "instruct"):
|
| 52 |
+
return {"success": False, "message": 'model must be "base" or "instruct"'}, 400
|
| 53 |
+
|
| 54 |
+
if source_page is None or source_page == "":
|
| 55 |
+
return {"success": False, "message": "Missing required field: source_page"}, 400
|
| 56 |
+
normalized_source_page = normalize_source_page(source_page)
|
| 57 |
+
if normalized_source_page is None:
|
| 58 |
+
allowed = ", ".join(sorted(ALLOWED_SOURCE_PAGES))
|
| 59 |
+
return {
|
| 60 |
+
"success": False,
|
| 61 |
+
"message": f"source_page must be one of: {allowed} (legacy *.html and gen_attribute accepted)",
|
| 62 |
+
}, 400
|
| 63 |
+
source_page = normalized_source_page
|
| 64 |
+
|
| 65 |
+
if flow_id is not None and not isinstance(flow_id, str):
|
| 66 |
+
return {"success": False, "message": "flow_id must be a string"}, 400
|
| 67 |
+
if flow_id == "":
|
| 68 |
+
return {"success": False, "message": "flow_id must not be empty"}, 400
|
| 69 |
+
if flow_step is not None and not isinstance(flow_step, int):
|
| 70 |
+
return {"success": False, "message": "flow_step must be an integer"}, 400
|
| 71 |
+
if flow_step is not None and flow_step < 0:
|
| 72 |
+
return {"success": False, "message": "flow_step must be >= 0"}, 400
|
| 73 |
+
|
| 74 |
+
is_causal_flow = source_page == "causal_flow"
|
| 75 |
+
if is_causal_flow:
|
| 76 |
+
if flow_id is None:
|
| 77 |
+
return {"success": False, "message": "Missing required field: flow_id for causal flow"}, 400
|
| 78 |
+
if flow_step is None:
|
| 79 |
+
return {"success": False, "message": "Missing required field: flow_step for causal flow"}, 400
|
| 80 |
+
elif flow_id is not None or flow_step is not None:
|
| 81 |
+
return {
|
| 82 |
+
"success": False,
|
| 83 |
+
"message": "flow_id/flow_step are only allowed when source_page is causal_flow",
|
| 84 |
+
}, 400
|
| 85 |
+
|
| 86 |
+
client_ip = get_client_ip()
|
| 87 |
+
start_time = time.perf_counter()
|
| 88 |
+
request_id = log_prediction_attribute_request(
|
| 89 |
+
context=context,
|
| 90 |
+
target_prediction=target_prediction,
|
| 91 |
+
target_token_id=target_token_id,
|
| 92 |
+
model=model,
|
| 93 |
+
source_page=source_page,
|
| 94 |
+
flow_id=flow_id,
|
| 95 |
+
flow_step=flow_step,
|
| 96 |
+
client_ip=client_ip,
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
lock_acquired = inference_lock.acquire(timeout=LOCK_WAIT_TIMEOUT)
|
| 100 |
+
if not lock_acquired:
|
| 101 |
+
return {
|
| 102 |
+
"success": False,
|
| 103 |
+
"message": (
|
| 104 |
+
f"Queue wait exceeded {LOCK_WAIT_TIMEOUT} seconds; "
|
| 105 |
+
"server is busy, please try again later."
|
| 106 |
+
),
|
| 107 |
+
}, 503
|
| 108 |
+
|
| 109 |
+
try:
|
| 110 |
+
result = analyze_ablation_attribution(
|
| 111 |
+
context,
|
| 112 |
+
target_prediction,
|
| 113 |
+
model=model,
|
| 114 |
+
target_token_id=target_token_id,
|
| 115 |
+
)
|
| 116 |
+
except ValueError as e:
|
| 117 |
+
return {"success": False, "message": str(e)}, 400
|
| 118 |
+
except Exception as e:
|
| 119 |
+
import traceback
|
| 120 |
+
traceback.print_exc()
|
| 121 |
+
exit_if_oom(e, defer_seconds=1)
|
| 122 |
+
return {"success": False, "message": str(e)}, 500
|
| 123 |
+
finally:
|
| 124 |
+
inference_lock.release()
|
| 125 |
+
gc.collect()
|
| 126 |
+
|
| 127 |
+
elapsed = time.perf_counter() - start_time
|
| 128 |
+
tokens = len(result.get("token_attribution", []))
|
| 129 |
+
target_token = result.get("target_token")
|
| 130 |
+
print(
|
| 131 |
+
f"\t📤 API ablation_attribute response: req_id={request_id}, "
|
| 132 |
+
f"target={target_token!r}, tokens={tokens}, response_time={elapsed:.4f}s"
|
| 133 |
+
)
|
| 134 |
+
|
| 135 |
+
return {"success": True, **result}, 200
|
backend/api/analyze.py
ADDED
|
@@ -0,0 +1,412 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""文本分析 API"""
|
| 2 |
+
import gc
|
| 3 |
+
import json
|
| 4 |
+
import time
|
| 5 |
+
import queue
|
| 6 |
+
import threading
|
| 7 |
+
from typing import Optional
|
| 8 |
+
from backend.platform.schemas import create_empty_analysis_result
|
| 9 |
+
from backend.models.model_manager import project_registry, DEFAULT_BASE_MODEL, inference_lock
|
| 10 |
+
from model_paths import resolve_hf_path
|
| 11 |
+
from backend.platform.oom import exit_if_oom
|
| 12 |
+
from backend.api.sse_utils import (
|
| 13 |
+
SSEProgressReporter,
|
| 14 |
+
consume_progress_queue,
|
| 15 |
+
send_result_event,
|
| 16 |
+
send_error_event,
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
# 自定义异常:排队超时
|
| 21 |
+
class QueueTimeoutError(Exception):
|
| 22 |
+
"""排队等待获取锁超时"""
|
| 23 |
+
pass
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
# 使用 model_manager 中的统一推理锁(与 analyze_semantic 共用)
|
| 27 |
+
# 单次分析的总处理时长限制(秒)
|
| 28 |
+
ANALYSIS_TIMEOUT = 60.0
|
| 29 |
+
# 等待获取锁的最大时间(秒)- 如果排队时间过长,直接拒绝请求
|
| 30 |
+
LOCK_WAIT_TIMEOUT = 10.0
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def _analyze_result_model_display(model: Optional[str]) -> Optional[str]:
|
| 34 |
+
"""主分析 result.model:对外统一为 HuggingFace 仓库 id(与 model_paths.resolve_hf_path 一致)。"""
|
| 35 |
+
if not model or not str(model).strip():
|
| 36 |
+
return None
|
| 37 |
+
return resolve_hf_path(str(model).strip())
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def _build_response(model: str, text: str, result):
|
| 41 |
+
"""构建标准响应"""
|
| 42 |
+
# 将 model 添加到 result 中,并确保 model 在最前面
|
| 43 |
+
if not isinstance(result, dict):
|
| 44 |
+
result = {}
|
| 45 |
+
result = result.copy()
|
| 46 |
+
# 如果 result 中已有 model,先移除
|
| 47 |
+
if 'model' in result:
|
| 48 |
+
model_value = result.pop('model')
|
| 49 |
+
else:
|
| 50 |
+
model_value = model
|
| 51 |
+
# 重新构建 result,确保 model 在最前面
|
| 52 |
+
result = {'model': _analyze_result_model_display(model_value), **result}
|
| 53 |
+
return {
|
| 54 |
+
"request": {'text': text},
|
| 55 |
+
"result": result
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def _error_response(model: str, text: str, message: str, status_code: int):
|
| 60 |
+
"""构建错误响应(统一格式)"""
|
| 61 |
+
# 统一错误格式:包含 success=false 和 message
|
| 62 |
+
result = create_empty_analysis_result(message, _analyze_result_model_display(model))
|
| 63 |
+
return {
|
| 64 |
+
"success": False,
|
| 65 |
+
"message": message,
|
| 66 |
+
"request": {'text': text or ''},
|
| 67 |
+
"result": result
|
| 68 |
+
}, status_code
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def _validate_and_prepare_request(analyze_request):
|
| 72 |
+
"""
|
| 73 |
+
验证请求并准备参数
|
| 74 |
+
|
| 75 |
+
Returns:
|
| 76 |
+
(model, text, error_msg, status_code) 元组
|
| 77 |
+
如果验证失败,返回 (None, None, error_msg, status_code)
|
| 78 |
+
如果成功,返回 (model, text, None, None)
|
| 79 |
+
"""
|
| 80 |
+
model = analyze_request.get('model')
|
| 81 |
+
text = analyze_request.get('text')
|
| 82 |
+
|
| 83 |
+
if not text:
|
| 84 |
+
return None, None, "缺少分析文本,请提供 text 字段", 400
|
| 85 |
+
|
| 86 |
+
# 获取默认模型(使用模块级上下文以获取持久化的当前活动模型)
|
| 87 |
+
from backend.platform.app_context import get_app_context
|
| 88 |
+
context = get_app_context(prefer_module_context=True)
|
| 89 |
+
default_model = context.base_model_id if context.base_model_id else DEFAULT_BASE_MODEL
|
| 90 |
+
|
| 91 |
+
# 处理 default、None 或空字符串,使用默认模型
|
| 92 |
+
if not model or model == 'default' or model == '':
|
| 93 |
+
model = default_model
|
| 94 |
+
else:
|
| 95 |
+
# 只允许使用默认模型,其他模型请求将被拒绝
|
| 96 |
+
if model != default_model:
|
| 97 |
+
return None, None, f"当前仅支持默认模型 '{default_model}',不允许使用其他模型", 400
|
| 98 |
+
|
| 99 |
+
return model, text, None, None
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def _load_project_with_error_handling(model):
|
| 103 |
+
"""
|
| 104 |
+
获取已加载的模型;若未加载则根据配置进行懒加载或返回错误。
|
| 105 |
+
|
| 106 |
+
Returns:
|
| 107 |
+
(project_obj, error_msg, status_code) 元组
|
| 108 |
+
如果成功,返回 (project_obj, None, None)
|
| 109 |
+
如果失败,返回 (None, error_msg, status_code)
|
| 110 |
+
"""
|
| 111 |
+
# 检查模型是否在注册表中
|
| 112 |
+
if not project_registry.is_available(model):
|
| 113 |
+
available_models = list(project_registry.available_model_names())
|
| 114 |
+
error_msg = f"❌ 模型 '{model}' 未注册。可用模型: {available_models}"
|
| 115 |
+
print(error_msg)
|
| 116 |
+
return None, error_msg, 404
|
| 117 |
+
|
| 118 |
+
# 检查模型是否已加载
|
| 119 |
+
p = project_registry.get(model)
|
| 120 |
+
if p is None:
|
| 121 |
+
from backend.platform.app_context import get_app_context
|
| 122 |
+
from backend.models.model_manager import ensure_base_slot_ready
|
| 123 |
+
|
| 124 |
+
context = get_app_context(prefer_module_context=True)
|
| 125 |
+
if context.model_loading:
|
| 126 |
+
error_msg = f"模型 '{model}' 正在后台加载中,请稍后重试"
|
| 127 |
+
print(f"⚠️ {error_msg}")
|
| 128 |
+
return None, error_msg, 503
|
| 129 |
+
# 懒加载模式 (--no_auto_load):首次请求仅初始化主槽位(权重 + QwenLM 项目)
|
| 130 |
+
if getattr(context.args, 'no_auto_load', False):
|
| 131 |
+
try:
|
| 132 |
+
ensure_base_slot_ready()
|
| 133 |
+
p = project_registry.get(model)
|
| 134 |
+
except Exception as e: # noqa: BLE001
|
| 135 |
+
import traceback
|
| 136 |
+
print(f"⚠️ 模���懒加载失败: {e}")
|
| 137 |
+
traceback.print_exc()
|
| 138 |
+
return None, f"模型加载失败: {str(e)}", 500
|
| 139 |
+
if p is None:
|
| 140 |
+
error_msg = f"模型 '{model}' 未加载,请联系管理员"
|
| 141 |
+
print(f"⚠️ {error_msg}")
|
| 142 |
+
return None, error_msg, 503
|
| 143 |
+
return p, None, None
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def _log_request(text, stream_mode=False, client_ip=None):
|
| 147 |
+
"""
|
| 148 |
+
打印请求日志
|
| 149 |
+
|
| 150 |
+
Returns:
|
| 151 |
+
int: 请求ID
|
| 152 |
+
"""
|
| 153 |
+
from backend.platform.access_log import log_analyze_request
|
| 154 |
+
return log_analyze_request(text, stream_mode, client_ip)
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
def _log_response(res, char_count, elapsed_time, stream_mode=False, request_id=None, wait_time=None):
|
| 158 |
+
"""打印响应日志"""
|
| 159 |
+
tokens = len(res.get('bpe_strings', []))
|
| 160 |
+
text_length = char_count
|
| 161 |
+
mode_str = "(stream)" if stream_mode else ""
|
| 162 |
+
|
| 163 |
+
# 构建日志消息
|
| 164 |
+
msg = f"\t📤 API analyze {mode_str} response:"
|
| 165 |
+
if request_id is not None:
|
| 166 |
+
msg += f" req_id={request_id},"
|
| 167 |
+
msg += f" tokens={tokens}, text_length={text_length}"
|
| 168 |
+
msg += f", response_time={elapsed_time:.4f}s"
|
| 169 |
+
|
| 170 |
+
print(msg)
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
def _validate_and_fix_result(res):
|
| 174 |
+
"""验证和修复结果结构"""
|
| 175 |
+
if not isinstance(res, dict):
|
| 176 |
+
res = {'bpe_strings': []}
|
| 177 |
+
if 'bpe_strings' not in res or not isinstance(res.get('bpe_strings'), list):
|
| 178 |
+
res['bpe_strings'] = res.get('bpe_strings', []) if isinstance(res.get('bpe_strings'), list) else []
|
| 179 |
+
return res
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def analyze(analyze_request):
|
| 183 |
+
"""
|
| 184 |
+
分析文本
|
| 185 |
+
|
| 186 |
+
Args:
|
| 187 |
+
analyze_request: 分析请求字典,包含:
|
| 188 |
+
- model: 模型名称
|
| 189 |
+
- text: 要分析的文本
|
| 190 |
+
- stream: 可选,如果为 True 则返回 SSE 流式响应(带进度信息)
|
| 191 |
+
|
| 192 |
+
Returns:
|
| 193 |
+
如果 stream=True: SSE 响应对象
|
| 194 |
+
否则: (响应字典, 状态码) 元组
|
| 195 |
+
"""
|
| 196 |
+
# 检查模型是否正在加载中(使用模块级上下文)
|
| 197 |
+
from backend.platform.app_context import get_app_context
|
| 198 |
+
context = get_app_context(prefer_module_context=True)
|
| 199 |
+
if context.model_loading:
|
| 200 |
+
return _error_response('', '', '模型正在加载中,请稍后重试', 503)
|
| 201 |
+
|
| 202 |
+
# 在请求上下文中获取 client_ip,流式响应时生成器内可能已失效
|
| 203 |
+
from backend.platform.access_log import get_client_ip
|
| 204 |
+
client_ip = get_client_ip()
|
| 205 |
+
|
| 206 |
+
# 检查是否启用流式响应
|
| 207 |
+
stream = analyze_request.get('stream', False)
|
| 208 |
+
if stream:
|
| 209 |
+
return _analyze_with_stream(analyze_request, client_ip)
|
| 210 |
+
return _analyze_plain(analyze_request, client_ip)
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
def _analyze_with_stream(analyze_request, client_ip):
|
| 214 |
+
"""
|
| 215 |
+
流式分析文本,通过SSE返回进度和结果(内部函数)
|
| 216 |
+
|
| 217 |
+
Args:
|
| 218 |
+
analyze_request: 分析请求字典,包含 model 和 text
|
| 219 |
+
client_ip: 客户端 IP,在入口处获取后传入
|
| 220 |
+
|
| 221 |
+
Returns:
|
| 222 |
+
SSE响应对象
|
| 223 |
+
"""
|
| 224 |
+
reporter = SSEProgressReporter(lambda: _generate_analyze_events(analyze_request, client_ip))
|
| 225 |
+
return reporter.create_response()
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
def _analyze_plain(analyze_request, client_ip):
|
| 229 |
+
"""
|
| 230 |
+
非流式分析:封装流式实现,消费事件流后返回 JSON。
|
| 231 |
+
供脚本等简单客户端使用。
|
| 232 |
+
"""
|
| 233 |
+
result = None
|
| 234 |
+
error_msg = None
|
| 235 |
+
status_code = 500
|
| 236 |
+
try:
|
| 237 |
+
for event_str in _generate_analyze_events(analyze_request, client_ip):
|
| 238 |
+
if not event_str.startswith('data: '):
|
| 239 |
+
continue
|
| 240 |
+
data = json.loads(event_str[6:].strip())
|
| 241 |
+
t = data.get('type')
|
| 242 |
+
if t == 'result':
|
| 243 |
+
result = data.get('data')
|
| 244 |
+
elif t == 'error':
|
| 245 |
+
error_msg = data.get('message', '分析失败')
|
| 246 |
+
status_code = data.get('status_code', 500)
|
| 247 |
+
break
|
| 248 |
+
except Exception as e:
|
| 249 |
+
import traceback
|
| 250 |
+
traceback.print_exc()
|
| 251 |
+
exit_if_oom(e, defer_seconds=1)
|
| 252 |
+
error_msg = f"分析失败: {str(e)}"
|
| 253 |
+
finally:
|
| 254 |
+
gc.collect()
|
| 255 |
+
|
| 256 |
+
if error_msg:
|
| 257 |
+
model = analyze_request.get('model') or ''
|
| 258 |
+
text = analyze_request.get('text') or ''
|
| 259 |
+
return _error_response(model, text, error_msg, status_code)
|
| 260 |
+
if result is None:
|
| 261 |
+
return _error_response('', '', '分析失败:未获取到结果', 500)
|
| 262 |
+
return result, 200
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
def _generate_analyze_events(analyze_request, client_ip):
|
| 266 |
+
"""
|
| 267 |
+
流式分析核心:生成 SSE 事件流(progress + result/error)。
|
| 268 |
+
供 _analyze_with_stream 和 _analyze_plain 复用。
|
| 269 |
+
client_ip 需在入口处获取并传入,因流式响应时生成器执行时请求上下文可能已失效。
|
| 270 |
+
"""
|
| 271 |
+
# 再次检查模型加载状态(在生成器内部,使用模块级上下文)
|
| 272 |
+
from backend.platform.app_context import get_app_context
|
| 273 |
+
context = get_app_context(prefer_module_context=True)
|
| 274 |
+
if context.model_loading:
|
| 275 |
+
yield send_error_event('模型正在加载中,请稍后重试', 503)
|
| 276 |
+
return
|
| 277 |
+
|
| 278 |
+
start_time = time.perf_counter()
|
| 279 |
+
|
| 280 |
+
# 验证和准备请求
|
| 281 |
+
model, text, error_msg, status_code = _validate_and_prepare_request(analyze_request)
|
| 282 |
+
if error_msg:
|
| 283 |
+
yield send_error_event(error_msg, status_code or 400)
|
| 284 |
+
return
|
| 285 |
+
|
| 286 |
+
# 加载模型
|
| 287 |
+
p, error_msg, status_code = _load_project_with_error_handling(model)
|
| 288 |
+
if error_msg:
|
| 289 |
+
yield send_error_event(error_msg, status_code or 500)
|
| 290 |
+
return
|
| 291 |
+
|
| 292 |
+
try:
|
| 293 |
+
char_count = len(text) if text else 0
|
| 294 |
+
request_id = _log_request(text, stream_mode=True, client_ip=client_ip)
|
| 295 |
+
|
| 296 |
+
# 创建线程安全的进度队列
|
| 297 |
+
progress_queue = queue.Queue()
|
| 298 |
+
analysis_done = threading.Event()
|
| 299 |
+
analysis_result = None
|
| 300 |
+
analysis_error = None
|
| 301 |
+
lock_wait_time = None # 记录等待锁的时间
|
| 302 |
+
|
| 303 |
+
def progress_callback_func(step: int, total_steps: int, stage: str, percentage: Optional[int]):
|
| 304 |
+
"""进度回调函数,将事件加入队列"""
|
| 305 |
+
progress_queue.put(('progress', step, total_steps, stage, percentage))
|
| 306 |
+
|
| 307 |
+
def run_analysis():
|
| 308 |
+
"""在单独线程中运行分析"""
|
| 309 |
+
nonlocal analysis_result, analysis_error, lock_wait_time
|
| 310 |
+
try:
|
| 311 |
+
# 记录开始等待锁的时间
|
| 312 |
+
lock_wait_start = time.perf_counter()
|
| 313 |
+
|
| 314 |
+
# 尝试获取锁,设置超时避免长时间排队
|
| 315 |
+
lock_acquired = inference_lock.acquire(timeout=LOCK_WAIT_TIMEOUT)
|
| 316 |
+
if not lock_acquired:
|
| 317 |
+
# 获取锁超时,说明前面有任务在执行且耗时较长
|
| 318 |
+
analysis_error = QueueTimeoutError(
|
| 319 |
+
f"排队等待超过 {LOCK_WAIT_TIMEOUT} 秒,服务繁忙,请稍后重试"
|
| 320 |
+
)
|
| 321 |
+
return
|
| 322 |
+
|
| 323 |
+
# 记录等待时间
|
| 324 |
+
lock_wait_time = time.perf_counter() - lock_wait_start
|
| 325 |
+
|
| 326 |
+
try:
|
| 327 |
+
from backend.platform.access_log import log_analyze_start
|
| 328 |
+
log_analyze_start(request_id, lock_wait_time, stream_mode=True)
|
| 329 |
+
|
| 330 |
+
# 在持有锁的情况下执行分析
|
| 331 |
+
# 注意:这里的执行时长也会受到 ANALYSIS_TIMEOUT 的监控(在外层循环中)
|
| 332 |
+
res = p.lm.analyze_text(text, progress_callback=progress_callback_func)
|
| 333 |
+
analysis_result = res
|
| 334 |
+
finally:
|
| 335 |
+
# 确保锁一定会被释放
|
| 336 |
+
inference_lock.release()
|
| 337 |
+
except Exception as e:
|
| 338 |
+
analysis_error = e
|
| 339 |
+
finally:
|
| 340 |
+
analysis_done.set()
|
| 341 |
+
progress_queue.put(('done', None, None)) # 发送完成信号
|
| 342 |
+
|
| 343 |
+
# 启动分析线程
|
| 344 |
+
analysis_thread = threading.Thread(target=run_analysis, daemon=True)
|
| 345 |
+
analysis_thread.start()
|
| 346 |
+
|
| 347 |
+
# 实时发送进度事件,并检查超时
|
| 348 |
+
timeout_reached = False
|
| 349 |
+
for kind, event_str in consume_progress_queue(
|
| 350 |
+
progress_queue, analysis_done, start_time, ANALYSIS_TIMEOUT, "分析"
|
| 351 |
+
):
|
| 352 |
+
if kind == 'timeout':
|
| 353 |
+
timeout_reached = True
|
| 354 |
+
yield event_str
|
| 355 |
+
break
|
| 356 |
+
if kind == 'progress':
|
| 357 |
+
yield event_str
|
| 358 |
+
elif kind == 'done':
|
| 359 |
+
break
|
| 360 |
+
|
| 361 |
+
# 如果超时,不等待分析完成,直接返回
|
| 362 |
+
if timeout_reached:
|
| 363 |
+
gc.collect()
|
| 364 |
+
return
|
| 365 |
+
|
| 366 |
+
# 检查是否有错误
|
| 367 |
+
# 注意:此时已收到 'done' 信号,分析线程已完成其工作(或发生错误)
|
| 368 |
+
# 线程是 daemon 的,会自动清理,无需显式等待
|
| 369 |
+
if analysis_error:
|
| 370 |
+
# 排队超时:返回友好的错误消息
|
| 371 |
+
if isinstance(analysis_error, QueueTimeoutError):
|
| 372 |
+
print(f"⏱️ 排队超时: {analysis_error}")
|
| 373 |
+
yield send_error_event(str(analysis_error), 503)
|
| 374 |
+
gc.collect()
|
| 375 |
+
return
|
| 376 |
+
# 其他错误:抛出异常,由外层的 try-except 处理
|
| 377 |
+
raise analysis_error
|
| 378 |
+
|
| 379 |
+
# 检查结果是否为空(理论上不应该发生,因为要么有结果,要么有错误)
|
| 380 |
+
if analysis_result is None:
|
| 381 |
+
print("⚠️ 分析结果为空,但没有错误信息")
|
| 382 |
+
yield send_error_event("分析失败:未获取到结果", 500)
|
| 383 |
+
gc.collect()
|
| 384 |
+
return
|
| 385 |
+
|
| 386 |
+
res = analysis_result
|
| 387 |
+
|
| 388 |
+
elapsed_time = time.perf_counter() - start_time
|
| 389 |
+
_log_response(res, char_count, elapsed_time, stream_mode=True,
|
| 390 |
+
request_id=request_id, wait_time=lock_wait_time)
|
| 391 |
+
|
| 392 |
+
# 验证和修复结果
|
| 393 |
+
res = _validate_and_fix_result(res)
|
| 394 |
+
|
| 395 |
+
# 构建最终响应
|
| 396 |
+
final_response = _build_response(model, text, res)
|
| 397 |
+
|
| 398 |
+
# 发送最终结果
|
| 399 |
+
yield send_result_event(final_response)
|
| 400 |
+
|
| 401 |
+
# 强制垃圾回收以释放内存
|
| 402 |
+
gc.collect()
|
| 403 |
+
|
| 404 |
+
except Exception as e:
|
| 405 |
+
import traceback
|
| 406 |
+
traceback.print_exc()
|
| 407 |
+
exit_if_oom(e, defer_seconds=1)
|
| 408 |
+
yield send_error_event(str(e), 500)
|
| 409 |
+
# 即使出错也进行垃圾回收
|
| 410 |
+
gc.collect()
|
| 411 |
+
|
| 412 |
+
|
backend/api/analyze_semantic.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Semantic analysis API:返回原文各 token 对 prompt 的平均关注度"""
|
| 2 |
+
import gc
|
| 3 |
+
import json
|
| 4 |
+
import queue
|
| 5 |
+
import threading
|
| 6 |
+
import time
|
| 7 |
+
from typing import Optional
|
| 8 |
+
|
| 9 |
+
from backend.models.model_manager import inference_lock
|
| 10 |
+
from backend.platform.oom import exit_if_oom
|
| 11 |
+
from backend.core.semantic_analyzer import analyze_semantic as _analyze_semantic
|
| 12 |
+
from backend.api.sse_utils import (
|
| 13 |
+
SSEProgressReporter,
|
| 14 |
+
consume_progress_queue,
|
| 15 |
+
send_result_event,
|
| 16 |
+
send_error_event,
|
| 17 |
+
)
|
| 18 |
+
from backend.platform.access_log import get_client_ip
|
| 19 |
+
from backend.api.analyze import QueueTimeoutError, ANALYSIS_TIMEOUT, LOCK_WAIT_TIMEOUT
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def _log_request(query, text, client_ip=None):
|
| 23 |
+
from backend.platform.access_log import log_analyze_semantic_request
|
| 24 |
+
return log_analyze_semantic_request(query, text, client_ip)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def _build_success_response(result, debug_info: bool = False):
|
| 28 |
+
"""构建成功响应。debug_info=True 时包含 debug_info 对象(abbrev、topk_tokens、topk_probs)"""
|
| 29 |
+
resp = {
|
| 30 |
+
"success": True,
|
| 31 |
+
"model": result["model"],
|
| 32 |
+
"token_attention": result["token_attention"],
|
| 33 |
+
"full_match_degree": result["full_match_degree"],
|
| 34 |
+
}
|
| 35 |
+
if debug_info and "debug_info" in result:
|
| 36 |
+
resp["debug_info"] = result["debug_info"]
|
| 37 |
+
return resp
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def _generate_semantic_events(
|
| 41 |
+
query: str, text: str, submode: Optional[str] = None, debug_info: bool = False,
|
| 42 |
+
full_match_degree_only: bool = False, client_ip: Optional[str] = None
|
| 43 |
+
):
|
| 44 |
+
"""
|
| 45 |
+
流式语义分析核心:生成 SSE 事件流(progress + result/error)。
|
| 46 |
+
供 _analyze_semantic_with_stream 和 _analyze_semantic_plain 复用。
|
| 47 |
+
client_ip 需在入口处获取并传入,因流式响应时生成器执行时请求上下文已失效。
|
| 48 |
+
"""
|
| 49 |
+
if client_ip is None:
|
| 50 |
+
client_ip = get_client_ip()
|
| 51 |
+
start_time = time.perf_counter()
|
| 52 |
+
request_id = _log_request(query, text, client_ip)
|
| 53 |
+
|
| 54 |
+
progress_queue = queue.Queue()
|
| 55 |
+
analysis_done = threading.Event()
|
| 56 |
+
analysis_result = None
|
| 57 |
+
analysis_error = None
|
| 58 |
+
lock_wait_time = None
|
| 59 |
+
|
| 60 |
+
def progress_callback(step: int, total_steps: int, stage: str, percentage: Optional[int]):
|
| 61 |
+
progress_queue.put(("progress", step, total_steps, stage, percentage))
|
| 62 |
+
|
| 63 |
+
def run_analysis():
|
| 64 |
+
nonlocal analysis_result, analysis_error, lock_wait_time
|
| 65 |
+
try:
|
| 66 |
+
lock_wait_start = time.perf_counter()
|
| 67 |
+
lock_acquired = inference_lock.acquire(timeout=LOCK_WAIT_TIMEOUT)
|
| 68 |
+
if not lock_acquired:
|
| 69 |
+
analysis_error = QueueTimeoutError(
|
| 70 |
+
f"排队等待超过 {LOCK_WAIT_TIMEOUT} 秒,服务繁忙,请稍后重试"
|
| 71 |
+
)
|
| 72 |
+
return
|
| 73 |
+
lock_wait_time = time.perf_counter() - lock_wait_start
|
| 74 |
+
|
| 75 |
+
try:
|
| 76 |
+
from backend.platform.access_log import log_analyze_semantic_start
|
| 77 |
+
log_analyze_semantic_start(request_id, lock_wait_time, stream_mode=True)
|
| 78 |
+
result = _analyze_semantic(query, text, submode_override=submode, progress_callback=progress_callback, debug_info=debug_info, full_match_degree_only=full_match_degree_only)
|
| 79 |
+
analysis_result = result
|
| 80 |
+
finally:
|
| 81 |
+
inference_lock.release()
|
| 82 |
+
except Exception as e:
|
| 83 |
+
analysis_error = e
|
| 84 |
+
finally:
|
| 85 |
+
analysis_done.set()
|
| 86 |
+
progress_queue.put(("done", None, None))
|
| 87 |
+
|
| 88 |
+
try:
|
| 89 |
+
analysis_thread = threading.Thread(target=run_analysis, daemon=True)
|
| 90 |
+
analysis_thread.start()
|
| 91 |
+
|
| 92 |
+
timeout_reached = False
|
| 93 |
+
for kind, event_str in consume_progress_queue(
|
| 94 |
+
progress_queue, analysis_done, start_time, ANALYSIS_TIMEOUT, "语义分析"
|
| 95 |
+
):
|
| 96 |
+
if kind == 'timeout':
|
| 97 |
+
timeout_reached = True
|
| 98 |
+
yield event_str
|
| 99 |
+
break
|
| 100 |
+
if kind == 'progress':
|
| 101 |
+
yield event_str
|
| 102 |
+
elif kind == 'done':
|
| 103 |
+
break
|
| 104 |
+
|
| 105 |
+
if timeout_reached:
|
| 106 |
+
gc.collect()
|
| 107 |
+
return
|
| 108 |
+
|
| 109 |
+
if analysis_error:
|
| 110 |
+
if isinstance(analysis_error, QueueTimeoutError):
|
| 111 |
+
print(f"⏱️ 排队超时: {analysis_error}")
|
| 112 |
+
yield send_error_event(str(analysis_error), 503)
|
| 113 |
+
gc.collect()
|
| 114 |
+
return
|
| 115 |
+
raise analysis_error
|
| 116 |
+
|
| 117 |
+
if analysis_result is None:
|
| 118 |
+
print("⚠️ 语义分析结果为空,但没有错误信息")
|
| 119 |
+
yield send_error_event("分析失败:未获取到结果", 500)
|
| 120 |
+
gc.collect()
|
| 121 |
+
return
|
| 122 |
+
|
| 123 |
+
elapsed = time.perf_counter() - start_time
|
| 124 |
+
tokens = len(analysis_result.get("token_attention", []))
|
| 125 |
+
print(
|
| 126 |
+
f"\t📤 API analyze_semantic (stream) response: req_id={request_id}, "
|
| 127 |
+
f"tokens={tokens}, response_time={elapsed:.4f}s"
|
| 128 |
+
)
|
| 129 |
+
yield send_result_event(_build_success_response(analysis_result, debug_info))
|
| 130 |
+
except Exception as e:
|
| 131 |
+
import traceback
|
| 132 |
+
traceback.print_exc()
|
| 133 |
+
exit_if_oom(e, defer_seconds=1)
|
| 134 |
+
yield send_error_event(str(e), 500)
|
| 135 |
+
finally:
|
| 136 |
+
gc.collect()
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
def _analyze_semantic_with_stream(
|
| 140 |
+
query: str, text: str, submode: Optional[str] = None, debug_info: bool = False,
|
| 141 |
+
full_match_degree_only: bool = False, client_ip: Optional[str] = None
|
| 142 |
+
):
|
| 143 |
+
"""流式语义分析,通过 SSE 返回阶段级进度"""
|
| 144 |
+
return SSEProgressReporter(
|
| 145 |
+
lambda: _generate_semantic_events(query, text, submode, debug_info, full_match_degree_only, client_ip)
|
| 146 |
+
).create_response()
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
def _analyze_semantic_plain(
|
| 150 |
+
query: str, text: str, submode: Optional[str] = None, debug_info: bool = False,
|
| 151 |
+
full_match_degree_only: bool = False, client_ip: Optional[str] = None
|
| 152 |
+
):
|
| 153 |
+
"""
|
| 154 |
+
非流式语义分析:封装流式实现,消费事件流后返回 JSON。
|
| 155 |
+
供脚本等简单客户端使用。
|
| 156 |
+
"""
|
| 157 |
+
result = None
|
| 158 |
+
error_msg = None
|
| 159 |
+
status_code = 500
|
| 160 |
+
try:
|
| 161 |
+
for event_str in _generate_semantic_events(query, text, submode, debug_info, full_match_degree_only, client_ip):
|
| 162 |
+
if not event_str.startswith('data: '):
|
| 163 |
+
continue
|
| 164 |
+
data = json.loads(event_str[6:].strip())
|
| 165 |
+
t = data.get('type')
|
| 166 |
+
if t == 'result':
|
| 167 |
+
result = data.get('data')
|
| 168 |
+
elif t == 'error':
|
| 169 |
+
error_msg = data.get('message', '分析失败')
|
| 170 |
+
status_code = data.get('status_code', 500)
|
| 171 |
+
break
|
| 172 |
+
except Exception as e:
|
| 173 |
+
import traceback
|
| 174 |
+
traceback.print_exc()
|
| 175 |
+
exit_if_oom(e, defer_seconds=1)
|
| 176 |
+
error_msg = str(e)
|
| 177 |
+
finally:
|
| 178 |
+
gc.collect()
|
| 179 |
+
|
| 180 |
+
if error_msg:
|
| 181 |
+
return {"success": False, "message": error_msg}, status_code
|
| 182 |
+
if result is None:
|
| 183 |
+
return {"success": False, "message": "分析失败:未获取到结果"}, 500
|
| 184 |
+
return result, 200
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
def analyze_semantic(semantic_request):
|
| 188 |
+
"""
|
| 189 |
+
分析原文 token 对 prompt 的关注度。
|
| 190 |
+
|
| 191 |
+
Args:
|
| 192 |
+
semantic_request: 包含 query、text、stream(可选)、submode(可选)的字典
|
| 193 |
+
|
| 194 |
+
Returns:
|
| 195 |
+
stream=True 时返回 SSE 响应;否则返回 (响应字典, 状态码) 元组
|
| 196 |
+
"""
|
| 197 |
+
query = (semantic_request.get("query") or "")
|
| 198 |
+
text = semantic_request.get("text") or ""
|
| 199 |
+
stream = semantic_request.get("stream", False)
|
| 200 |
+
submode = (semantic_request.get("submode") or "").strip() or None
|
| 201 |
+
debug_info = bool(semantic_request.get("debug_info", False))
|
| 202 |
+
full_match_degree_only = bool(semantic_request.get("full_match_degree_only", False))
|
| 203 |
+
|
| 204 |
+
if not query:
|
| 205 |
+
return {"success": False, "message": "缺少 query 字段"}, 400
|
| 206 |
+
if not text:
|
| 207 |
+
return {"success": False, "message": "缺少 text 字段"}, 400
|
| 208 |
+
|
| 209 |
+
client_ip = get_client_ip()
|
| 210 |
+
if stream:
|
| 211 |
+
return _analyze_semantic_with_stream(query, text, submode, debug_info, full_match_degree_only, client_ip)
|
| 212 |
+
return _analyze_semantic_plain(query, text, submode, debug_info, full_match_degree_only, client_ip)
|
backend/api/branch_next.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""分叉树 branch-next API"""
|
| 2 |
+
import gc
|
| 3 |
+
import time
|
| 4 |
+
|
| 5 |
+
from backend.models.model_manager import inference_lock
|
| 6 |
+
from backend.platform.oom import exit_if_oom
|
| 7 |
+
from backend.core.branch_next import expand_branch_next, BRANCH_NEXT_TOP_K_MAX
|
| 8 |
+
from backend.core.completion_generator import PromptTooLongError
|
| 9 |
+
from backend.api.analyze import LOCK_WAIT_TIMEOUT
|
| 10 |
+
from backend.platform.access_log import get_client_ip, log_request
|
| 11 |
+
from backend.platform.source_page import ALLOWED_SOURCE_PAGES, normalize_source_page
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def branch_next(branch_next_request):
|
| 15 |
+
prefix = branch_next_request.get("prefix")
|
| 16 |
+
model = branch_next_request.get("model")
|
| 17 |
+
source_page = branch_next_request.get("source_page")
|
| 18 |
+
top_k = branch_next_request.get("top_k")
|
| 19 |
+
|
| 20 |
+
if prefix is None or prefix == "":
|
| 21 |
+
return {"success": False, "message": "Missing required field: prefix"}, 400
|
| 22 |
+
if not isinstance(prefix, str):
|
| 23 |
+
return {"success": False, "message": "prefix must be a string"}, 400
|
| 24 |
+
|
| 25 |
+
if model is None:
|
| 26 |
+
return {"success": False, "message": "Missing required field: model"}, 400
|
| 27 |
+
if model not in ("base", "instruct"):
|
| 28 |
+
return {"success": False, "message": 'model must be "base" or "instruct"'}, 400
|
| 29 |
+
|
| 30 |
+
if source_page is None or source_page == "":
|
| 31 |
+
return {"success": False, "message": "Missing required field: source_page"}, 400
|
| 32 |
+
normalized_source_page = normalize_source_page(source_page)
|
| 33 |
+
if normalized_source_page is None:
|
| 34 |
+
allowed = ", ".join(sorted(ALLOWED_SOURCE_PAGES))
|
| 35 |
+
return {"success": False, "message": f"source_page must be one of: {allowed}"}, 400
|
| 36 |
+
source_page = normalized_source_page
|
| 37 |
+
|
| 38 |
+
if top_k is not None:
|
| 39 |
+
if not isinstance(top_k, int):
|
| 40 |
+
return {"success": False, "message": "top_k must be an integer"}, 400
|
| 41 |
+
if top_k < 1:
|
| 42 |
+
return {"success": False, "message": f"top_k must be >= 1"}, 400
|
| 43 |
+
if top_k > BRANCH_NEXT_TOP_K_MAX:
|
| 44 |
+
# clamp 而不是报错(设计 D3)
|
| 45 |
+
top_k = BRANCH_NEXT_TOP_K_MAX
|
| 46 |
+
|
| 47 |
+
client_ip = get_client_ip()
|
| 48 |
+
start_time = time.perf_counter()
|
| 49 |
+
log_request("📥 branch_next 请求", f"model={model!r}, source_page={source_page!r}, prefix_chars={len(prefix)}", client_ip)
|
| 50 |
+
|
| 51 |
+
lock_acquired = inference_lock.acquire(timeout=LOCK_WAIT_TIMEOUT)
|
| 52 |
+
if not lock_acquired:
|
| 53 |
+
return {"success": False, "message": f"Queue wait exceeded {LOCK_WAIT_TIMEOUT} seconds; server is busy, please try again later."}, 503
|
| 54 |
+
|
| 55 |
+
kwargs = {"model": model}
|
| 56 |
+
if top_k is not None:
|
| 57 |
+
kwargs["top_k"] = top_k
|
| 58 |
+
|
| 59 |
+
try:
|
| 60 |
+
result = expand_branch_next(prefix, **kwargs)
|
| 61 |
+
except PromptTooLongError as e:
|
| 62 |
+
return {"success": False, "message": str(e)}, 400
|
| 63 |
+
except ValueError as e:
|
| 64 |
+
return {"success": False, "message": str(e)}, 400
|
| 65 |
+
except Exception as e:
|
| 66 |
+
import traceback
|
| 67 |
+
traceback.print_exc()
|
| 68 |
+
exit_if_oom(e, defer_seconds=1)
|
| 69 |
+
return {"success": False, "message": str(e)}, 500
|
| 70 |
+
finally:
|
| 71 |
+
inference_lock.release()
|
| 72 |
+
gc.collect()
|
| 73 |
+
|
| 74 |
+
elapsed = time.perf_counter() - start_time
|
| 75 |
+
print(
|
| 76 |
+
f"\t📤 API branch_next response: "
|
| 77 |
+
f"prefix_tokens={result.get('prefix_tokens')}, "
|
| 78 |
+
f"candidates={len(result.get('candidates', []))}, "
|
| 79 |
+
f"response_time={elapsed:.4f}s"
|
| 80 |
+
)
|
| 81 |
+
return {"success": True, **result}, 200
|
backend/api/client_activity.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from urllib.parse import unquote
|
| 2 |
+
|
| 3 |
+
from backend.api.utils import request_has_valid_admin
|
| 4 |
+
from backend.platform.access_log import log_request
|
| 5 |
+
from backend.platform.online_presence import WINDOW_SEC, get_online_now, record_heartbeat
|
| 6 |
+
from backend.platform.visit_stats import normalize_page_key, record_activity_report, record_gen_attr_opt_sec
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def _activity_response() -> dict:
|
| 10 |
+
return {"ok": True, "online_now": get_online_now(), "online_window_sec": WINDOW_SEC}
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def _sparse_page_activity_log_cum(cum: int) -> bool:
|
| 14 |
+
"""无服务端状态:累计秒为 2、10、20(前 20s 内与前端档位一致)或 40、80、160…(40·2^k)时打访问日志。"""
|
| 15 |
+
if cum in (2, 10, 20):
|
| 16 |
+
return True
|
| 17 |
+
if cum < 40 or cum % 40:
|
| 18 |
+
return False
|
| 19 |
+
q = cum // 40
|
| 20 |
+
return q > 0 and (q & (q - 1)) == 0
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def client_activity_report(activity_body=None):
|
| 24 |
+
d = activity_body if isinstance(activity_body, dict) else {}
|
| 25 |
+
p = str(d.get("page_path") or "")[:512].strip()
|
| 26 |
+
try:
|
| 27 |
+
cum = int(d.get("total_active_sec"))
|
| 28 |
+
dlt = int(d.get("delta_active_sec"))
|
| 29 |
+
if not p or cum < 1 or dlt < 0:
|
| 30 |
+
return _activity_response()
|
| 31 |
+
except (TypeError, ValueError):
|
| 32 |
+
return _activity_response()
|
| 33 |
+
path_only = p.split("?", 1)[0].split("#", 1)[0].strip()
|
| 34 |
+
page_key = path_only.rstrip("/").split("/")[-1] or path_only
|
| 35 |
+
if not page_key:
|
| 36 |
+
return _activity_response()
|
| 37 |
+
if "?" in p:
|
| 38 |
+
qs = p.split("?", 1)[1].split("#", 1)[0]
|
| 39 |
+
log_path = f"{page_key}?{unquote(qs)}" if qs else page_key
|
| 40 |
+
else:
|
| 41 |
+
log_path = page_key
|
| 42 |
+
|
| 43 |
+
page_key = normalize_page_key(page_key)
|
| 44 |
+
|
| 45 |
+
if not request_has_valid_admin():
|
| 46 |
+
raw_os = d.get("client_os")
|
| 47 |
+
client_os = str(raw_os).strip() if raw_os is not None else None
|
| 48 |
+
|
| 49 |
+
record_heartbeat()
|
| 50 |
+
record_activity_report(page_key, dlt, cum, client_os)
|
| 51 |
+
if page_key == "causal_flow.html":
|
| 52 |
+
raw_opts = d.get("page_opts")
|
| 53 |
+
if isinstance(raw_opts, dict):
|
| 54 |
+
record_gen_attr_opt_sec(dlt, {k: bool(v) for k, v in raw_opts.items() if isinstance(k, str)})
|
| 55 |
+
if _sparse_page_activity_log_cum(cum):
|
| 56 |
+
log_request(
|
| 57 |
+
"📄 页面活跃",
|
| 58 |
+
f"path(sampled)={log_path!r} total_sec={cum} delta_sec={dlt}",
|
| 59 |
+
)
|
| 60 |
+
return _activity_response()
|
backend/api/demo.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Demo 文件管理 API"""
|
| 2 |
+
from backend.demo.data_utils import save_demo_payload
|
| 3 |
+
from backend.demo.demo_folder import (
|
| 4 |
+
list_demo_items,
|
| 5 |
+
move_demo_file,
|
| 6 |
+
rename_demo_file,
|
| 7 |
+
delete_demo_file,
|
| 8 |
+
move_folder,
|
| 9 |
+
)
|
| 10 |
+
from backend.api.utils import (
|
| 11 |
+
get_demo_directory,
|
| 12 |
+
handle_api_error,
|
| 13 |
+
handle_api_success,
|
| 14 |
+
require_admin,
|
| 15 |
+
validate_admin_token,
|
| 16 |
+
)
|
| 17 |
+
from backend.platform.access_log import log_check_admin
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def list_demos(path: str = ""):
|
| 21 |
+
"""
|
| 22 |
+
扫描demo目录下的文件夹和文件,返回列表
|
| 23 |
+
支持指定路径参数,返回指定路径下的内容
|
| 24 |
+
文件名(去掉.json后缀)作为demo名称
|
| 25 |
+
支持中文文件名和路径
|
| 26 |
+
从data/demo目录读取(更专业的数据目录结构)
|
| 27 |
+
|
| 28 |
+
Args:
|
| 29 |
+
path: 可选,指定要列出的路径,默认为根目录(空字符串)
|
| 30 |
+
"""
|
| 31 |
+
demo_dir = get_demo_directory(create=False)
|
| 32 |
+
try:
|
| 33 |
+
result = list_demo_items(demo_dir, path)
|
| 34 |
+
# if not result.get("items"):
|
| 35 |
+
# print(f"⚠️ 路径 '{path}' 下没有内容: {demo_dir}")
|
| 36 |
+
# else:
|
| 37 |
+
# items_count = len(result["items"])
|
| 38 |
+
# folders_count = sum(1 for item in result["items"] if item["type"] == "folder")
|
| 39 |
+
# files_count = sum(1 for item in result["items"] if item["type"] == "file")
|
| 40 |
+
# print(f"✓ 路径 '{path}': {folders_count} 个文件夹, {files_count} 个文件 (共 {items_count} 项)")
|
| 41 |
+
return result
|
| 42 |
+
except Exception as e:
|
| 43 |
+
error_result = handle_api_error("Failed to scan demo directory", e)
|
| 44 |
+
return {"path": path, "items": []}
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
@require_admin
|
| 48 |
+
def save_demo(save_request):
|
| 49 |
+
"""
|
| 50 |
+
保存demo文件到data/demo目录
|
| 51 |
+
请求格式: { name: string, data: AnalyzeResponse, path?: string, overwrite?: boolean }
|
| 52 |
+
path: 可选,保存路径,默认为根目录("/")
|
| 53 |
+
overwrite: 可选,是否覆盖已存在的文件,默认为False
|
| 54 |
+
"""
|
| 55 |
+
name = save_request.get('name')
|
| 56 |
+
data = save_request.get('data')
|
| 57 |
+
path = save_request.get('path', '/') # 默认为根目录
|
| 58 |
+
overwrite = save_request.get('overwrite', False) # 默认为False
|
| 59 |
+
|
| 60 |
+
if not name or not data:
|
| 61 |
+
return {
|
| 62 |
+
'success': False,
|
| 63 |
+
'message': 'Missing required parameters: name or data'
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
try:
|
| 67 |
+
demo_dir = get_demo_directory(create=True)
|
| 68 |
+
result = save_demo_payload(demo_dir, name, data, path, overwrite)
|
| 69 |
+
if result.get('success'):
|
| 70 |
+
print(f"✓ Demo已保存: {demo_dir / result['file']}")
|
| 71 |
+
else:
|
| 72 |
+
print(f"❌ Save failed: {result.get('message')}")
|
| 73 |
+
return result
|
| 74 |
+
except Exception as e:
|
| 75 |
+
return handle_api_error('Save failed', e)
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
@require_admin
|
| 79 |
+
def delete_demo(delete_request):
|
| 80 |
+
"""
|
| 81 |
+
将demo文件移动到deleted文件夹(软删除)
|
| 82 |
+
请求格式: { file: string } # 文件名(包含.json后缀)
|
| 83 |
+
"""
|
| 84 |
+
file = delete_request.get('file')
|
| 85 |
+
|
| 86 |
+
if not file:
|
| 87 |
+
return {
|
| 88 |
+
'success': False,
|
| 89 |
+
'message': 'Missing required parameter: file'
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
try:
|
| 93 |
+
demo_dir = get_demo_directory(create=False)
|
| 94 |
+
result = delete_demo_file(demo_dir, file)
|
| 95 |
+
return handle_api_success(result)
|
| 96 |
+
except Exception as e:
|
| 97 |
+
return handle_api_error('Delete failed', e)
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
@require_admin
|
| 101 |
+
def move_demo(move_request):
|
| 102 |
+
"""
|
| 103 |
+
移动demo文件或文件夹
|
| 104 |
+
请求格式: { file: string, target_path: string } 或 { path: string, target_path: string }
|
| 105 |
+
"""
|
| 106 |
+
file = move_request.get('file')
|
| 107 |
+
path = move_request.get('path')
|
| 108 |
+
target_path = move_request.get('target_path', '')
|
| 109 |
+
|
| 110 |
+
if not target_path and target_path != '':
|
| 111 |
+
return {
|
| 112 |
+
'success': False,
|
| 113 |
+
'message': 'Missing required parameter: target_path'
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
if not file and not path:
|
| 117 |
+
return {
|
| 118 |
+
'success': False,
|
| 119 |
+
'message': 'Missing required parameter: file or path'
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
try:
|
| 123 |
+
demo_dir = get_demo_directory(create=False)
|
| 124 |
+
|
| 125 |
+
if file:
|
| 126 |
+
# 移动文件
|
| 127 |
+
result = move_demo_file(demo_dir, file, target_path)
|
| 128 |
+
else:
|
| 129 |
+
# 移动文件夹
|
| 130 |
+
result = move_folder(demo_dir, path, target_path)
|
| 131 |
+
|
| 132 |
+
return handle_api_success(result)
|
| 133 |
+
except Exception as e:
|
| 134 |
+
return handle_api_error('Move failed', e)
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
@require_admin
|
| 138 |
+
def rename_demo(rename_request):
|
| 139 |
+
"""
|
| 140 |
+
重命名demo文件
|
| 141 |
+
请求格式: { file: string, new_name: string }
|
| 142 |
+
"""
|
| 143 |
+
file = rename_request.get('file')
|
| 144 |
+
new_name = rename_request.get('new_name')
|
| 145 |
+
|
| 146 |
+
if not file or not new_name:
|
| 147 |
+
return {
|
| 148 |
+
'success': False,
|
| 149 |
+
'message': 'Missing required parameter: file or new_name'
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
try:
|
| 153 |
+
demo_dir = get_demo_directory(create=False)
|
| 154 |
+
result = rename_demo_file(demo_dir, file, new_name)
|
| 155 |
+
return handle_api_success(result)
|
| 156 |
+
except Exception as e:
|
| 157 |
+
return handle_api_error('Rename failed', e)
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
def check_admin(check_request):
|
| 161 |
+
from flask import request
|
| 162 |
+
|
| 163 |
+
request_token = check_request.get('token') or request.headers.get('X-Admin-Token')
|
| 164 |
+
is_valid, error_message = validate_admin_token(request_token)
|
| 165 |
+
log_check_admin(is_valid, token=request_token)
|
| 166 |
+
|
| 167 |
+
if is_valid:
|
| 168 |
+
return {"success": True}
|
| 169 |
+
else:
|
| 170 |
+
return {
|
| 171 |
+
'success': False,
|
| 172 |
+
'message': error_message
|
| 173 |
+
}
|
| 174 |
+
|
backend/api/fetch_url.py
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""URL 文本提取 API"""
|
| 2 |
+
import json
|
| 3 |
+
import re
|
| 4 |
+
from urllib.parse import urlparse
|
| 5 |
+
import trafilatura
|
| 6 |
+
import requests
|
| 7 |
+
from backend.api.utils import handle_api_error
|
| 8 |
+
|
| 9 |
+
# 单次提取的最大字符数上限(防止异常大页面影响性能)
|
| 10 |
+
MAX_EXTRACTED_TEXT_LENGTH = 20000
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def _is_valid_url(url: str) -> bool:
|
| 14 |
+
"""验证 URL 格式"""
|
| 15 |
+
try:
|
| 16 |
+
result = urlparse(url)
|
| 17 |
+
return all([result.scheme in ['http', 'https'], result.netloc])
|
| 18 |
+
except Exception:
|
| 19 |
+
return False
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def _is_local_or_private(url: str) -> bool:
|
| 23 |
+
"""检查是否为本地或私有网络地址(防止 SSRF 攻击)"""
|
| 24 |
+
try:
|
| 25 |
+
parsed = urlparse(url)
|
| 26 |
+
hostname = parsed.hostname
|
| 27 |
+
|
| 28 |
+
if not hostname:
|
| 29 |
+
return True
|
| 30 |
+
|
| 31 |
+
# 检查是否为 localhost
|
| 32 |
+
if hostname in ['localhost', '127.0.0.1', '::1']:
|
| 33 |
+
return True
|
| 34 |
+
|
| 35 |
+
# 检查是否为私有 IP 地址
|
| 36 |
+
private_patterns = [
|
| 37 |
+
r'^10\.', # 10.0.0.0/8
|
| 38 |
+
r'^172\.(1[6-9]|2[0-9]|3[0-1])\.', # 172.16.0.0/12
|
| 39 |
+
r'^192\.168\.', # 192.168.0.0/16
|
| 40 |
+
r'^169\.254\.', # 169.254.0.0/16 (link-local)
|
| 41 |
+
]
|
| 42 |
+
|
| 43 |
+
for pattern in private_patterns:
|
| 44 |
+
if re.match(pattern, hostname):
|
| 45 |
+
return True
|
| 46 |
+
|
| 47 |
+
return False
|
| 48 |
+
except Exception:
|
| 49 |
+
return True # 解析失败时保守处理,拒绝访问
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def _format_article_text(metadata: dict) -> str:
|
| 53 |
+
"""
|
| 54 |
+
将元数据和正文格式化为类似网页显示的纯文本
|
| 55 |
+
|
| 56 |
+
Args:
|
| 57 |
+
metadata: trafilatura 提取的 JSON 数据(已解析为字典)
|
| 58 |
+
|
| 59 |
+
Returns:
|
| 60 |
+
格式化后的文章文本
|
| 61 |
+
"""
|
| 62 |
+
lines = []
|
| 63 |
+
|
| 64 |
+
# 标题
|
| 65 |
+
if metadata.get('title'):
|
| 66 |
+
lines.append(metadata['title'])
|
| 67 |
+
lines.append('')
|
| 68 |
+
|
| 69 |
+
# 元数据信息(无标签,直接显示内容)
|
| 70 |
+
meta_parts = []
|
| 71 |
+
if metadata.get('author'):
|
| 72 |
+
meta_parts.append(metadata['author'])
|
| 73 |
+
if metadata.get('date'):
|
| 74 |
+
meta_parts.append(metadata['date'])
|
| 75 |
+
# if metadata.get('hostname'):
|
| 76 |
+
# meta_parts.append(metadata['hostname'])
|
| 77 |
+
if metadata.get('source-hostname'):
|
| 78 |
+
meta_parts.append(metadata['source-hostname'])
|
| 79 |
+
# if metadata.get('filedate'):
|
| 80 |
+
# meta_parts.append(metadata['filedate'])
|
| 81 |
+
|
| 82 |
+
if meta_parts:
|
| 83 |
+
lines.append(' | '.join(meta_parts))
|
| 84 |
+
lines.append('')
|
| 85 |
+
|
| 86 |
+
# 正文
|
| 87 |
+
if metadata.get('text'):
|
| 88 |
+
lines.append(metadata['text'])
|
| 89 |
+
|
| 90 |
+
return '\n'.join(lines)
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def fetch_url(fetch_request):
|
| 94 |
+
"""
|
| 95 |
+
从 URL 提取文本内容
|
| 96 |
+
|
| 97 |
+
Args:
|
| 98 |
+
fetch_request: 包含 url 字段的字典
|
| 99 |
+
|
| 100 |
+
Returns:
|
| 101 |
+
(响应字典, 状态码) 元组
|
| 102 |
+
"""
|
| 103 |
+
url = fetch_request.get('url', '').strip()
|
| 104 |
+
|
| 105 |
+
# 验证 URL
|
| 106 |
+
if not url:
|
| 107 |
+
return {
|
| 108 |
+
'success': False,
|
| 109 |
+
'message': '缺少 URL 参数,请提供 url 字段'
|
| 110 |
+
}, 400
|
| 111 |
+
|
| 112 |
+
if not _is_valid_url(url):
|
| 113 |
+
return {
|
| 114 |
+
'success': False,
|
| 115 |
+
'message': f'无效的 URL 格式: {url}'
|
| 116 |
+
}, 400
|
| 117 |
+
|
| 118 |
+
# 安全检查:防止 SSRF 攻击
|
| 119 |
+
if _is_local_or_private(url):
|
| 120 |
+
return {
|
| 121 |
+
'success': False,
|
| 122 |
+
'message': '不允许访问本地或私有网络地址'
|
| 123 |
+
}, 400
|
| 124 |
+
|
| 125 |
+
# 提取文本和元数据
|
| 126 |
+
try:
|
| 127 |
+
from backend.platform.access_log import log_fetch_url
|
| 128 |
+
log_fetch_url(url)
|
| 129 |
+
|
| 130 |
+
# 使用 requests 下载网页,设置浏览器 User-Agent 和请求头
|
| 131 |
+
headers = {
|
| 132 |
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
| 133 |
+
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
| 134 |
+
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
| 135 |
+
'Accept-Encoding': 'gzip, deflate, br',
|
| 136 |
+
'Connection': 'keep-alive',
|
| 137 |
+
'Upgrade-Insecure-Requests': '1',
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
# 下载网页内容(设置超时和请求头)
|
| 141 |
+
response = requests.get(url, headers=headers, timeout=10, allow_redirects=True)
|
| 142 |
+
response.raise_for_status()
|
| 143 |
+
|
| 144 |
+
# 检查响应内容类型
|
| 145 |
+
content_type = response.headers.get('Content-Type', '').lower()
|
| 146 |
+
if 'text/html' not in content_type and 'text/xml' not in content_type:
|
| 147 |
+
return {
|
| 148 |
+
'success': False,
|
| 149 |
+
'message': f'不支持的内容类型: {content_type},仅支持 HTML/XML 页面'
|
| 150 |
+
}, 400
|
| 151 |
+
|
| 152 |
+
# 使用 trafilatura 提取结构化数据(包含元数据和正文)
|
| 153 |
+
result_json = trafilatura.extract(
|
| 154 |
+
response.text,
|
| 155 |
+
url=url,
|
| 156 |
+
with_metadata=True,
|
| 157 |
+
output_format='json'
|
| 158 |
+
)
|
| 159 |
+
|
| 160 |
+
if not result_json:
|
| 161 |
+
print("⚠️ 无法提取页面内容")
|
| 162 |
+
return {
|
| 163 |
+
'success': False,
|
| 164 |
+
'message': '无法从网页中提取文本内容,可能不是文章页面或页面需要验证'
|
| 165 |
+
}, 400
|
| 166 |
+
|
| 167 |
+
# 解析 JSON 数据
|
| 168 |
+
metadata = json.loads(result_json)
|
| 169 |
+
|
| 170 |
+
# 检查是否有正文内容
|
| 171 |
+
if not metadata.get('text') or not metadata['text'].strip():
|
| 172 |
+
print("⚠️ 提取到元数据但无正文内容")
|
| 173 |
+
print("元数据:", json.dumps(metadata, ensure_ascii=False, indent=2))
|
| 174 |
+
return {
|
| 175 |
+
'success': False,
|
| 176 |
+
'message': '无法从网页中提取正文内容'
|
| 177 |
+
}, 400
|
| 178 |
+
|
| 179 |
+
# 格式化文本(元数据 + 正文)
|
| 180 |
+
formatted_text = _format_article_text(metadata)
|
| 181 |
+
original_char_count = len(formatted_text)
|
| 182 |
+
|
| 183 |
+
# 构建返回消息(如果截断了,添加提示)
|
| 184 |
+
message = None
|
| 185 |
+
# 检查并截断超长文本
|
| 186 |
+
if original_char_count > MAX_EXTRACTED_TEXT_LENGTH:
|
| 187 |
+
formatted_text = formatted_text[:MAX_EXTRACTED_TEXT_LENGTH]
|
| 188 |
+
message = f'内容较长,已截断为前 {MAX_EXTRACTED_TEXT_LENGTH} 字符(原始长度: {original_char_count} 字符)'
|
| 189 |
+
|
| 190 |
+
char_count = len(formatted_text)
|
| 191 |
+
|
| 192 |
+
# 打印提取结果
|
| 193 |
+
# print(formatted_text.split('\n')[:4])
|
| 194 |
+
# print(f"✓ 提取成功: {char_count} 字符" + (f" (截断前: {original_char_count} 字符)" if original_char_count > char_count else ""))
|
| 195 |
+
# 打印除正文外的metadata内容
|
| 196 |
+
metadata_less = metadata.copy()
|
| 197 |
+
metadata_less['raw_text'] = ''
|
| 198 |
+
metadata_less['text'] = ''
|
| 199 |
+
# print(json.dumps(metadata_less, ensure_ascii=False, indent=2))
|
| 200 |
+
|
| 201 |
+
return {
|
| 202 |
+
'success': True,
|
| 203 |
+
'text': formatted_text,
|
| 204 |
+
'url': url,
|
| 205 |
+
'char_count': char_count,
|
| 206 |
+
'message': message
|
| 207 |
+
}, 200
|
| 208 |
+
|
| 209 |
+
except requests.exceptions.Timeout:
|
| 210 |
+
return {
|
| 211 |
+
'success': False,
|
| 212 |
+
'message': '请求超时,请检查网络连接或稍后重试'
|
| 213 |
+
}, 400
|
| 214 |
+
except requests.exceptions.RequestException as e:
|
| 215 |
+
return {
|
| 216 |
+
'success': False,
|
| 217 |
+
'message': f'无法访问 URL: {str(e)}'
|
| 218 |
+
}, 400
|
| 219 |
+
except Exception as e: # noqa: BLE001
|
| 220 |
+
error_response = handle_api_error('URL 文本提取失败', e)
|
| 221 |
+
return error_response, 500
|
backend/api/folder.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""文件夹管理 API"""
|
| 2 |
+
from backend.demo.demo_folder import (
|
| 3 |
+
get_all_folders,
|
| 4 |
+
move_folder,
|
| 5 |
+
rename_folder,
|
| 6 |
+
delete_folder,
|
| 7 |
+
create_folder,
|
| 8 |
+
)
|
| 9 |
+
from backend.api.utils import (
|
| 10 |
+
get_demo_directory,
|
| 11 |
+
handle_api_error,
|
| 12 |
+
handle_api_success,
|
| 13 |
+
require_admin,
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def _move_folder_internal(demo_dir, path, target_path):
|
| 18 |
+
"""内部函数:移动文件夹"""
|
| 19 |
+
return move_folder(demo_dir, path, target_path)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
@require_admin
|
| 23 |
+
def rename_folder_api(rename_request):
|
| 24 |
+
"""
|
| 25 |
+
重命名文件夹
|
| 26 |
+
请求格式: { path: string, new_name: string }
|
| 27 |
+
"""
|
| 28 |
+
path = rename_request.get('path')
|
| 29 |
+
new_name = rename_request.get('new_name')
|
| 30 |
+
|
| 31 |
+
if not path or not new_name:
|
| 32 |
+
return {
|
| 33 |
+
'success': False,
|
| 34 |
+
'message': 'Missing required parameter: path or new_name'
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
try:
|
| 38 |
+
demo_dir = get_demo_directory(create=False)
|
| 39 |
+
result = rename_folder(demo_dir, path, new_name)
|
| 40 |
+
return handle_api_success(result)
|
| 41 |
+
except Exception as e:
|
| 42 |
+
return handle_api_error('Rename failed', e)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
@require_admin
|
| 46 |
+
def delete_folder_api(delete_request):
|
| 47 |
+
"""
|
| 48 |
+
删除文件夹(移动到.deleted目录)
|
| 49 |
+
请求格式: { path: string }
|
| 50 |
+
"""
|
| 51 |
+
path = delete_request.get('path')
|
| 52 |
+
|
| 53 |
+
if not path:
|
| 54 |
+
return {
|
| 55 |
+
'success': False,
|
| 56 |
+
'message': 'Missing required parameter: path'
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
try:
|
| 60 |
+
demo_dir = get_demo_directory(create=False)
|
| 61 |
+
result = delete_folder(demo_dir, path)
|
| 62 |
+
return handle_api_success(result)
|
| 63 |
+
except Exception as e:
|
| 64 |
+
return handle_api_error('Delete failed', e)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def list_all_folders():
|
| 68 |
+
"""
|
| 69 |
+
获取所有文件夹列表(用于移动操作的选择器)
|
| 70 |
+
返回格式: { folders: string[] }
|
| 71 |
+
"""
|
| 72 |
+
try:
|
| 73 |
+
demo_dir = get_demo_directory(create=False)
|
| 74 |
+
folders = get_all_folders(demo_dir)
|
| 75 |
+
return {'folders': folders}
|
| 76 |
+
except Exception as e:
|
| 77 |
+
handle_api_error("Failed to get folder list", e)
|
| 78 |
+
return {'folders': []}
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
@require_admin
|
| 82 |
+
def create_folder_api(create_request):
|
| 83 |
+
"""
|
| 84 |
+
创建新文件夹
|
| 85 |
+
请求格式: { parent_path: string, folder_name: string }
|
| 86 |
+
"""
|
| 87 |
+
parent_path = create_request.get('parent_path', '/')
|
| 88 |
+
folder_name = create_request.get('folder_name')
|
| 89 |
+
|
| 90 |
+
if not folder_name:
|
| 91 |
+
return {
|
| 92 |
+
'success': False,
|
| 93 |
+
'message': 'Missing required parameter: folder_name'
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
try:
|
| 97 |
+
demo_dir = get_demo_directory(create=False)
|
| 98 |
+
result = create_folder(demo_dir, parent_path, folder_name)
|
| 99 |
+
return handle_api_success(result)
|
| 100 |
+
except Exception as e:
|
| 101 |
+
return handle_api_error('Create failed', e)
|
| 102 |
+
|
backend/api/logit_lens.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Logit Lens API"""
|
| 2 |
+
import gc
|
| 3 |
+
import time
|
| 4 |
+
|
| 5 |
+
from backend.models.model_manager import inference_lock
|
| 6 |
+
from backend.platform.oom import exit_if_oom
|
| 7 |
+
from backend.core.logit_lens import analyze_logit_lens
|
| 8 |
+
from backend.api.analyze import LOCK_WAIT_TIMEOUT
|
| 9 |
+
from backend.platform.access_log import get_client_ip, log_prediction_attribute_request
|
| 10 |
+
from backend.platform.source_page import ALLOWED_SOURCE_PAGES, normalize_source_page
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def logit_lens(logit_lens_request):
|
| 14 |
+
context = logit_lens_request.get("context")
|
| 15 |
+
target_prediction = logit_lens_request.get("target_prediction")
|
| 16 |
+
target_token_id = logit_lens_request.get("target_token_id")
|
| 17 |
+
model = logit_lens_request.get("model")
|
| 18 |
+
source_page = logit_lens_request.get("source_page")
|
| 19 |
+
flow_id = logit_lens_request.get("flow_id")
|
| 20 |
+
flow_step = logit_lens_request.get("flow_step")
|
| 21 |
+
|
| 22 |
+
if context is None or context == "":
|
| 23 |
+
return {"success": False, "message": "Missing required field: context"}, 400
|
| 24 |
+
if not isinstance(context, str):
|
| 25 |
+
return {"success": False, "message": "context must be a string"}, 400
|
| 26 |
+
if target_prediction is not None and not isinstance(target_prediction, str):
|
| 27 |
+
return {"success": False, "message": "target_prediction must be a string"}, 400
|
| 28 |
+
if target_prediction == "":
|
| 29 |
+
return {"success": False, "message": "target_prediction must not be empty"}, 400
|
| 30 |
+
if target_token_id is not None and not isinstance(target_token_id, int):
|
| 31 |
+
return {"success": False, "message": "target_token_id must be an integer"}, 400
|
| 32 |
+
if target_token_id is not None and target_token_id < 0:
|
| 33 |
+
return {"success": False, "message": "target_token_id must be >= 0"}, 400
|
| 34 |
+
if target_prediction is not None and target_token_id is not None:
|
| 35 |
+
return {"success": False, "message": "target_prediction and target_token_id are mutually exclusive"}, 400
|
| 36 |
+
|
| 37 |
+
if model is None:
|
| 38 |
+
return {"success": False, "message": "Missing required field: model"}, 400
|
| 39 |
+
if model not in ("base", "instruct"):
|
| 40 |
+
return {"success": False, "message": 'model must be "base" or "instruct"'}, 400
|
| 41 |
+
|
| 42 |
+
if source_page is None or source_page == "":
|
| 43 |
+
return {"success": False, "message": "Missing required field: source_page"}, 400
|
| 44 |
+
normalized_source_page = normalize_source_page(source_page)
|
| 45 |
+
if normalized_source_page is None:
|
| 46 |
+
allowed = ", ".join(sorted(ALLOWED_SOURCE_PAGES))
|
| 47 |
+
return {"success": False, "message": f"source_page must be one of: {allowed}"}, 400
|
| 48 |
+
source_page = normalized_source_page
|
| 49 |
+
|
| 50 |
+
if flow_id is not None and not isinstance(flow_id, str):
|
| 51 |
+
return {"success": False, "message": "flow_id must be a string"}, 400
|
| 52 |
+
if flow_id == "":
|
| 53 |
+
return {"success": False, "message": "flow_id must not be empty"}, 400
|
| 54 |
+
if flow_step is not None and not isinstance(flow_step, int):
|
| 55 |
+
return {"success": False, "message": "flow_step must be an integer"}, 400
|
| 56 |
+
if flow_step is not None and flow_step < 0:
|
| 57 |
+
return {"success": False, "message": "flow_step must be >= 0"}, 400
|
| 58 |
+
|
| 59 |
+
is_causal_flow = source_page == "causal_flow"
|
| 60 |
+
if is_causal_flow:
|
| 61 |
+
if flow_id is None:
|
| 62 |
+
return {"success": False, "message": "Missing required field: flow_id for causal flow"}, 400
|
| 63 |
+
if flow_step is None:
|
| 64 |
+
return {"success": False, "message": "Missing required field: flow_step for causal flow"}, 400
|
| 65 |
+
elif flow_id is not None or flow_step is not None:
|
| 66 |
+
return {"success": False, "message": "flow_id/flow_step are only allowed when source_page is causal_flow"}, 400
|
| 67 |
+
|
| 68 |
+
client_ip = get_client_ip()
|
| 69 |
+
start_time = time.perf_counter()
|
| 70 |
+
request_id = log_prediction_attribute_request(
|
| 71 |
+
context=context,
|
| 72 |
+
target_prediction=target_prediction,
|
| 73 |
+
target_token_id=target_token_id,
|
| 74 |
+
model=model,
|
| 75 |
+
source_page=source_page,
|
| 76 |
+
flow_id=flow_id,
|
| 77 |
+
flow_step=flow_step,
|
| 78 |
+
client_ip=client_ip,
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
lock_acquired = inference_lock.acquire(timeout=LOCK_WAIT_TIMEOUT)
|
| 82 |
+
if not lock_acquired:
|
| 83 |
+
return {"success": False, "message": f"Queue wait exceeded {LOCK_WAIT_TIMEOUT} seconds; server is busy, please try again later."}, 503
|
| 84 |
+
|
| 85 |
+
try:
|
| 86 |
+
result = analyze_logit_lens(context, target_prediction, model=model, target_token_id=target_token_id)
|
| 87 |
+
except ValueError as e:
|
| 88 |
+
return {"success": False, "message": str(e)}, 400
|
| 89 |
+
except Exception as e:
|
| 90 |
+
import traceback
|
| 91 |
+
traceback.print_exc()
|
| 92 |
+
exit_if_oom(e, defer_seconds=1)
|
| 93 |
+
return {"success": False, "message": str(e)}, 500
|
| 94 |
+
finally:
|
| 95 |
+
inference_lock.release()
|
| 96 |
+
gc.collect()
|
| 97 |
+
|
| 98 |
+
elapsed = time.perf_counter() - start_time
|
| 99 |
+
print(
|
| 100 |
+
f"\t📤 API logit_lens response: req_id={request_id}, "
|
| 101 |
+
f"target={result.get('target_token')!r}, n_layers={result.get('n_layers')}, "
|
| 102 |
+
f"response_time={elapsed:.4f}s"
|
| 103 |
+
)
|
| 104 |
+
return {"success": True, **result}, 200
|
backend/api/model_switch.py
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""模型切换 API"""
|
| 2 |
+
import gc
|
| 3 |
+
import os
|
| 4 |
+
from typing import Optional
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
from backend.models import REGISTERED_MODELS
|
| 8 |
+
from backend.models.model_manager import project_registry
|
| 9 |
+
from backend.platform.app_context import get_app_context
|
| 10 |
+
from backend.api.utils import require_admin
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def get_available_models():
|
| 14 |
+
"""获取所有可用的模型列表"""
|
| 15 |
+
return {
|
| 16 |
+
'success': True,
|
| 17 |
+
'models': list(REGISTERED_MODELS.keys())
|
| 18 |
+
}, 200
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def _get_device_type() -> str:
|
| 22 |
+
"""获取当前设备类型"""
|
| 23 |
+
if torch.cuda.is_available():
|
| 24 |
+
return "cuda"
|
| 25 |
+
elif hasattr(torch.backends, 'mps') and torch.backends.mps.is_available():
|
| 26 |
+
return "mps"
|
| 27 |
+
else:
|
| 28 |
+
return "cpu"
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def _restore_env_vars(old_force_int8: Optional[str], old_force_bfloat16: Optional[str]) -> None:
|
| 32 |
+
"""恢复环境变量配置"""
|
| 33 |
+
if old_force_int8 is not None:
|
| 34 |
+
os.environ['FORCE_INT8'] = old_force_int8
|
| 35 |
+
else:
|
| 36 |
+
os.environ.pop('FORCE_INT8', None)
|
| 37 |
+
|
| 38 |
+
if old_force_bfloat16 is not None:
|
| 39 |
+
os.environ['CPU_FORCE_BFLOAT16'] = old_force_bfloat16
|
| 40 |
+
else:
|
| 41 |
+
os.environ.pop('CPU_FORCE_BFLOAT16', None)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def get_current_model():
|
| 45 |
+
"""获取当前使用的模型及量化配置"""
|
| 46 |
+
# 使用模块级上下文以获取持久化的模型状态
|
| 47 |
+
context = get_app_context(prefer_module_context=True)
|
| 48 |
+
device_type = _get_device_type()
|
| 49 |
+
|
| 50 |
+
return {
|
| 51 |
+
'success': True,
|
| 52 |
+
'model': context.base_model_id,
|
| 53 |
+
'loading': context.model_loading,
|
| 54 |
+
'device_type': device_type,
|
| 55 |
+
'use_int8': os.environ.get('FORCE_INT8') == '1',
|
| 56 |
+
'use_bfloat16': os.environ.get('CPU_FORCE_BFLOAT16') == '1'
|
| 57 |
+
}, 200
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
@require_admin
|
| 61 |
+
def switch_model(switch_request):
|
| 62 |
+
"""
|
| 63 |
+
切换模型(需要管理员权限)
|
| 64 |
+
|
| 65 |
+
Args:
|
| 66 |
+
switch_request: 切换请求字典,包含:
|
| 67 |
+
- model: 目标模型名称
|
| 68 |
+
- use_int8: 是否使用 INT8 量化(可选)
|
| 69 |
+
- use_bfloat16: 是否使用 bfloat16(可选,仅CPU)
|
| 70 |
+
|
| 71 |
+
Returns:
|
| 72 |
+
(响应字典, 状态码) 元组
|
| 73 |
+
"""
|
| 74 |
+
if False: # 原在线切换逻辑保留,不执行;恢复时请删除此守卫并测试
|
| 75 |
+
target_model = switch_request.get('model')
|
| 76 |
+
use_int8 = switch_request.get('use_int8', False)
|
| 77 |
+
use_bfloat16 = switch_request.get('use_bfloat16', False)
|
| 78 |
+
|
| 79 |
+
# 验证请求
|
| 80 |
+
if not target_model:
|
| 81 |
+
return {
|
| 82 |
+
'success': False,
|
| 83 |
+
'message': 'Missing model parameter'
|
| 84 |
+
}, 400
|
| 85 |
+
|
| 86 |
+
# 检查模型是否可用
|
| 87 |
+
if target_model not in REGISTERED_MODELS:
|
| 88 |
+
available_models = list(REGISTERED_MODELS.keys())
|
| 89 |
+
return {
|
| 90 |
+
'success': False,
|
| 91 |
+
'message': f'Model {target_model} does not exist. Available models: {", ".join(available_models)}'
|
| 92 |
+
}, 404
|
| 93 |
+
|
| 94 |
+
# 获取设备类型
|
| 95 |
+
device_type = _get_device_type()
|
| 96 |
+
|
| 97 |
+
# 验证量化参数与设备兼容性
|
| 98 |
+
if use_int8 and device_type == "mps":
|
| 99 |
+
return {
|
| 100 |
+
'success': False,
|
| 101 |
+
'message': 'INT8 quantization is not supported on MPS device'
|
| 102 |
+
}, 400
|
| 103 |
+
|
| 104 |
+
if use_bfloat16 and device_type != "cpu":
|
| 105 |
+
return {
|
| 106 |
+
'success': False,
|
| 107 |
+
'message': 'bfloat16 quantization is only supported on CPU device'
|
| 108 |
+
}, 400
|
| 109 |
+
|
| 110 |
+
if use_int8 and use_bfloat16:
|
| 111 |
+
return {
|
| 112 |
+
'success': False,
|
| 113 |
+
'message': 'Cannot enable both INT8 and bfloat16 quantization'
|
| 114 |
+
}, 400
|
| 115 |
+
|
| 116 |
+
# 使用模块级上下文以确保状态修改持久化(不会被后续请求重置)
|
| 117 |
+
context = get_app_context(prefer_module_context=True)
|
| 118 |
+
current_model = context.base_model_id
|
| 119 |
+
|
| 120 |
+
# 保存当前环境变量配置(用于回滚)
|
| 121 |
+
old_force_int8 = os.environ.get('FORCE_INT8')
|
| 122 |
+
old_force_bfloat16 = os.environ.get('CPU_FORCE_BFLOAT16')
|
| 123 |
+
|
| 124 |
+
# 检查是否已经是目标模型且量化配置相同
|
| 125 |
+
current_int8 = os.environ.get('FORCE_INT8') == '1'
|
| 126 |
+
current_bfloat16 = os.environ.get('CPU_FORCE_BFLOAT16') == '1'
|
| 127 |
+
|
| 128 |
+
if (current_model == target_model and
|
| 129 |
+
current_int8 == use_int8 and
|
| 130 |
+
current_bfloat16 == use_bfloat16):
|
| 131 |
+
return {
|
| 132 |
+
'success': True,
|
| 133 |
+
'message': f'Already using model {target_model} (same quantization configuration)',
|
| 134 |
+
'model': target_model
|
| 135 |
+
}, 200
|
| 136 |
+
|
| 137 |
+
# 检查模型是否正在加载中(初始加载或切换)
|
| 138 |
+
if context.model_loading:
|
| 139 |
+
return {
|
| 140 |
+
'success': False,
|
| 141 |
+
'message': 'Model is currently loading, please try again later'
|
| 142 |
+
}, 503
|
| 143 |
+
|
| 144 |
+
try:
|
| 145 |
+
# 标记开始加载
|
| 146 |
+
context.set_model_loading(True)
|
| 147 |
+
print(f"🔄 开始切换模型: {current_model} -> {target_model}")
|
| 148 |
+
|
| 149 |
+
# 设置新的量化环境变量
|
| 150 |
+
if use_int8:
|
| 151 |
+
os.environ['FORCE_INT8'] = '1'
|
| 152 |
+
print(" 设置量化: INT8")
|
| 153 |
+
else:
|
| 154 |
+
os.environ.pop('FORCE_INT8', None)
|
| 155 |
+
|
| 156 |
+
if use_bfloat16:
|
| 157 |
+
os.environ['CPU_FORCE_BFLOAT16'] = '1'
|
| 158 |
+
print(" 设置量化: bfloat16")
|
| 159 |
+
else:
|
| 160 |
+
os.environ.pop('CPU_FORCE_BFLOAT16', None)
|
| 161 |
+
|
| 162 |
+
# 卸载旧模型
|
| 163 |
+
if current_model and current_model in project_registry:
|
| 164 |
+
print(f" 卸载旧模型: {current_model}")
|
| 165 |
+
project_registry.unload(current_model)
|
| 166 |
+
gc.collect()
|
| 167 |
+
if device_type == "cuda":
|
| 168 |
+
torch.cuda.empty_cache()
|
| 169 |
+
elif device_type == "mps":
|
| 170 |
+
torch.mps.empty_cache()
|
| 171 |
+
|
| 172 |
+
# 加载新模型
|
| 173 |
+
print(f" 加载新模型: {target_model}")
|
| 174 |
+
project_registry.ensure_loaded(target_model)
|
| 175 |
+
|
| 176 |
+
# 更新当前模型
|
| 177 |
+
context.set_current_model(target_model)
|
| 178 |
+
|
| 179 |
+
print(f"✅ 模型切换成功: {target_model}")
|
| 180 |
+
|
| 181 |
+
return {
|
| 182 |
+
'success': True,
|
| 183 |
+
'message': f'Model switched to {target_model}',
|
| 184 |
+
'model': target_model
|
| 185 |
+
}, 200
|
| 186 |
+
|
| 187 |
+
except KeyError:
|
| 188 |
+
# 模型不存在(虽然前面已经检查过,但以防万一)
|
| 189 |
+
print(f"❌ 模型切换失败: 模型 {target_model} 未注册")
|
| 190 |
+
# 回滚:恢复旧模型名称和环境变量
|
| 191 |
+
context.set_current_model(current_model)
|
| 192 |
+
_restore_env_vars(old_force_int8, old_force_bfloat16)
|
| 193 |
+
return {
|
| 194 |
+
'success': False,
|
| 195 |
+
'message': f'Model {target_model} is not registered'
|
| 196 |
+
}, 404
|
| 197 |
+
|
| 198 |
+
except Exception as e:
|
| 199 |
+
# 加载失败,尝试回滚
|
| 200 |
+
print(f"❌ 模型切换失败: {e}")
|
| 201 |
+
print(f" 尝试回滚到旧模型: {current_model}")
|
| 202 |
+
|
| 203 |
+
try:
|
| 204 |
+
# 回滚:恢复环境变量和重新加载旧模型
|
| 205 |
+
_restore_env_vars(old_force_int8, old_force_bfloat16)
|
| 206 |
+
if current_model:
|
| 207 |
+
project_registry.ensure_loaded(current_model)
|
| 208 |
+
context.set_current_model(current_model)
|
| 209 |
+
print(f"✅ 已回滚到旧模型: {current_model}")
|
| 210 |
+
except Exception as rollback_error:
|
| 211 |
+
print(f"⚠️ 回滚失败: {rollback_error}")
|
| 212 |
+
|
| 213 |
+
return {
|
| 214 |
+
'success': False,
|
| 215 |
+
'message': f'Model switch failed: {str(e)}'
|
| 216 |
+
}, 500
|
| 217 |
+
|
| 218 |
+
finally:
|
| 219 |
+
# 无论成功还是失败,都要清除加载标志
|
| 220 |
+
context.set_model_loading(False)
|
| 221 |
+
gc.collect()
|
| 222 |
+
|
| 223 |
+
return (
|
| 224 |
+
{
|
| 225 |
+
'success': False,
|
| 226 |
+
'message': '在线模型切换已禁用,请通过命令行 --base_model / --instruct_model 指定后重启服务',
|
| 227 |
+
},
|
| 228 |
+
501,
|
| 229 |
+
)
|
backend/api/openai_completions.py
ADDED
|
@@ -0,0 +1,571 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""OpenAI 兼容 /v1/completions:语义分析同款模型续写,其余响应字段固定。"""
|
| 2 |
+
|
| 3 |
+
import gc
|
| 4 |
+
import queue
|
| 5 |
+
import threading
|
| 6 |
+
import time
|
| 7 |
+
import traceback
|
| 8 |
+
from typing import Any, Callable, Dict, List, Optional, Tuple
|
| 9 |
+
|
| 10 |
+
from backend.models.model_manager import (
|
| 11 |
+
ModelSlot,
|
| 12 |
+
inference_lock,
|
| 13 |
+
get_base_model_display_name,
|
| 14 |
+
get_instruct_model_display_name,
|
| 15 |
+
)
|
| 16 |
+
from backend.core.prediction_attributor import slot_for_prediction_attr_model
|
| 17 |
+
from backend.platform.oom import exit_if_oom, is_oom_error
|
| 18 |
+
from backend.api.utils import request_has_valid_admin
|
| 19 |
+
from backend.core.completion_generator import (
|
| 20 |
+
ModelContextLimitUnknownError,
|
| 21 |
+
PromptTooLongError,
|
| 22 |
+
apply_chat_template_for_completion,
|
| 23 |
+
compute_tool_append_suffix,
|
| 24 |
+
completion_cancel_requested,
|
| 25 |
+
completion_max_token_length,
|
| 26 |
+
generate_completion_text,
|
| 27 |
+
global_completion_stop_event,
|
| 28 |
+
inference_shutdown_event,
|
| 29 |
+
)
|
| 30 |
+
from backend.api.analyze import LOCK_WAIT_TIMEOUT, QueueTimeoutError
|
| 31 |
+
from backend.api.sse_utils import (
|
| 32 |
+
SSEProgressReporter,
|
| 33 |
+
send_completion_delta_event,
|
| 34 |
+
send_error_event,
|
| 35 |
+
send_result_event,
|
| 36 |
+
)
|
| 37 |
+
from backend.platform.access_log import get_client_ip
|
| 38 |
+
|
| 39 |
+
# 单次续写 SSE:从进入流式生成器起算的墙钟上限(含排队等推理锁 + 生成)。
|
| 40 |
+
COMPLETION_WALL_CLOCK_TIMEOUT_SEC = 300.0
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def _log_cmpl_issue(request_id: int, msg: str) -> None:
|
| 44 |
+
"""续写非正常结束时一行说明(与成功时的 ``_log_completion_finished`` 二选一)。"""
|
| 45 |
+
print(f"\t⚠️ openai_completions req_id={request_id}: {msg}")
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def _log_request(model: str, prompt: str, client_ip=None):
|
| 49 |
+
from backend.platform.access_log import log_openai_completions_request
|
| 50 |
+
return log_openai_completions_request(model, prompt, client_ip)
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def _model_display_name_for_slot(slot: ModelSlot) -> str:
|
| 54 |
+
if slot == ModelSlot.BASE:
|
| 55 |
+
return get_base_model_display_name()
|
| 56 |
+
return get_instruct_model_display_name()
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def _build_response(
|
| 60 |
+
completion_text: str,
|
| 61 |
+
finish_reason: str,
|
| 62 |
+
prompt_tokens: int,
|
| 63 |
+
completion_tokens: int,
|
| 64 |
+
bpe_strings: List[Dict[str, Any]],
|
| 65 |
+
*,
|
| 66 |
+
model_display: str,
|
| 67 |
+
):
|
| 68 |
+
"""OpenAICompletionsResponse:choices + usage;info_radar 为续写 token 级数据。"""
|
| 69 |
+
total = prompt_tokens + completion_tokens
|
| 70 |
+
return {
|
| 71 |
+
"id": "cmpl-stub-info-radar",
|
| 72 |
+
"object": "text_completion",
|
| 73 |
+
"created": int(time.time()),
|
| 74 |
+
"model": model_display,
|
| 75 |
+
"choices": [
|
| 76 |
+
{
|
| 77 |
+
"text": completion_text,
|
| 78 |
+
"index": 0,
|
| 79 |
+
"finish_reason": finish_reason,
|
| 80 |
+
}
|
| 81 |
+
],
|
| 82 |
+
"usage": {
|
| 83 |
+
"prompt_tokens": prompt_tokens,
|
| 84 |
+
"completion_tokens": completion_tokens,
|
| 85 |
+
"total_tokens": total,
|
| 86 |
+
},
|
| 87 |
+
"info_radar": {
|
| 88 |
+
"bpe_strings": bpe_strings,
|
| 89 |
+
},
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
# 与 generate_completion_text 返回一致(末项 TTFT 秒;未生成时为 None)
|
| 94 |
+
CompletionRunResult = Tuple[str, str, int, int, List[Dict[str, Any]], Optional[float]]
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def _completion_inference_after_lock(
|
| 98 |
+
prompt: str,
|
| 99 |
+
request_id: int,
|
| 100 |
+
lock_wait_time: float,
|
| 101 |
+
*,
|
| 102 |
+
slot: ModelSlot,
|
| 103 |
+
stream_delta: Optional[Callable[[str, bool], None]] = None,
|
| 104 |
+
max_tokens: Optional[int] = None,
|
| 105 |
+
bypass_site_context_limit: bool = False,
|
| 106 |
+
) -> CompletionRunResult:
|
| 107 |
+
"""
|
| 108 |
+
在已持有推理锁的上下文中执行续写(旧版非流式路径的持锁体内逻辑)。
|
| 109 |
+
流式可传 stream_delta;中止由 ``completion_cancel_requested()`` 统一判断。
|
| 110 |
+
"""
|
| 111 |
+
from backend.platform.access_log import log_openai_completions_start
|
| 112 |
+
|
| 113 |
+
log_openai_completions_start(request_id, lock_wait_time)
|
| 114 |
+
return generate_completion_text(
|
| 115 |
+
prompt,
|
| 116 |
+
stream_delta=stream_delta,
|
| 117 |
+
max_tokens=max_tokens,
|
| 118 |
+
bypass_site_context_limit=bypass_site_context_limit,
|
| 119 |
+
slot=slot,
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def _log_completion_finished(
|
| 124 |
+
request_id: int,
|
| 125 |
+
prompt_tokens: int,
|
| 126 |
+
completion_tokens: int,
|
| 127 |
+
elapsed: float,
|
| 128 |
+
ttft_s: Optional[float],
|
| 129 |
+
) -> None:
|
| 130 |
+
"""旧非流式分支在返回 JSON 前、流式在发出末条 result 前的同一行日志。
|
| 131 |
+
|
| 132 |
+
prompt tokens/s = prompt_tokens / TTFT;generate tokens/s = completion_tokens / (elapsed − TTFT)。
|
| 133 |
+
``elapsed`` 为 SSE 起点至结束;与 TTFT 计时原点不完全一致时,吞吐率为近似值。
|
| 134 |
+
无 TTFT(``ttft_s`` 为 ``None``)时不输出时间与吞吐字段。
|
| 135 |
+
"""
|
| 136 |
+
if ttft_s is None:
|
| 137 |
+
tps_part = ""
|
| 138 |
+
else:
|
| 139 |
+
decode_s = elapsed - ttft_s
|
| 140 |
+
prompt_time_s = f"{ttft_s:.4f}" if ttft_s > 0 else "n/a"
|
| 141 |
+
gen_time_s = f"{decode_s:.4f}" if decode_s > 0 else "n/a"
|
| 142 |
+
prompt_part = f"{prompt_tokens / ttft_s:.2f}" if ttft_s > 0 else "n/a"
|
| 143 |
+
gen_part = (
|
| 144 |
+
f"{completion_tokens / decode_s:.2f}"
|
| 145 |
+
if completion_tokens and decode_s > 0
|
| 146 |
+
else "n/a"
|
| 147 |
+
)
|
| 148 |
+
tps_part = (
|
| 149 |
+
f", time= {prompt_time_s} / {gen_time_s}s, "
|
| 150 |
+
f"tokens/s= {prompt_part} / {gen_part}"
|
| 151 |
+
)
|
| 152 |
+
print(
|
| 153 |
+
f"\t📤 API openai_completions response: req_id={request_id}, "
|
| 154 |
+
f"prompt/generate tokens= {prompt_tokens} / {completion_tokens}, "
|
| 155 |
+
f"{tps_part}"
|
| 156 |
+
)
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
def _generate_completion_events(
|
| 160 |
+
prompt: str,
|
| 161 |
+
request_id: int,
|
| 162 |
+
*,
|
| 163 |
+
slot: ModelSlot,
|
| 164 |
+
model_display: str,
|
| 165 |
+
max_tokens: Optional[int] = None,
|
| 166 |
+
bypass_site_context_limit: bool = False,
|
| 167 |
+
):
|
| 168 |
+
global_completion_stop_event.clear()
|
| 169 |
+
q: queue.Queue = queue.Queue()
|
| 170 |
+
start_time = time.perf_counter()
|
| 171 |
+
|
| 172 |
+
def run():
|
| 173 |
+
try:
|
| 174 |
+
lock_wait_start = time.perf_counter()
|
| 175 |
+
lock_acquired = inference_lock.acquire(timeout=LOCK_WAIT_TIMEOUT)
|
| 176 |
+
if not lock_acquired:
|
| 177 |
+
q.put(("error", QueueTimeoutError(
|
| 178 |
+
f"排队等待超过 {LOCK_WAIT_TIMEOUT} 秒,服务繁忙,请稍后重试"
|
| 179 |
+
)))
|
| 180 |
+
return
|
| 181 |
+
lock_wait_time = time.perf_counter() - lock_wait_start
|
| 182 |
+
try:
|
| 183 |
+
def stream_delta(text: str, stream_end: bool) -> None:
|
| 184 |
+
if completion_cancel_requested():
|
| 185 |
+
return
|
| 186 |
+
q.put(("delta", text, stream_end))
|
| 187 |
+
|
| 188 |
+
result = _completion_inference_after_lock(
|
| 189 |
+
prompt,
|
| 190 |
+
request_id,
|
| 191 |
+
lock_wait_time,
|
| 192 |
+
slot=slot,
|
| 193 |
+
stream_delta=stream_delta,
|
| 194 |
+
max_tokens=max_tokens,
|
| 195 |
+
bypass_site_context_limit=bypass_site_context_limit,
|
| 196 |
+
)
|
| 197 |
+
finally:
|
| 198 |
+
inference_lock.release()
|
| 199 |
+
gc.collect()
|
| 200 |
+
q.put(("result", result))
|
| 201 |
+
except Exception as e:
|
| 202 |
+
q.put(("error", e))
|
| 203 |
+
|
| 204 |
+
worker = threading.Thread(target=run, daemon=True)
|
| 205 |
+
worker.start()
|
| 206 |
+
|
| 207 |
+
wall_clock_timed_out = False
|
| 208 |
+
|
| 209 |
+
# 墙钟超时与用户 Stop 同路:置位 global_completion_stop_event,等 worker 末条 result(abort)。
|
| 210 |
+
# 正常路径由 completion_cancel_requested + StoppingCriteria 结束 generate;排队仅 LOCK_WAIT_TIMEOUT。
|
| 211 |
+
# 仅在与 Stop 相同的推理僵死(如 CUDA 挂死)时 SSE 可能一直等 result,旧 504 即时断开亦无法回收 worker。
|
| 212 |
+
try:
|
| 213 |
+
while True:
|
| 214 |
+
elapsed = time.perf_counter() - start_time
|
| 215 |
+
if (
|
| 216 |
+
not wall_clock_timed_out
|
| 217 |
+
and elapsed >= COMPLETION_WALL_CLOCK_TIMEOUT_SEC
|
| 218 |
+
):
|
| 219 |
+
global_completion_stop_event.set()
|
| 220 |
+
wall_clock_timed_out = True
|
| 221 |
+
_log_cmpl_issue(
|
| 222 |
+
request_id,
|
| 223 |
+
f"墙钟超时 {elapsed:.1f}s / 上限 {COMPLETION_WALL_CLOCK_TIMEOUT_SEC:.0f}s",
|
| 224 |
+
)
|
| 225 |
+
try:
|
| 226 |
+
item = q.get(timeout=0.1)
|
| 227 |
+
except queue.Empty:
|
| 228 |
+
continue
|
| 229 |
+
kind = item[0]
|
| 230 |
+
if kind == "delta":
|
| 231 |
+
_, text, stream_end = item
|
| 232 |
+
if text or stream_end:
|
| 233 |
+
yield send_completion_delta_event(text, stream_end)
|
| 234 |
+
elif kind == "result":
|
| 235 |
+
(
|
| 236 |
+
_completion_text,
|
| 237 |
+
finish_reason,
|
| 238 |
+
prompt_tokens,
|
| 239 |
+
completion_tokens,
|
| 240 |
+
bpe_strings,
|
| 241 |
+
ttft_s,
|
| 242 |
+
) = item[1]
|
| 243 |
+
elapsed = time.perf_counter() - start_time
|
| 244 |
+
if global_completion_stop_event.is_set() or inference_shutdown_event.is_set():
|
| 245 |
+
finish_reason = "abort"
|
| 246 |
+
if inference_shutdown_event.is_set():
|
| 247 |
+
_log_cmpl_issue(
|
| 248 |
+
request_id,
|
| 249 |
+
f"进程终止,续写中止 elapsed={elapsed:.2f}s "
|
| 250 |
+
f"tokens={prompt_tokens}/{completion_tokens}",
|
| 251 |
+
)
|
| 252 |
+
elif global_completion_stop_event.is_set():
|
| 253 |
+
stop_label = "墙钟超时" if wall_clock_timed_out else "用户 Stop"
|
| 254 |
+
_log_cmpl_issue(
|
| 255 |
+
request_id,
|
| 256 |
+
f"{stop_label},续写中止 elapsed={elapsed:.2f}s "
|
| 257 |
+
f"tokens={prompt_tokens}/{completion_tokens}",
|
| 258 |
+
)
|
| 259 |
+
else:
|
| 260 |
+
_log_completion_finished(
|
| 261 |
+
request_id,
|
| 262 |
+
prompt_tokens,
|
| 263 |
+
completion_tokens,
|
| 264 |
+
elapsed,
|
| 265 |
+
ttft_s,
|
| 266 |
+
)
|
| 267 |
+
yield send_result_event(
|
| 268 |
+
_build_response(
|
| 269 |
+
_completion_text,
|
| 270 |
+
finish_reason,
|
| 271 |
+
prompt_tokens,
|
| 272 |
+
completion_tokens,
|
| 273 |
+
bpe_strings,
|
| 274 |
+
model_display=model_display,
|
| 275 |
+
)
|
| 276 |
+
)
|
| 277 |
+
return
|
| 278 |
+
elif kind == "error":
|
| 279 |
+
err = item[1]
|
| 280 |
+
if isinstance(err, (PromptTooLongError, ModelContextLimitUnknownError)):
|
| 281 |
+
_log_cmpl_issue(request_id, str(err))
|
| 282 |
+
yield send_error_event(str(err), 400)
|
| 283 |
+
elif isinstance(err, QueueTimeoutError):
|
| 284 |
+
_log_cmpl_issue(request_id, f"排队超时: {err}")
|
| 285 |
+
yield send_error_event(str(err), 503)
|
| 286 |
+
else:
|
| 287 |
+
exit_if_oom(err, defer_seconds=1)
|
| 288 |
+
if is_oom_error(err):
|
| 289 |
+
yield send_error_event(str(err), 500)
|
| 290 |
+
return
|
| 291 |
+
_log_cmpl_issue(
|
| 292 |
+
request_id,
|
| 293 |
+
"".join(
|
| 294 |
+
traceback.format_exception(
|
| 295 |
+
type(err), err, err.__traceback__
|
| 296 |
+
)
|
| 297 |
+
).strip(),
|
| 298 |
+
)
|
| 299 |
+
yield send_error_event(str(err), 500)
|
| 300 |
+
return
|
| 301 |
+
finally:
|
| 302 |
+
gc.collect()
|
| 303 |
+
|
| 304 |
+
|
| 305 |
+
def _completions_sse_response(
|
| 306 |
+
prompt: str,
|
| 307 |
+
request_id: int,
|
| 308 |
+
*,
|
| 309 |
+
slot: ModelSlot,
|
| 310 |
+
model_display: str,
|
| 311 |
+
max_tokens: Optional[int] = None,
|
| 312 |
+
bypass_site_context_limit: bool = False,
|
| 313 |
+
):
|
| 314 |
+
return SSEProgressReporter(
|
| 315 |
+
lambda: _generate_completion_events(
|
| 316 |
+
prompt,
|
| 317 |
+
request_id,
|
| 318 |
+
slot=slot,
|
| 319 |
+
model_display=model_display,
|
| 320 |
+
max_tokens=max_tokens,
|
| 321 |
+
bypass_site_context_limit=bypass_site_context_limit,
|
| 322 |
+
)
|
| 323 |
+
).create_response()
|
| 324 |
+
|
| 325 |
+
|
| 326 |
+
def completions_stop():
|
| 327 |
+
"""
|
| 328 |
+
单用户串行:置位全局停止标志,使当前续写在 generate 与 SSE 回调中尽快结束。
|
| 329 |
+
无需 body;新一次 POST /v1/completions 时会在流式生成器入口清除该标志。
|
| 330 |
+
"""
|
| 331 |
+
global_completion_stop_event.set()
|
| 332 |
+
return {"ok": True}, 200
|
| 333 |
+
|
| 334 |
+
|
| 335 |
+
def _parse_chat_messages_from_prompt_request(
|
| 336 |
+
body: Dict[str, Any],
|
| 337 |
+
) -> Tuple[Optional[List[Dict[str, Any]]], Optional[Dict[str, Any]]]:
|
| 338 |
+
"""从 completions/prompt 请求体解析 messages。返回 (messages, error_response)。"""
|
| 339 |
+
raw_messages = body.get("messages")
|
| 340 |
+
if raw_messages is None:
|
| 341 |
+
return None, {"success": False, "message": "缺少 messages 字段"}
|
| 342 |
+
if not isinstance(raw_messages, list) or len(raw_messages) == 0:
|
| 343 |
+
return None, {"success": False, "message": "messages 须为非空数组"}
|
| 344 |
+
messages: List[Dict[str, Any]] = []
|
| 345 |
+
for i, item in enumerate(raw_messages):
|
| 346 |
+
if not isinstance(item, dict):
|
| 347 |
+
return None, {"success": False, "message": f"messages[{i}] 须为对象"}
|
| 348 |
+
role = item.get("role")
|
| 349 |
+
if role not in ("system", "user", "assistant", "tool"):
|
| 350 |
+
return None, {
|
| 351 |
+
"success": False,
|
| 352 |
+
"message": f"messages[{i}].role 无效: {role!r}",
|
| 353 |
+
}
|
| 354 |
+
content = item.get("content")
|
| 355 |
+
if not isinstance(content, str):
|
| 356 |
+
return None, {
|
| 357 |
+
"success": False,
|
| 358 |
+
"message": f"messages[{i}].content 须为字符串",
|
| 359 |
+
}
|
| 360 |
+
msg: Dict[str, Any] = {"role": role, "content": content}
|
| 361 |
+
if role == "tool":
|
| 362 |
+
name = item.get("name")
|
| 363 |
+
if not isinstance(name, str) or not name:
|
| 364 |
+
return None, {
|
| 365 |
+
"success": False,
|
| 366 |
+
"message": f"messages[{i}].name 在 role=tool 时必填",
|
| 367 |
+
}
|
| 368 |
+
msg["name"] = name
|
| 369 |
+
messages.append(msg)
|
| 370 |
+
return messages, None
|
| 371 |
+
|
| 372 |
+
|
| 373 |
+
def _parse_tools_from_prompt_request(
|
| 374 |
+
body: Dict[str, Any],
|
| 375 |
+
) -> Tuple[Optional[List[Dict[str, Any]]], Optional[Dict[str, Any]]]:
|
| 376 |
+
"""解析 tools 数组;``enable_tool_calling`` 已废弃,须显式传 tools。"""
|
| 377 |
+
tools_raw = body.get("tools")
|
| 378 |
+
if tools_raw is None:
|
| 379 |
+
enable_tool_calling_raw = body.get("enable_tool_calling")
|
| 380 |
+
if enable_tool_calling_raw is True:
|
| 381 |
+
return None, {
|
| 382 |
+
"success": False,
|
| 383 |
+
"message": "enable_tool_calling 已废弃,请传 tools 数组",
|
| 384 |
+
}
|
| 385 |
+
return None, None
|
| 386 |
+
|
| 387 |
+
if not isinstance(tools_raw, list):
|
| 388 |
+
return None, {"success": False, "message": "tools 须为数组"}
|
| 389 |
+
return tools_raw, None
|
| 390 |
+
|
| 391 |
+
|
| 392 |
+
def completions_prompt(completions_prompt_request):
|
| 393 |
+
"""
|
| 394 |
+
将 messages 套用 chat template,返回实际送入续写接口的完整 prompt 字符串(JSON)。
|
| 395 |
+
|
| 396 |
+
Args:
|
| 397 |
+
completions_prompt_request: 含 model、messages,见 server_openai_definitions.yaml
|
| 398 |
+
|
| 399 |
+
Returns:
|
| 400 |
+
(dict with prompt_used, 200) 或校验/过长错误
|
| 401 |
+
"""
|
| 402 |
+
if not isinstance(completions_prompt_request, dict):
|
| 403 |
+
completions_prompt_request = {}
|
| 404 |
+
model = completions_prompt_request.get("model")
|
| 405 |
+
|
| 406 |
+
if not model:
|
| 407 |
+
return {"success": False, "message": "缺少 model 字段"}, 400
|
| 408 |
+
|
| 409 |
+
messages, msg_err = _parse_chat_messages_from_prompt_request(completions_prompt_request)
|
| 410 |
+
if msg_err is not None:
|
| 411 |
+
return msg_err, 400
|
| 412 |
+
|
| 413 |
+
tools, tools_err = _parse_tools_from_prompt_request(completions_prompt_request)
|
| 414 |
+
if tools_err is not None:
|
| 415 |
+
return tools_err, 400
|
| 416 |
+
|
| 417 |
+
enable_thinking_raw = completions_prompt_request.get("enable_thinking")
|
| 418 |
+
if enable_thinking_raw is None:
|
| 419 |
+
enable_thinking = False
|
| 420 |
+
elif not isinstance(enable_thinking_raw, bool):
|
| 421 |
+
return {"success": False, "message": "enable_thinking 必须为布尔值"}, 400
|
| 422 |
+
else:
|
| 423 |
+
enable_thinking = enable_thinking_raw
|
| 424 |
+
|
| 425 |
+
client_ip = get_client_ip()
|
| 426 |
+
from backend.platform.access_log import log_openai_completions_prompt_request
|
| 427 |
+
|
| 428 |
+
log_openai_completions_prompt_request(
|
| 429 |
+
model,
|
| 430 |
+
messages=messages,
|
| 431 |
+
enable_thinking=enable_thinking,
|
| 432 |
+
tools_count=len(tools) if tools else 0,
|
| 433 |
+
client_ip=client_ip,
|
| 434 |
+
)
|
| 435 |
+
|
| 436 |
+
try:
|
| 437 |
+
slot = slot_for_prediction_attr_model(model)
|
| 438 |
+
except ValueError as e:
|
| 439 |
+
return {"success": False, "message": str(e)}, 400
|
| 440 |
+
|
| 441 |
+
try:
|
| 442 |
+
prompt_used = apply_chat_template_for_completion(
|
| 443 |
+
messages,
|
| 444 |
+
slot=slot,
|
| 445 |
+
enable_thinking=enable_thinking,
|
| 446 |
+
tools=tools,
|
| 447 |
+
)
|
| 448 |
+
except PromptTooLongError as e:
|
| 449 |
+
return {"success": False, "message": str(e)}, 400
|
| 450 |
+
|
| 451 |
+
return {"prompt_used": prompt_used}, 200
|
| 452 |
+
|
| 453 |
+
|
| 454 |
+
def completions_prompt_incremental(completions_prompt_incremental_request):
|
| 455 |
+
"""
|
| 456 |
+
计算多轮 wire 模式下 tool response 的增量后缀(incremental_suffix)。
|
| 457 |
+
|
| 458 |
+
wire 在模型输出 O_n(含 <|im_end|>)后,需追加本函数返回的字符串,
|
| 459 |
+
以构成下一轮生成的完整输入。suffix 仅取决于 tool_content 和 enable_thinking,
|
| 460 |
+
与前序历史内容无关。
|
| 461 |
+
|
| 462 |
+
Args:
|
| 463 |
+
completions_prompt_incremental_request: 含 model、tool_content,见 server_openai_definitions.yaml
|
| 464 |
+
|
| 465 |
+
Returns:
|
| 466 |
+
(dict with incremental_suffix, 200) 或校验错误
|
| 467 |
+
"""
|
| 468 |
+
if not isinstance(completions_prompt_incremental_request, dict):
|
| 469 |
+
completions_prompt_incremental_request = {}
|
| 470 |
+
model = completions_prompt_incremental_request.get("model")
|
| 471 |
+
|
| 472 |
+
if not model:
|
| 473 |
+
return {"success": False, "message": "缺少 model 字段"}, 400
|
| 474 |
+
|
| 475 |
+
tool_content = completions_prompt_incremental_request.get("tool_content")
|
| 476 |
+
if not isinstance(tool_content, str):
|
| 477 |
+
return {"success": False, "message": "tool_content 须为字符串"}, 400
|
| 478 |
+
|
| 479 |
+
tool_name = completions_prompt_incremental_request.get("tool_name")
|
| 480 |
+
if tool_name is not None and not isinstance(tool_name, str):
|
| 481 |
+
return {"success": False, "message": "tool_name 须为字符串"}, 400
|
| 482 |
+
|
| 483 |
+
enable_thinking_raw = completions_prompt_incremental_request.get("enable_thinking")
|
| 484 |
+
if enable_thinking_raw is None:
|
| 485 |
+
enable_thinking = False
|
| 486 |
+
elif not isinstance(enable_thinking_raw, bool):
|
| 487 |
+
return {"success": False, "message": "enable_thinking 必须为布尔值"}, 400
|
| 488 |
+
else:
|
| 489 |
+
enable_thinking = enable_thinking_raw
|
| 490 |
+
|
| 491 |
+
try:
|
| 492 |
+
slot = slot_for_prediction_attr_model(model)
|
| 493 |
+
except ValueError as e:
|
| 494 |
+
return {"success": False, "message": str(e)}, 400
|
| 495 |
+
|
| 496 |
+
try:
|
| 497 |
+
suffix = compute_tool_append_suffix(
|
| 498 |
+
tool_content,
|
| 499 |
+
enable_thinking=enable_thinking,
|
| 500 |
+
tool_name=tool_name or None,
|
| 501 |
+
slot=slot,
|
| 502 |
+
)
|
| 503 |
+
except RuntimeError as e:
|
| 504 |
+
return {"success": False, "message": str(e)}, 500
|
| 505 |
+
|
| 506 |
+
return {"incremental_suffix": suffix}, 200
|
| 507 |
+
|
| 508 |
+
|
| 509 |
+
def completions(completions_request):
|
| 510 |
+
"""
|
| 511 |
+
文本补写:与 analyze_semantic 共用推理锁与 semantic 模型;响应恒为 text/event-stream(SSE)。
|
| 512 |
+
``prompt`` 须为已确定的完整模型输入(需 chat template 时请先调 POST /v1/completions/prompt)。
|
| 513 |
+
|
| 514 |
+
Args:
|
| 515 |
+
completions_request: 含 model、prompt 等,见 server_openai_definitions.yaml
|
| 516 |
+
|
| 517 |
+
Returns:
|
| 518 |
+
SSE Response;校验失败时 (错误体, 400/503/500)
|
| 519 |
+
"""
|
| 520 |
+
if not isinstance(completions_request, dict):
|
| 521 |
+
completions_request = {}
|
| 522 |
+
model = completions_request.get("model")
|
| 523 |
+
prompt = completions_request.get("prompt")
|
| 524 |
+
|
| 525 |
+
if not model:
|
| 526 |
+
return {"success": False, "message": "缺少 model 字段"}, 400
|
| 527 |
+
if prompt is None:
|
| 528 |
+
return {"success": False, "message": "缺少 prompt 字段"}, 400
|
| 529 |
+
if not isinstance(prompt, str):
|
| 530 |
+
return {"success": False, "message": "prompt 必须为字符串"}, 400
|
| 531 |
+
|
| 532 |
+
max_tokens_raw = completions_request.get("max_tokens")
|
| 533 |
+
max_tokens: Optional[int]
|
| 534 |
+
if max_tokens_raw is None:
|
| 535 |
+
max_tokens = None
|
| 536 |
+
elif type(max_tokens_raw) is not int:
|
| 537 |
+
return {"success": False, "message": "max_tokens 须为正整数"}, 400
|
| 538 |
+
elif max_tokens_raw <= 0:
|
| 539 |
+
return {"success": False, "message": "max_tokens 须 > 0"}, 400
|
| 540 |
+
else:
|
| 541 |
+
max_tokens = max_tokens_raw
|
| 542 |
+
|
| 543 |
+
bypass_site = request_has_valid_admin() and max_tokens is not None
|
| 544 |
+
if (
|
| 545 |
+
not bypass_site
|
| 546 |
+
and max_tokens is not None
|
| 547 |
+
and max_tokens > completion_max_token_length
|
| 548 |
+
):
|
| 549 |
+
return {
|
| 550 |
+
"success": False,
|
| 551 |
+
"message": (
|
| 552 |
+
f"max_tokens 不得超过续写上下文上限 {completion_max_token_length}"
|
| 553 |
+
),
|
| 554 |
+
}, 400
|
| 555 |
+
|
| 556 |
+
try:
|
| 557 |
+
slot = slot_for_prediction_attr_model(model)
|
| 558 |
+
except ValueError as e:
|
| 559 |
+
return {"success": False, "message": str(e)}, 400
|
| 560 |
+
|
| 561 |
+
client_ip = get_client_ip()
|
| 562 |
+
request_id = _log_request(model, prompt, client_ip)
|
| 563 |
+
|
| 564 |
+
return _completions_sse_response(
|
| 565 |
+
prompt,
|
| 566 |
+
request_id,
|
| 567 |
+
slot=slot,
|
| 568 |
+
model_display=_model_display_name_for_slot(slot),
|
| 569 |
+
max_tokens=max_tokens,
|
| 570 |
+
bypass_site_context_limit=bypass_site,
|
| 571 |
+
)
|
backend/api/prediction_attribute.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""预测归因 API"""
|
| 2 |
+
import gc
|
| 3 |
+
import time
|
| 4 |
+
|
| 5 |
+
from backend.models.model_manager import inference_lock
|
| 6 |
+
from backend.platform.oom import exit_if_oom
|
| 7 |
+
from backend.core.prediction_attributor import analyze_prediction_attribution
|
| 8 |
+
from backend.api.analyze import LOCK_WAIT_TIMEOUT
|
| 9 |
+
from backend.platform.access_log import get_client_ip, log_prediction_attribute_request
|
| 10 |
+
from backend.platform.source_page import ALLOWED_SOURCE_PAGES, normalize_source_page
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def prediction_attribute(attribution_request):
|
| 14 |
+
"""
|
| 15 |
+
对上下文文本的下一 token 预测做归因分析。
|
| 16 |
+
|
| 17 |
+
Args:
|
| 18 |
+
attribution_request: 须含 ``context``、``model``。归因目标二选一:
|
| 19 |
+
省略 ``target_prediction`` 且省略 ``target_token_id`` 时为 top-1;
|
| 20 |
+
或传非空 ``target_prediction``(字符串首 token);
|
| 21 |
+
或传 ``target_token_id``(非负整数词表 id);二者不可同时出现。
|
| 22 |
+
|
| 23 |
+
Returns:
|
| 24 |
+
(响应字典, 状态码) 元组
|
| 25 |
+
"""
|
| 26 |
+
context = attribution_request.get("context")
|
| 27 |
+
target_prediction = attribution_request.get("target_prediction")
|
| 28 |
+
target_token_id = attribution_request.get("target_token_id")
|
| 29 |
+
model = attribution_request.get("model")
|
| 30 |
+
source_page = attribution_request.get("source_page")
|
| 31 |
+
flow_id = attribution_request.get("flow_id")
|
| 32 |
+
flow_step = attribution_request.get("flow_step")
|
| 33 |
+
|
| 34 |
+
if context is None:
|
| 35 |
+
return {"success": False, "message": "Missing required field: context"}, 400
|
| 36 |
+
if not isinstance(context, str):
|
| 37 |
+
return {"success": False, "message": "context must be a string"}, 400
|
| 38 |
+
if context == "":
|
| 39 |
+
return {"success": False, "message": "Missing required field: context"}, 400
|
| 40 |
+
|
| 41 |
+
if target_prediction is not None and not isinstance(target_prediction, str):
|
| 42 |
+
return {"success": False, "message": "target_prediction must be a string"}, 400
|
| 43 |
+
if target_prediction == "":
|
| 44 |
+
return {"success": False, "message": "target_prediction must not be empty"}, 400
|
| 45 |
+
if target_token_id is not None and not isinstance(target_token_id, int):
|
| 46 |
+
return {"success": False, "message": "target_token_id must be an integer"}, 400
|
| 47 |
+
if target_token_id is not None and target_token_id < 0:
|
| 48 |
+
return {"success": False, "message": "target_token_id must be >= 0"}, 400
|
| 49 |
+
if target_prediction is not None and target_token_id is not None:
|
| 50 |
+
return {"success": False, "message": "target_prediction and target_token_id are mutually exclusive"}, 400
|
| 51 |
+
|
| 52 |
+
if model is None:
|
| 53 |
+
return {"success": False, "message": "Missing required field: model"}, 400
|
| 54 |
+
if not isinstance(model, str):
|
| 55 |
+
return {"success": False, "message": "model must be a string"}, 400
|
| 56 |
+
if model not in ("base", "instruct"):
|
| 57 |
+
return {"success": False, "message": 'model must be "base" or "instruct"'}, 400
|
| 58 |
+
|
| 59 |
+
if source_page is None:
|
| 60 |
+
return {"success": False, "message": "Missing required field: source_page"}, 400
|
| 61 |
+
if not isinstance(source_page, str):
|
| 62 |
+
return {"success": False, "message": "source_page must be a string"}, 400
|
| 63 |
+
if source_page == "":
|
| 64 |
+
return {"success": False, "message": "source_page must not be empty"}, 400
|
| 65 |
+
normalized_source_page = normalize_source_page(source_page)
|
| 66 |
+
if normalized_source_page is None:
|
| 67 |
+
allowed = ", ".join(sorted(ALLOWED_SOURCE_PAGES))
|
| 68 |
+
return {
|
| 69 |
+
"success": False,
|
| 70 |
+
"message": f"source_page must be one of: {allowed} (legacy *.html and gen_attribute accepted)",
|
| 71 |
+
}, 400
|
| 72 |
+
source_page = normalized_source_page
|
| 73 |
+
|
| 74 |
+
if flow_id is not None and not isinstance(flow_id, str):
|
| 75 |
+
return {"success": False, "message": "flow_id must be a string"}, 400
|
| 76 |
+
if flow_id == "":
|
| 77 |
+
return {"success": False, "message": "flow_id must not be empty"}, 400
|
| 78 |
+
if flow_step is not None and not isinstance(flow_step, int):
|
| 79 |
+
return {"success": False, "message": "flow_step must be an integer"}, 400
|
| 80 |
+
if flow_step is not None and flow_step < 0:
|
| 81 |
+
return {"success": False, "message": "flow_step must be >= 0"}, 400
|
| 82 |
+
|
| 83 |
+
is_causal_flow = source_page == "causal_flow"
|
| 84 |
+
if is_causal_flow:
|
| 85 |
+
if flow_id is None:
|
| 86 |
+
return {"success": False, "message": "Missing required field: flow_id for causal flow"}, 400
|
| 87 |
+
if flow_step is None:
|
| 88 |
+
return {"success": False, "message": "Missing required field: flow_step for causal flow"}, 400
|
| 89 |
+
elif flow_id is not None or flow_step is not None:
|
| 90 |
+
return {
|
| 91 |
+
"success": False,
|
| 92 |
+
"message": "flow_id/flow_step are only allowed when source_page is causal_flow",
|
| 93 |
+
}, 400
|
| 94 |
+
|
| 95 |
+
client_ip = get_client_ip()
|
| 96 |
+
start_time = time.perf_counter()
|
| 97 |
+
request_id = log_prediction_attribute_request(
|
| 98 |
+
context=context,
|
| 99 |
+
target_prediction=target_prediction,
|
| 100 |
+
target_token_id=target_token_id,
|
| 101 |
+
model=model,
|
| 102 |
+
source_page=source_page,
|
| 103 |
+
flow_id=flow_id,
|
| 104 |
+
flow_step=flow_step,
|
| 105 |
+
client_ip=client_ip,
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
lock_acquired = inference_lock.acquire(timeout=LOCK_WAIT_TIMEOUT)
|
| 109 |
+
if not lock_acquired:
|
| 110 |
+
return {
|
| 111 |
+
"success": False,
|
| 112 |
+
"message": (
|
| 113 |
+
f"Queue wait exceeded {LOCK_WAIT_TIMEOUT} seconds; "
|
| 114 |
+
"server is busy, please try again later."
|
| 115 |
+
),
|
| 116 |
+
}, 503
|
| 117 |
+
|
| 118 |
+
try:
|
| 119 |
+
result = analyze_prediction_attribution(
|
| 120 |
+
context,
|
| 121 |
+
target_prediction,
|
| 122 |
+
model=model,
|
| 123 |
+
target_token_id=target_token_id,
|
| 124 |
+
)
|
| 125 |
+
except ValueError as e:
|
| 126 |
+
return {"success": False, "message": str(e)}, 400
|
| 127 |
+
except Exception as e:
|
| 128 |
+
import traceback
|
| 129 |
+
traceback.print_exc()
|
| 130 |
+
exit_if_oom(e, defer_seconds=1)
|
| 131 |
+
return {"success": False, "message": str(e)}, 500
|
| 132 |
+
finally:
|
| 133 |
+
inference_lock.release()
|
| 134 |
+
gc.collect()
|
| 135 |
+
|
| 136 |
+
elapsed = time.perf_counter() - start_time
|
| 137 |
+
tokens = len(result.get("token_attribution", []))
|
| 138 |
+
target_token = result.get("target_token")
|
| 139 |
+
if flow_id is None:
|
| 140 |
+
print(
|
| 141 |
+
f"\t📤 API prediction_attribute response: req_id={request_id}, "
|
| 142 |
+
f"target={target_token!r}, tokens={tokens}, response_time={elapsed:.4f}s"
|
| 143 |
+
)
|
| 144 |
+
else:
|
| 145 |
+
print(
|
| 146 |
+
f"\t📤 API prediction_attribute response: req_id={request_id}, "
|
| 147 |
+
f"flow_id={flow_id!r}, flow_step={flow_step}, "
|
| 148 |
+
f"target={target_token!r}, tokens={tokens}, response_time={elapsed:.4f}s"
|
| 149 |
+
)
|
| 150 |
+
|
| 151 |
+
return {"success": True, **result}, 200
|
backend/api/sse_utils.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Server-Sent Events (SSE) 工具模块"""
|
| 2 |
+
import json
|
| 3 |
+
import queue
|
| 4 |
+
import time
|
| 5 |
+
from typing import Callable, Generator, Optional, Tuple
|
| 6 |
+
from flask import Response
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class SSEProgressReporter:
|
| 10 |
+
"""SSE进度报告器"""
|
| 11 |
+
|
| 12 |
+
def __init__(self, generator_func: Callable):
|
| 13 |
+
"""
|
| 14 |
+
初始化SSE进度报告器
|
| 15 |
+
|
| 16 |
+
Args:
|
| 17 |
+
generator_func: 生成器函数,用于生成SSE事件
|
| 18 |
+
"""
|
| 19 |
+
self.generator_func = generator_func
|
| 20 |
+
|
| 21 |
+
def generate(self):
|
| 22 |
+
"""生成SSE事件流"""
|
| 23 |
+
try:
|
| 24 |
+
for event in self.generator_func():
|
| 25 |
+
yield event
|
| 26 |
+
except Exception as e:
|
| 27 |
+
# 发送错误事件
|
| 28 |
+
error_data = {
|
| 29 |
+
'type': 'error',
|
| 30 |
+
'message': str(e)
|
| 31 |
+
}
|
| 32 |
+
yield f"data: {json.dumps(error_data)}\n\n"
|
| 33 |
+
|
| 34 |
+
def create_response(self) -> Response:
|
| 35 |
+
"""创建SSE响应"""
|
| 36 |
+
return Response(
|
| 37 |
+
self.generate(),
|
| 38 |
+
mimetype='text/event-stream',
|
| 39 |
+
headers={
|
| 40 |
+
'Cache-Control': 'no-cache',
|
| 41 |
+
'X-Accel-Buffering': 'no', # 禁用nginx缓冲
|
| 42 |
+
'Connection': 'keep-alive'
|
| 43 |
+
}
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def send_progress_event(step: int, total_steps: int, stage: str, percentage: Optional[int] = None, message: Optional[str] = None) -> str:
|
| 48 |
+
"""
|
| 49 |
+
生成SSE进度事件
|
| 50 |
+
|
| 51 |
+
Args:
|
| 52 |
+
step: 当前步骤 (1-based)
|
| 53 |
+
total_steps: 总步骤数
|
| 54 |
+
stage: 阶段名称 (encoding, inference, processing)
|
| 55 |
+
percentage: 可选的进度百分比 (0-100),仅在需要显示百分比的阶段提供
|
| 56 |
+
message: 可选的进度消息
|
| 57 |
+
|
| 58 |
+
Returns:
|
| 59 |
+
SSE格式的事件字符串
|
| 60 |
+
"""
|
| 61 |
+
data = {
|
| 62 |
+
'type': 'progress',
|
| 63 |
+
'step': step,
|
| 64 |
+
'total_steps': total_steps,
|
| 65 |
+
'stage': stage
|
| 66 |
+
}
|
| 67 |
+
if percentage is not None:
|
| 68 |
+
data['percentage'] = percentage
|
| 69 |
+
if message:
|
| 70 |
+
data['message'] = message
|
| 71 |
+
return f"data: {json.dumps(data)}\n\n"
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def send_result_event(result: dict) -> str:
|
| 75 |
+
"""
|
| 76 |
+
生成SSE结果事件
|
| 77 |
+
|
| 78 |
+
Args:
|
| 79 |
+
result: 分析结果字典
|
| 80 |
+
|
| 81 |
+
Returns:
|
| 82 |
+
SSE格式的事件字符串
|
| 83 |
+
"""
|
| 84 |
+
data = {
|
| 85 |
+
'type': 'result',
|
| 86 |
+
'data': result
|
| 87 |
+
}
|
| 88 |
+
return f"data: {json.dumps(data)}\n\n"
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def send_completion_delta_event(text: str, stream_end: bool) -> str:
|
| 92 |
+
"""续写流式:与 analyze 的 progress/result 并列,type=delta。"""
|
| 93 |
+
data = {
|
| 94 |
+
"type": "delta",
|
| 95 |
+
"text": text,
|
| 96 |
+
}
|
| 97 |
+
if stream_end:
|
| 98 |
+
data["stream_end"] = True
|
| 99 |
+
return f"data: {json.dumps(data)}\n\n"
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def send_prompt_used_event(prompt_used: str) -> str:
|
| 103 |
+
"""续写流式:在首条 delta 之前下发实际送入模型的 prompt 原文。"""
|
| 104 |
+
data = {
|
| 105 |
+
"type": "prompt_used",
|
| 106 |
+
"prompt_used": prompt_used,
|
| 107 |
+
}
|
| 108 |
+
return f"data: {json.dumps(data)}\n\n"
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def send_error_event(message: str, status_code: Optional[int] = None) -> str:
|
| 112 |
+
"""
|
| 113 |
+
生成SSE错误事件
|
| 114 |
+
|
| 115 |
+
Args:
|
| 116 |
+
message: 错误消息
|
| 117 |
+
status_code: 可选 HTTP 状态码,供非流式封装解析
|
| 118 |
+
|
| 119 |
+
Returns:
|
| 120 |
+
SSE格式的事件字符串
|
| 121 |
+
"""
|
| 122 |
+
data = {'type': 'error', 'message': message}
|
| 123 |
+
if status_code is not None:
|
| 124 |
+
data['status_code'] = status_code
|
| 125 |
+
return f"data: {json.dumps(data)}\n\n"
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def consume_progress_queue(
|
| 129 |
+
progress_queue: queue.Queue,
|
| 130 |
+
analysis_done,
|
| 131 |
+
start_time: float,
|
| 132 |
+
timeout_seconds: float,
|
| 133 |
+
timeout_label: str = "分析",
|
| 134 |
+
) -> Generator[Tuple[str, str], None, None]:
|
| 135 |
+
"""
|
| 136 |
+
消费进度队列,yield (kind, event_str)。
|
| 137 |
+
kind: 'progress' | 'timeout' | 'done'
|
| 138 |
+
event_str: SSE 格式字符串(timeout 时含错误信息,done 时为空)
|
| 139 |
+
"""
|
| 140 |
+
done_received = False
|
| 141 |
+
last_progress_info = None
|
| 142 |
+
|
| 143 |
+
while True:
|
| 144 |
+
elapsed = time.perf_counter() - start_time
|
| 145 |
+
if elapsed >= timeout_seconds:
|
| 146 |
+
progress_str = f" | {last_progress_info}" if last_progress_info else ""
|
| 147 |
+
print(f"⏱️ {timeout_label}超时: 处理时长 {elapsed:.2f}s 超过限制 {timeout_seconds}s,已放弃{progress_str}")
|
| 148 |
+
yield ('timeout', send_error_event(f"分析超时:处理时长超过 {timeout_seconds} 秒限制,已放弃"))
|
| 149 |
+
return
|
| 150 |
+
|
| 151 |
+
try:
|
| 152 |
+
event_data = progress_queue.get(timeout=0.1)
|
| 153 |
+
event_type = event_data[0]
|
| 154 |
+
if event_type == 'progress':
|
| 155 |
+
_, step, total_steps, stage, percentage = event_data
|
| 156 |
+
if total_steps > 0:
|
| 157 |
+
last_progress_info = f"step={step}/{total_steps}"
|
| 158 |
+
else:
|
| 159 |
+
last_progress_info = f"step={step}"
|
| 160 |
+
if stage:
|
| 161 |
+
last_progress_info += f" stage={stage}"
|
| 162 |
+
if percentage is not None:
|
| 163 |
+
last_progress_info += f" {percentage}%"
|
| 164 |
+
yield ('progress', send_progress_event(step, total_steps, stage, percentage))
|
| 165 |
+
elif event_type == 'done':
|
| 166 |
+
done_received = True
|
| 167 |
+
while not progress_queue.empty():
|
| 168 |
+
try:
|
| 169 |
+
remaining = progress_queue.get_nowait()
|
| 170 |
+
if remaining[0] == 'progress':
|
| 171 |
+
_, step, total_steps, stage, percentage = remaining
|
| 172 |
+
yield ('progress', send_progress_event(step, total_steps, stage, percentage))
|
| 173 |
+
except queue.Empty:
|
| 174 |
+
break
|
| 175 |
+
yield ('done', '')
|
| 176 |
+
return
|
| 177 |
+
except queue.Empty:
|
| 178 |
+
if analysis_done.is_set() and done_received:
|
| 179 |
+
yield ('done', '')
|
| 180 |
+
return
|
| 181 |
+
|
backend/api/static.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""静态文件路由"""
|
| 2 |
+
import mimetypes
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
from urllib.parse import unquote
|
| 5 |
+
|
| 6 |
+
from flask import Response, redirect, abort, request
|
| 7 |
+
from werkzeug.utils import safe_join
|
| 8 |
+
|
| 9 |
+
from backend.platform.access_log import log_cached_demo, log_json_demo, log_page_load
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def _read_static_file(directory: str, path: str) -> Response:
|
| 13 |
+
"""读取静态文件并返回 Response,避免 send_from_directory 在 ASGI/a2wsgi 下
|
| 14 |
+
流式传输导致的 Content-Length 不匹配(RuntimeError: Response content shorter than Content-Length)。
|
| 15 |
+
"""
|
| 16 |
+
base = Path(directory).resolve()
|
| 17 |
+
safe_path = safe_join(str(base), path)
|
| 18 |
+
if safe_path is None:
|
| 19 |
+
abort(404)
|
| 20 |
+
full_path = Path(safe_path)
|
| 21 |
+
if not full_path.is_file() or not str(full_path.resolve()).startswith(str(base)):
|
| 22 |
+
abort(404)
|
| 23 |
+
content = full_path.read_bytes()
|
| 24 |
+
mimetype, _ = mimetypes.guess_type(path)
|
| 25 |
+
mimetype = mimetype or "application/octet-stream"
|
| 26 |
+
return Response(content, mimetype=mimetype, headers={"Content-Length": str(len(content))})
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def register_static_routes(app):
|
| 30 |
+
"""注册静态文件路由"""
|
| 31 |
+
|
| 32 |
+
@app.route('/')
|
| 33 |
+
def redir():
|
| 34 |
+
target = 'client/index.html'
|
| 35 |
+
if request.query_string:
|
| 36 |
+
target += '?' + request.query_string.decode()
|
| 37 |
+
return redirect(target)
|
| 38 |
+
|
| 39 |
+
@app.route('/client/<path:path>')
|
| 40 |
+
def send_static(path):
|
| 41 |
+
"""serves all files from ./client/dist/ to ``/client/<path:path>``"""
|
| 42 |
+
if path == 'gen_attribute.html':
|
| 43 |
+
target = 'causal_flow.html'
|
| 44 |
+
if request.query_string:
|
| 45 |
+
target += '?' + request.query_string.decode()
|
| 46 |
+
return redirect(f'/client/{target}', code=301)
|
| 47 |
+
if path.endswith('.html'):
|
| 48 |
+
log_page_load(path)
|
| 49 |
+
if path.endswith('.json'):
|
| 50 |
+
log_cached_demo(path)
|
| 51 |
+
return _read_static_file('client/dist', path)
|
| 52 |
+
|
| 53 |
+
@app.route('/demo/<path:path>')
|
| 54 |
+
def send_demo(path):
|
| 55 |
+
"""serves all demo files from the demo dir to ``/demo/<path:path>``"""
|
| 56 |
+
from backend.platform.app_context import get_data_dir
|
| 57 |
+
data_dir = get_data_dir()
|
| 58 |
+
log_json_demo(path)
|
| 59 |
+
try:
|
| 60 |
+
decoded_path = unquote(path)
|
| 61 |
+
return _read_static_file(str(data_dir), decoded_path)
|
| 62 |
+
except Exception:
|
| 63 |
+
try:
|
| 64 |
+
return _read_static_file(str(data_dir), path)
|
| 65 |
+
except Exception:
|
| 66 |
+
abort(404)
|
| 67 |
+
|