engaku 0.2.0__tar.gz
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.
- engaku-0.2.0/PKG-INFO +82 -0
- engaku-0.2.0/README.md +66 -0
- engaku-0.2.0/pyproject.toml +41 -0
- engaku-0.2.0/setup.cfg +4 -0
- engaku-0.2.0/src/engaku/__init__.py +1 -0
- engaku-0.2.0/src/engaku/__main__.py +3 -0
- engaku-0.2.0/src/engaku/cli.py +64 -0
- engaku-0.2.0/src/engaku/cmd_apply.py +130 -0
- engaku-0.2.0/src/engaku/cmd_init.py +185 -0
- engaku-0.2.0/src/engaku/cmd_inject.py +108 -0
- engaku-0.2.0/src/engaku/cmd_prompt_check.py +124 -0
- engaku-0.2.0/src/engaku/cmd_task_review.py +93 -0
- engaku-0.2.0/src/engaku/constants.py +11 -0
- engaku-0.2.0/src/engaku/templates/agents/dev.agent.md +36 -0
- engaku-0.2.0/src/engaku/templates/agents/planner.agent.md +150 -0
- engaku-0.2.0/src/engaku/templates/agents/reviewer.agent.md +71 -0
- engaku-0.2.0/src/engaku/templates/agents/scanner.agent.md +20 -0
- engaku-0.2.0/src/engaku/templates/ai/engaku.json +1 -0
- engaku-0.2.0/src/engaku/templates/ai/overview.md +26 -0
- engaku-0.2.0/src/engaku/templates/copilot-instructions.md +10 -0
- engaku-0.2.0/src/engaku/templates/instructions/hooks.instructions.md +8 -0
- engaku-0.2.0/src/engaku/templates/instructions/templates.instructions.md +8 -0
- engaku-0.2.0/src/engaku/templates/instructions/tests.instructions.md +8 -0
- engaku-0.2.0/src/engaku/templates/skills/frontend-design/SKILL.md +76 -0
- engaku-0.2.0/src/engaku/templates/skills/proactive-initiative/SKILL.md +46 -0
- engaku-0.2.0/src/engaku/templates/skills/systematic-debugging/SKILL.md +96 -0
- engaku-0.2.0/src/engaku/templates/skills/verification-before-completion/SKILL.md +110 -0
- engaku-0.2.0/src/engaku/utils.py +111 -0
- engaku-0.2.0/src/engaku.egg-info/PKG-INFO +82 -0
- engaku-0.2.0/src/engaku.egg-info/SOURCES.txt +36 -0
- engaku-0.2.0/src/engaku.egg-info/dependency_links.txt +1 -0
- engaku-0.2.0/src/engaku.egg-info/entry_points.txt +2 -0
- engaku-0.2.0/src/engaku.egg-info/top_level.txt +1 -0
- engaku-0.2.0/tests/test_apply.py +122 -0
- engaku-0.2.0/tests/test_init.py +116 -0
- engaku-0.2.0/tests/test_inject.py +236 -0
- engaku-0.2.0/tests/test_prompt_check.py +209 -0
- engaku-0.2.0/tests/test_task_review.py +228 -0
engaku-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: engaku
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: AI persistent memory layer for VS Code Copilot
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/JorgenLiu/engaku
|
|
7
|
+
Project-URL: Repository, https://github.com/JorgenLiu/engaku
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
14
|
+
Requires-Python: >=3.8
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# engaku
|
|
18
|
+
|
|
19
|
+
AI persistent memory layer for VS Code Copilot — keeps project context, rules, and active tasks in front of the agent at every turn through VS Code Agent Hooks.
|
|
20
|
+
|
|
21
|
+
## What it does
|
|
22
|
+
|
|
23
|
+
`engaku` gives VS Code Copilot durable project memory stored in `.ai/` Markdown files. Agent Hooks automatically inject current context into every conversation, surface active-task steps on each prompt, and remind the agent when a task plan is complete and ready for review.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install engaku
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Or install directly from source:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install git+https://github.com/JorgenLiu/engaku.git
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Quick Start
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# Bootstrap .ai/ and .github/ structure in your repo
|
|
41
|
+
engaku init
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
After running `init`, VS Code Agent Hooks are active. The `@dev`, `@planner`, `@reviewer`, and `@scanner` agents are available via `.github/agents/`. No further manual steps are needed — hooks fire automatically on SessionStart, UserPromptSubmit, Stop, and PreCompact.
|
|
45
|
+
|
|
46
|
+
## What `engaku init` creates
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
.ai/
|
|
50
|
+
overview.md — project description, constraints, tech stack
|
|
51
|
+
tasks/ — planner-managed task plans
|
|
52
|
+
decisions/ — architecture decision records
|
|
53
|
+
.github/
|
|
54
|
+
copilot-instructions.md — global agent rules
|
|
55
|
+
agents/ — dev, planner, reviewer, scanner agent definitions
|
|
56
|
+
instructions/ — .instructions.md stubs for hooks, templates, tests
|
|
57
|
+
skills/ — bundled skills (systematic-debugging, verification-before-completion, frontend-design)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Subcommands
|
|
61
|
+
|
|
62
|
+
| Command | Purpose |
|
|
63
|
+
|---------|---------|
|
|
64
|
+
| `init` | Bootstrap `.ai/`, `.github/` structure and install VS Code Agent Hooks |
|
|
65
|
+
| `inject` | Inject `.ai/overview.md` + active-task context (SessionStart / PreCompact hook) |
|
|
66
|
+
| `prompt-check` | Detect rule/constraint in user prompt and inject active-task steps (UserPromptSubmit hook) |
|
|
67
|
+
| `task-review` | Detect completed task plans and emit handoff reminder (Stop hook) |
|
|
68
|
+
| `apply` | Apply `.ai/engaku.json` model config to `.github/agents/` frontmatter |
|
|
69
|
+
|
|
70
|
+
## How it works
|
|
71
|
+
|
|
72
|
+
After `engaku init`, four Agent Hooks fire automatically:
|
|
73
|
+
|
|
74
|
+
- **`SessionStart`** → `engaku inject`: injects `overview.md` and the current active-task context at the start of every session.
|
|
75
|
+
- **`PreCompact`** → `engaku inject`: re-injects context before conversation compaction so the agent doesn't lose project memory.
|
|
76
|
+
- **`UserPromptSubmit`** → `engaku prompt-check`: scans each user prompt for new rules or constraints and injects the active-task's unchecked steps as a system message so the agent always knows what to do next.
|
|
77
|
+
- **`Stop`** → `engaku task-review`: after each agent turn, checks whether all steps in an in-progress task plan are ticked and emits a handoff reminder if so.
|
|
78
|
+
|
|
79
|
+
## Requirements
|
|
80
|
+
|
|
81
|
+
- Python ≥ 3.8 (stdlib only, no third-party dependencies)
|
|
82
|
+
- VS Code with GitHub Copilot
|
engaku-0.2.0/README.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# engaku
|
|
2
|
+
|
|
3
|
+
AI persistent memory layer for VS Code Copilot — keeps project context, rules, and active tasks in front of the agent at every turn through VS Code Agent Hooks.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
`engaku` gives VS Code Copilot durable project memory stored in `.ai/` Markdown files. Agent Hooks automatically inject current context into every conversation, surface active-task steps on each prompt, and remind the agent when a task plan is complete and ready for review.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install engaku
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or install directly from source:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install git+https://github.com/JorgenLiu/engaku.git
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# Bootstrap .ai/ and .github/ structure in your repo
|
|
25
|
+
engaku init
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
After running `init`, VS Code Agent Hooks are active. The `@dev`, `@planner`, `@reviewer`, and `@scanner` agents are available via `.github/agents/`. No further manual steps are needed — hooks fire automatically on SessionStart, UserPromptSubmit, Stop, and PreCompact.
|
|
29
|
+
|
|
30
|
+
## What `engaku init` creates
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
.ai/
|
|
34
|
+
overview.md — project description, constraints, tech stack
|
|
35
|
+
tasks/ — planner-managed task plans
|
|
36
|
+
decisions/ — architecture decision records
|
|
37
|
+
.github/
|
|
38
|
+
copilot-instructions.md — global agent rules
|
|
39
|
+
agents/ — dev, planner, reviewer, scanner agent definitions
|
|
40
|
+
instructions/ — .instructions.md stubs for hooks, templates, tests
|
|
41
|
+
skills/ — bundled skills (systematic-debugging, verification-before-completion, frontend-design)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Subcommands
|
|
45
|
+
|
|
46
|
+
| Command | Purpose |
|
|
47
|
+
|---------|---------|
|
|
48
|
+
| `init` | Bootstrap `.ai/`, `.github/` structure and install VS Code Agent Hooks |
|
|
49
|
+
| `inject` | Inject `.ai/overview.md` + active-task context (SessionStart / PreCompact hook) |
|
|
50
|
+
| `prompt-check` | Detect rule/constraint in user prompt and inject active-task steps (UserPromptSubmit hook) |
|
|
51
|
+
| `task-review` | Detect completed task plans and emit handoff reminder (Stop hook) |
|
|
52
|
+
| `apply` | Apply `.ai/engaku.json` model config to `.github/agents/` frontmatter |
|
|
53
|
+
|
|
54
|
+
## How it works
|
|
55
|
+
|
|
56
|
+
After `engaku init`, four Agent Hooks fire automatically:
|
|
57
|
+
|
|
58
|
+
- **`SessionStart`** → `engaku inject`: injects `overview.md` and the current active-task context at the start of every session.
|
|
59
|
+
- **`PreCompact`** → `engaku inject`: re-injects context before conversation compaction so the agent doesn't lose project memory.
|
|
60
|
+
- **`UserPromptSubmit`** → `engaku prompt-check`: scans each user prompt for new rules or constraints and injects the active-task's unchecked steps as a system message so the agent always knows what to do next.
|
|
61
|
+
- **`Stop`** → `engaku task-review`: after each agent turn, checks whether all steps in an in-progress task plan are ticked and emits a handoff reminder if so.
|
|
62
|
+
|
|
63
|
+
## Requirements
|
|
64
|
+
|
|
65
|
+
- Python ≥ 3.8 (stdlib only, no third-party dependencies)
|
|
66
|
+
- VS Code with GitHub Copilot
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=45"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "engaku"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "AI persistent memory layer for VS Code Copilot"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
requires-python = ">=3.8"
|
|
12
|
+
dependencies = []
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Environment :: Console",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
Homepage = "https://github.com/JorgenLiu/engaku"
|
|
24
|
+
Repository = "https://github.com/JorgenLiu/engaku"
|
|
25
|
+
|
|
26
|
+
[project.scripts]
|
|
27
|
+
engaku = "engaku.cli:main"
|
|
28
|
+
|
|
29
|
+
[tool.setuptools.packages.find]
|
|
30
|
+
where = ["src"]
|
|
31
|
+
|
|
32
|
+
[tool.setuptools.package-data]
|
|
33
|
+
"engaku" = [
|
|
34
|
+
"templates/*.md",
|
|
35
|
+
"templates/*.json",
|
|
36
|
+
"templates/agents/*.md",
|
|
37
|
+
"templates/ai/*.md",
|
|
38
|
+
"templates/ai/*.json",
|
|
39
|
+
"templates/instructions/*.md",
|
|
40
|
+
"templates/skills/*/*.md",
|
|
41
|
+
]
|
engaku-0.2.0/setup.cfg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.0"
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from engaku import __version__
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def main():
|
|
8
|
+
parser = argparse.ArgumentParser(
|
|
9
|
+
prog="engaku",
|
|
10
|
+
description="AI persistent memory layer for VS Code Copilot",
|
|
11
|
+
)
|
|
12
|
+
parser.add_argument(
|
|
13
|
+
"--version", action="version", version="%(prog)s " + __version__
|
|
14
|
+
)
|
|
15
|
+
subparsers = parser.add_subparsers(dest="command", metavar="COMMAND")
|
|
16
|
+
subparsers.required = True
|
|
17
|
+
|
|
18
|
+
# engaku init
|
|
19
|
+
subparsers.add_parser(
|
|
20
|
+
"init",
|
|
21
|
+
help="Initialize .ai/ knowledge structure and .github/ hooks/agents in target repo",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# engaku inject
|
|
25
|
+
subparsers.add_parser(
|
|
26
|
+
"inject",
|
|
27
|
+
help="Inject .ai/overview.md + active-task context (SessionStart/PreCompact/SubagentStart hook)",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# engaku prompt-check
|
|
31
|
+
subparsers.add_parser(
|
|
32
|
+
"prompt-check",
|
|
33
|
+
help="Detect potential rule/constraint in user prompt (UserPromptSubmit hook)",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# engaku task-review
|
|
37
|
+
subparsers.add_parser(
|
|
38
|
+
"task-review",
|
|
39
|
+
help="Detect all-complete task plans and emit handoff reminder (Stop hook)",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# engaku apply
|
|
43
|
+
subparsers.add_parser(
|
|
44
|
+
"apply",
|
|
45
|
+
help="Apply .ai/engaku.json model config to .github/agents/ frontmatter",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
args = parser.parse_args()
|
|
49
|
+
|
|
50
|
+
if args.command == "init":
|
|
51
|
+
from engaku.cmd_init import run
|
|
52
|
+
sys.exit(run())
|
|
53
|
+
elif args.command == "inject":
|
|
54
|
+
from engaku.cmd_inject import run
|
|
55
|
+
sys.exit(run())
|
|
56
|
+
elif args.command == "prompt-check":
|
|
57
|
+
from engaku.cmd_prompt_check import run
|
|
58
|
+
sys.exit(run())
|
|
59
|
+
elif args.command == "task-review":
|
|
60
|
+
from engaku.cmd_task_review import run
|
|
61
|
+
sys.exit(run())
|
|
62
|
+
elif args.command == "apply":
|
|
63
|
+
from engaku.cmd_apply import run
|
|
64
|
+
sys.exit(run())
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""
|
|
2
|
+
engaku apply
|
|
3
|
+
Apply .ai/engaku.json configuration to .github/agents/ files.
|
|
4
|
+
|
|
5
|
+
Reads model assignments from .ai/engaku.json and writes the model: field
|
|
6
|
+
into each matching .github/agents/{name}.agent.md frontmatter. If an agent
|
|
7
|
+
file has no model: field, one is inserted immediately after the name: line.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
engaku apply
|
|
11
|
+
"""
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import re
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
from engaku.constants import CONFIG_FILE
|
|
18
|
+
from engaku.utils import load_config
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _update_agent_model(agent_path, model):
|
|
22
|
+
"""Update or insert model: field in agent frontmatter.
|
|
23
|
+
|
|
24
|
+
Returns (changed, reason) where changed is True if the file was written.
|
|
25
|
+
"""
|
|
26
|
+
with open(agent_path, "r", encoding="utf-8") as f:
|
|
27
|
+
content = f.read()
|
|
28
|
+
|
|
29
|
+
if not content.startswith("---\n"):
|
|
30
|
+
return False, "no frontmatter"
|
|
31
|
+
|
|
32
|
+
close = content.find("\n---", 4)
|
|
33
|
+
if close == -1:
|
|
34
|
+
return False, "unclosed frontmatter"
|
|
35
|
+
|
|
36
|
+
# Guard against false match (--- embedded inside a multiline value)
|
|
37
|
+
after_close = content[close + 4:]
|
|
38
|
+
if after_close and after_close[0] not in ("\n", "\r"):
|
|
39
|
+
return False, "malformed frontmatter"
|
|
40
|
+
|
|
41
|
+
fm = content[4:close] # YAML body — no trailing newline
|
|
42
|
+
rest = content[close:] # starts with \n---
|
|
43
|
+
|
|
44
|
+
model_line = "model: ['{}']".format(model)
|
|
45
|
+
|
|
46
|
+
if re.search(r"^model:", fm, re.MULTILINE):
|
|
47
|
+
new_fm = re.sub(r"^model:.*$", model_line, fm, flags=re.MULTILINE)
|
|
48
|
+
else:
|
|
49
|
+
# Insert immediately after the name: line if present, else append.
|
|
50
|
+
if re.search(r"^name:", fm, re.MULTILINE):
|
|
51
|
+
new_fm = re.sub(
|
|
52
|
+
r"(^name:.*$)",
|
|
53
|
+
r"\1\n" + model_line,
|
|
54
|
+
fm,
|
|
55
|
+
count=1,
|
|
56
|
+
flags=re.MULTILINE,
|
|
57
|
+
)
|
|
58
|
+
else:
|
|
59
|
+
new_fm = fm + "\n" + model_line
|
|
60
|
+
|
|
61
|
+
if new_fm == fm:
|
|
62
|
+
return False, "no change"
|
|
63
|
+
|
|
64
|
+
with open(agent_path, "w", encoding="utf-8") as f:
|
|
65
|
+
f.write("---\n" + new_fm + rest)
|
|
66
|
+
return True, "ok"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def run(cwd=None):
|
|
70
|
+
if cwd is None:
|
|
71
|
+
cwd = os.getcwd()
|
|
72
|
+
|
|
73
|
+
config_path = os.path.join(cwd, CONFIG_FILE)
|
|
74
|
+
if not os.path.isfile(config_path):
|
|
75
|
+
sys.stderr.write(
|
|
76
|
+
"error: {} not found.\n"
|
|
77
|
+
"Run 'engaku init' to create it.\n".format(config_path)
|
|
78
|
+
)
|
|
79
|
+
return 1
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
|
83
|
+
json.load(f)
|
|
84
|
+
except ValueError as exc:
|
|
85
|
+
sys.stderr.write(
|
|
86
|
+
"error: invalid JSON in {}: {}\n".format(config_path, exc)
|
|
87
|
+
)
|
|
88
|
+
return 1
|
|
89
|
+
|
|
90
|
+
config = load_config(cwd)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# ── agents → .github/agents/ ─────────────────────────────────────────────
|
|
94
|
+
agents_config = config.get("agents", {})
|
|
95
|
+
agents_dir = os.path.join(cwd, ".github", "agents")
|
|
96
|
+
changed = 0
|
|
97
|
+
skipped = 0
|
|
98
|
+
|
|
99
|
+
for agent_name in sorted(agents_config):
|
|
100
|
+
model = agents_config[agent_name]
|
|
101
|
+
agent_path = os.path.join(agents_dir, "{}.agent.md".format(agent_name))
|
|
102
|
+
if not os.path.isfile(agent_path):
|
|
103
|
+
sys.stdout.write(
|
|
104
|
+
" [skip] {}.agent.md (file not found)\n".format(agent_name)
|
|
105
|
+
)
|
|
106
|
+
skipped += 1
|
|
107
|
+
continue
|
|
108
|
+
|
|
109
|
+
updated, reason = _update_agent_model(agent_path, model)
|
|
110
|
+
if updated:
|
|
111
|
+
sys.stdout.write(
|
|
112
|
+
" [updated] {}.agent.md -> {}\n".format(agent_name, model)
|
|
113
|
+
)
|
|
114
|
+
changed += 1
|
|
115
|
+
else:
|
|
116
|
+
sys.stdout.write(
|
|
117
|
+
" [skip] {}.agent.md ({})\n".format(agent_name, reason)
|
|
118
|
+
)
|
|
119
|
+
skipped += 1
|
|
120
|
+
|
|
121
|
+
total_changed = changed
|
|
122
|
+
total_skipped = skipped
|
|
123
|
+
sys.stdout.write(
|
|
124
|
+
"\napply complete: {} updated, {} skipped.\n".format(total_changed, total_skipped)
|
|
125
|
+
)
|
|
126
|
+
return 0
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def main():
|
|
130
|
+
sys.exit(run())
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""
|
|
2
|
+
engaku init
|
|
3
|
+
Initialize .ai/ knowledge structure and .github/ hooks + agents in the current
|
|
4
|
+
git repository.
|
|
5
|
+
|
|
6
|
+
Files created (never overwritten if they already exist):
|
|
7
|
+
.ai/
|
|
8
|
+
overview.md
|
|
9
|
+
engaku.json
|
|
10
|
+
decisions/.gitkeep
|
|
11
|
+
tasks/.gitkeep
|
|
12
|
+
docs/.gitkeep
|
|
13
|
+
.github/
|
|
14
|
+
agents/
|
|
15
|
+
dev.agent.md
|
|
16
|
+
planner.agent.md
|
|
17
|
+
reviewer.agent.md
|
|
18
|
+
scanner.agent.md
|
|
19
|
+
instructions/
|
|
20
|
+
hooks.instructions.md
|
|
21
|
+
tests.instructions.md
|
|
22
|
+
templates.instructions.md
|
|
23
|
+
skills/
|
|
24
|
+
systematic-debugging/SKILL.md
|
|
25
|
+
verification-before-completion/SKILL.md
|
|
26
|
+
frontend-design/SKILL.md
|
|
27
|
+
proactive-initiative/SKILL.md
|
|
28
|
+
copilot-instructions.md
|
|
29
|
+
"""
|
|
30
|
+
import os
|
|
31
|
+
import shutil
|
|
32
|
+
import subprocess
|
|
33
|
+
import sys
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _templates_dir():
|
|
37
|
+
return os.path.join(os.path.dirname(__file__), "templates")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _is_git_repo(cwd):
|
|
41
|
+
try:
|
|
42
|
+
result = subprocess.run(
|
|
43
|
+
["git", "rev-parse", "--git-dir"],
|
|
44
|
+
cwd=cwd,
|
|
45
|
+
stdout=subprocess.PIPE,
|
|
46
|
+
stderr=subprocess.PIPE,
|
|
47
|
+
)
|
|
48
|
+
return result.returncode == 0
|
|
49
|
+
except OSError:
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _copy_template(src, dst, out):
|
|
54
|
+
"""Copy src to dst unless dst already exists. Prints [create] or [skip]."""
|
|
55
|
+
if os.path.exists(dst):
|
|
56
|
+
out.append("[skip] {}".format(dst))
|
|
57
|
+
return
|
|
58
|
+
dst_dir = os.path.dirname(dst)
|
|
59
|
+
if dst_dir and not os.path.isdir(dst_dir):
|
|
60
|
+
os.makedirs(dst_dir)
|
|
61
|
+
shutil.copy2(src, dst)
|
|
62
|
+
out.append("[create] {}".format(dst))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _touch_gitkeep(path, out):
|
|
66
|
+
"""Create an empty .gitkeep inside path, creating path if needed."""
|
|
67
|
+
if not os.path.isdir(path):
|
|
68
|
+
os.makedirs(path)
|
|
69
|
+
gk = os.path.join(path, ".gitkeep")
|
|
70
|
+
if os.path.exists(gk):
|
|
71
|
+
out.append("[skip] {}".format(gk))
|
|
72
|
+
else:
|
|
73
|
+
open(gk, "w").close()
|
|
74
|
+
out.append("[create] {}".format(gk))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _ensure_vscode_setting(cwd, key, value, out):
|
|
78
|
+
"""Merge a single key/value into .vscode/settings.json, creating it if needed.
|
|
79
|
+
|
|
80
|
+
Uses simple JSON load/dump so existing user settings are preserved.
|
|
81
|
+
Skips if the key is already set to the desired value.
|
|
82
|
+
"""
|
|
83
|
+
import json
|
|
84
|
+
vscode_dir = os.path.join(cwd, ".vscode")
|
|
85
|
+
settings_path = os.path.join(vscode_dir, "settings.json")
|
|
86
|
+
|
|
87
|
+
if not os.path.isdir(vscode_dir):
|
|
88
|
+
os.makedirs(vscode_dir)
|
|
89
|
+
|
|
90
|
+
settings = {}
|
|
91
|
+
if os.path.exists(settings_path):
|
|
92
|
+
try:
|
|
93
|
+
with open(settings_path, "r", encoding="utf-8") as f:
|
|
94
|
+
settings = json.load(f)
|
|
95
|
+
except (ValueError, OSError):
|
|
96
|
+
settings = {}
|
|
97
|
+
|
|
98
|
+
if settings.get(key) == value:
|
|
99
|
+
out.append("[skip] {} ({} already set)".format(settings_path, key))
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
settings[key] = value
|
|
103
|
+
with open(settings_path, "w", encoding="utf-8") as f:
|
|
104
|
+
json.dump(settings, f, indent=2)
|
|
105
|
+
f.write("\n")
|
|
106
|
+
out.append("[create] {} ({} = {})".format(settings_path, key, json.dumps(value)))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def run(cwd=None):
|
|
110
|
+
if cwd is None:
|
|
111
|
+
cwd = os.getcwd()
|
|
112
|
+
|
|
113
|
+
if not _is_git_repo(cwd):
|
|
114
|
+
sys.stderr.write(
|
|
115
|
+
"Error: {} is not a git repository.\n"
|
|
116
|
+
"Run `git init` first, then re-run `engaku init`.\n".format(cwd)
|
|
117
|
+
)
|
|
118
|
+
return 1
|
|
119
|
+
|
|
120
|
+
tpl = _templates_dir()
|
|
121
|
+
out = []
|
|
122
|
+
|
|
123
|
+
# ── .ai/ skeleton ────────────────────────────────────────────────────────
|
|
124
|
+
_copy_template(
|
|
125
|
+
os.path.join(tpl, "ai", "overview.md"),
|
|
126
|
+
os.path.join(cwd, ".ai", "overview.md"),
|
|
127
|
+
out,
|
|
128
|
+
)
|
|
129
|
+
_copy_template(
|
|
130
|
+
os.path.join(tpl, "ai", "engaku.json"),
|
|
131
|
+
os.path.join(cwd, ".ai", "engaku.json"),
|
|
132
|
+
out,
|
|
133
|
+
)
|
|
134
|
+
_touch_gitkeep(os.path.join(cwd, ".ai", "decisions"), out)
|
|
135
|
+
_touch_gitkeep(os.path.join(cwd, ".ai", "tasks"), out)
|
|
136
|
+
_touch_gitkeep(os.path.join(cwd, ".ai", "docs"), out)
|
|
137
|
+
|
|
138
|
+
# ── .github/agents/ ──────────────────────────────────────────────────────
|
|
139
|
+
agents_dir = os.path.join(cwd, ".github", "agents")
|
|
140
|
+
for name in ("dev.agent.md", "planner.agent.md",
|
|
141
|
+
"reviewer.agent.md", "scanner.agent.md"):
|
|
142
|
+
_copy_template(os.path.join(tpl, "agents", name), os.path.join(agents_dir, name), out)
|
|
143
|
+
|
|
144
|
+
# ── .github/instructions/ ─────────────────────────────────────────────
|
|
145
|
+
instructions_dir = os.path.join(cwd, ".github", "instructions")
|
|
146
|
+
for name in ("hooks.instructions.md", "tests.instructions.md", "templates.instructions.md"):
|
|
147
|
+
_copy_template(
|
|
148
|
+
os.path.join(tpl, "instructions", name),
|
|
149
|
+
os.path.join(instructions_dir, name),
|
|
150
|
+
out,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# ── .github/skills/ ──────────────────────────────────────────────────────
|
|
154
|
+
skills_dir = os.path.join(cwd, ".github", "skills")
|
|
155
|
+
for skill in ("systematic-debugging", "verification-before-completion", "frontend-design", "proactive-initiative"):
|
|
156
|
+
_copy_template(
|
|
157
|
+
os.path.join(tpl, "skills", skill, "SKILL.md"),
|
|
158
|
+
os.path.join(skills_dir, skill, "SKILL.md"),
|
|
159
|
+
out,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# ── .github/copilot-instructions.md ──────────────────────────────────────
|
|
163
|
+
_copy_template(
|
|
164
|
+
os.path.join(tpl, "copilot-instructions.md"),
|
|
165
|
+
os.path.join(cwd, ".github", "copilot-instructions.md"),
|
|
166
|
+
out,
|
|
167
|
+
)
|
|
168
|
+
# ── .vscode/settings.json ── enable agent-scoped hooks (Preview) ─────────
|
|
169
|
+
_ensure_vscode_setting(cwd, "chat.useCustomAgentHooks", True, out)
|
|
170
|
+
|
|
171
|
+
for line in out:
|
|
172
|
+
sys.stdout.write(line + "\n")
|
|
173
|
+
|
|
174
|
+
created = sum(1 for l in out if l.startswith("[create]"))
|
|
175
|
+
skipped = sum(1 for l in out if l.startswith("[skip]"))
|
|
176
|
+
sys.stdout.write(
|
|
177
|
+
"\nDone. {} file(s) created, {} skipped.\n"
|
|
178
|
+
"Tip: Run the scanner agent in Copilot chat to generate .instructions.md files.\n".format(created, skipped)
|
|
179
|
+
)
|
|
180
|
+
return 0
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def main(argv=None):
|
|
184
|
+
sys.exit(run())
|
|
185
|
+
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from engaku.utils import parse_frontmatter, read_hook_input
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _find_active_task(cwd):
|
|
9
|
+
"""Scan .ai/tasks/*.md for first file with status: in-progress.
|
|
10
|
+
|
|
11
|
+
Returns (title, unchecked_lines) tuple or None.
|
|
12
|
+
"""
|
|
13
|
+
tasks_dir = os.path.join(cwd, ".ai", "tasks")
|
|
14
|
+
if not os.path.isdir(tasks_dir):
|
|
15
|
+
return None
|
|
16
|
+
try:
|
|
17
|
+
entries = sorted(os.listdir(tasks_dir))
|
|
18
|
+
except OSError:
|
|
19
|
+
return None
|
|
20
|
+
for filename in entries:
|
|
21
|
+
if not filename.endswith(".md"):
|
|
22
|
+
continue
|
|
23
|
+
filepath = os.path.join(tasks_dir, filename)
|
|
24
|
+
try:
|
|
25
|
+
with open(filepath, "r", encoding="utf-8") as f:
|
|
26
|
+
content = f.read()
|
|
27
|
+
except OSError:
|
|
28
|
+
continue
|
|
29
|
+
fm, body = parse_frontmatter(content)
|
|
30
|
+
if fm is None:
|
|
31
|
+
continue
|
|
32
|
+
status = None
|
|
33
|
+
title = None
|
|
34
|
+
for line in fm.splitlines():
|
|
35
|
+
if line.startswith("status:"):
|
|
36
|
+
status = line[len("status:"):].strip()
|
|
37
|
+
elif line.startswith("title:"):
|
|
38
|
+
title = line[len("title:"):].strip()
|
|
39
|
+
if status != "in-progress":
|
|
40
|
+
continue
|
|
41
|
+
if title is None:
|
|
42
|
+
title = filename[:-3]
|
|
43
|
+
unchecked = [l for l in body.splitlines() if l.strip().startswith("- [ ]")]
|
|
44
|
+
return (title, unchecked)
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def run(cwd=None):
|
|
49
|
+
if cwd is None:
|
|
50
|
+
cwd = os.getcwd()
|
|
51
|
+
ai_dir = os.path.join(cwd, ".ai")
|
|
52
|
+
overview_path = os.path.join(ai_dir, "overview.md")
|
|
53
|
+
|
|
54
|
+
context_parts = []
|
|
55
|
+
|
|
56
|
+
if os.path.isfile(overview_path):
|
|
57
|
+
with open(overview_path, "r", encoding="utf-8") as f:
|
|
58
|
+
content = f.read().strip()
|
|
59
|
+
if content:
|
|
60
|
+
context_parts.append(content)
|
|
61
|
+
|
|
62
|
+
inner = "\n\n---\n\n".join(context_parts)
|
|
63
|
+
project_context = "<project-context>\n{}\n</project-context>".format(inner) if inner else ""
|
|
64
|
+
|
|
65
|
+
parts = []
|
|
66
|
+
if project_context:
|
|
67
|
+
parts.append(project_context)
|
|
68
|
+
|
|
69
|
+
active_task = _find_active_task(cwd)
|
|
70
|
+
if active_task:
|
|
71
|
+
title, unchecked = active_task
|
|
72
|
+
task_lines = ["<active-task>", "## {}".format(title)]
|
|
73
|
+
task_lines.extend(unchecked)
|
|
74
|
+
task_lines.append("</active-task>")
|
|
75
|
+
parts.append("\n".join(task_lines))
|
|
76
|
+
|
|
77
|
+
additional_context = "\n\n".join(parts)
|
|
78
|
+
|
|
79
|
+
# Determine event from stdin so PreCompact can use a different output format.
|
|
80
|
+
# PreCompact uses the common output format (systemMessage), not hookSpecificOutput.
|
|
81
|
+
hook_input = read_hook_input()
|
|
82
|
+
event = hook_input.get("hookEventName", "SessionStart")
|
|
83
|
+
|
|
84
|
+
if event == "PreCompact":
|
|
85
|
+
output = {"systemMessage": additional_context}
|
|
86
|
+
elif event == "SubagentStart":
|
|
87
|
+
output = {
|
|
88
|
+
"hookSpecificOutput": {
|
|
89
|
+
"hookEventName": "SubagentStart",
|
|
90
|
+
"additionalContext": additional_context,
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
else:
|
|
94
|
+
# SessionStart (and default)
|
|
95
|
+
output = {
|
|
96
|
+
"hookSpecificOutput": {
|
|
97
|
+
"hookEventName": "SessionStart",
|
|
98
|
+
"additionalContext": additional_context,
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
sys.stdout.write(json.dumps(output, ensure_ascii=False))
|
|
103
|
+
sys.stdout.flush()
|
|
104
|
+
return 0
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def main(argv=None):
|
|
108
|
+
sys.exit(run())
|