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.
Files changed (38) hide show
  1. engaku-0.2.0/PKG-INFO +82 -0
  2. engaku-0.2.0/README.md +66 -0
  3. engaku-0.2.0/pyproject.toml +41 -0
  4. engaku-0.2.0/setup.cfg +4 -0
  5. engaku-0.2.0/src/engaku/__init__.py +1 -0
  6. engaku-0.2.0/src/engaku/__main__.py +3 -0
  7. engaku-0.2.0/src/engaku/cli.py +64 -0
  8. engaku-0.2.0/src/engaku/cmd_apply.py +130 -0
  9. engaku-0.2.0/src/engaku/cmd_init.py +185 -0
  10. engaku-0.2.0/src/engaku/cmd_inject.py +108 -0
  11. engaku-0.2.0/src/engaku/cmd_prompt_check.py +124 -0
  12. engaku-0.2.0/src/engaku/cmd_task_review.py +93 -0
  13. engaku-0.2.0/src/engaku/constants.py +11 -0
  14. engaku-0.2.0/src/engaku/templates/agents/dev.agent.md +36 -0
  15. engaku-0.2.0/src/engaku/templates/agents/planner.agent.md +150 -0
  16. engaku-0.2.0/src/engaku/templates/agents/reviewer.agent.md +71 -0
  17. engaku-0.2.0/src/engaku/templates/agents/scanner.agent.md +20 -0
  18. engaku-0.2.0/src/engaku/templates/ai/engaku.json +1 -0
  19. engaku-0.2.0/src/engaku/templates/ai/overview.md +26 -0
  20. engaku-0.2.0/src/engaku/templates/copilot-instructions.md +10 -0
  21. engaku-0.2.0/src/engaku/templates/instructions/hooks.instructions.md +8 -0
  22. engaku-0.2.0/src/engaku/templates/instructions/templates.instructions.md +8 -0
  23. engaku-0.2.0/src/engaku/templates/instructions/tests.instructions.md +8 -0
  24. engaku-0.2.0/src/engaku/templates/skills/frontend-design/SKILL.md +76 -0
  25. engaku-0.2.0/src/engaku/templates/skills/proactive-initiative/SKILL.md +46 -0
  26. engaku-0.2.0/src/engaku/templates/skills/systematic-debugging/SKILL.md +96 -0
  27. engaku-0.2.0/src/engaku/templates/skills/verification-before-completion/SKILL.md +110 -0
  28. engaku-0.2.0/src/engaku/utils.py +111 -0
  29. engaku-0.2.0/src/engaku.egg-info/PKG-INFO +82 -0
  30. engaku-0.2.0/src/engaku.egg-info/SOURCES.txt +36 -0
  31. engaku-0.2.0/src/engaku.egg-info/dependency_links.txt +1 -0
  32. engaku-0.2.0/src/engaku.egg-info/entry_points.txt +2 -0
  33. engaku-0.2.0/src/engaku.egg-info/top_level.txt +1 -0
  34. engaku-0.2.0/tests/test_apply.py +122 -0
  35. engaku-0.2.0/tests/test_init.py +116 -0
  36. engaku-0.2.0/tests/test_inject.py +236 -0
  37. engaku-0.2.0/tests/test_prompt_check.py +209 -0
  38. 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ __version__ = "0.2.0"
@@ -0,0 +1,3 @@
1
+ from engaku.cli import main
2
+
3
+ main()
@@ -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())