@1mancompany/onemancompany 0.7.79 → 0.7.83

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1mancompany/onemancompany",
3
- "version": "0.7.79",
3
+ "version": "0.7.83",
4
4
  "description": "The AI Operating System for One-Person Companies",
5
5
  "bin": {
6
6
  "onemancompany": "bin/cli.js"
package/pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "onemancompany"
3
- version = "0.7.79"
3
+ version = "0.7.83"
4
4
  description = "A one-man company simulation with pixel art visualization and LangChain AI agents"
5
5
  requires-python = ">=3.12"
6
6
  dependencies = [
@@ -86,6 +86,29 @@ async def create_product_tool(
86
86
  return f"Error: {e}"
87
87
 
88
88
 
89
+ @tool
90
+ async def add_product_key_result(
91
+ product_slug: str,
92
+ title: str,
93
+ target: float,
94
+ unit: str = "",
95
+ ) -> str:
96
+ """Add a Key Result to an existing product (use during product planning).
97
+
98
+ Args:
99
+ product_slug: The product slug (e.g. "omc-website")
100
+ title: KR title (e.g. "DAU达到1000")
101
+ target: Numeric target value
102
+ unit: Unit of measurement (e.g. "users", "seconds")
103
+ """
104
+ try:
105
+ kr = prod.add_key_result(product_slug, title=title, target=target, unit=unit)
106
+ logger.debug("add_product_key_result: {} for {}", kr["id"], product_slug)
107
+ return f"Added key result '{title}' (target={target} {unit}) to {product_slug}"
108
+ except (ValueError, FileNotFoundError) as e:
109
+ return f"Error: {e}"
110
+
111
+
89
112
  @tool
90
113
  async def create_product_issue(
91
114
  product_slug: str,
@@ -789,6 +812,7 @@ async def transfer_product_ownership_tool(
789
812
 
790
813
  PRODUCT_TOOLS = [
791
814
  create_product_tool,
815
+ add_product_key_result,
792
816
  create_product_issue,
793
817
  update_product_issue,
794
818
  close_product_issue,
@@ -7444,6 +7444,33 @@ async def api_start_product_planning(slug: str) -> dict:
7444
7444
  product_slug=slug,
7445
7445
  product_id=product["id"],
7446
7446
  )
7447
+
7448
+ # Kickoff: persist a synthetic system message and dispatch to the EA adapter
7449
+ # so the agent posts an opening message and can begin creating KRs/issues.
7450
+ # Use SYSTEM_SENDER so the UI doesn't render this as the CEO speaking — the
7451
+ # prompt builder uses `role` only as a label, so EA still gets a coherent prompt.
7452
+ kickoff_text = f"开始为产品「{product['name']}」做规划。请先帮我梳理目标和关键结果。"
7453
+ try:
7454
+ kickoff_msg = await conversation_service.send_message(
7455
+ conv.id, sender=SYSTEM_SENDER, role="System", text=kickoff_text,
7456
+ )
7457
+ task = asyncio.create_task(_dispatch_conversation_to_adapter(conv.id, kickoff_msg))
7458
+ _active_adapter_tasks.add(task)
7459
+ _active_adapter_by_conv[conv.id] = task
7460
+ def _cleanup(t, _cid=conv.id):
7461
+ _active_adapter_tasks.discard(t)
7462
+ _active_adapter_by_conv.pop(_cid, None)
7463
+ task.add_done_callback(_cleanup)
7464
+ except Exception:
7465
+ logger.exception("[product_planning] failed to kick off EA for product {}", slug)
7466
+ try:
7467
+ await conversation_service.send_message(
7468
+ conv.id, sender=SYSTEM_SENDER, role="System",
7469
+ text="(Failed to start the planning agent. Please send a message to retry.)",
7470
+ )
7471
+ except Exception:
7472
+ logger.exception("[product_planning] failed to send kickoff-failure notice for {}", conv.id)
7473
+
7447
7474
  return {"conversation_id": conv.id, "existing": False}
7448
7475
 
7449
7476
 
@@ -129,6 +129,11 @@ def _build_conversation_prompt(
129
129
  lines.append(shared_prompt)
130
130
  elif conversation.type == ConversationType.CEO_INBOX:
131
131
  lines.append("The CEO is responding to your request. Answer their questions.")
132
+ elif (
133
+ conversation.type == ConversationType.PRODUCT
134
+ or conversation.type == ConversationType.PRODUCT.value
135
+ ):
136
+ lines.append(_build_product_planning_prompt(conversation))
132
137
  elif conversation.type == ConversationType.EA_CHAT:
133
138
  lines.append(
134
139
  "This is a direct chat with the CEO. You are their EA (Executive Assistant).\n"
@@ -158,6 +163,55 @@ def _build_conversation_prompt(
158
163
  return "\n".join(lines)
159
164
 
160
165
 
166
+ def _build_product_planning_prompt(conversation: Conversation) -> str:
167
+ """Build planning instructions + current product context for PRODUCT conversations."""
168
+ from onemancompany.core import product as prod
169
+ from onemancompany.core.models import IssueStatus
170
+
171
+ slug = (conversation.metadata or {}).get("product_slug", "")
172
+ product = prod.load_product(slug) if slug else None
173
+ if not product:
174
+ return (
175
+ "This is a product planning conversation, but the product could not be loaded. "
176
+ f"Slug: {slug!r}. Ask the CEO to clarify."
177
+ )
178
+
179
+ parts = [
180
+ "This is a product planning conversation with the CEO. You are the EA (Executive Assistant).",
181
+ "Your job:",
182
+ " 1. Clarify the product's objective and success metrics through targeted questions.",
183
+ " 2. Use add_product_key_result(product_slug, title, target, unit) to record measurable KRs.",
184
+ " 3. Use create_product_issue(product_slug, title, description, priority) to record concrete work items.",
185
+ " 4. Keep questions concise and one at a time. Confirm before creating each KR/issue.",
186
+ "",
187
+ f"## Current product: {product['name']} (slug: {slug})",
188
+ ]
189
+ if product.get("description"):
190
+ parts.append(f"Description: {product['description']}")
191
+
192
+ krs = product.get("key_results", []) or []
193
+ if krs:
194
+ parts.append("\n### Existing Key Results")
195
+ for kr in krs:
196
+ target = kr.get("target", 0)
197
+ current = kr.get("current", 0)
198
+ parts.append(f"- {kr.get('title','')}: {current}/{target} {kr.get('unit','')}".rstrip())
199
+ else:
200
+ parts.append("\n### Existing Key Results: (none yet — propose some after clarifying with CEO)")
201
+
202
+ issues = prod.list_issues(slug) or []
203
+ terminal = {IssueStatus.DONE.value, IssueStatus.RELEASED.value}
204
+ open_issues = [i for i in issues if i.get("status") not in terminal]
205
+ if open_issues:
206
+ parts.append(f"\n### Open Issues ({len(open_issues)})")
207
+ for issue in open_issues[:10]:
208
+ parts.append(f"- [{issue.get('priority','?')}] {issue.get('title','')} ({issue.get('id','')})")
209
+ else:
210
+ parts.append("\n### Open Issues: (none yet)")
211
+
212
+ return "\n".join(parts)
213
+
214
+
161
215
  def _load_oneonone_workspace_shared_prompt() -> str:
162
216
  """Load workspace policy prompt for one-on-one from shared_prompts."""
163
217
  from onemancompany.core.config import SHARED_PROMPTS_DIR, SOURCE_ROOT, read_text_utf