cccmmd commited on
Commit
76b5743
·
0 Parent(s):

init: TokenTrace - LLM interpretability toolbox

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .cursorindexingignore +3 -0
  2. .dockerignore +51 -0
  3. .gitattributes +36 -0
  4. .gitignore +45 -0
  5. .kilocode/skills/openspec-apply-change/SKILL.md +156 -0
  6. .kilocode/skills/openspec-archive-change/SKILL.md +114 -0
  7. .kilocode/skills/openspec-explore/SKILL.md +288 -0
  8. .kilocode/skills/openspec-propose/SKILL.md +110 -0
  9. .kilocode/workflows/opsx-apply.md +145 -0
  10. .kilocode/workflows/opsx-archive.md +150 -0
  11. .kilocode/workflows/opsx-explore.md +166 -0
  12. .kilocode/workflows/opsx-propose.md +99 -0
  13. .kiro/prompts/opsx-apply.prompt.md +149 -0
  14. .kiro/prompts/opsx-archive.prompt.md +154 -0
  15. .kiro/prompts/opsx-explore.prompt.md +170 -0
  16. .kiro/prompts/opsx-propose.prompt.md +103 -0
  17. .kiro/skills/openspec-apply-change/SKILL.md +156 -0
  18. .kiro/skills/openspec-archive-change/SKILL.md +114 -0
  19. .kiro/skills/openspec-explore/SKILL.md +288 -0
  20. .kiro/skills/openspec-propose/SKILL.md +110 -0
  21. .opencode/commands/opsx-apply.md +149 -0
  22. .opencode/commands/opsx-archive.md +154 -0
  23. .opencode/commands/opsx-explore.md +170 -0
  24. .opencode/commands/opsx-propose.md +103 -0
  25. .opencode/skills/openspec-apply-change/SKILL.md +156 -0
  26. .opencode/skills/openspec-archive-change/SKILL.md +114 -0
  27. .opencode/skills/openspec-explore/SKILL.md +288 -0
  28. .opencode/skills/openspec-propose/SKILL.md +110 -0
  29. AGENTS.md +13 -0
  30. CONTEXT.md +29 -0
  31. Dockerfile +67 -0
  32. LICENSE +201 -0
  33. NOTICE +8 -0
  34. README.md +148 -0
  35. backend/__init__.py +5 -0
  36. backend/api/__init__.py +2 -0
  37. backend/api/ablation_attribute.py +135 -0
  38. backend/api/analyze.py +412 -0
  39. backend/api/analyze_semantic.py +212 -0
  40. backend/api/branch_next.py +81 -0
  41. backend/api/client_activity.py +60 -0
  42. backend/api/demo.py +174 -0
  43. backend/api/fetch_url.py +221 -0
  44. backend/api/folder.py +102 -0
  45. backend/api/logit_lens.py +104 -0
  46. backend/api/model_switch.py +229 -0
  47. backend/api/openai_completions.py +571 -0
  48. backend/api/prediction_attribute.py +151 -0
  49. backend/api/sse_utils.py +181 -0
  50. 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
+