@researai/deepscientist 1.5.15 → 1.5.17

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.
Files changed (202) hide show
  1. package/README.md +385 -104
  2. package/bin/ds.js +1241 -110
  3. package/docs/en/00_QUICK_START.md +100 -19
  4. package/docs/en/01_SETTINGS_REFERENCE.md +34 -1
  5. package/docs/en/02_START_RESEARCH_GUIDE.md +7 -0
  6. package/docs/en/05_TUI_GUIDE.md +6 -0
  7. package/docs/en/06_RUNTIME_AND_CANVAS.md +4 -3
  8. package/docs/en/09_DOCTOR.md +25 -8
  9. package/docs/en/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +63 -13
  10. package/docs/en/15_CODEX_PROVIDER_SETUP.md +37 -11
  11. package/docs/en/19_EXTERNAL_CONTROLLER_GUIDE.md +226 -0
  12. package/docs/en/19_LOCAL_BROWSER_AUTH.md +70 -0
  13. package/docs/en/20_WORKSPACE_MODES_GUIDE.md +250 -0
  14. package/docs/en/21_LOCAL_MODEL_BACKENDS_GUIDE.md +283 -0
  15. package/docs/en/91_DEVELOPMENT.md +237 -0
  16. package/docs/en/README.md +24 -2
  17. package/docs/zh/00_QUICK_START.md +89 -19
  18. package/docs/zh/01_SETTINGS_REFERENCE.md +34 -1
  19. package/docs/zh/02_START_RESEARCH_GUIDE.md +7 -0
  20. package/docs/zh/05_TUI_GUIDE.md +6 -0
  21. package/docs/zh/09_DOCTOR.md +26 -9
  22. package/docs/zh/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +63 -13
  23. package/docs/zh/15_CODEX_PROVIDER_SETUP.md +37 -11
  24. package/docs/zh/19_EXTERNAL_CONTROLLER_GUIDE.md +226 -0
  25. package/docs/zh/19_LOCAL_BROWSER_AUTH.md +68 -0
  26. package/docs/zh/20_WORKSPACE_MODES_GUIDE.md +251 -0
  27. package/docs/zh/21_LOCAL_MODEL_BACKENDS_GUIDE.md +281 -0
  28. package/docs/zh/README.md +24 -2
  29. package/install.sh +46 -4
  30. package/package.json +2 -1
  31. package/pyproject.toml +1 -1
  32. package/src/deepscientist/__init__.py +1 -1
  33. package/src/deepscientist/acp/envelope.py +6 -0
  34. package/src/deepscientist/artifact/service.py +647 -22
  35. package/src/deepscientist/bash_exec/service.py +234 -9
  36. package/src/deepscientist/bridges/connectors.py +8 -2
  37. package/src/deepscientist/cli.py +115 -19
  38. package/src/deepscientist/codex_cli_compat.py +367 -22
  39. package/src/deepscientist/config/models.py +2 -1
  40. package/src/deepscientist/config/service.py +183 -13
  41. package/src/deepscientist/daemon/api/handlers.py +255 -31
  42. package/src/deepscientist/daemon/api/router.py +9 -0
  43. package/src/deepscientist/daemon/app.py +1146 -105
  44. package/src/deepscientist/diagnostics/__init__.py +6 -0
  45. package/src/deepscientist/diagnostics/runner_failures.py +130 -0
  46. package/src/deepscientist/doctor.py +207 -3
  47. package/src/deepscientist/gitops/__init__.py +10 -1
  48. package/src/deepscientist/gitops/diff.py +129 -0
  49. package/src/deepscientist/gitops/service.py +4 -1
  50. package/src/deepscientist/mcp/server.py +39 -0
  51. package/src/deepscientist/prompts/builder.py +275 -34
  52. package/src/deepscientist/quest/layout.py +15 -2
  53. package/src/deepscientist/quest/service.py +707 -55
  54. package/src/deepscientist/quest/stage_views.py +6 -1
  55. package/src/deepscientist/runners/codex.py +143 -43
  56. package/src/deepscientist/shared.py +19 -0
  57. package/src/deepscientist/skills/__init__.py +2 -2
  58. package/src/deepscientist/skills/installer.py +196 -5
  59. package/src/deepscientist/skills/registry.py +66 -0
  60. package/src/prompts/connectors/qq.md +18 -8
  61. package/src/prompts/connectors/weixin.md +16 -6
  62. package/src/prompts/contracts/shared_interaction.md +14 -2
  63. package/src/prompts/system.md +23 -5
  64. package/src/prompts/system_copilot.md +56 -0
  65. package/src/skills/analysis-campaign/SKILL.md +1 -0
  66. package/src/skills/baseline/SKILL.md +8 -0
  67. package/src/skills/decision/SKILL.md +8 -0
  68. package/src/skills/experiment/SKILL.md +8 -0
  69. package/src/skills/figure-polish/SKILL.md +1 -0
  70. package/src/skills/finalize/SKILL.md +1 -0
  71. package/src/skills/idea/SKILL.md +1 -0
  72. package/src/skills/intake-audit/SKILL.md +8 -0
  73. package/src/skills/mentor/SKILL.md +217 -0
  74. package/src/skills/mentor/references/correction-rules.md +210 -0
  75. package/src/skills/mentor/references/knowledge-profile.md +91 -0
  76. package/src/skills/mentor/references/persona-profile.md +138 -0
  77. package/src/skills/mentor/references/taste-profile.md +128 -0
  78. package/src/skills/mentor/references/thought-style-profile.md +138 -0
  79. package/src/skills/mentor/references/work-profile.md +289 -0
  80. package/src/skills/mentor/references/workflow-profile.md +240 -0
  81. package/src/skills/optimize/SKILL.md +1 -0
  82. package/src/skills/rebuttal/SKILL.md +1 -0
  83. package/src/skills/review/SKILL.md +1 -0
  84. package/src/skills/scout/SKILL.md +8 -0
  85. package/src/skills/write/SKILL.md +1 -0
  86. package/src/tui/dist/app/AppContainer.js +19 -11
  87. package/src/tui/dist/index.js +4 -1
  88. package/src/tui/dist/lib/api.js +33 -3
  89. package/src/tui/package.json +1 -1
  90. package/src/ui/dist/assets/AiManusChatView-Bv-Z8YpU.js +204 -0
  91. package/src/ui/dist/assets/AnalysisPlugin-BCKAfjba.js +1 -0
  92. package/src/ui/dist/assets/CliPlugin-BCKcpc35.js +109 -0
  93. package/src/ui/dist/assets/CodeEditorPlugin-DbOfSJ8K.js +2 -0
  94. package/src/ui/dist/assets/CodeViewerPlugin-CbaFRrUU.js +270 -0
  95. package/src/ui/dist/assets/DocViewerPlugin-DAjLVeQD.js +7 -0
  96. package/src/ui/dist/assets/GitCommitViewerPlugin-CIUqbUDO.js +1 -0
  97. package/src/ui/dist/assets/GitDiffViewerPlugin-CQACjoAA.js +6 -0
  98. package/src/ui/dist/assets/GitSnapshotViewer-0r4nLPke.js +30 -0
  99. package/src/ui/dist/assets/ImageViewerPlugin-nBOmI2v_.js +26 -0
  100. package/src/ui/dist/assets/LabCopilotPanel-BHxOxF4z.js +14 -0
  101. package/src/ui/dist/assets/LabPlugin-BKoZGs95.js +22 -0
  102. package/src/ui/dist/assets/LatexPlugin-ZwtV8pIp.js +25 -0
  103. package/src/ui/dist/assets/MarkdownViewerPlugin-DKqVfKyW.js +128 -0
  104. package/src/ui/dist/assets/MarketplacePlugin-BwxStZ9D.js +13 -0
  105. package/src/ui/dist/assets/NotebookEditor-BEQhaQbt.js +81 -0
  106. package/src/ui/dist/assets/{NotebookEditor-CccQYZjX.css → NotebookEditor-BHH8rdGj.css} +1 -1
  107. package/src/ui/dist/assets/NotebookEditor-BOr3x3Ej.css +1 -0
  108. package/src/ui/dist/assets/NotebookEditor-DB9N_T9q.js +361 -0
  109. package/src/ui/dist/assets/PdfLoader-Cy5jtWrr.css +1 -0
  110. package/src/ui/dist/assets/PdfLoader-eWBONbQP.js +16 -0
  111. package/src/ui/dist/assets/PdfMarkdownPlugin-D22YOZL3.js +1 -0
  112. package/src/ui/dist/assets/PdfViewerPlugin-c-RK9DLM.js +17 -0
  113. package/src/ui/dist/assets/PdfViewerPlugin-nwwE-fjJ.css +1 -0
  114. package/src/ui/dist/assets/SearchPlugin-CxF9ytAx.js +16 -0
  115. package/src/ui/dist/assets/SearchPlugin-DA4en4hK.css +1 -0
  116. package/src/ui/dist/assets/TextViewerPlugin-C5xqeeUH.js +54 -0
  117. package/src/ui/dist/assets/VNCViewer-BoLGLnHz.js +11 -0
  118. package/src/ui/dist/assets/bot-DREQOxzP.js +6 -0
  119. package/src/ui/dist/assets/browser-CTB2jwNe.js +8 -0
  120. package/src/ui/dist/assets/chevron-up-C9Qpx4DE.js +6 -0
  121. package/src/ui/dist/assets/code-WlFHE7z_.js +6 -0
  122. package/src/ui/dist/assets/file-content-BZMz3RYp.js +1 -0
  123. package/src/ui/dist/assets/file-diff-panel-CQhw0jS2.js +1 -0
  124. package/src/ui/dist/assets/file-jump-queue-DA-SdG__.js +1 -0
  125. package/src/ui/dist/assets/file-socket-CfQPKQKj.js +1 -0
  126. package/src/ui/dist/assets/git-commit-horizontal-DxZ8DCZh.js +6 -0
  127. package/src/ui/dist/assets/image-Bgl4VIyx.js +6 -0
  128. package/src/ui/dist/assets/index-BpV6lusQ.css +33 -0
  129. package/src/ui/dist/assets/index-CBNVuWcP.js +2496 -0
  130. package/src/ui/dist/assets/index-CwNu1aH4.js +11 -0
  131. package/src/ui/dist/assets/index-DrUnlf6K.js +1 -0
  132. package/src/ui/dist/assets/index-NW-h8VzN.js +1 -0
  133. package/src/ui/dist/assets/monaco-CiHMMNH_.js +1 -0
  134. package/src/ui/dist/assets/pdf-effect-queue-J8OnM0jE.js +6 -0
  135. package/src/ui/dist/assets/plugin-monaco-C8UgLomw.js +19 -0
  136. package/src/ui/dist/assets/plugin-notebook-HbW2K-1c.js +169 -0
  137. package/src/ui/dist/assets/plugin-pdf-CR8hgQBV.js +357 -0
  138. package/src/ui/dist/assets/plugin-terminal-MXFIPun8.js +227 -0
  139. package/src/ui/dist/assets/popover-CLc0pPP8.js +1 -0
  140. package/src/ui/dist/assets/project-sync-C9IdzdZW.js +1 -0
  141. package/src/ui/dist/assets/select-Cs2PmzwL.js +11 -0
  142. package/src/ui/dist/assets/sigma-ClKcHAXm.js +6 -0
  143. package/src/ui/dist/assets/trash-DwpbFr3w.js +11 -0
  144. package/src/ui/dist/assets/useCliAccess-NQ8m0Let.js +1 -0
  145. package/src/ui/dist/assets/useFileDiffOverlay-FuhcnKiw.js +1 -0
  146. package/src/ui/dist/assets/wrap-text-BC-Hltpd.js +11 -0
  147. package/src/ui/dist/assets/zoom-out-E_gaeAxL.js +11 -0
  148. package/src/ui/dist/index.html +5 -2
  149. package/src/ui/dist/assets/AiManusChatView-DDjbFnbt.js +0 -26597
  150. package/src/ui/dist/assets/AnalysisPlugin-Yb5IdmaU.js +0 -123
  151. package/src/ui/dist/assets/CliPlugin-e64sreyu.js +0 -31037
  152. package/src/ui/dist/assets/CodeEditorPlugin-C4D2TIkU.js +0 -427
  153. package/src/ui/dist/assets/CodeViewerPlugin-BVoNZIvC.js +0 -905
  154. package/src/ui/dist/assets/DocViewerPlugin-CLChbllo.js +0 -278
  155. package/src/ui/dist/assets/GitDiffViewerPlugin-C4xeFyFQ.js +0 -2661
  156. package/src/ui/dist/assets/ImageViewerPlugin-OiMUAcLi.js +0 -500
  157. package/src/ui/dist/assets/LabCopilotPanel-BjD2ThQF.js +0 -4104
  158. package/src/ui/dist/assets/LabPlugin-DQPg-NrB.js +0 -2677
  159. package/src/ui/dist/assets/LatexPlugin-CI05XAV9.js +0 -1792
  160. package/src/ui/dist/assets/MarkdownViewerPlugin-DpeBLYZf.js +0 -308
  161. package/src/ui/dist/assets/MarketplacePlugin-DolE58Q2.js +0 -413
  162. package/src/ui/dist/assets/NotebookEditor-7Qm2rSWD.js +0 -4214
  163. package/src/ui/dist/assets/NotebookEditor-C1kWaxKi.js +0 -84873
  164. package/src/ui/dist/assets/NotebookEditor-C3VQ7ylN.css +0 -1405
  165. package/src/ui/dist/assets/PdfLoader-BfOHw8Zw.js +0 -25468
  166. package/src/ui/dist/assets/PdfLoader-C-Y707R3.css +0 -49
  167. package/src/ui/dist/assets/PdfMarkdownPlugin-BulDREv1.js +0 -409
  168. package/src/ui/dist/assets/PdfViewerPlugin-C-daaOaL.js +0 -3095
  169. package/src/ui/dist/assets/PdfViewerPlugin-DQ11QcSf.css +0 -3627
  170. package/src/ui/dist/assets/SearchPlugin-CjpaiJ3A.js +0 -741
  171. package/src/ui/dist/assets/SearchPlugin-DDMrGDkh.css +0 -379
  172. package/src/ui/dist/assets/TextViewerPlugin-BxIyqPQC.js +0 -472
  173. package/src/ui/dist/assets/VNCViewer-HAg9mF7M.js +0 -18821
  174. package/src/ui/dist/assets/awareness-C0NPR2Dj.js +0 -292
  175. package/src/ui/dist/assets/bot-0DYntytV.js +0 -21
  176. package/src/ui/dist/assets/browser-BAcuE0Xj.js +0 -2895
  177. package/src/ui/dist/assets/code-B20Slj_w.js +0 -17
  178. package/src/ui/dist/assets/file-content-DT24KFma.js +0 -377
  179. package/src/ui/dist/assets/file-diff-panel-DK13YPql.js +0 -92
  180. package/src/ui/dist/assets/file-jump-queue-r5XKgJEV.js +0 -16
  181. package/src/ui/dist/assets/file-socket-B4T2o4nR.js +0 -58
  182. package/src/ui/dist/assets/function-B5QZkkHC.js +0 -1895
  183. package/src/ui/dist/assets/image-DSeR_sDS.js +0 -18
  184. package/src/ui/dist/assets/index-BrFje2Uk.js +0 -120
  185. package/src/ui/dist/assets/index-BwRJaoTl.js +0 -25
  186. package/src/ui/dist/assets/index-D_E4281X.js +0 -221322
  187. package/src/ui/dist/assets/index-DnYB3xb1.js +0 -159
  188. package/src/ui/dist/assets/index-G7AcWcMu.css +0 -12594
  189. package/src/ui/dist/assets/monaco-LExaAN3Y.js +0 -623
  190. package/src/ui/dist/assets/pdf-effect-queue-BJk5okWJ.js +0 -47
  191. package/src/ui/dist/assets/pdf_viewer-e0g1is2C.js +0 -8206
  192. package/src/ui/dist/assets/popover-D3Gg_FoV.js +0 -476
  193. package/src/ui/dist/assets/project-sync-C_ygLlVU.js +0 -297
  194. package/src/ui/dist/assets/select-CpAK6uWm.js +0 -1690
  195. package/src/ui/dist/assets/sigma-DEccaSgk.js +0 -22
  196. package/src/ui/dist/assets/square-check-big-uUfyVsbD.js +0 -17
  197. package/src/ui/dist/assets/trash-CXvwwSe8.js +0 -32
  198. package/src/ui/dist/assets/useCliAccess-Bnop4mgR.js +0 -957
  199. package/src/ui/dist/assets/useFileDiffOverlay-B8eUAX0I.js +0 -53
  200. package/src/ui/dist/assets/wrap-text-9vbOBpkW.js +0 -35
  201. package/src/ui/dist/assets/yjs-DncrqiZ8.js +0 -11243
  202. package/src/ui/dist/assets/zoom-out-BgVMmOW4.js +0 -34
@@ -6,30 +6,21 @@ from pathlib import Path
6
6
 
7
7
  from ..connector_runtime import normalize_conversation_id, parse_conversation_id
8
8
  from ..config import ConfigManager
9
+ from ..home import repo_root
9
10
  from ..memory import MemoryService
10
11
  from ..memory.frontmatter import load_markdown_document
11
12
  from ..quest import QuestService
12
13
  from ..registries import BaselineRegistry
13
14
  from ..shared import read_json, read_text, read_yaml
15
+ from ..skills import SkillInstaller, companion_skill_ids, stage_skill_ids
14
16
 
15
- STANDARD_SKILLS = (
16
- "scout",
17
- "baseline",
18
- "idea",
19
- "optimize",
20
- "experiment",
21
- "analysis-campaign",
22
- "write",
23
- "finalize",
24
- "decision",
25
- )
26
-
27
- COMPANION_SKILLS = (
28
- "figure-polish",
29
- "intake-audit",
30
- "review",
31
- "rebuttal",
32
- )
17
+ # Backward-compatible snapshots for modules or tests that still import these names directly.
18
+ # Runtime routing should call `current_standard_skills(...)` / `current_companion_skills(...)`.
19
+ STANDARD_SKILLS = stage_skill_ids(repo_root())
20
+
21
+ _AUTO_CONTINUE_MONITOR_INTERVAL_SECONDS = 240
22
+
23
+ COMPANION_SKILLS = companion_skill_ids(repo_root())
33
24
 
34
25
  STAGE_MEMORY_PLAN = {
35
26
  "scout": {
@@ -71,6 +62,14 @@ STAGE_MEMORY_PLAN = {
71
62
  }
72
63
 
73
64
 
65
+ def current_standard_skills(repo_root_path: Path | None = None) -> tuple[str, ...]:
66
+ return stage_skill_ids(repo_root_path or repo_root())
67
+
68
+
69
+ def current_companion_skills(repo_root_path: Path | None = None) -> tuple[str, ...]:
70
+ return companion_skill_ids(repo_root_path or repo_root())
71
+
72
+
74
73
  def classify_turn_intent(user_message: str) -> str:
75
74
  text = str(user_message or "").strip()
76
75
  if not text:
@@ -104,13 +103,15 @@ def classify_turn_intent(user_message: str) -> str:
104
103
 
105
104
 
106
105
  class PromptBuilder:
107
- def __init__(self, repo_root: Path, home: Path) -> None:
106
+ def __init__(self, repo_root: Path, home: Path, *, prompt_version_selection: str | None = None) -> None:
108
107
  self.repo_root = repo_root
109
108
  self.home = home
110
109
  self.quest_service = QuestService(home)
111
110
  self.memory_service = MemoryService(home)
112
111
  self.baseline_registry = BaselineRegistry(home)
113
112
  self.config_manager = ConfigManager(home)
113
+ self.skill_installer = SkillInstaller(repo_root, home)
114
+ self.prompt_version_selection = str(prompt_version_selection or "").strip() or None
114
115
 
115
116
  def build(
116
117
  self,
@@ -128,9 +129,14 @@ class PromptBuilder:
128
129
  runtime_config = self.config_manager.load_named("config")
129
130
  connectors_config = self.config_manager.load_named_normalized("connectors")
130
131
  quest_root = Path(snapshot["quest_root"])
132
+ self.skill_installer.sync_quest_prompts(quest_root)
131
133
  active_anchor = str(snapshot.get("active_anchor") or skill_id)
132
134
  default_locale = str(runtime_config.get("default_locale") or "en-US")
133
- system_block = self._prompt_fragment("system.md", quest_root=quest_root)
135
+ workspace_mode = self._workspace_mode(snapshot)
136
+ system_block = self._prompt_fragment(
137
+ "system_copilot.md" if workspace_mode == "copilot" else "system.md",
138
+ quest_root=quest_root,
139
+ )
134
140
  shared_interaction_block = self._prompt_fragment(
135
141
  Path("contracts") / "shared_interaction.md",
136
142
  quest_root=quest_root,
@@ -159,7 +165,7 @@ class PromptBuilder:
159
165
  f"conversation_id: quest:{quest_id}",
160
166
  f"default_locale: {default_locale}",
161
167
  "built_in_mcp_namespaces: memory, artifact, bash_exec",
162
- "mcp_namespace_note: any shell-like command execution must use bash_exec, including curl/python/bash/node and similar CLI tools; do not use transient shell snippets.",
168
+ "mcp_namespace_note: **any shell-like command execution must use `bash_exec(...)`, including curl/python/bash/node/git/npm/uv and similar CLI tools; do not use native `shell_command` / `command_execution`.**",
163
169
  "",
164
170
  "Canonical stage skills root:",
165
171
  str((self.repo_root / "src" / "skills").resolve()),
@@ -229,6 +235,14 @@ class PromptBuilder:
229
235
  "## Recovery Resume Packet",
230
236
  self._recovery_resume_block(snapshot=snapshot, turn_reason=turn_reason),
231
237
  "",
238
+ "## Resume Context Spine",
239
+ self._resume_context_spine_block(
240
+ quest_id=quest_id,
241
+ quest_root=quest_root,
242
+ snapshot=snapshot,
243
+ turn_reason=turn_reason,
244
+ ),
245
+ "",
232
246
  "## Interaction Style",
233
247
  self._interaction_style_block(default_locale=default_locale, user_message=user_message, snapshot=snapshot),
234
248
  "",
@@ -486,8 +500,14 @@ class PromptBuilder:
486
500
  ]
487
501
  )
488
502
  if str(turn_reason or "").strip() == "auto_continue":
489
- lines.append(
490
- "- auto_continue_rule: this turn has no new user message; continue from the active requirements, durable artifacts, and current quest state instead of replaying the previous user message"
503
+ lines.extend(
504
+ [
505
+ "- auto_continue_rule: this turn has no new user message; continue from the active requirements, durable artifacts, current quest state, and resume context spine instead of replaying the previous user message",
506
+ f"- auto_continue_interval_rule: when a real long-running external task is already active, background-progress auto-continue becomes a low-frequency monitoring pass, about every {_AUTO_CONTINUE_MONITOR_INTERVAL_SECONDS} seconds rather than sub-minute polling",
507
+ "- auto_continue_fast_prepare_rule: in autonomous mode before a real external long-running task exists, auto-continue may advance quickly, around 0.2 seconds between turns, so the agent can keep preparing or launching the real work without idling",
508
+ "- autonomous_prepare_rule: in autonomous mode, if no real long-running external task is active yet, use the next turns to keep preparing, launching, or durably deciding the next real unit of work instead of parking idly",
509
+ "- copilot_park_rule: in copilot mode, once the current requested unit is complete, it is normal to park and wait for the next user message or `/resume` instead of continuing autonomously",
510
+ ]
491
511
  )
492
512
  else:
493
513
  lines.append(
@@ -614,6 +634,76 @@ class PromptBuilder:
614
634
  lines.append(f"- remaining_attachment_count: {len(attachments) - 6}")
615
635
  return "\n".join(lines)
616
636
 
637
+ def _resume_context_spine_block(self, *, quest_id: str, quest_root: Path, snapshot: dict, turn_reason: str) -> str:
638
+ if str(turn_reason or "").strip() != "auto_continue":
639
+ return "- none"
640
+ lines = [
641
+ "- resume_spine_rule: on auto_continue turns, first continue from the latest durable user requirement, the latest assistant checkpoint, the latest run summary, and recent memory cues instead of reconstructing intent from scratch",
642
+ ]
643
+ bash_running_count = int(((snapshot.get("counts") or {}).get("bash_running_count")) or 0)
644
+ latest_bash_session = (
645
+ dict((snapshot.get("summary") or {}).get("latest_bash_session") or {})
646
+ if isinstance((snapshot.get("summary") or {}).get("latest_bash_session"), dict)
647
+ else {}
648
+ )
649
+ lines.append(f"- active_bash_exec_run_count: {bash_running_count}")
650
+ if latest_bash_session:
651
+ command_preview = " ".join(str(latest_bash_session.get("command") or "").split())
652
+ if len(command_preview) > 180:
653
+ command_preview = command_preview[:177].rstrip() + "..."
654
+ lines.append(
655
+ f"- latest_bash_exec_session: bash_id={str(latest_bash_session.get('bash_id') or 'none')} | "
656
+ f"status={str(latest_bash_session.get('status') or 'unknown')} | "
657
+ f"command={command_preview or 'none'}"
658
+ )
659
+ latest_user = self._latest_user_message(quest_id)
660
+ if latest_user is not None:
661
+ preview = " ".join(str(latest_user.get("content") or "").split())
662
+ if len(preview) > 320:
663
+ preview = preview[:317].rstrip() + "..."
664
+ lines.append(
665
+ f"- latest_user_message: {str(latest_user.get('created_at') or 'unknown')} | "
666
+ f"source={str(latest_user.get('source') or 'unknown')} | "
667
+ f"reply_to={str(latest_user.get('reply_to_interaction_id') or 'none')} | "
668
+ f"preview={preview or 'none'}"
669
+ )
670
+ latest_assistant = self._latest_assistant_message(quest_id)
671
+ if latest_assistant is not None:
672
+ preview = " ".join(str(latest_assistant.get("content") or "").split())
673
+ if len(preview) > 360:
674
+ preview = preview[:357].rstrip() + "..."
675
+ lines.append(
676
+ f"- latest_assistant_checkpoint: {str(latest_assistant.get('created_at') or 'unknown')} | "
677
+ f"skill={str(latest_assistant.get('skill_id') or 'none')} | "
678
+ f"run_id={str(latest_assistant.get('run_id') or 'none')} | "
679
+ f"preview={preview or 'none'}"
680
+ )
681
+ latest_run = self._latest_run_result(quest_root)
682
+ if latest_run is not None:
683
+ preview = " ".join(str(latest_run.get("preview") or "").split())
684
+ if len(preview) > 360:
685
+ preview = preview[:357].rstrip() + "..."
686
+ lines.append(
687
+ f"- latest_run_result: {str(latest_run.get('completed_at') or 'unknown')} | "
688
+ f"run_id={str(latest_run.get('run_id') or 'none')} | "
689
+ f"exit_code={latest_run.get('exit_code') if latest_run.get('exit_code') is not None else 'none'} | "
690
+ f"preview={preview or 'none'}"
691
+ )
692
+ recent_memory = self.memory_service.list_recent(scope="quest", quest_root=quest_root, limit=3)
693
+ if recent_memory:
694
+ lines.append("- recent_memory_cues:")
695
+ for item in recent_memory:
696
+ title = str(item.get("title") or "memory").strip() or "memory"
697
+ card_type = str(item.get("type") or "memory").strip() or "memory"
698
+ excerpt = " ".join(str(item.get("excerpt") or "").split())
699
+ if len(excerpt) > 200:
700
+ excerpt = excerpt[:197].rstrip() + "..."
701
+ lines.append(f" - [{card_type}] {title}: {excerpt or 'no excerpt'}")
702
+ else:
703
+ lines.append("- recent_memory_cues: none")
704
+ lines.append("- resume_spine_conflict_rule: if these spine items conflict with newer durable files or artifacts, trust the newer durable state and update the summary rather than replaying the older plan verbatim")
705
+ return "\n".join(lines)
706
+
617
707
  def _retry_recovery_block(self, retry_context: dict | None) -> str:
618
708
  if not isinstance(retry_context, dict) or not retry_context:
619
709
  return "- none"
@@ -726,6 +816,19 @@ class PromptBuilder:
726
816
  def _prompt_path(self, relative_path: str | Path, *, quest_root: Path | None = None) -> Path:
727
817
  normalized = Path(relative_path)
728
818
  if quest_root is not None:
819
+ selected_version = str(self.prompt_version_selection or "").strip()
820
+ if selected_version and selected_version not in {"latest", "current", "active"}:
821
+ selected_root = self.skill_installer.resolve_prompt_version_root(quest_root, selected_version)
822
+ if selected_root is None:
823
+ raise FileNotFoundError(
824
+ f"Prompt version `{selected_version}` is unavailable for quest `{quest_root.name}`."
825
+ )
826
+ selected_path = selected_root / normalized
827
+ if not selected_path.exists():
828
+ raise FileNotFoundError(
829
+ f"Prompt version `{selected_version}` does not include `{normalized.as_posix()}` for quest `{quest_root.name}`."
830
+ )
831
+ return selected_path
729
832
  quest_path = quest_root / ".codex" / "prompts" / normalized
730
833
  if quest_path.exists():
731
834
  return quest_path
@@ -737,16 +840,45 @@ class PromptBuilder:
737
840
  return item
738
841
  return None
739
842
 
843
+ def _latest_assistant_message(self, quest_id: str) -> dict | None:
844
+ for item in reversed(self.quest_service.history(quest_id, limit=120)):
845
+ if str(item.get("role") or "") == "assistant":
846
+ return item
847
+ return None
848
+
849
+ @staticmethod
850
+ def _latest_run_result(quest_root: Path) -> dict[str, object] | None:
851
+ runs_root = quest_root / ".ds" / "runs"
852
+ if not runs_root.exists():
853
+ return None
854
+ candidates = [path for path in runs_root.glob("*/result.json") if path.is_file()]
855
+ if not candidates:
856
+ return None
857
+ latest = max(candidates, key=lambda path: path.stat().st_mtime)
858
+ payload = read_json(latest, {})
859
+ if not isinstance(payload, dict):
860
+ return None
861
+ preview = (
862
+ str(payload.get("output_text") or "").strip()
863
+ or str(payload.get("stderr_text") or "").strip()
864
+ )
865
+ return {
866
+ "run_id": latest.parent.name,
867
+ "completed_at": str(payload.get("completed_at") or "").strip() or None,
868
+ "exit_code": payload.get("exit_code"),
869
+ "preview": preview,
870
+ }
871
+
740
872
  def _skill_paths_block(self) -> str:
741
873
  lines = []
742
- for skill_id in STANDARD_SKILLS:
874
+ for skill_id in current_standard_skills(self.repo_root):
743
875
  primary = (self.repo_root / "src" / "skills" / skill_id / "SKILL.md").resolve()
744
876
  lines.append(f"- {skill_id}: primary={primary}")
745
877
  return "\n".join(lines)
746
878
 
747
879
  def _companion_skill_paths_block(self) -> str:
748
880
  lines = []
749
- for skill_id in COMPANION_SKILLS:
881
+ for skill_id in current_companion_skills(self.repo_root):
750
882
  primary = (self.repo_root / "src" / "skills" / skill_id / "SKILL.md").resolve()
751
883
  lines.append(f"- {skill_id}: primary={primary}")
752
884
  return "\n".join(lines)
@@ -760,6 +892,18 @@ class PromptBuilder:
760
892
  return value
761
893
  return True
762
894
 
895
+ @staticmethod
896
+ def _workspace_mode(snapshot: dict) -> str:
897
+ value = str(snapshot.get("workspace_mode") or "").strip().lower()
898
+ if value in {"copilot", "autonomous"}:
899
+ return value
900
+ startup_contract = snapshot.get("startup_contract")
901
+ if isinstance(startup_contract, dict):
902
+ value = str(startup_contract.get("workspace_mode") or "").strip().lower()
903
+ if value in {"copilot", "autonomous"}:
904
+ return value
905
+ return "autonomous"
906
+
763
907
  @staticmethod
764
908
  def _decision_policy(snapshot: dict) -> str:
765
909
  startup_contract = snapshot.get("startup_contract")
@@ -824,6 +968,18 @@ class PromptBuilder:
824
968
  return "none"
825
969
 
826
970
  def _research_delivery_policy_block(self, snapshot: dict) -> str:
971
+ if self._workspace_mode(snapshot) == "copilot":
972
+ return "\n".join(
973
+ [
974
+ "- workspace_mode: copilot",
975
+ "- delivery_goal: complete the user-requested unit of work instead of forcing the full research graph by default.",
976
+ "- task_scope_rule: arbitrary research tasks such as reading, coding, debugging, experiment design, run inspection, analysis, writing, and planning can all be handled directly in this mode.",
977
+ "- autonomy_boundary: only expand into longer autonomous continuation when the user explicitly asks for end-to-end or unattended progress.",
978
+ "- routing_rule: open only the skills actually needed for the current request.",
979
+ "- durability_rule: keep important plan, evidence, decisions, and outputs durable in quest files or artifacts so later turns can resume cleanly.",
980
+ "- completion_rule: after the requested unit is complete, summarize what changed and stop instead of auto-continuing.",
981
+ ]
982
+ )
827
983
  need_research_paper = self._need_research_paper(snapshot)
828
984
  launch_mode = self._launch_mode(snapshot)
829
985
  standard_profile = self._standard_profile(snapshot)
@@ -1029,6 +1185,41 @@ class PromptBuilder:
1029
1185
  def _interaction_style_block(self, *, default_locale: str, user_message: str, snapshot: dict) -> str:
1030
1186
  normalized_locale = str(default_locale or "").lower()
1031
1187
  chinese_turn = normalized_locale.startswith("zh") or bool(re.search(r"[\u4e00-\u9fff]", user_message))
1188
+ if self._workspace_mode(snapshot) == "copilot":
1189
+ lines = [
1190
+ f"- configured_default_locale: {default_locale}",
1191
+ f"- current_turn_language_bias: {'zh' if chinese_turn else 'en'}",
1192
+ "- collaboration_mode: user-directed copilot",
1193
+ "- freeform_task_rule: if the user asks for a concrete research task, solve that task directly before introducing stage-routing language.",
1194
+ "- requested_skill_hint_rule: in copilot mode, treat `requested_skill` as a lightweight routing hint, not as an instruction to default into `decision` for ordinary direct tasks.",
1195
+ "- turn_self_routing_rule: before substantial work, classify the current turn as `direct_answer`, `direct_action`, `stage_continue`, or `route_decision`.",
1196
+ "- direct_answer_rule: if the user mainly wants an answer or clarification, answer with the narrowest sufficient context and avoid reading large stage state unless needed.",
1197
+ "- direct_action_rule: if the user mainly wants one concrete task, execute the smallest useful unit first and do not expand into background research continuation in the same turn unless the user asked for it.",
1198
+ "- stage_continue_rule: if the user mainly wants the quest to keep moving, continue from the active durable stage state after acknowledging the request.",
1199
+ "- route_decision_rule: switch into `decision`-style reasoning only when safe continuation depends on a real route, scope, cost, branch, or scientific-direction judgment.",
1200
+ "- decision_skill_escalation_rule: if a turn upgrades into `route_decision`, explicitly read the `decision` skill before substantial route-changing work.",
1201
+ "- response_pattern: say what changed -> say what it means -> say what happens next",
1202
+ "- mailbox_protocol: artifact.interact(include_recent_inbound_messages=True) remains the queued human-message mailbox and should be checked whenever human continuity matters.",
1203
+ "- planning_rule: before non-trivial execution, make the immediate plan explicit and keep the first step small.",
1204
+ "- tool_rule: use memory for durable recall, artifact for quest state and git-aware research operations, and bash_exec for terminal execution.",
1205
+ "- copilot_sop_rule: classify the request first, choose the narrowest correct tool path, execute the smallest useful unit, persist the important result, then answer plainly.",
1206
+ "- shell_tool_mandate: **for any shell, CLI, Python, bash, node, git, npm, uv, or environment command execution, use `bash_exec(...)`; do not use native `shell_command` or Codex `command_execution`.**",
1207
+ "- git_tool_mandate: for git work inside the current quest repository or worktree, prefer `artifact.git(...)` before raw shell git commands.",
1208
+ "- git_test_rule: if the user wants a generic git smoke test rather than a quest-repo mutation, use `bash_exec(...)` in an isolated scratch repository.",
1209
+ "- decision_entry_rule: use `decision` only for real route, scope, cost, branch, or scientific-direction judgments; do not default to it for ordinary repo, code, environment, or execution tasks.",
1210
+ "- micro_task_stop_rule: after finishing a `direct_answer` or `direct_action` turn, report the result plainly and wait instead of auto-continuing.",
1211
+ "- stop_rule: once the current requested unit is done, send a concise update and wait for the next message or `/resume`.",
1212
+ "- escalation_rule: if a route change materially affects cost, scope, or scientific direction, ask before proceeding.",
1213
+ ]
1214
+ if chinese_turn:
1215
+ lines.append(
1216
+ "- tone_hint: 使用自然、礼貌、专业、带一点活泼感的中文;像靠谱又主动汇报进展的研究搭子,不要冷冰冰或官话腔;对真实好消息可自然用“都搞定啦”“有结果了”这种轻微庆祝开头,但下一句要立刻说清具体结果。"
1217
+ )
1218
+ else:
1219
+ lines.append(
1220
+ "- tone_hint: use concise, natural, warm English, lead with the conclusion, and avoid sounding cold, bureaucratic, or log-like."
1221
+ )
1222
+ return "\n".join(lines)
1032
1223
  bound_conversations = snapshot.get("bound_conversations") or []
1033
1224
  need_research_paper = self._need_research_paper(snapshot)
1034
1225
  decision_policy = self._decision_policy(snapshot)
@@ -1044,9 +1235,16 @@ class PromptBuilder:
1044
1235
  f"- standard_profile: {standard_profile if launch_mode == 'standard' else 'n/a'}",
1045
1236
  f"- custom_profile: {custom_profile if launch_mode == 'custom' else 'n/a'}",
1046
1237
  "- collaboration_mode: long-horizon, continuity-first, artifact-aware",
1238
+ "- user_turn_self_routing_rule: on a fresh user message, first classify the turn as `direct_answer`, `direct_action`, `stage_continue`, or `route_decision` before reading additional skills or large quest context.",
1239
+ "- direct_answer_rule: if the user mainly wants an answer or clarification, answer with the narrowest sufficient context and avoid reading large stage state unless needed.",
1240
+ "- direct_action_rule: if the user mainly wants one concrete task, execute the smallest useful unit first and do not silently expand into broader autonomous continuation in the same turn unless the user asked for it.",
1241
+ "- stage_continue_rule: if the user is clearly asking to continue quest progress, resume from the active durable stage state.",
1242
+ "- route_decision_rule: open `decision`-style reasoning only when safe continuation genuinely depends on a real route, scope, cost, branch, or scientific-direction judgment.",
1243
+ "- decision_skill_escalation_rule: if a fresh user-message turn upgrades into `route_decision`, explicitly read the `decision` skill before substantial route-changing work.",
1047
1244
  "- response_pattern: say what changed -> say what it means -> say what happens next",
1048
1245
  "- interaction_protocol: first message may be plain conversation; after that, treat artifact.interact threads and mailbox polls as the main continuity spine across TUI, web, and connectors",
1049
1246
  "- shared_interaction_contract_precedence: use the shared interaction contract as the default user-facing cadence; the rules below add runtime-specific execution behavior instead of restating the same chat cadence",
1247
+ "- shell_tool_mandate: **native `shell_command` / `command_execution` is forbidden; all shell-like execution must use `bash_exec(...)`.**",
1050
1248
  "- mailbox_protocol: artifact.interact(include_recent_inbound_messages=True) is the queued human-message mailbox; when it returns user text, treat that input as higher priority than background subtasks until it has been acknowledged",
1051
1249
  "- acknowledgment_protocol: after artifact.interact returns any human message, immediately send one substantive artifact.interact(...) follow-up; if the active connector runtime already emitted a transport-level receipt acknowledgement, do not send a redundant receipt-only message; if answerable, answer directly, otherwise state the short plan, nearest checkpoint, and that the current background subtask is paused",
1052
1250
  "- subtask_boundary_protocol: send a user-visible update whenever the active subtask changes materially, especially across intake -> audit, audit -> experiment planning, experiment planning -> run launch, run result -> drafting, or drafting -> review/rebuttal",
@@ -1055,6 +1253,10 @@ class PromptBuilder:
1055
1253
  "- long_run_reporting_protocol: inspect real logs/status after each meaningful await cycle and at least once every 30 minutes at worst, but only send a user-visible update when there is a human-meaningful delta, blocker, recovery, route change, or the visibility bound would otherwise be exceeded",
1056
1254
  "- intervention_threshold_protocol: do not kill or restart a run merely because a short watch window passed without final completion; intervene only on explicit failure, clear invalidity, process exit, or no meaningful delta across a sufficiently long observation window",
1057
1255
  "- timeout_protocol: before using bash_exec(mode='await', ...), estimate whether the command can finish within the selected wait window; if runtime is uncertain or likely longer, use bash_exec(mode='detach', ...) and monitor instead of guessing a fake deadline",
1256
+ f"- auto_continue_monitoring_protocol: if the runtime schedules background-progress auto_continue turns while a real external task is already active, treat them as low-frequency monitoring passes roughly every {_AUTO_CONTINUE_MONITOR_INTERVAL_SECONDS} seconds rather than as a fast polling loop",
1257
+ "- auto_continue_prepare_protocol: in autonomous mode before a real long-running external task exists, rapid auto-continue passes around 0.2 seconds apart are acceptable only for active preparation, launch, or durable route closure work; they are not a substitute for starting the real task",
1258
+ "- long_run_ownership_protocol: real long-running execution should stay alive in detached bash_exec sessions or the runtime process it launched; do not rely on repeated model turns to simulate continuous execution",
1259
+ "- auto_continue_resume_protocol: on auto_continue turns, read the resume context spine first and continue from the latest durable user requirement, latest assistant checkpoint, latest run summary, recent memory cues, and current bash_exec state before changing route",
1058
1260
  "- blocking_protocol: use reply_mode='blocking' only for true unresolved user decisions; ordinary progress updates should stay threaded and non-blocking",
1059
1261
  "- credential_blocking_protocol: if continuation requires user-supplied external credentials or secrets such as an API key, GitHub key/token, or Hugging Face key/token, emit one structured blocking decision request that asks the user to provide the credential or choose an alternative route; do not invent placeholders or silently skip the blocked step",
1060
1262
  "- credential_wait_protocol: if that credential request remains unanswered, keep the quest waiting rather than self-resolving; if you are resumed without new credentials and no other work is possible, a long low-frequency park such as `bash_exec(command='sleep 3600', mode='await', timeout_seconds=3700)` is acceptable to avoid busy-looping",
@@ -1066,6 +1268,7 @@ class PromptBuilder:
1066
1268
  "- example_and_numbers_protocol: when it materially improves understanding, include one short example or 1 to 3 key numbers or comparisons instead of relying only on vague adjectives such as better, slower, or more stable.",
1067
1269
  "- omission_protocol: for ordinary user-facing updates, omit file paths, file names, artifact ids, branch/worktree ids, session ids, raw commands, raw logs, and internal tool names unless the user asked for them or needs them to act",
1068
1270
  "- compaction_protocol: ordinary artifact.interact progress updates should usually fit in 2 to 4 short sentences and should not read like a monitoring transcript or execution diary",
1271
+ "- micro_task_stop_rule: after a fresh user-message turn that was only `direct_answer` or `direct_action`, finish that unit and do not silently turn the same turn into a broader autonomous stage pass unless the user asked for it.",
1069
1272
  "- watchdog_payload_protocol: if a tool result includes `watchdog_notes`, `progress_watchdog_note`, `visibility_watchdog_note`, or `state_change_watchdog_note`, treat that as an action item to inspect state and decide whether a fresh user-visible update is actually needed; do not emit duplicate progress by reflex",
1070
1273
  "- human_progress_shape_protocol: ordinary progress updates should usually make three things explicit in human language: the current task, the main difficulty or latest real progress, and the concrete next measure you will take",
1071
1274
  "- stage_contract_protocol: stage-specific plan/checklist rules, milestone rules, literature rules, and writing rules belong in the requested skill; do not expect this runtime block to restate them",
@@ -1107,14 +1310,14 @@ class PromptBuilder:
1107
1310
  if chinese_turn:
1108
1311
  lines.extend(
1109
1312
  [
1110
- "- tone_hint: 使用自然、礼貌、专业、偏正式的中文;必要时可自然称呼用户为“老师”,但不要每句重复;避免机械模板腔。",
1313
+ "- tone_hint: 使用自然、礼貌、专业、带一点活泼感的中文;必要时可自然称呼用户为“老师”,但不要每句重复;像靠谱又主动汇报进展的研究搭子,避免冷冰冰、官话化、机械模板腔;对真实好消息可自然用“都搞定啦”“有结果了”这种轻微庆祝开头,但下一句要立刻说清结果。",
1111
1314
  "- connector_reply_hint: 在聊天面里优先简明说明当前状态、下一步动作、预计回传内容。",
1112
1315
  ]
1113
1316
  )
1114
1317
  else:
1115
1318
  lines.extend(
1116
1319
  [
1117
- "- tone_hint: use a polite, professional, gentlemanly English tone.",
1320
+ "- tone_hint: use a polite, professional, warm English tone; avoid sounding cold, bureaucratic, or like a monitoring log.",
1118
1321
  "- connector_reply_hint: keep chat replies concise but operational, with explicit next steps and evidence targets.",
1119
1322
  ]
1120
1323
  )
@@ -1212,13 +1415,51 @@ class PromptBuilder:
1212
1415
  plan = STAGE_MEMORY_PLAN.get(stage, STAGE_MEMORY_PLAN["decision"])
1213
1416
  quest_kinds = ", ".join(plan.get("quest", ())) or "none"
1214
1417
  global_kinds = ", ".join(plan.get("global", ())) or "none"
1215
- return "\n".join(
1216
- [
1217
- f"- stage_memory_rule: for `{stage}`, prefer quest memory kinds [{quest_kinds}] and global memory kinds [{global_kinds}] when memory lookup is needed.",
1218
- "- memory_lookup_tool: call memory.list_recent(...) to recover context after pause/restart and memory.search(...) before repeating prior work.",
1219
- "- memory_injection_rule: memory is intentionally not pre-expanded here; pull only the cards that matter now.",
1220
- ]
1221
- )
1418
+ lines = [
1419
+ f"- stage_memory_rule: for `{stage}`, prefer quest memory kinds [{quest_kinds}] and global memory kinds [{global_kinds}] when memory lookup is needed.",
1420
+ "- memory_lookup_tool: call memory.list_recent(...) to recover context after pause/restart and memory.search(...) before repeating prior work.",
1421
+ "- memory_injection_rule: keep the injected memory compact, but do not drop all continuity on auto_continue turns; reuse a few recent durable cues directly when they materially anchor the next action.",
1422
+ ]
1423
+ selected: list[dict] = []
1424
+ seen_paths: set[str] = set()
1425
+ for kind in plan.get("quest", ())[:2]:
1426
+ for card in self.memory_service.list_recent(scope="quest", quest_root=quest_root, limit=2, kind=kind)[:1]:
1427
+ self._append_priority_memory(
1428
+ selected,
1429
+ seen_paths,
1430
+ card=card,
1431
+ scope="quest",
1432
+ quest_root=quest_root,
1433
+ reason=f"recent quest memory for stage `{stage}`",
1434
+ )
1435
+ for kind in plan.get("global", ())[:2]:
1436
+ for card in self.memory_service.list_recent(scope="global", limit=2, kind=kind)[:1]:
1437
+ self._append_priority_memory(
1438
+ selected,
1439
+ seen_paths,
1440
+ card=card,
1441
+ scope="global",
1442
+ quest_root=quest_root,
1443
+ reason=f"recent global memory for stage `{stage}`",
1444
+ )
1445
+ for query in self._memory_queries(user_message)[:2]:
1446
+ for scope in ("quest", "global"):
1447
+ for card in self.memory_service.search(
1448
+ query,
1449
+ scope=scope if scope == "global" else "quest",
1450
+ quest_root=quest_root if scope == "quest" else None,
1451
+ limit=1,
1452
+ ):
1453
+ self._append_priority_memory(
1454
+ selected,
1455
+ seen_paths,
1456
+ card=card,
1457
+ scope=scope,
1458
+ quest_root=quest_root,
1459
+ reason=f"matched current-turn query `{query}`",
1460
+ )
1461
+ lines.extend(["- selected_memory:", self._format_priority_memory(selected)])
1462
+ return "\n".join(lines)
1222
1463
 
1223
1464
  def _append_priority_memory(
1224
1465
  self,
@@ -52,11 +52,17 @@ def initial_quest_yaml(
52
52
  startup_contract: dict | None = None,
53
53
  ) -> dict:
54
54
  timestamp = utc_now()
55
+ workspace_mode = (
56
+ str((startup_contract or {}).get("workspace_mode") or "").strip().lower()
57
+ if isinstance(startup_contract, dict)
58
+ else ""
59
+ )
60
+ initial_status_value = "idle" if workspace_mode == "copilot" else "active"
55
61
  return {
56
62
  "quest_id": quest_id,
57
63
  "title": title or goal,
58
64
  "quest_root": str(quest_root.resolve()),
59
- "status": "active",
65
+ "status": initial_status_value,
60
66
  "active_anchor": "baseline",
61
67
  "baseline_gate": "pending",
62
68
  "confirmed_baseline_ref": None,
@@ -100,7 +106,14 @@ def initial_plan() -> str:
100
106
  )
101
107
 
102
108
 
103
- def initial_status() -> str:
109
+ def initial_status(startup_contract: dict | None = None) -> str:
110
+ workspace_mode = (
111
+ str((startup_contract or {}).get("workspace_mode") or "").strip().lower()
112
+ if isinstance(startup_contract, dict)
113
+ else ""
114
+ )
115
+ if workspace_mode == "copilot":
116
+ return "# Status\n\nReady for your first instruction.\n"
104
117
  return "# Status\n\nQuest created. Waiting for baseline setup or reuse.\n"
105
118
 
106
119