@modus-ai/modus 0.1.1 → 0.1.2
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/dist/cli/index.js +41 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/global.d.ts +15 -0
- package/dist/commands/global.d.ts.map +1 -0
- package/dist/commands/global.js +222 -0
- package/dist/commands/global.js.map +1 -0
- package/dist/commands/init.d.ts +17 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +278 -58
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/update.d.ts +8 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +27 -4
- package/dist/commands/update.js.map +1 -1
- package/dist/generators/codebuddy.d.ts +5 -1
- package/dist/generators/codebuddy.d.ts.map +1 -1
- package/dist/generators/codebuddy.js +358 -5
- package/dist/generators/codebuddy.js.map +1 -1
- package/dist/utils/config.d.ts +17 -0
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/config.js +15 -0
- package/dist/utils/config.js.map +1 -1
- package/package.json +1 -1
- package/templates/agents/modus-harness-00-skills-builder.md +12 -0
- package/templates/agents/modus-harness-01-5-design.md +40 -0
- package/templates/agents/modus-harness-01-analysis.md +14 -0
- package/templates/agents/modus-harness-02-dev.md +16 -0
- package/templates/agents/modus-harness-03-test.md +16 -0
- package/templates/agents/modus-harness-04-perf.md +16 -0
- package/templates/agents/modus-harness-05-security.md +16 -0
- package/templates/agents/modus-harness-06-review.md +16 -0
- package/templates/agents/modus-harness-07-deploy.md +16 -0
- package/templates/commands/modus.md +25 -0
- package/templates/hooks/post-tool-use-lint.py +78 -0
- package/templates/hooks/pre-compact-save.py +117 -0
- package/templates/hooks/pre-tool-use-safety.py +80 -0
- package/templates/hooks/session-start.py +86 -0
- package/templates/hooks/stop-update-skills.py +91 -0
- package/templates/hooks-config.json +83 -0
- package/templates/rules/modus-constitution/RULE.mdc +40 -0
- package/templates/rules/modus-design-brief/RULE.mdc +127 -0
- package/templates/rules/modus-workflow/RULE.mdc +64 -0
- package/templates/skills/modus-design-brief/SKILL.md +324 -0
- package/templates/skills/modus-harness/SKILL.md +17 -0
- package/templates/skills/modus-harness-agents/00-skills-builder/SKILL.md +46 -3
- package/templates/skills/modus-harness-agents/01-5-design/SKILL.md +140 -0
- package/templates/skills/modus-harness-agents/01-analysis/SKILL.md +7 -0
- package/templates/skills/modus-harness-agents/02-dev/SKILL.md +7 -0
- package/templates/skills/modus-harness-agents/03-test/SKILL.md +7 -0
- package/templates/skills/modus-harness-agents/04-perf/SKILL.md +7 -0
- package/templates/skills/modus-harness-agents/05-security/SKILL.md +7 -0
- package/templates/skills/modus-harness-agents/06-review/SKILL.md +7 -0
- package/templates/skills/modus-harness-agents/07-deploy/SKILL.md +7 -0
- package/templates/skills/modus-init/SKILL.md +24 -0
- package/templates/skills/modus-plan/SKILL.md +65 -8
- package/templates/skills/modus-spec/SKILL.md +71 -4
- package/templates/skills/modus-vibe/SKILL.md +73 -6
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: modus-harness-06-review
|
|
3
|
+
description: Use this agent when the Harness orchestrator needs comprehensive code review after all audit reports are ready (Gate B passed). Reviews all artifacts (01-analysis.md through 05-security-report.md) plus actual code changes, grades issues as P1/P2/P3, and produces cr-report.md. If P1/P2 issues exist, orchestrator triggers Loop 2 re-entry into SubAgent 02.
|
|
4
|
+
agentMode: agentic
|
|
5
|
+
tools: Read, Write, Glob
|
|
6
|
+
enabledAutoRun: true
|
|
7
|
+
enabled: true
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
读取并严格遵循 `.codebuddy/skills/modus-harness-06-review/SKILL.md` 中的执行规范。
|
|
11
|
+
|
|
12
|
+
输入依赖:全部产出物(01-analysis.md 至 05-security-report.md)的 HANDOFF 块 + 代码变更。
|
|
13
|
+
|
|
14
|
+
产出物路径:`modus/plans/active/{story-id}/cr-report.md`。
|
|
15
|
+
|
|
16
|
+
产出物头部 HANDOFF 块中的 `issues` 字段列出所有 P1/P2 问题,`gate_status: passed` 表示无 P1/P2 可进入 Gate C。
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: modus-harness-07-deploy
|
|
3
|
+
description: Use this agent when the Harness orchestrator needs to verify deployment after Gate C passes (cr-report has no P1/P2 issues). Monitors CI/CD pipeline status, validates service health across environments (dev→uat→pre→prd), executes smoke tests on P0 interfaces, monitors ERROR log volume for 30 minutes post-deployment, and produces 07-deploy-status.md. prd environment requires manual confirmation.
|
|
4
|
+
agentMode: agentic
|
|
5
|
+
tools: Read, Write, Glob, Bash, WebFetch
|
|
6
|
+
enabledAutoRun: false
|
|
7
|
+
enabled: true
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
读取并严格遵循 `.codebuddy/skills/modus-harness-07-deploy/SKILL.md` 中的执行规范。
|
|
11
|
+
|
|
12
|
+
输入依赖:`cr-report.md`(HANDOFF.gate_status = passed)+ `02-sprint-contract.md`。
|
|
13
|
+
|
|
14
|
+
产出物路径:`modus/plans/active/{story-id}/07-deploy-status.md`。
|
|
15
|
+
|
|
16
|
+
**注意:** `enabledAutoRun: false` — prd 环境部署操作需等待人工确认后方可继续。产出物头部必须包含 HANDOFF 块。
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# /modus
|
|
2
|
+
|
|
3
|
+
Modus — Business-grounded AI coding accelerator.
|
|
4
|
+
|
|
5
|
+
## Available Commands
|
|
6
|
+
|
|
7
|
+
| Command | Description |
|
|
8
|
+
|---|---|
|
|
9
|
+
| `/modus:init` | Scan codebase, classify business domains, generate domain Skill files |
|
|
10
|
+
| `/modus:vibe` | Context-aware vibe coding with business Skill backing |
|
|
11
|
+
| `/modus:plan` | Plan mode with Skill-backed business context |
|
|
12
|
+
| `/modus:spec` | OpenSpec-style spec-driven development |
|
|
13
|
+
| `/modus:harness` | Full dual-loop multi-agent Harness (analyze → dev → test → review → deploy) |
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
1. Run `/modus:init` once to scan your project and build business Skills
|
|
18
|
+
2. Use `/modus:vibe` for day-to-day coding with project context
|
|
19
|
+
3. Use `/modus:harness` for full-cycle feature delivery from a TAPD Story URL
|
|
20
|
+
|
|
21
|
+
## Tips
|
|
22
|
+
|
|
23
|
+
- Business Skill files live in `.codebuddy/skills/modus-biz-*/SKILL.md`
|
|
24
|
+
- Re-run `/modus:init` any time your domain model changes
|
|
25
|
+
- All commands fall back gracefully if business Skills are missing
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Modus PostToolUse Lint Hook
|
|
4
|
+
After a file is written or edited, emits a lightweight lint reminder for
|
|
5
|
+
Java and TypeScript files so the AI agent can self-correct common issues.
|
|
6
|
+
"""
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
JAVA_REMINDERS = [
|
|
12
|
+
"确认事务方法不在同类中内部调用(应通过 AopContext.currentProxy() 或拆分到独立 Bean)",
|
|
13
|
+
"确认 tenantId 来自 UserContext,而非 request 参数",
|
|
14
|
+
"确认金额字段使用 Long(单位:分),避免浮点精度问题",
|
|
15
|
+
"确认 Mapper 接口位于 dao 包下",
|
|
16
|
+
"确认枚举值使用 name() 而非 ordinal() 进行持久化",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
TS_REMINDERS = [
|
|
20
|
+
"确认异步函数有 await/catch 异常处理",
|
|
21
|
+
"确认 React 组件使用函数式写法,避免 class 组件",
|
|
22
|
+
"确认 API 调用有 loading 状态和错误提示",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
EXT_MAP: dict[str, list[str]] = {
|
|
26
|
+
".java": JAVA_REMINDERS,
|
|
27
|
+
".ts": TS_REMINDERS,
|
|
28
|
+
".tsx": TS_REMINDERS,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_reminders(file_path: str) -> list[str]:
|
|
33
|
+
_, ext = os.path.splitext(file_path.lower())
|
|
34
|
+
return EXT_MAP.get(ext, [])
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def main() -> int:
|
|
38
|
+
input_data = json.loads(sys.stdin.read())
|
|
39
|
+
|
|
40
|
+
tool_name = input_data.get("tool_name", "")
|
|
41
|
+
# Support both CLI-style and IDE-style tool names
|
|
42
|
+
if tool_name not in ("Write", "Edit", "write_to_file", "replace_in_file"):
|
|
43
|
+
print(json.dumps({"continue": True}))
|
|
44
|
+
return 0
|
|
45
|
+
|
|
46
|
+
tool_input = input_data.get("tool_input", {})
|
|
47
|
+
file_path = (
|
|
48
|
+
tool_input.get("path")
|
|
49
|
+
or tool_input.get("filePath")
|
|
50
|
+
or tool_input.get("file_path")
|
|
51
|
+
or ""
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
reminders = get_reminders(file_path)
|
|
55
|
+
if not reminders:
|
|
56
|
+
print(json.dumps({"continue": True}))
|
|
57
|
+
return 0
|
|
58
|
+
|
|
59
|
+
lang = os.path.splitext(file_path)[1].lstrip(".")
|
|
60
|
+
reminder_text = (
|
|
61
|
+
f"[Modus Lint 提示 — {file_path}]\n"
|
|
62
|
+
+ "\n".join(f"- {r}" for r in reminders)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
output = {
|
|
66
|
+
"continue": True,
|
|
67
|
+
"hookSpecificOutput": {
|
|
68
|
+
"hookEventName": "PostToolUse",
|
|
69
|
+
"additionalContext": reminder_text,
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
print(json.dumps(output, ensure_ascii=False))
|
|
74
|
+
return 0
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
if __name__ == "__main__":
|
|
78
|
+
sys.exit(main())
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Modus PreCompact Hook
|
|
4
|
+
Before context is compacted (auto or manual), saves the current state of
|
|
5
|
+
active Harness plans and appends a summary to the compaction instructions
|
|
6
|
+
so critical in-progress decisions are preserved.
|
|
7
|
+
"""
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import shutil
|
|
11
|
+
import sys
|
|
12
|
+
from datetime import datetime, timezone
|
|
13
|
+
|
|
14
|
+
TRACKED_ARTIFACTS = [
|
|
15
|
+
"01-analysis.md",
|
|
16
|
+
"02-sprint-contract.md",
|
|
17
|
+
"cr-report.md",
|
|
18
|
+
".modus-state.yaml",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def snapshot_active_plans(project_dir: str) -> list[str]:
|
|
23
|
+
"""Copies active plan artifacts to a timestamped snapshot dir. Returns list of saved paths."""
|
|
24
|
+
active_dir = os.path.join(project_dir, "modus", "plans", "active")
|
|
25
|
+
if not os.path.isdir(active_dir):
|
|
26
|
+
return []
|
|
27
|
+
|
|
28
|
+
ts = datetime.now(tz=timezone.utc).strftime("%Y%m%dT%H%M%SZ")
|
|
29
|
+
snapshot_base = os.path.join(project_dir, "modus", ".compact-snapshots", ts)
|
|
30
|
+
saved: list[str] = []
|
|
31
|
+
|
|
32
|
+
for story_id in os.listdir(active_dir):
|
|
33
|
+
plan_dir = os.path.join(active_dir, story_id)
|
|
34
|
+
if not os.path.isdir(plan_dir):
|
|
35
|
+
continue
|
|
36
|
+
for artifact in TRACKED_ARTIFACTS:
|
|
37
|
+
src = os.path.join(plan_dir, artifact)
|
|
38
|
+
if os.path.exists(src):
|
|
39
|
+
dst_dir = os.path.join(snapshot_base, story_id)
|
|
40
|
+
os.makedirs(dst_dir, exist_ok=True)
|
|
41
|
+
shutil.copy2(src, os.path.join(dst_dir, artifact))
|
|
42
|
+
saved.append(f"{story_id}/{artifact}")
|
|
43
|
+
|
|
44
|
+
return saved
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def build_context_hint(project_dir: str) -> str:
|
|
48
|
+
"""Reads HANDOFF blocks from active plans to give the compactor key context."""
|
|
49
|
+
active_dir = os.path.join(project_dir, "modus", "plans", "active")
|
|
50
|
+
hints: list[str] = []
|
|
51
|
+
|
|
52
|
+
if not os.path.isdir(active_dir):
|
|
53
|
+
return ""
|
|
54
|
+
|
|
55
|
+
for story_id in os.listdir(active_dir):
|
|
56
|
+
plan_dir = os.path.join(active_dir, story_id)
|
|
57
|
+
if not os.path.isdir(plan_dir):
|
|
58
|
+
continue
|
|
59
|
+
|
|
60
|
+
# Read .modus-state.yaml for current phase
|
|
61
|
+
state_path = os.path.join(plan_dir, ".modus-state.yaml")
|
|
62
|
+
state_snippet = ""
|
|
63
|
+
if os.path.exists(state_path):
|
|
64
|
+
try:
|
|
65
|
+
with open(state_path, encoding="utf-8") as f:
|
|
66
|
+
state_snippet = f.read(500)
|
|
67
|
+
except OSError:
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
hints.append(
|
|
71
|
+
f"[Story {story_id}]\n{state_snippet.strip() if state_snippet else '(状态未知)'}"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
if not hints:
|
|
75
|
+
return ""
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
"=== Modus 进行中任务(压缩时必须保留)===\n\n"
|
|
79
|
+
+ "\n\n".join(hints)
|
|
80
|
+
+ "\n\n重要:压缩后必须能继续执行上述 Harness 任务的下一步骤。"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def main() -> int:
|
|
85
|
+
input_data = json.loads(sys.stdin.read())
|
|
86
|
+
|
|
87
|
+
project_dir = os.environ.get("CODEBUDDY_PROJECT_DIR") or os.environ.get(
|
|
88
|
+
"CLAUDE_PROJECT_DIR", input_data.get("cwd", "")
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
saved = snapshot_active_plans(project_dir)
|
|
92
|
+
hint = build_context_hint(project_dir)
|
|
93
|
+
|
|
94
|
+
if not hint and not saved:
|
|
95
|
+
print(json.dumps({"continue": True}))
|
|
96
|
+
return 0
|
|
97
|
+
|
|
98
|
+
lines = []
|
|
99
|
+
if saved:
|
|
100
|
+
lines.append(f"已备份活跃计划快照({len(saved)} 个文件)到 modus/.compact-snapshots/")
|
|
101
|
+
if hint:
|
|
102
|
+
lines.append(hint)
|
|
103
|
+
|
|
104
|
+
output = {
|
|
105
|
+
"continue": True,
|
|
106
|
+
"hookSpecificOutput": {
|
|
107
|
+
"hookEventName": "PreCompact",
|
|
108
|
+
"additionalContext": "\n\n".join(lines),
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
print(json.dumps(output, ensure_ascii=False))
|
|
113
|
+
return 0
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
if __name__ == "__main__":
|
|
117
|
+
sys.exit(main())
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Modus PreToolUse Safety Hook
|
|
4
|
+
Intercepts Bash commands and blocks dangerous operations that could cause
|
|
5
|
+
irreversible damage to the project or infrastructure.
|
|
6
|
+
"""
|
|
7
|
+
import json
|
|
8
|
+
import re
|
|
9
|
+
import sys
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
DENY_PATTERNS: list[tuple[str, str]] = [
|
|
13
|
+
(r"rm\s+-rf\s+/", "禁止删除根目录"),
|
|
14
|
+
(r"rm\s+-rf\s+~", "禁止删除家目录"),
|
|
15
|
+
(r"git\s+push\s+.*--force\s+.*(?:main|master|release)", "禁止强制推送到主干分支"),
|
|
16
|
+
(r"git\s+push\s+(?:origin\s+)?(?:main|master|release)\s+--force", "禁止强制推送到主干分支"),
|
|
17
|
+
(r"dd\s+if=/dev/zero", "禁止 dd 覆写设备"),
|
|
18
|
+
(r"mkfs\.", "禁止格式化分区"),
|
|
19
|
+
(r">\s*/dev/sd[a-z]", "禁止直接写入块设备"),
|
|
20
|
+
(r"chmod\s+-R\s+777\s+/", "禁止对根目录递归授权 777"),
|
|
21
|
+
(r"DROP\s+DATABASE", "禁止 DROP DATABASE(请在数据库客户端手动执行)"),
|
|
22
|
+
(r"TRUNCATE\s+TABLE\s+\w+\s*;?\s*$", "高危 TRUNCATE 操作,请确认后手动执行"),
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
ASK_PATTERNS: list[tuple[str, str]] = [
|
|
26
|
+
(r"git\s+push\s+.*--force", "检测到 git force push,请确认目标分支安全"),
|
|
27
|
+
(r"git\s+reset\s+--hard", "检测到 git reset --hard,将丢失未提交的变更"),
|
|
28
|
+
(r"git\s+clean\s+-fd", "检测到 git clean -fd,将删除未跟踪文件"),
|
|
29
|
+
(r"npm\s+publish", "检测到 npm publish,将发布包到 registry"),
|
|
30
|
+
(r"kubectl\s+delete", "检测到 kubectl delete,将删除 K8s 资源"),
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def check_command(command: str) -> tuple[str, Optional[str]]:
|
|
35
|
+
"""Returns (decision, reason): decision is 'allow' | 'deny' | 'ask'."""
|
|
36
|
+
for pattern, reason in DENY_PATTERNS:
|
|
37
|
+
if re.search(pattern, command, re.IGNORECASE):
|
|
38
|
+
return "deny", reason
|
|
39
|
+
for pattern, reason in ASK_PATTERNS:
|
|
40
|
+
if re.search(pattern, command, re.IGNORECASE):
|
|
41
|
+
return "ask", reason
|
|
42
|
+
return "allow", None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def main() -> int:
|
|
46
|
+
input_data = json.loads(sys.stdin.read())
|
|
47
|
+
|
|
48
|
+
tool_name = input_data.get("tool_name", "")
|
|
49
|
+
# Support both CLI-style (Bash) and IDE-style (execute_command) tool names
|
|
50
|
+
if tool_name not in ("Bash", "execute_command"):
|
|
51
|
+
print(json.dumps({"continue": True}))
|
|
52
|
+
return 0
|
|
53
|
+
|
|
54
|
+
command = input_data.get("tool_input", {}).get("command", "")
|
|
55
|
+
if not command:
|
|
56
|
+
print(json.dumps({"continue": True}))
|
|
57
|
+
return 0
|
|
58
|
+
|
|
59
|
+
decision, reason = check_command(command)
|
|
60
|
+
|
|
61
|
+
output: dict = {
|
|
62
|
+
"continue": decision != "deny",
|
|
63
|
+
"hookSpecificOutput": {
|
|
64
|
+
"hookEventName": "PreToolUse",
|
|
65
|
+
"permissionDecision": decision,
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if reason:
|
|
70
|
+
output["hookSpecificOutput"]["permissionDecisionReason"] = reason
|
|
71
|
+
|
|
72
|
+
if decision == "deny":
|
|
73
|
+
output["stopReason"] = f"[Modus 安全拦截] {reason}"
|
|
74
|
+
|
|
75
|
+
print(json.dumps(output, ensure_ascii=False))
|
|
76
|
+
return 0
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
if __name__ == "__main__":
|
|
80
|
+
sys.exit(main())
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Modus SessionStart Hook
|
|
4
|
+
Injects project context (knowledge-catalog + active plans + constitution) into
|
|
5
|
+
the session so every conversation starts with business knowledge pre-loaded.
|
|
6
|
+
"""
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def read_file_safe(path: str, max_bytes: int = 8000) -> str:
|
|
13
|
+
try:
|
|
14
|
+
with open(path, encoding="utf-8") as f:
|
|
15
|
+
return f.read(max_bytes)
|
|
16
|
+
except OSError:
|
|
17
|
+
return ""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def main() -> int:
|
|
21
|
+
input_data = json.loads(sys.stdin.read())
|
|
22
|
+
project_dir = os.environ.get("CODEBUDDY_PROJECT_DIR") or os.environ.get(
|
|
23
|
+
"CLAUDE_PROJECT_DIR", input_data.get("cwd", "")
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
context_parts: list[str] = []
|
|
27
|
+
|
|
28
|
+
# 1. knowledge-catalog — Level 1 index (~200 tokens)
|
|
29
|
+
catalog_path = os.path.join(project_dir, "modus", "knowledge-catalog.md")
|
|
30
|
+
catalog = read_file_safe(catalog_path, 3000)
|
|
31
|
+
if catalog:
|
|
32
|
+
context_parts.append(f"[Modus 知识目录]\n{catalog}")
|
|
33
|
+
|
|
34
|
+
# 2. constitution from config.yaml
|
|
35
|
+
config_path = os.path.join(project_dir, "modus", "config.yaml")
|
|
36
|
+
config_raw = read_file_safe(config_path, 2000)
|
|
37
|
+
if config_raw:
|
|
38
|
+
# Extract only the constitution section to keep tokens low
|
|
39
|
+
in_constitution = False
|
|
40
|
+
constitution_lines: list[str] = []
|
|
41
|
+
for line in config_raw.splitlines():
|
|
42
|
+
if line.strip().startswith("constitution:"):
|
|
43
|
+
in_constitution = True
|
|
44
|
+
if in_constitution:
|
|
45
|
+
constitution_lines.append(line)
|
|
46
|
+
# Stop at the next top-level key (non-indented, not constitution)
|
|
47
|
+
if constitution_lines and len(constitution_lines) > 1 and line and not line[0].isspace() and not line.strip().startswith("constitution"):
|
|
48
|
+
constitution_lines.pop()
|
|
49
|
+
break
|
|
50
|
+
if constitution_lines:
|
|
51
|
+
context_parts.append(f"[项目宪法 (constitution)]\n" + "\n".join(constitution_lines))
|
|
52
|
+
|
|
53
|
+
# 3. Active Harness plans — list story IDs in progress
|
|
54
|
+
active_dir = os.path.join(project_dir, "modus", "plans", "active")
|
|
55
|
+
if os.path.isdir(active_dir):
|
|
56
|
+
active_stories = [
|
|
57
|
+
d for d in os.listdir(active_dir)
|
|
58
|
+
if os.path.isdir(os.path.join(active_dir, d))
|
|
59
|
+
]
|
|
60
|
+
if active_stories:
|
|
61
|
+
context_parts.append(
|
|
62
|
+
"[进行中的 Harness 任务]\n" + "\n".join(f"- {s}" for s in active_stories)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
if not context_parts:
|
|
66
|
+
output = {"continue": True}
|
|
67
|
+
else:
|
|
68
|
+
additional_context = (
|
|
69
|
+
"=== Modus 项目上下文(自动注入)===\n\n"
|
|
70
|
+
+ "\n\n".join(context_parts)
|
|
71
|
+
+ "\n\n提示:使用 /modus:vibe、/modus:plan、/modus:spec 或 /modus:harness 开始工作。"
|
|
72
|
+
)
|
|
73
|
+
output = {
|
|
74
|
+
"continue": True,
|
|
75
|
+
"hookSpecificOutput": {
|
|
76
|
+
"hookEventName": "SessionStart",
|
|
77
|
+
"additionalContext": additional_context,
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
print(json.dumps(output, ensure_ascii=False))
|
|
82
|
+
return 0
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
if __name__ == "__main__":
|
|
86
|
+
sys.exit(main())
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Modus Stop Hook
|
|
4
|
+
After the agent finishes responding, checks if any Harness active plans have
|
|
5
|
+
new artifacts that haven't been reflected in Skills yet, and prompts the agent
|
|
6
|
+
to trigger a knowledge write-back.
|
|
7
|
+
"""
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
|
|
13
|
+
TRACKED_ARTIFACTS = [
|
|
14
|
+
"01-analysis.md",
|
|
15
|
+
"02-sprint-contract.md",
|
|
16
|
+
"03-test-report.md",
|
|
17
|
+
"04-perf-report.md",
|
|
18
|
+
"05-security-report.md",
|
|
19
|
+
"cr-report.md",
|
|
20
|
+
"07-deploy-status.md",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
WRITEBACK_MARKER = ".skills-synced"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def check_active_plans(project_dir: str) -> list[dict]:
|
|
27
|
+
"""Returns list of {story_id, new_artifacts} for plans with unsynced artifacts."""
|
|
28
|
+
active_dir = os.path.join(project_dir, "modus", "plans", "active")
|
|
29
|
+
pending: list[dict] = []
|
|
30
|
+
|
|
31
|
+
if not os.path.isdir(active_dir):
|
|
32
|
+
return pending
|
|
33
|
+
|
|
34
|
+
for story_id in os.listdir(active_dir):
|
|
35
|
+
plan_dir = os.path.join(active_dir, story_id)
|
|
36
|
+
if not os.path.isdir(plan_dir):
|
|
37
|
+
continue
|
|
38
|
+
|
|
39
|
+
marker_path = os.path.join(plan_dir, WRITEBACK_MARKER)
|
|
40
|
+
last_sync = 0.0
|
|
41
|
+
if os.path.exists(marker_path):
|
|
42
|
+
last_sync = os.path.getmtime(marker_path)
|
|
43
|
+
|
|
44
|
+
new_artifacts = [
|
|
45
|
+
a for a in TRACKED_ARTIFACTS
|
|
46
|
+
if os.path.exists(os.path.join(plan_dir, a))
|
|
47
|
+
and os.path.getmtime(os.path.join(plan_dir, a)) > last_sync
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
if new_artifacts:
|
|
51
|
+
pending.append({"story_id": story_id, "new_artifacts": new_artifacts})
|
|
52
|
+
|
|
53
|
+
return pending
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def main() -> int:
|
|
57
|
+
input_data = json.loads(sys.stdin.read())
|
|
58
|
+
|
|
59
|
+
# Avoid infinite loop: if a stop hook is already active, skip
|
|
60
|
+
if input_data.get("stop_hook_active"):
|
|
61
|
+
print(json.dumps({"continue": True}))
|
|
62
|
+
return 0
|
|
63
|
+
|
|
64
|
+
project_dir = os.environ.get("CODEBUDDY_PROJECT_DIR") or os.environ.get(
|
|
65
|
+
"CLAUDE_PROJECT_DIR", input_data.get("cwd", "")
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
pending = check_active_plans(project_dir)
|
|
69
|
+
if not pending:
|
|
70
|
+
print(json.dumps({"continue": True}))
|
|
71
|
+
return 0
|
|
72
|
+
|
|
73
|
+
lines = ["[Modus] 检测到以下 Harness 产出物尚未同步到 Skills 知识库:"]
|
|
74
|
+
for item in pending:
|
|
75
|
+
lines.append(f" Story {item['story_id']}: {', '.join(item['new_artifacts'])}")
|
|
76
|
+
lines.append(
|
|
77
|
+
"\n建议:调用 SubAgent 00(模式 D:知识提取)将新产出物的 [decision]/[guideline]/[pitfall] 回写到对应 Skill 文件。"
|
|
78
|
+
f"\n回写完成后请创建 {WRITEBACK_MARKER} 标记文件以避免重复提示。"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
output = {
|
|
82
|
+
"continue": False,
|
|
83
|
+
"stopReason": "\n".join(lines),
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
print(json.dumps(output, ensure_ascii=False))
|
|
87
|
+
return 2 # exit code 2 → feedback injected into next agent message
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
if __name__ == "__main__":
|
|
91
|
+
sys.exit(main())
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"SessionStart": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "startup",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "$CODEBUDDY_PROJECT_DIR/.codebuddy/hooks/session-start.py",
|
|
10
|
+
"timeout": 10
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"PreToolUse": [
|
|
16
|
+
{
|
|
17
|
+
"matcher": "Bash|execute_command",
|
|
18
|
+
"hooks": [
|
|
19
|
+
{
|
|
20
|
+
"type": "command",
|
|
21
|
+
"command": "$CODEBUDDY_PROJECT_DIR/.codebuddy/hooks/pre-tool-use-safety.py",
|
|
22
|
+
"timeout": 5
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"matcher": "Write|Edit|write_to_file|replace_in_file",
|
|
28
|
+
"hooks": [
|
|
29
|
+
{
|
|
30
|
+
"type": "command",
|
|
31
|
+
"command": "$CODEBUDDY_PROJECT_DIR/.codebuddy/hooks/post-tool-use-lint.py",
|
|
32
|
+
"timeout": 5
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
"PostToolUse": [
|
|
38
|
+
{
|
|
39
|
+
"matcher": "Write|Edit|write_to_file|replace_in_file",
|
|
40
|
+
"hooks": [
|
|
41
|
+
{
|
|
42
|
+
"type": "command",
|
|
43
|
+
"command": "$CODEBUDDY_PROJECT_DIR/.codebuddy/hooks/post-tool-use-lint.py",
|
|
44
|
+
"timeout": 5
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
],
|
|
49
|
+
"Stop": [
|
|
50
|
+
{
|
|
51
|
+
"hooks": [
|
|
52
|
+
{
|
|
53
|
+
"type": "command",
|
|
54
|
+
"command": "$CODEBUDDY_PROJECT_DIR/.codebuddy/hooks/stop-update-skills.py",
|
|
55
|
+
"timeout": 10
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
],
|
|
60
|
+
"PreCompact": [
|
|
61
|
+
{
|
|
62
|
+
"matcher": "auto",
|
|
63
|
+
"hooks": [
|
|
64
|
+
{
|
|
65
|
+
"type": "command",
|
|
66
|
+
"command": "$CODEBUDDY_PROJECT_DIR/.codebuddy/hooks/pre-compact-save.py",
|
|
67
|
+
"timeout": 15
|
|
68
|
+
}
|
|
69
|
+
]
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"matcher": "manual",
|
|
73
|
+
"hooks": [
|
|
74
|
+
{
|
|
75
|
+
"type": "command",
|
|
76
|
+
"command": "$CODEBUDDY_PROJECT_DIR/.codebuddy/hooks/pre-compact-save.py",
|
|
77
|
+
"timeout": 15
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
}
|
|
81
|
+
]
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Project constitution (hard rules) extracted from modus/config.yaml. Load this rule when implementing code, reviewing code, or running any Modus command that requires project-specific constraints. These rules override all other guidelines.
|
|
3
|
+
alwaysApply: false
|
|
4
|
+
enabled: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 项目宪法(Project Constitution)
|
|
8
|
+
|
|
9
|
+
> 此规则由 `modus init` 根据 `modus/config.yaml` 中的 `constitution` 字段自动生成。
|
|
10
|
+
> 如需修改,请编辑 `modus/config.yaml` 后重新运行 `modus update`。
|
|
11
|
+
|
|
12
|
+
## 技术栈
|
|
13
|
+
|
|
14
|
+
{{TECH_STACK}}
|
|
15
|
+
|
|
16
|
+
## 构建命令
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
{{BUILD_COMMAND}}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## 测试命令
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
{{TEST_COMMAND}}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## 硬性约束(Hard Rules)
|
|
29
|
+
|
|
30
|
+
以下规则在所有 Modus 命令执行时强制遵守,不因业务需要而妥协:
|
|
31
|
+
|
|
32
|
+
{{HARD_RULES}}
|
|
33
|
+
|
|
34
|
+
## 项目特有模式
|
|
35
|
+
|
|
36
|
+
{{KEY_PATTERNS}}
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
*优先级:宪法 > Skill 建议 > 通用最佳实践*
|