cadence-skill-installer 0.2.13 → 0.2.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/skill/SKILL.md +7 -1
- package/skill/agents/openai.yaml +1 -1
- package/skill/assets/cadence.json +36 -1
- package/skill/config/commit-conventions.json +6 -0
- package/skill/scripts/ideation_research.py +307 -0
- package/skill/scripts/inject-ideation.py +7 -1
- package/skill/scripts/run-research-pass.py +964 -0
- package/skill/scripts/scaffold-project.sh +36 -1
- package/skill/scripts/workflow_state.py +75 -2
- package/skill/skills/ideation-updater/SKILL.md +4 -3
- package/skill/skills/ideator/SKILL.md +4 -3
- package/skill/skills/project-progress/SKILL.md +1 -0
- package/skill/skills/project-progress/agents/openai.yaml +1 -1
- package/skill/skills/researcher/SKILL.md +35 -0
- package/skill/skills/researcher/agents/openai.yaml +4 -0
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -44,7 +44,7 @@ description: Structured project operating system for end-to-end greenfield or br
|
|
|
44
44
|
|
|
45
45
|
## Prerequisite Gate (Conditional)
|
|
46
46
|
1. Invoke `skills/prerequisite-gate/SKILL.md` only when `route.skill_name` is `prerequisite-gate`.
|
|
47
|
-
2. If `route.skill_name` is not `prerequisite-gate` (for example `ideator`), skip prerequisite gate and follow the active route instead.
|
|
47
|
+
2. If `route.skill_name` is not `prerequisite-gate` (for example `ideator` or `researcher`), skip prerequisite gate and follow the active route instead.
|
|
48
48
|
|
|
49
49
|
## Progress / Resume Flow
|
|
50
50
|
1. Invoke `skills/project-progress/SKILL.md` when the user asks to continue/resume or requests progress status (for example: "continue the project", "how far along are we?", "where did we leave off?").
|
|
@@ -63,6 +63,12 @@ description: Structured project operating system for end-to-end greenfield or br
|
|
|
63
63
|
4. If the user asks to define the project or provides a brief while route is `ideator`, invoke `skills/ideator/SKILL.md`.
|
|
64
64
|
5. If route is `ideator` and the user has not provided ideation input yet, prompt with the same handoff line and wait.
|
|
65
65
|
|
|
66
|
+
## Research Flow
|
|
67
|
+
1. If the workflow route is `researcher`, invoke `skills/researcher/SKILL.md`.
|
|
68
|
+
2. Enforce one research pass per conversation so context stays bounded.
|
|
69
|
+
3. When the researcher flow reports additional passes remain, end with the exact handoff line: `Start a new chat and say "continue research".`
|
|
70
|
+
4. Continue routing to researcher on subsequent chats until workflow reports the research task complete.
|
|
71
|
+
|
|
66
72
|
## Ideation Update Flow
|
|
67
73
|
1. If the user wants to modify or discuss existing ideation, invoke `skills/ideation-updater/SKILL.md`.
|
|
68
74
|
2. Use this update flow only for existing project ideation changes; use the new-chat handoff in Ideation Flow for net-new ideation discovery.
|
package/skill/agents/openai.yaml
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
interface:
|
|
2
2
|
display_name: "Cadence"
|
|
3
3
|
short_description: "Lifecycle + delivery system for structured project execution"
|
|
4
|
-
default_prompt: "Use Cadence to guide this project from lifecycle setup through phased execution, traceability, audit, and milestone completion. Always read and apply the active SOUL persona from .cadence/SOUL.json (fallback: SOUL.json). Keep user-facing responses concise and outcome-focused, and never expose internal skill-routing or command-execution traces unless the user explicitly asks. At the start of each Cadence turn, run scripts/check-project-repo-status.py, run scaffold only when .cadence is missing, then run scripts/read-workflow-state.py and treat route.skill_name as authoritative for the next state-changing skill. Invoke skills/prerequisite-gate/SKILL.md only when route.skill_name is prerequisite-gate. If route.skill_name is ideator, do not run prerequisite again. For net-new project starts where scaffold and prerequisite just completed in-thread, hand off with: Start a new chat and either say \"help me define my project\" or share your project brief. In later chats, if route.skill_name is ideator and the user asks to define the project or provides a brief, invoke skills/ideator/SKILL.md. If user intent indicates resuming/continuing work or asking progress, invoke skills/project-progress/SKILL.md first, report current phase, then route to the next step. If the user manually requests a Cadence subskill, resolve PROJECT_ROOT with scripts/resolve-project-root.py and then run scripts/assert-workflow-route.py --skill-name <subskill> --project-root \"$PROJECT_ROOT\" before any state-changing actions."
|
|
4
|
+
default_prompt: "Use Cadence to guide this project from lifecycle setup through phased execution, traceability, audit, and milestone completion. Always read and apply the active SOUL persona from .cadence/SOUL.json (fallback: SOUL.json). Keep user-facing responses concise and outcome-focused, and never expose internal skill-routing or command-execution traces unless the user explicitly asks. At the start of each Cadence turn, run scripts/check-project-repo-status.py, run scaffold only when .cadence is missing, then run scripts/read-workflow-state.py and treat route.skill_name as authoritative for the next state-changing skill. Invoke skills/prerequisite-gate/SKILL.md only when route.skill_name is prerequisite-gate. If route.skill_name is ideator, do not run prerequisite again. For net-new project starts where scaffold and prerequisite just completed in-thread, hand off with: Start a new chat and either say \"help me define my project\" or share your project brief. In later chats, if route.skill_name is ideator and the user asks to define the project or provides a brief, invoke skills/ideator/SKILL.md. If route.skill_name is researcher, invoke skills/researcher/SKILL.md and enforce one pass per conversation; when more passes remain, end with: Start a new chat and say \"continue research\". If user intent indicates resuming/continuing work or asking progress, invoke skills/project-progress/SKILL.md first, report current phase, then route to the next step. If the user manually requests a Cadence subskill, resolve PROJECT_ROOT with scripts/resolve-project-root.py and then run scripts/assert-workflow-route.py --skill-name <subskill> --project-root \"$PROJECT_ROOT\" before any state-changing actions."
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
"prerequisites-pass": false,
|
|
3
3
|
"state": {
|
|
4
4
|
"ideation-completed": false,
|
|
5
|
+
"research-completed": false,
|
|
5
6
|
"cadence-scripts-dir": "",
|
|
6
7
|
"repo-enabled": false
|
|
7
8
|
},
|
|
8
9
|
"workflow": {
|
|
9
|
-
"schema_version":
|
|
10
|
+
"schema_version": 3,
|
|
10
11
|
"plan": [
|
|
11
12
|
{
|
|
12
13
|
"id": "milestone-foundation",
|
|
@@ -52,6 +53,16 @@
|
|
|
52
53
|
"skill_path": "skills/ideator/SKILL.md",
|
|
53
54
|
"reason": "Ideation has not been completed yet."
|
|
54
55
|
}
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"id": "task-research",
|
|
59
|
+
"kind": "task",
|
|
60
|
+
"title": "Research ideation agenda",
|
|
61
|
+
"route": {
|
|
62
|
+
"skill_name": "researcher",
|
|
63
|
+
"skill_path": "skills/researcher/SKILL.md",
|
|
64
|
+
"reason": "Ideation research agenda has not been completed yet."
|
|
65
|
+
}
|
|
55
66
|
}
|
|
56
67
|
]
|
|
57
68
|
}
|
|
@@ -73,6 +84,30 @@
|
|
|
73
84
|
"blocks": [],
|
|
74
85
|
"entity_registry": [],
|
|
75
86
|
"topic_index": {}
|
|
87
|
+
},
|
|
88
|
+
"research_execution": {
|
|
89
|
+
"version": 1,
|
|
90
|
+
"status": "pending",
|
|
91
|
+
"planning": {
|
|
92
|
+
"target_effort_per_pass": 12,
|
|
93
|
+
"max_topics_per_pass": 4,
|
|
94
|
+
"latest_round": 0
|
|
95
|
+
},
|
|
96
|
+
"summary": {
|
|
97
|
+
"topic_total": 0,
|
|
98
|
+
"topic_complete": 0,
|
|
99
|
+
"topic_needs_followup": 0,
|
|
100
|
+
"topic_pending": 0,
|
|
101
|
+
"pass_pending": 0,
|
|
102
|
+
"pass_complete": 0,
|
|
103
|
+
"next_pass_id": ""
|
|
104
|
+
},
|
|
105
|
+
"topic_status": {},
|
|
106
|
+
"pass_queue": [],
|
|
107
|
+
"pass_history": [],
|
|
108
|
+
"source_registry": [],
|
|
109
|
+
"handoff_required": false,
|
|
110
|
+
"handoff_message": "Start a new chat and say \"continue research\"."
|
|
76
111
|
}
|
|
77
112
|
}
|
|
78
113
|
}
|
|
@@ -115,6 +115,12 @@
|
|
|
115
115
|
"checkpoints": {
|
|
116
116
|
"progress-checked": "check progress and route next action"
|
|
117
117
|
}
|
|
118
|
+
},
|
|
119
|
+
"researcher": {
|
|
120
|
+
"description": "Ideation research pass execution",
|
|
121
|
+
"checkpoints": {
|
|
122
|
+
"research-pass-recorded": "persist one ideation research pass"
|
|
123
|
+
}
|
|
118
124
|
}
|
|
119
125
|
}
|
|
120
126
|
}
|
|
@@ -9,6 +9,10 @@ from typing import Any
|
|
|
9
9
|
|
|
10
10
|
RESEARCH_SCHEMA_VERSION = 1
|
|
11
11
|
PRIORITY_LEVELS = {"low", "medium", "high"}
|
|
12
|
+
RESEARCH_EXECUTION_SCHEMA_VERSION = 1
|
|
13
|
+
RESEARCH_EXECUTION_STATUSES = {"pending", "in_progress", "complete"}
|
|
14
|
+
RESEARCH_TOPIC_STATUSES = {"pending", "in_progress", "needs_followup", "complete"}
|
|
15
|
+
DEFAULT_RESEARCH_HANDOFF_MESSAGE = 'Start a new chat and say "continue research".'
|
|
12
16
|
|
|
13
17
|
|
|
14
18
|
class ResearchAgendaValidationError(ValueError):
|
|
@@ -73,6 +77,269 @@ def default_research_agenda() -> dict[str, Any]:
|
|
|
73
77
|
}
|
|
74
78
|
|
|
75
79
|
|
|
80
|
+
def default_research_execution() -> dict[str, Any]:
|
|
81
|
+
return {
|
|
82
|
+
"version": RESEARCH_EXECUTION_SCHEMA_VERSION,
|
|
83
|
+
"status": "pending",
|
|
84
|
+
"planning": {
|
|
85
|
+
"target_effort_per_pass": 12,
|
|
86
|
+
"max_topics_per_pass": 4,
|
|
87
|
+
"latest_round": 0,
|
|
88
|
+
},
|
|
89
|
+
"summary": {
|
|
90
|
+
"topic_total": 0,
|
|
91
|
+
"topic_complete": 0,
|
|
92
|
+
"topic_needs_followup": 0,
|
|
93
|
+
"topic_pending": 0,
|
|
94
|
+
"pass_pending": 0,
|
|
95
|
+
"pass_complete": 0,
|
|
96
|
+
"next_pass_id": "",
|
|
97
|
+
},
|
|
98
|
+
"topic_status": {},
|
|
99
|
+
"pass_queue": [],
|
|
100
|
+
"pass_history": [],
|
|
101
|
+
"source_registry": [],
|
|
102
|
+
"handoff_required": False,
|
|
103
|
+
"handoff_message": DEFAULT_RESEARCH_HANDOFF_MESSAGE,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _coerce_research_execution_status(value: Any) -> str:
|
|
108
|
+
status = _string(value, "pending").lower()
|
|
109
|
+
if status not in RESEARCH_EXECUTION_STATUSES:
|
|
110
|
+
return "pending"
|
|
111
|
+
return status
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _coerce_research_topic_status(value: Any) -> str:
|
|
115
|
+
status = _string(value, "pending").lower()
|
|
116
|
+
if status not in RESEARCH_TOPIC_STATUSES:
|
|
117
|
+
return "pending"
|
|
118
|
+
return status
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _agenda_topic_index(agenda: dict[str, Any]) -> dict[str, dict[str, Any]]:
|
|
122
|
+
blocks = agenda.get("blocks")
|
|
123
|
+
if not isinstance(blocks, list):
|
|
124
|
+
return {}
|
|
125
|
+
|
|
126
|
+
index: dict[str, dict[str, Any]] = {}
|
|
127
|
+
for block in blocks:
|
|
128
|
+
if not isinstance(block, dict):
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
block_id = _string(block.get("block_id"))
|
|
132
|
+
block_title = _string(block.get("title"))
|
|
133
|
+
topics = block.get("topics")
|
|
134
|
+
if not isinstance(topics, list):
|
|
135
|
+
continue
|
|
136
|
+
|
|
137
|
+
for topic in topics:
|
|
138
|
+
if not isinstance(topic, dict):
|
|
139
|
+
continue
|
|
140
|
+
topic_id = _string(topic.get("topic_id"))
|
|
141
|
+
if not topic_id:
|
|
142
|
+
continue
|
|
143
|
+
index[topic_id] = {
|
|
144
|
+
"topic_id": topic_id,
|
|
145
|
+
"title": _string(topic.get("title"), topic_id),
|
|
146
|
+
"priority": _string(topic.get("priority"), "medium").lower(),
|
|
147
|
+
"category": _string(topic.get("category"), "general"),
|
|
148
|
+
"research_questions": _string_list(topic.get("research_questions")),
|
|
149
|
+
"keywords": _string_list(topic.get("keywords")),
|
|
150
|
+
"related_entities": _string_list(topic.get("related_entities")),
|
|
151
|
+
"block_id": block_id,
|
|
152
|
+
"block_title": block_title,
|
|
153
|
+
}
|
|
154
|
+
return index
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _normalize_research_execution(agenda: dict[str, Any], raw_execution: Any) -> dict[str, Any]:
|
|
158
|
+
topic_index = _agenda_topic_index(agenda)
|
|
159
|
+
normalized = default_research_execution()
|
|
160
|
+
if not isinstance(raw_execution, dict):
|
|
161
|
+
raw_execution = {}
|
|
162
|
+
|
|
163
|
+
normalized["status"] = _coerce_research_execution_status(raw_execution.get("status"))
|
|
164
|
+
normalized["handoff_required"] = bool(raw_execution.get("handoff_required", False))
|
|
165
|
+
handoff_message = _string(raw_execution.get("handoff_message"), DEFAULT_RESEARCH_HANDOFF_MESSAGE)
|
|
166
|
+
normalized["handoff_message"] = handoff_message or DEFAULT_RESEARCH_HANDOFF_MESSAGE
|
|
167
|
+
|
|
168
|
+
planning = raw_execution.get("planning")
|
|
169
|
+
planning = dict(planning) if isinstance(planning, dict) else {}
|
|
170
|
+
try:
|
|
171
|
+
target_effort = int(planning.get("target_effort_per_pass", 12))
|
|
172
|
+
except (TypeError, ValueError):
|
|
173
|
+
target_effort = 12
|
|
174
|
+
if target_effort < 1:
|
|
175
|
+
target_effort = 1
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
max_topics = int(planning.get("max_topics_per_pass", 4))
|
|
179
|
+
except (TypeError, ValueError):
|
|
180
|
+
max_topics = 4
|
|
181
|
+
if max_topics < 1:
|
|
182
|
+
max_topics = 1
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
latest_round = int(planning.get("latest_round", 0))
|
|
186
|
+
except (TypeError, ValueError):
|
|
187
|
+
latest_round = 0
|
|
188
|
+
if latest_round < 0:
|
|
189
|
+
latest_round = 0
|
|
190
|
+
|
|
191
|
+
normalized["planning"] = {
|
|
192
|
+
"target_effort_per_pass": target_effort,
|
|
193
|
+
"max_topics_per_pass": max_topics,
|
|
194
|
+
"latest_round": latest_round,
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
raw_topic_status = raw_execution.get("topic_status")
|
|
198
|
+
raw_topic_status = dict(raw_topic_status) if isinstance(raw_topic_status, dict) else {}
|
|
199
|
+
topic_status: dict[str, dict[str, Any]] = {}
|
|
200
|
+
for topic_id, topic_meta in topic_index.items():
|
|
201
|
+
existing = raw_topic_status.get(topic_id)
|
|
202
|
+
existing = dict(existing) if isinstance(existing, dict) else {}
|
|
203
|
+
status = _coerce_research_topic_status(existing.get("status"))
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
passes_attempted = int(existing.get("passes_attempted", 0))
|
|
207
|
+
except (TypeError, ValueError):
|
|
208
|
+
passes_attempted = 0
|
|
209
|
+
if passes_attempted < 0:
|
|
210
|
+
passes_attempted = 0
|
|
211
|
+
|
|
212
|
+
topic_status[topic_id] = {
|
|
213
|
+
"topic_id": topic_id,
|
|
214
|
+
"title": topic_meta["title"],
|
|
215
|
+
"status": status,
|
|
216
|
+
"passes_attempted": passes_attempted,
|
|
217
|
+
"last_pass_id": _string(existing.get("last_pass_id")),
|
|
218
|
+
"latest_summary": _string(existing.get("latest_summary")),
|
|
219
|
+
"unresolved_questions": _string_list(existing.get("unresolved_questions")),
|
|
220
|
+
"source_ids": _string_list(existing.get("source_ids")),
|
|
221
|
+
"updated_at": _string(existing.get("updated_at")),
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
raw_queue = raw_execution.get("pass_queue")
|
|
225
|
+
raw_queue = list(raw_queue) if isinstance(raw_queue, list) else []
|
|
226
|
+
pass_queue: list[dict[str, Any]] = []
|
|
227
|
+
for entry in raw_queue:
|
|
228
|
+
if not isinstance(entry, dict):
|
|
229
|
+
continue
|
|
230
|
+
pass_id = _string(entry.get("pass_id"))
|
|
231
|
+
if not pass_id:
|
|
232
|
+
continue
|
|
233
|
+
topic_ids = [topic_id for topic_id in _string_list(entry.get("topic_ids")) if topic_id in topic_index]
|
|
234
|
+
if not topic_ids:
|
|
235
|
+
continue
|
|
236
|
+
status = _string(entry.get("status"), "pending").lower()
|
|
237
|
+
if status not in {"pending", "in_progress"}:
|
|
238
|
+
status = "pending"
|
|
239
|
+
|
|
240
|
+
try:
|
|
241
|
+
round_number = int(entry.get("round", 0))
|
|
242
|
+
except (TypeError, ValueError):
|
|
243
|
+
round_number = 0
|
|
244
|
+
if round_number < 0:
|
|
245
|
+
round_number = 0
|
|
246
|
+
|
|
247
|
+
try:
|
|
248
|
+
planned_effort = int(entry.get("planned_effort", 0))
|
|
249
|
+
except (TypeError, ValueError):
|
|
250
|
+
planned_effort = 0
|
|
251
|
+
if planned_effort < 0:
|
|
252
|
+
planned_effort = 0
|
|
253
|
+
|
|
254
|
+
pass_queue.append(
|
|
255
|
+
{
|
|
256
|
+
"pass_id": pass_id,
|
|
257
|
+
"round": round_number,
|
|
258
|
+
"status": status,
|
|
259
|
+
"topic_ids": topic_ids,
|
|
260
|
+
"planned_effort": planned_effort,
|
|
261
|
+
"created_at": _string(entry.get("created_at")),
|
|
262
|
+
"started_at": _string(entry.get("started_at")),
|
|
263
|
+
}
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
raw_history = raw_execution.get("pass_history")
|
|
267
|
+
raw_history = list(raw_history) if isinstance(raw_history, list) else []
|
|
268
|
+
pass_history: list[dict[str, Any]] = []
|
|
269
|
+
for entry in raw_history:
|
|
270
|
+
if not isinstance(entry, dict):
|
|
271
|
+
continue
|
|
272
|
+
pass_id = _string(entry.get("pass_id"))
|
|
273
|
+
if not pass_id:
|
|
274
|
+
continue
|
|
275
|
+
pass_history.append(dict(entry))
|
|
276
|
+
|
|
277
|
+
raw_sources = raw_execution.get("source_registry")
|
|
278
|
+
raw_sources = list(raw_sources) if isinstance(raw_sources, list) else []
|
|
279
|
+
source_registry: list[dict[str, Any]] = []
|
|
280
|
+
for source in raw_sources:
|
|
281
|
+
if not isinstance(source, dict):
|
|
282
|
+
continue
|
|
283
|
+
source_id = _string(source.get("source_id"))
|
|
284
|
+
url = _string(source.get("url"))
|
|
285
|
+
if not source_id or not url:
|
|
286
|
+
continue
|
|
287
|
+
source_registry.append(
|
|
288
|
+
{
|
|
289
|
+
"source_id": source_id,
|
|
290
|
+
"url": url,
|
|
291
|
+
"title": _string(source.get("title")),
|
|
292
|
+
"publisher": _string(source.get("publisher")),
|
|
293
|
+
"published_at": _string(source.get("published_at")),
|
|
294
|
+
"notes": _string(source.get("notes")),
|
|
295
|
+
"topic_ids": [topic_id for topic_id in _string_list(source.get("topic_ids")) if topic_id in topic_index],
|
|
296
|
+
"pass_id": _string(source.get("pass_id")),
|
|
297
|
+
"captured_at": _string(source.get("captured_at")),
|
|
298
|
+
}
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
normalized["version"] = RESEARCH_EXECUTION_SCHEMA_VERSION
|
|
302
|
+
normalized["topic_status"] = topic_status
|
|
303
|
+
normalized["pass_queue"] = pass_queue
|
|
304
|
+
normalized["pass_history"] = pass_history
|
|
305
|
+
normalized["source_registry"] = source_registry
|
|
306
|
+
|
|
307
|
+
total_topics = len(topic_status)
|
|
308
|
+
topic_complete = len([entry for entry in topic_status.values() if entry.get("status") == "complete"])
|
|
309
|
+
topic_needs_followup = len(
|
|
310
|
+
[entry for entry in topic_status.values() if entry.get("status") == "needs_followup"]
|
|
311
|
+
)
|
|
312
|
+
topic_pending = total_topics - topic_complete - topic_needs_followup
|
|
313
|
+
|
|
314
|
+
in_progress_queue = [entry for entry in pass_queue if entry.get("status") == "in_progress"]
|
|
315
|
+
pending_queue = [entry for entry in pass_queue if entry.get("status") == "pending"]
|
|
316
|
+
next_pass_id = in_progress_queue[0]["pass_id"] if in_progress_queue else (pending_queue[0]["pass_id"] if pending_queue else "")
|
|
317
|
+
|
|
318
|
+
if total_topics == 0:
|
|
319
|
+
execution_status = "pending"
|
|
320
|
+
elif topic_complete == total_topics:
|
|
321
|
+
execution_status = "complete"
|
|
322
|
+
elif in_progress_queue or pending_queue:
|
|
323
|
+
execution_status = "in_progress"
|
|
324
|
+
else:
|
|
325
|
+
execution_status = "pending"
|
|
326
|
+
|
|
327
|
+
normalized["status"] = execution_status
|
|
328
|
+
if execution_status == "complete":
|
|
329
|
+
normalized["handoff_required"] = False
|
|
330
|
+
|
|
331
|
+
normalized["summary"] = {
|
|
332
|
+
"topic_total": total_topics,
|
|
333
|
+
"topic_complete": topic_complete,
|
|
334
|
+
"topic_needs_followup": topic_needs_followup,
|
|
335
|
+
"topic_pending": max(topic_pending, 0),
|
|
336
|
+
"pass_pending": len(pass_queue),
|
|
337
|
+
"pass_complete": len(pass_history),
|
|
338
|
+
"next_pass_id": next_pass_id,
|
|
339
|
+
}
|
|
340
|
+
return normalized
|
|
341
|
+
|
|
342
|
+
|
|
76
343
|
def ensure_ideation_research_defaults(ideation: Any) -> dict[str, Any]:
|
|
77
344
|
if not isinstance(ideation, dict):
|
|
78
345
|
ideation = {}
|
|
@@ -80,6 +347,7 @@ def ensure_ideation_research_defaults(ideation: Any) -> dict[str, Any]:
|
|
|
80
347
|
agenda = ideation.get("research_agenda")
|
|
81
348
|
if not isinstance(agenda, dict):
|
|
82
349
|
ideation["research_agenda"] = default_research_agenda()
|
|
350
|
+
ideation["research_execution"] = default_research_execution()
|
|
83
351
|
return ideation
|
|
84
352
|
|
|
85
353
|
normalized = dict(agenda)
|
|
@@ -106,9 +374,48 @@ def ensure_ideation_research_defaults(ideation: Any) -> dict[str, Any]:
|
|
|
106
374
|
normalized["summary"] = summary
|
|
107
375
|
|
|
108
376
|
ideation["research_agenda"] = normalized
|
|
377
|
+
ideation["research_execution"] = _normalize_research_execution(
|
|
378
|
+
normalized,
|
|
379
|
+
ideation.get("research_execution"),
|
|
380
|
+
)
|
|
109
381
|
return ideation
|
|
110
382
|
|
|
111
383
|
|
|
384
|
+
def reset_research_execution(ideation: Any) -> dict[str, Any]:
|
|
385
|
+
normalized = ensure_ideation_research_defaults(ideation)
|
|
386
|
+
agenda = normalized.get("research_agenda")
|
|
387
|
+
agenda = dict(agenda) if isinstance(agenda, dict) else default_research_agenda()
|
|
388
|
+
|
|
389
|
+
topic_index = _agenda_topic_index(agenda)
|
|
390
|
+
execution = default_research_execution()
|
|
391
|
+
execution["topic_status"] = {
|
|
392
|
+
topic_id: {
|
|
393
|
+
"topic_id": topic_id,
|
|
394
|
+
"title": topic.get("title", topic_id),
|
|
395
|
+
"status": "pending",
|
|
396
|
+
"passes_attempted": 0,
|
|
397
|
+
"last_pass_id": "",
|
|
398
|
+
"latest_summary": "",
|
|
399
|
+
"unresolved_questions": [],
|
|
400
|
+
"source_ids": [],
|
|
401
|
+
"updated_at": "",
|
|
402
|
+
}
|
|
403
|
+
for topic_id, topic in topic_index.items()
|
|
404
|
+
}
|
|
405
|
+
execution["summary"] = {
|
|
406
|
+
"topic_total": len(topic_index),
|
|
407
|
+
"topic_complete": 0,
|
|
408
|
+
"topic_needs_followup": 0,
|
|
409
|
+
"topic_pending": len(topic_index),
|
|
410
|
+
"pass_pending": 0,
|
|
411
|
+
"pass_complete": 0,
|
|
412
|
+
"next_pass_id": "",
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
normalized["research_execution"] = execution
|
|
416
|
+
return normalized
|
|
417
|
+
|
|
418
|
+
|
|
112
419
|
def _coerce_entity_refs(raw: Any) -> tuple[list[str], dict[str, str]]:
|
|
113
420
|
refs = []
|
|
114
421
|
if isinstance(raw, (list, tuple, set)):
|
|
@@ -7,7 +7,11 @@ import subprocess
|
|
|
7
7
|
import sys
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
|
|
10
|
-
from ideation_research import
|
|
10
|
+
from ideation_research import (
|
|
11
|
+
ResearchAgendaValidationError,
|
|
12
|
+
normalize_ideation_research,
|
|
13
|
+
reset_research_execution,
|
|
14
|
+
)
|
|
11
15
|
from workflow_state import default_data, reconcile_workflow_state
|
|
12
16
|
|
|
13
17
|
|
|
@@ -82,6 +86,7 @@ def parse_payload(args):
|
|
|
82
86
|
|
|
83
87
|
def apply_completion_state(data, completion_state):
|
|
84
88
|
state = data.setdefault("state", {})
|
|
89
|
+
state["research-completed"] = False
|
|
85
90
|
if completion_state == "complete":
|
|
86
91
|
state["ideation-completed"] = True
|
|
87
92
|
elif completion_state == "incomplete":
|
|
@@ -136,6 +141,7 @@ def main():
|
|
|
136
141
|
data.get("ideation", {}),
|
|
137
142
|
require_topics=require_research_topics,
|
|
138
143
|
)
|
|
144
|
+
data["ideation"] = reset_research_execution(data.get("ideation", {}))
|
|
139
145
|
except ResearchAgendaValidationError as exc:
|
|
140
146
|
print(str(exc), file=sys.stderr)
|
|
141
147
|
return 2
|