eai-supinit 0.5.0__py3-none-any.whl
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.
- eai_supinit/__init__.py +3 -0
- eai_supinit/__main__.py +6 -0
- eai_supinit/agent_phrases.py +57 -0
- eai_supinit/assess.py +105 -0
- eai_supinit/bootstrap/__init__.py +0 -0
- eai_supinit/bootstrap/runner.py +201 -0
- eai_supinit/cli.py +555 -0
- eai_supinit/cli_journal.py +61 -0
- eai_supinit/cli_messages.py +54 -0
- eai_supinit/cli_workflow.py +165 -0
- eai_supinit/commit_guidance.py +124 -0
- eai_supinit/doctor.py +135 -0
- eai_supinit/model/__init__.py +0 -0
- eai_supinit/model/contract.py +42 -0
- eai_supinit/model/serialize.py +51 -0
- eai_supinit/paths.py +113 -0
- eai_supinit/plan.py +60 -0
- eai_supinit/prompting/__init__.py +0 -0
- eai_supinit/prompting/generate.py +66 -0
- eai_supinit/reset.py +121 -0
- eai_supinit/workflow_preferences.py +62 -0
- eai_supinit/workflow_state.py +285 -0
- eai_supinit-0.5.0.dist-info/METADATA +30 -0
- eai_supinit-0.5.0.dist-info/RECORD +27 -0
- eai_supinit-0.5.0.dist-info/WHEEL +5 -0
- eai_supinit-0.5.0.dist-info/entry_points.txt +2 -0
- eai_supinit-0.5.0.dist-info/top_level.txt +1 -0
eai_supinit/__init__.py
ADDED
eai_supinit/__main__.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Copy-paste agent phrases for eai-supinit."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from eai_supinit.workflow_state import WorkflowState
|
|
9
|
+
|
|
10
|
+
PHRASE_BOOTSTRAP = (
|
|
11
|
+
"Run `eai-supinit bootstrap --with-ai-rules --with-cursor --yes` for this workspace."
|
|
12
|
+
)
|
|
13
|
+
PHRASE_ASSESS = "Run `eai-supinit assess` for this workspace."
|
|
14
|
+
PHRASE_PLAN = "Run `eai-supinit plan` for this workspace."
|
|
15
|
+
PHRASE_PROMPT = "Run `eai-supinit prompt` for this workspace."
|
|
16
|
+
PHRASE_DOCTOR = "Run `eai-supinit doctor` for this workspace."
|
|
17
|
+
PHRASE_SIGN_OFF = (
|
|
18
|
+
"Run `eai-supinit workflow sign-off` after reviewing init deliverables "
|
|
19
|
+
"(do not edit workflow-state.yaml by hand)."
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def agent_phrase_rules_markdown() -> str:
|
|
24
|
+
return """## Agent phrase rules
|
|
25
|
+
|
|
26
|
+
Prefer the **Tell your agent** lines printed by `eai-supinit workflow`.
|
|
27
|
+
Do not invent alternate CLI flags when a printed phrase exists.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def format_suggested_tell_agent_line(state: WorkflowState) -> str | None:
|
|
32
|
+
if state.next_milestone is None:
|
|
33
|
+
if state.deliverables.signed_off:
|
|
34
|
+
return None
|
|
35
|
+
if state.deliverables.deliverables_complete:
|
|
36
|
+
return PHRASE_SIGN_OFF
|
|
37
|
+
return None
|
|
38
|
+
gate = state.next_milestone.gate_id
|
|
39
|
+
if gate == "layout_ready" or gate == "ai_rules_mounted":
|
|
40
|
+
return PHRASE_BOOTSTRAP
|
|
41
|
+
if gate == "prompts_ready":
|
|
42
|
+
return PHRASE_PROMPT
|
|
43
|
+
if gate == "doctor_clean":
|
|
44
|
+
return PHRASE_DOCTOR
|
|
45
|
+
if gate == "init_signed_off":
|
|
46
|
+
return PHRASE_SIGN_OFF
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
NEXT_COMMAND_PHRASE_KEYS = {
|
|
51
|
+
"bootstrap": PHRASE_BOOTSTRAP,
|
|
52
|
+
"assess": PHRASE_ASSESS,
|
|
53
|
+
"plan": PHRASE_PLAN,
|
|
54
|
+
"prompt": PHRASE_PROMPT,
|
|
55
|
+
"doctor": PHRASE_DOCTOR,
|
|
56
|
+
"sign-off": PHRASE_SIGN_OFF,
|
|
57
|
+
}
|
eai_supinit/assess.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Inspect workspace and write greenfield workspace contract."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from eai_supcore.git_ops import is_git_repository
|
|
9
|
+
from eai_supcore.io_util import write_text
|
|
10
|
+
from eai_supinit import __version__
|
|
11
|
+
from eai_supinit.model.contract import (
|
|
12
|
+
LayoutPresence,
|
|
13
|
+
WorkspaceContract,
|
|
14
|
+
WorkspaceMetadata,
|
|
15
|
+
WorkspaceReadiness,
|
|
16
|
+
)
|
|
17
|
+
from eai_supinit.model.serialize import contract_to_yaml
|
|
18
|
+
from eai_supinit.paths import InitPaths
|
|
19
|
+
from eai_supinit.workflow_state import refresh_workflow_state
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _layout_presence(workspace: Path) -> LayoutPresence:
|
|
23
|
+
return LayoutPresence(
|
|
24
|
+
src=(workspace / "src").is_dir(),
|
|
25
|
+
libs=(workspace / "libs").is_dir(),
|
|
26
|
+
docs_adr=(workspace / "docs" / "adr").is_dir(),
|
|
27
|
+
agents_md=(workspace / "AGENTS.md").is_file(),
|
|
28
|
+
ai_rules=(workspace / "tools" / "ai-rules").is_dir()
|
|
29
|
+
and is_git_repository(workspace / "tools" / "ai-rules"),
|
|
30
|
+
cursor_rules=(workspace / ".cursor" / "rules").is_dir(),
|
|
31
|
+
cmake_root=(workspace / "CMakeLists.txt").is_file(),
|
|
32
|
+
conan_root=(workspace / "conanfile.py").is_file() or (workspace / "conan").is_dir(),
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def render_summary(contract: WorkspaceContract) -> str:
|
|
37
|
+
layout = contract.layout
|
|
38
|
+
lines = [
|
|
39
|
+
"# Workspace summary",
|
|
40
|
+
"",
|
|
41
|
+
f"Generated by eai-supinit {contract.metadata.tool_version} at "
|
|
42
|
+
f"{contract.metadata.generated_at}.",
|
|
43
|
+
"",
|
|
44
|
+
"## Layout presence",
|
|
45
|
+
"",
|
|
46
|
+
f"- `src/`: {'yes' if layout.src else 'no'}",
|
|
47
|
+
f"- `libs/`: {'yes' if layout.libs else 'no'}",
|
|
48
|
+
f"- `docs/adr/`: {'yes' if layout.docs_adr else 'no'}",
|
|
49
|
+
f"- `AGENTS.md`: {'yes' if layout.agents_md else 'no'}",
|
|
50
|
+
f"- `tools/ai-rules/`: {'yes' if layout.ai_rules else 'no'}",
|
|
51
|
+
f"- `.cursor/rules/`: {'yes' if layout.cursor_rules else 'no'}",
|
|
52
|
+
f"- Root `CMakeLists.txt`: {'yes' if layout.cmake_root else 'no'}",
|
|
53
|
+
f"- Conan entry: {'yes' if layout.conan_root else 'no'}",
|
|
54
|
+
"",
|
|
55
|
+
f"**Readiness:** {contract.readiness.completeness}",
|
|
56
|
+
"",
|
|
57
|
+
f"**Next:** {contract.readiness.recommended_next_action}",
|
|
58
|
+
"",
|
|
59
|
+
]
|
|
60
|
+
if contract.readiness.blockers:
|
|
61
|
+
lines.extend(["## Blockers", ""])
|
|
62
|
+
lines.extend(f"- {b}" for b in contract.readiness.blockers)
|
|
63
|
+
lines.append("")
|
|
64
|
+
if contract.notes:
|
|
65
|
+
lines.extend(["## Notes", ""])
|
|
66
|
+
lines.extend(f"- {n}" for n in contract.notes)
|
|
67
|
+
lines.append("")
|
|
68
|
+
return "\n".join(lines)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def run_assessment(paths: InitPaths) -> WorkspaceContract:
|
|
72
|
+
paths.ensure_tool_state_dirs()
|
|
73
|
+
layout = _layout_presence(paths.workspace)
|
|
74
|
+
notes: list[str] = [
|
|
75
|
+
"Greenfield assess — no legacy Google-repo inventory.",
|
|
76
|
+
"Target steady-state layout: src/, libs/, docs/adr/, AGENTS.md, tools/ai-rules/.",
|
|
77
|
+
]
|
|
78
|
+
blockers: list[str] = []
|
|
79
|
+
if not paths.workspace.is_dir():
|
|
80
|
+
blockers.append("Workspace directory missing")
|
|
81
|
+
|
|
82
|
+
recommended = "eai-supinit plan"
|
|
83
|
+
if not layout.src or not layout.libs or not layout.docs_adr:
|
|
84
|
+
recommended = "eai-supinit bootstrap --with-ai-rules --with-cursor --yes"
|
|
85
|
+
notes.append("Layout dirs incomplete — bootstrap will scaffold steady-state dirs.")
|
|
86
|
+
|
|
87
|
+
completeness = "good" if not blockers else "partial"
|
|
88
|
+
contract = WorkspaceContract(
|
|
89
|
+
metadata=WorkspaceMetadata(
|
|
90
|
+
generated_at=datetime.now(UTC).isoformat(),
|
|
91
|
+
tool_version=__version__,
|
|
92
|
+
workspace_path=str(paths.workspace),
|
|
93
|
+
),
|
|
94
|
+
layout=layout,
|
|
95
|
+
notes=notes,
|
|
96
|
+
readiness=WorkspaceReadiness(
|
|
97
|
+
completeness=completeness,
|
|
98
|
+
blockers=blockers,
|
|
99
|
+
recommended_next_action=recommended,
|
|
100
|
+
),
|
|
101
|
+
)
|
|
102
|
+
write_text(paths.contract_path, contract_to_yaml(contract))
|
|
103
|
+
write_text(paths.summary_path, render_summary(contract))
|
|
104
|
+
refresh_workflow_state(paths)
|
|
105
|
+
return contract
|
|
File without changes
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Bootstrap greenfield steady-state layout via eai-supcore."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
|
|
7
|
+
from eai_supcore.bootstrap import (
|
|
8
|
+
SharedBootstrapResult,
|
|
9
|
+
ensure_steady_state_dirs,
|
|
10
|
+
materialize_cursor_rules,
|
|
11
|
+
mount_ai_rules,
|
|
12
|
+
scaffold_consumer_dev_rails,
|
|
13
|
+
write_cursor_overlay,
|
|
14
|
+
)
|
|
15
|
+
from eai_supcore.io_util import write_text
|
|
16
|
+
from eai_supcore.paths import DEFAULT_AI_RULES_URL
|
|
17
|
+
from eai_supinit.agent_phrases import agent_phrase_rules_markdown
|
|
18
|
+
from eai_supinit.paths import LOCAL_CURSOR_OVERLAY, InitPaths
|
|
19
|
+
from eai_supinit.plan import load_contract
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class BootstrapResult:
|
|
24
|
+
created_dirs: list[str] = field(default_factory=list)
|
|
25
|
+
created_files: list[str] = field(default_factory=list)
|
|
26
|
+
submodule_added: bool = False
|
|
27
|
+
ai_rules_found: bool = False
|
|
28
|
+
cursor_rules_materialized: list[str] = field(default_factory=list)
|
|
29
|
+
warnings: list[str] = field(default_factory=list)
|
|
30
|
+
errors: list[str] = field(default_factory=list)
|
|
31
|
+
|
|
32
|
+
def merge_shared(self, shared: SharedBootstrapResult) -> None:
|
|
33
|
+
self.created_dirs.extend(shared.created_dirs)
|
|
34
|
+
self.created_files.extend(shared.created_files)
|
|
35
|
+
self.submodule_added = self.submodule_added or shared.submodule_added
|
|
36
|
+
self.ai_rules_found = self.ai_rules_found or shared.ai_rules_found
|
|
37
|
+
self.cursor_rules_materialized.extend(shared.cursor_rules_materialized)
|
|
38
|
+
self.warnings.extend(shared.warnings)
|
|
39
|
+
self.errors.extend(shared.errors)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _scaffold_agents(paths: InitPaths, result: BootstrapResult) -> None:
|
|
43
|
+
if paths.agents_path.exists():
|
|
44
|
+
return
|
|
45
|
+
content = f"""# AGENTS.md
|
|
46
|
+
|
|
47
|
+
Local superbuild workspace. Read this file first, then `tools/ai-rules/AGENTS.md`.
|
|
48
|
+
|
|
49
|
+
## Workspace role
|
|
50
|
+
|
|
51
|
+
Greenfield multi-repo / superbuild. Steady-state layout:
|
|
52
|
+
|
|
53
|
+
- `src/` — first-party member repositories
|
|
54
|
+
- `libs/` — third-party or patched deps not from Conan
|
|
55
|
+
- `docs/adr/` — workspace ADRs
|
|
56
|
+
- `tools/ai-rules/` — shared superbuild-workflow policy (submodule)
|
|
57
|
+
|
|
58
|
+
## Init artifacts
|
|
59
|
+
|
|
60
|
+
Journey state: `.tools/esys/eai-supinit/` (committed tool state).
|
|
61
|
+
Prompts: `prompts/` — commit after `eai-supinit prompt`.
|
|
62
|
+
|
|
63
|
+
## Tell your agent
|
|
64
|
+
|
|
65
|
+
Use phrases in `prompts/` or run `eai-supinit workflow`.
|
|
66
|
+
|
|
67
|
+
{agent_phrase_rules_markdown()}
|
|
68
|
+
|
|
69
|
+
See `tools/ai-rules/docs/init-assistant.md` when mounted.
|
|
70
|
+
"""
|
|
71
|
+
write_text(paths.agents_path, content)
|
|
72
|
+
result.created_files.append("AGENTS.md")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _write_init_overlay(paths: InitPaths, shared: SharedBootstrapResult) -> None:
|
|
76
|
+
content = f"""---
|
|
77
|
+
description: Local init overlay — workspace-specific exceptions
|
|
78
|
+
alwaysApply: true
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
# Local init overlay
|
|
82
|
+
|
|
83
|
+
Read `.tools/esys/eai-supinit/workspace-contract.yaml` and root `AGENTS.md`.
|
|
84
|
+
Shared policy: `tools/ai-rules/AGENTS.md`.
|
|
85
|
+
|
|
86
|
+
Document exceptions here; do not fork shared policy under `tools/ai-rules/`.
|
|
87
|
+
|
|
88
|
+
{agent_phrase_rules_markdown()}
|
|
89
|
+
|
|
90
|
+
## Version control at checkpoints
|
|
91
|
+
|
|
92
|
+
When `eai-supinit` prints **Commit checkpoint**, remind the user to commit (or confirm
|
|
93
|
+
auto-commit ran). Commit **`prompts/`** with **`.tools/esys/eai-supinit/`**.
|
|
94
|
+
"""
|
|
95
|
+
write_cursor_overlay(
|
|
96
|
+
paths.workspace,
|
|
97
|
+
shared,
|
|
98
|
+
overlay_name=LOCAL_CURSOR_OVERLAY,
|
|
99
|
+
content=content,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def run_bootstrap(
|
|
104
|
+
paths: InitPaths,
|
|
105
|
+
*,
|
|
106
|
+
with_ai_rules: bool = False,
|
|
107
|
+
ai_rules_url: str = DEFAULT_AI_RULES_URL,
|
|
108
|
+
with_cursor: bool = False,
|
|
109
|
+
with_dev_hooks: bool = True,
|
|
110
|
+
with_esysrepo_manifest: bool = True,
|
|
111
|
+
install_hooks: bool = False,
|
|
112
|
+
dry_run: bool = False,
|
|
113
|
+
) -> BootstrapResult:
|
|
114
|
+
result = BootstrapResult()
|
|
115
|
+
paths.ensure_tool_state_dirs()
|
|
116
|
+
result.created_dirs.append(".tools/esys/eai-supinit")
|
|
117
|
+
result.created_dirs.append("prompts")
|
|
118
|
+
|
|
119
|
+
if dry_run:
|
|
120
|
+
result.warnings.append("Dry run — no files or submodules modified")
|
|
121
|
+
return result
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
load_contract(paths)
|
|
125
|
+
except FileNotFoundError:
|
|
126
|
+
result.warnings.append(
|
|
127
|
+
"No workspace contract; run `eai-supinit assess` for richer bootstrap"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
shared = SharedBootstrapResult()
|
|
131
|
+
ensure_steady_state_dirs(paths.workspace, shared)
|
|
132
|
+
scaffold_consumer_dev_rails(
|
|
133
|
+
paths.workspace,
|
|
134
|
+
shared,
|
|
135
|
+
with_hooks=with_dev_hooks,
|
|
136
|
+
with_manifest=with_esysrepo_manifest,
|
|
137
|
+
install_hooks=install_hooks,
|
|
138
|
+
)
|
|
139
|
+
result.merge_shared(shared)
|
|
140
|
+
|
|
141
|
+
_scaffold_agents(paths, result)
|
|
142
|
+
|
|
143
|
+
shared2 = SharedBootstrapResult()
|
|
144
|
+
if with_ai_rules:
|
|
145
|
+
mount_ai_rules(paths.workspace, shared2, ai_rules_url=ai_rules_url)
|
|
146
|
+
if with_cursor:
|
|
147
|
+
materialize_cursor_rules(paths.workspace, shared2)
|
|
148
|
+
_write_init_overlay(paths, shared2)
|
|
149
|
+
result.merge_shared(shared2)
|
|
150
|
+
|
|
151
|
+
next_steps = ["# Next steps", ""]
|
|
152
|
+
step = 1
|
|
153
|
+
if not paths.contract_path.is_file():
|
|
154
|
+
next_steps.append(f"{step}. Run `eai-supinit assess`")
|
|
155
|
+
step += 1
|
|
156
|
+
else:
|
|
157
|
+
next_steps.append(f"{step}. Review `.tools/esys/eai-supinit/workspace-summary.md`")
|
|
158
|
+
step += 1
|
|
159
|
+
if not paths.plan_path.is_file():
|
|
160
|
+
next_steps.append(f"{step}. Run `eai-supinit plan`")
|
|
161
|
+
step += 1
|
|
162
|
+
next_steps.append(f"{step}. Run `eai-supinit prompt` for AI handoff")
|
|
163
|
+
step += 1
|
|
164
|
+
if result.submodule_added:
|
|
165
|
+
next_steps.append(f"{step}. `git submodule update --init tools/ai-rules`")
|
|
166
|
+
step += 1
|
|
167
|
+
if with_dev_hooks and not install_hooks:
|
|
168
|
+
next_steps.append(f"{step}. `python scripts/setup-dev.py` (pre-commit + gitlint)")
|
|
169
|
+
step += 1
|
|
170
|
+
next_steps.append(
|
|
171
|
+
f"{step}. Optional: `eai-supinit extension add cmake` when using CMake builds"
|
|
172
|
+
)
|
|
173
|
+
step += 1
|
|
174
|
+
next_steps.append(f"{step}. Run `eai-supinit doctor` then `eai-supinit workflow sign-off`")
|
|
175
|
+
next_steps.append("")
|
|
176
|
+
write_text(paths.next_steps_path, "\n".join(next_steps))
|
|
177
|
+
result.created_files.append(".tools/esys/eai-supinit/next-steps.md")
|
|
178
|
+
|
|
179
|
+
summary_lines = [
|
|
180
|
+
"# Bootstrap summary",
|
|
181
|
+
"",
|
|
182
|
+
f"- AI rules checkout found: {result.ai_rules_found}",
|
|
183
|
+
f"- Submodule added: {result.submodule_added}",
|
|
184
|
+
f"- Cursor rules materialized: {len(result.cursor_rules_materialized)}",
|
|
185
|
+
f"- Dev hooks scaffolded: {with_dev_hooks}",
|
|
186
|
+
f"- esysrepo manifest scaffolded: {with_esysrepo_manifest}",
|
|
187
|
+
"",
|
|
188
|
+
"## Created directories",
|
|
189
|
+
"",
|
|
190
|
+
]
|
|
191
|
+
summary_lines.extend(f"- `{d}`" for d in result.created_dirs)
|
|
192
|
+
summary_lines.extend(["", "## Created files", ""])
|
|
193
|
+
summary_lines.extend(f"- `{f}`" for f in result.created_files)
|
|
194
|
+
if result.warnings:
|
|
195
|
+
summary_lines.extend(["", "## Warnings", ""])
|
|
196
|
+
summary_lines.extend(f"- {w}" for w in result.warnings)
|
|
197
|
+
if result.errors:
|
|
198
|
+
summary_lines.extend(["", "## Errors", ""])
|
|
199
|
+
summary_lines.extend(f"- {e}" for e in result.errors)
|
|
200
|
+
write_text(paths.bootstrap_summary_path, "\n".join(summary_lines))
|
|
201
|
+
return result
|