nobrainer-tech-flow 0.0.0-stage → 2.4.0

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 (149) hide show
  1. package/.agents/plugins/marketplace.json +20 -0
  2. package/.claude-plugin/marketplace.json +18 -0
  3. package/.claude-plugin/plugin.json +21 -0
  4. package/.codex-plugin/plugin.json +47 -0
  5. package/.opencode/INSTALL.md +37 -0
  6. package/.opencode/plugins/nobrainer-tech-skills.js +66 -0
  7. package/.pi/extensions/nobrainer-tech-skills.js +64 -0
  8. package/AGENTS.md +204 -0
  9. package/CLAUDE.md +204 -0
  10. package/CONTRIBUTING.md +70 -0
  11. package/GEMINI.md +1 -0
  12. package/LICENSE +21 -0
  13. package/README.md +658 -2
  14. package/SECURITY.md +33 -0
  15. package/adapters/bootstrap.md +27 -0
  16. package/assets/nobrainer-tech-logo.svg +1 -0
  17. package/assets/nobrainer-workflow.svg +155 -0
  18. package/assets/plugin-logo.png +0 -0
  19. package/bin/nobrainer-tech-flow.mjs +48 -0
  20. package/docs/BOUNDED_RUNNER.md +60 -0
  21. package/docs/CHAT_PLUGINS.md +29 -0
  22. package/docs/COMPATIBILITY.md +371 -0
  23. package/docs/INSTALL.md +626 -0
  24. package/docs/MIGRATION_TO_FLOW.md +117 -0
  25. package/docs/NAMING.md +39 -0
  26. package/docs/PRIVACY.md +9 -0
  27. package/docs/SESSION_RESTART.md +70 -0
  28. package/docs/SKILL_CURATION.md +124 -0
  29. package/docs/TESTING.md +232 -0
  30. package/docs/TRY_IT.md +109 -0
  31. package/docs/releases/v1.0.0.md +81 -0
  32. package/docs/releases/v1.1.0.md +85 -0
  33. package/docs/releases/v1.10.0.md +34 -0
  34. package/docs/releases/v1.11.0-publication-readback.md +25 -0
  35. package/docs/releases/v1.11.0.md +42 -0
  36. package/docs/releases/v1.12.0.md +30 -0
  37. package/docs/releases/v1.13.0.md +46 -0
  38. package/docs/releases/v1.14.0.md +43 -0
  39. package/docs/releases/v1.14.1.md +21 -0
  40. package/docs/releases/v1.2.0.md +87 -0
  41. package/docs/releases/v1.2.1.md +92 -0
  42. package/docs/releases/v1.3.0-publication-readback.md +69 -0
  43. package/docs/releases/v1.3.0.md +66 -0
  44. package/docs/releases/v1.3.1-publication-readback.md +76 -0
  45. package/docs/releases/v1.3.1.md +59 -0
  46. package/docs/releases/v1.4.0-publication-readback.md +81 -0
  47. package/docs/releases/v1.4.0.md +43 -0
  48. package/docs/releases/v1.5.0-publication-readback.md +76 -0
  49. package/docs/releases/v1.5.0.md +54 -0
  50. package/docs/releases/v1.6.0.md +64 -0
  51. package/docs/releases/v1.6.1-publication-readback.md +53 -0
  52. package/docs/releases/v1.6.1.md +82 -0
  53. package/docs/releases/v1.7.0.md +42 -0
  54. package/docs/releases/v1.7.1-publication-readback.md +64 -0
  55. package/docs/releases/v1.7.1.md +49 -0
  56. package/docs/releases/v1.8.0.md +63 -0
  57. package/docs/releases/v1.8.1.md +32 -0
  58. package/docs/releases/v1.9.0.md +33 -0
  59. package/docs/releases/v2.0.0-candidate.md +28 -0
  60. package/docs/releases/v2.0.0.md +53 -0
  61. package/docs/releases/v2.0.1.md +135 -0
  62. package/docs/releases/v2.0.2.md +189 -0
  63. package/docs/releases/v2.1.0.md +64 -0
  64. package/docs/releases/v2.1.1.md +43 -0
  65. package/docs/releases/v2.2.0.md +64 -0
  66. package/docs/releases/v2.2.1.md +11 -0
  67. package/docs/releases/v2.3.0.md +97 -0
  68. package/docs/releases/v2.3.1.md +71 -0
  69. package/docs/releases/v2.3.2.md +242 -0
  70. package/docs/releases/v2.4.0.md +126 -0
  71. package/gemini-extension.json +6 -0
  72. package/hooks/claude-hooks.json +17 -0
  73. package/hooks/hooks-cursor.json +10 -0
  74. package/hooks/run-hook.cmd +36 -0
  75. package/hooks/session-start +38 -0
  76. package/package.json +69 -4
  77. package/plugin.json +51 -0
  78. package/scripts/build_plugin.py +50 -0
  79. package/scripts/check_flow_update.py +18 -0
  80. package/scripts/check_public_commit.py +86 -0
  81. package/scripts/fetch_public_refs.py +69 -0
  82. package/scripts/install.py +600 -0
  83. package/scripts/install.sh +1392 -0
  84. package/scripts/install_guards.py +279 -0
  85. package/scripts/install_personalization.py +527 -0
  86. package/scripts/install_skills.py +951 -0
  87. package/scripts/npm_install.py +85 -0
  88. package/scripts/recommend_flow_setup.py +1522 -0
  89. package/scripts/validate_skills.py +465 -0
  90. package/skills/nobrainer-auto-fine-tune/SKILL.md +79 -0
  91. package/skills/nobrainer-auto-fine-tune/references/capacity-audit.md +49 -0
  92. package/skills/nobrainer-autoimprove/SKILL.md +212 -0
  93. package/skills/nobrainer-autoimprove/scripts/count_words.py +22 -0
  94. package/skills/nobrainer-browser/SKILL.md +234 -0
  95. package/skills/nobrainer-browser/references/cdp-profile-restart.md +93 -0
  96. package/skills/nobrainer-build/SKILL.md +173 -0
  97. package/skills/nobrainer-codex-context/SKILL.md +111 -0
  98. package/skills/nobrainer-codex-context/scripts/codex_context.py +175 -0
  99. package/skills/nobrainer-decide/SKILL.md +198 -0
  100. package/skills/nobrainer-dispatcher/SKILL.md +256 -0
  101. package/skills/nobrainer-dispatcher/agents/openai.yaml +4 -0
  102. package/skills/nobrainer-rca/SKILL.md +163 -0
  103. package/skills/nobrainer-research/SKILL.md +131 -0
  104. package/skills/nobrainer-review/SKILL.md +195 -0
  105. package/skills/nobrainer-security/SKILL.md +166 -0
  106. package/skills/nobrainer-sessions/SKILL.md +223 -0
  107. package/skills/nobrainer-sessions/references/evidence-receipt.md +55 -0
  108. package/skills/nobrainer-sessions/references/protocol.md +337 -0
  109. package/skills/nobrainer-sessions/references/session-restart.md +255 -0
  110. package/skills/nobrainer-sessions/scripts/evidence_receipt.py +373 -0
  111. package/skills/nobrainer-sessions/scripts/restart_gate.py +140 -0
  112. package/skills/nobrainer-sessions/scripts/run_bounded.py +390 -0
  113. package/skills/nobrainer-sessions/scripts/session_title.py +43 -0
  114. package/skills/nobrainer-skill-doctor/SKILL.md +149 -0
  115. package/skills/nobrainer-skill-doctor/references/astra.md +19 -0
  116. package/skills/nobrainer-skill-doctor/scripts/audit.py +100 -0
  117. package/skills/nobrainer-skill-doctor/scripts/context_candidates.py +133 -0
  118. package/skills/nobrainer-spec-driven-development/SKILL.md +132 -0
  119. package/skills/nobrainer-spec-driven-development/references/spec-template.md +114 -0
  120. package/skills/nobrainer-team/SKILL.md +165 -0
  121. package/skills/nobrainer-team/references/team-plan.md +64 -0
  122. package/skills/nobrainer-tech-flow/SKILL.md +239 -0
  123. package/skills/nobrainer-tech-flow/VERSION +1 -0
  124. package/skills/nobrainer-tech-flow/references/autopilot.md +72 -0
  125. package/skills/nobrainer-tech-flow/references/codex-routing.md +21 -0
  126. package/skills/nobrainer-tech-flow/references/communication.md +108 -0
  127. package/skills/nobrainer-tech-flow/references/correction-hooks.md +110 -0
  128. package/skills/nobrainer-tech-flow/references/daily-update.md +28 -0
  129. package/skills/nobrainer-tech-flow/references/delivery.md +243 -0
  130. package/skills/nobrainer-tech-flow/references/idea-to-delivery.md +82 -0
  131. package/skills/nobrainer-tech-flow/references/long-run-state.md +209 -0
  132. package/skills/nobrainer-tech-flow/references/model-routing.md +83 -0
  133. package/skills/nobrainer-tech-flow/references/optional-decisions.md +83 -0
  134. package/skills/nobrainer-tech-flow/references/project-start.md +22 -0
  135. package/skills/nobrainer-tech-flow/references/routing.md +125 -0
  136. package/skills/nobrainer-tech-flow/references/setup.md +180 -0
  137. package/skills/nobrainer-tech-flow/references/worktrees.md +138 -0
  138. package/skills/nobrainer-tech-flow/scripts/check_flow_update.py +298 -0
  139. package/skills/nobrainer-tech-flow/scripts/cleanup_worktree.py +327 -0
  140. package/skills/nobrainer-tech-flow/scripts/decision_config.py +107 -0
  141. package/skills/nobrainer-tech-flow/scripts/flow_guard.py +252 -0
  142. package/skills/nobrainer-tech-flow/scripts/typed_decisions.py +210 -0
  143. package/skills/nobrainer-wiki/SKILL.md +164 -0
  144. package/skills/nobrainer-wiki/references/setup.md +88 -0
  145. package/skills/nobrainer-writing/SKILL.md +272 -0
  146. package/skills/nobrainer-writing/agents/openai.yaml +4 -0
  147. package/skills/nobrainer-writing/references/brief-artifacts.md +357 -0
  148. package/skills/nobrainer-writing/references/plain-technical-english.md +76 -0
  149. package/skills/nobrainer-writing/references/research.md +86 -0
@@ -0,0 +1,279 @@
1
+ #!/usr/bin/env python3
2
+ """Preview, add or remove the optional nobrainer-tech-flow guards for Claude Code.
3
+
4
+ The guards are three hooks in the Claude Code user settings.json: a PreToolUse hook
5
+ that asks before common risky shell commands, and Stop and SubagentStop hooks that
6
+ ask for a proof line once. Every run is a preview until --apply. The hooks point at
7
+ flow_guard.py through the skill link that scripts/install.py created, so they keep
8
+ working when the checkout is updated. Our entries are the hooks whose command is
9
+ exactly one this script generates; --undo --apply removes those and nothing else.
10
+ The preview names only those hooks and the settings path, never other settings,
11
+ which can hold tokens.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import argparse
17
+ import importlib.util
18
+ import json
19
+ import os
20
+ import shlex
21
+ import shutil
22
+ import sys
23
+ import tempfile
24
+ import time
25
+ from pathlib import Path
26
+
27
+
28
+ ROOT = Path(__file__).resolve().parents[1]
29
+ INSTALL_SKILLS = ROOT / "scripts" / "install_skills.py"
30
+ GUARD = Path("nobrainer-tech-flow") / "scripts" / "flow_guard.py"
31
+ EVENTS = (("PreToolUse", "Bash", "pre-tool"), ("Stop", None, "stop"), ("SubagentStop", None, "stop"))
32
+ TIMEOUT = 10
33
+ DISCLAIMER = (
34
+ "NOTE: a seatbelt: it asks before common risky commands; it is not a security "
35
+ "boundary, and sh -c or variables bypass it."
36
+ )
37
+
38
+
39
+ class Refused(Exception):
40
+ """The run stopped; the reason is the message and nothing was written."""
41
+
42
+
43
+ def client_destinations():
44
+ spec = importlib.util.spec_from_file_location("_flow_install_skills", INSTALL_SKILLS)
45
+ if spec is None or spec.loader is None:
46
+ raise RuntimeError(f"cannot load {INSTALL_SKILLS}")
47
+ module = importlib.util.module_from_spec(spec)
48
+ spec.loader.exec_module(module)
49
+ return module.client_destinations
50
+
51
+
52
+ def locations(home: Path | None) -> tuple[Path, Path]:
53
+ """(settings.json, flow_guard.py through the skill link) for Claude Code.
54
+
55
+ An explicit home selects ~/.claude under it and ignores CLAUDE_CONFIG_DIR, as
56
+ scripts/install.py does.
57
+ """
58
+
59
+ environ = {} if home is not None else os.environ
60
+ skills = client_destinations()(environ, home or Path.home(), "claude")["claude"]
61
+ return skills.parent / "settings.json", skills / GUARD
62
+
63
+
64
+ def command(guard: Path, mode: str) -> str:
65
+ # A missing or failing guard (the skills were unlinked, no python3) must stay silent:
66
+ # exit 2 from a hook would block every shell command or every stop.
67
+ path = shlex.quote(str(guard))
68
+ return f"if [ -f {path} ]; then python3 {path} {mode} || true; fi"
69
+
70
+
71
+ def ours(hook: object, guard: Path) -> bool:
72
+ # The exact generated text, not a path substring: the path may be shell-quoted, and
73
+ # another hook that merely mentions the path is not ours.
74
+ return isinstance(hook, dict) and hook.get("command") in {command(guard, mode) for _, _, mode in EVENTS}
75
+
76
+
77
+ def read_settings(path: Path) -> tuple[str | None, dict]:
78
+ if not path.exists():
79
+ return None, {}
80
+ text = path.read_text(encoding="utf-8")
81
+ try:
82
+ settings = json.loads(text)
83
+ except ValueError as exc:
84
+ raise Refused(f"{path} is not valid JSON ({exc}); fix it first") from exc
85
+ if not isinstance(settings, dict):
86
+ raise Refused(f"{path} does not hold a JSON object")
87
+ hooks = settings.get("hooks", {})
88
+ if not isinstance(hooks, dict):
89
+ raise Refused(f"{path}: \"hooks\" is not an object")
90
+ for event, groups in hooks.items():
91
+ if not isinstance(groups, list) or not all(
92
+ isinstance(group, dict) and isinstance(group.get("hooks", []), list) for group in groups
93
+ ):
94
+ raise Refused(f"{path}: hooks.{event} is not a list of matcher groups")
95
+ return text, settings
96
+
97
+
98
+ def without_ours(settings: dict, guard: Path) -> dict:
99
+ """SETTINGS with our hooks removed, and the groups and events they leave empty."""
100
+
101
+ result = json.loads(json.dumps(settings))
102
+ hooks = result.get("hooks")
103
+ if not isinstance(hooks, dict):
104
+ return result
105
+ for event in list(hooks):
106
+ groups = []
107
+ for group in hooks[event]:
108
+ kept = [hook for hook in group.get("hooks", []) if not ours(hook, guard)]
109
+ if kept or not group.get("hooks"):
110
+ groups.append({**group, "hooks": kept} if "hooks" in group else group)
111
+ if groups or not hooks[event]:
112
+ hooks[event] = groups
113
+ else:
114
+ del hooks[event]
115
+ if not hooks and "hooks" in settings and settings["hooks"]:
116
+ del result["hooks"]
117
+ return result
118
+
119
+
120
+ def with_ours(settings: dict, guard: Path) -> dict:
121
+ result = without_ours(settings, guard)
122
+ hooks = result.setdefault("hooks", {})
123
+ for event, matcher, mode in EVENTS:
124
+ group: dict = {"matcher": matcher} if matcher else {}
125
+ group["hooks"] = [{"type": "command", "command": command(guard, mode), "timeout": TIMEOUT}]
126
+ hooks.setdefault(event, []).append(group)
127
+ return result
128
+
129
+
130
+ def render(settings: dict) -> str:
131
+ return json.dumps(settings, indent=2) + "\n"
132
+
133
+
134
+ def listed(settings: dict, guard: Path) -> list[str]:
135
+ """One line per hook of ours in SETTINGS: its event, matcher and command."""
136
+
137
+ lines = []
138
+ for event, groups in settings.get("hooks", {}).items():
139
+ for group in groups:
140
+ matcher = f" (matcher {group['matcher']})" if group.get("matcher") else ""
141
+ lines.extend(f" {event}{matcher}: {hook['command']}" for hook in group.get("hooks", []) if ours(hook, guard))
142
+ return lines
143
+
144
+
145
+ def backup_copy(target: Path) -> Path:
146
+ """Copy TARGET to a new backup file; an existing file of that name is never replaced."""
147
+
148
+ stamp = time.strftime("%Y%m%d-%H%M%S")
149
+ for counter in range(1000):
150
+ suffix = f"-{counter}" if counter else ""
151
+ backup = target.with_name(f"{target.name}.flow-guards-{stamp}{suffix}.bak")
152
+ try:
153
+ # Exclusive creation also refuses a symlink planted at the name. The settings can
154
+ # hold tokens, so the copy is private from its first byte whatever the umask is.
155
+ descriptor = os.open(backup, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
156
+ except FileExistsError:
157
+ continue
158
+ try:
159
+ with os.fdopen(descriptor, "wb") as handle, target.open("rb") as source:
160
+ shutil.copyfileobj(source, handle)
161
+ # Only the times are copied: copying the mode would undo the privacy above.
162
+ status = target.stat()
163
+ os.utime(backup, ns=(status.st_atime_ns, status.st_mtime_ns))
164
+ except BaseException:
165
+ backup.unlink(missing_ok=True)
166
+ raise
167
+ return backup
168
+ raise RuntimeError(f"no free backup name for {target}")
169
+
170
+
171
+ def write(path: Path, before: str | None, after: str) -> Path | None:
172
+ """Write AFTER atomically, keeping a backup of BEFORE; the backup path or None."""
173
+
174
+ target = path.resolve() if path.is_symlink() else path
175
+ target.parent.mkdir(parents=True, exist_ok=True)
176
+ backup = None
177
+ if before is not None:
178
+ backup = backup_copy(target)
179
+ descriptor, staged = tempfile.mkstemp(prefix=f".{target.name}.", dir=target.parent)
180
+ try:
181
+ with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
182
+ handle.write(after)
183
+ handle.flush()
184
+ os.fsync(handle.fileno())
185
+ if before is not None:
186
+ shutil.copymode(target, staged)
187
+ os.replace(staged, target)
188
+ except BaseException:
189
+ Path(staged).unlink(missing_ok=True)
190
+ raise
191
+ if target.read_text(encoding="utf-8") != after:
192
+ raise RuntimeError(f"readback of {target} does not match what was written")
193
+ return backup
194
+
195
+
196
+ def installed(settings_path: Path, guard: Path) -> bool:
197
+ """Whether SETTINGS_PATH holds any of our hooks; unreadable settings count as no."""
198
+
199
+ try:
200
+ _, settings = read_settings(settings_path)
201
+ except (OSError, Refused):
202
+ return False
203
+ return without_ours(settings, guard) != settings
204
+
205
+
206
+ def run(apply: bool, undo: bool, home: Path | None, rerun: str = "python3 scripts/install_guards.py {flags}") -> int:
207
+ """RERUN is the command that continues this run, with {flags} where --apply goes."""
208
+
209
+ settings_path, guard = locations(home)
210
+ if not undo and not guard.is_file():
211
+ print(
212
+ f"UNSUPPORTED: no linked nobrainer-tech-flow skill at {guard.parent.parent}. The guards "
213
+ "point at the skill that scripts/install.py --client claude links; a plugin-only "
214
+ "install is not supported in this version. Install the skills first, then rerun."
215
+ )
216
+ return 3
217
+ before, settings = read_settings(settings_path)
218
+ updated = without_ours(settings, guard) if undo else with_ours(settings, guard)
219
+ if updated == settings:
220
+ if undo:
221
+ print(f"NOTHING TO UNDO: no Flow guards in {settings_path}; nothing was changed")
222
+ else:
223
+ print(f"UP TO DATE: the Flow guards are already in {settings_path}; nothing changed")
224
+ return 0
225
+ after = render(updated)
226
+ if not apply:
227
+ hooks = listed(settings if undo else updated, guard)
228
+ verb = f"remove {len(hooks)} hooks from" if undo else f"add {len(hooks)} hooks to"
229
+ print(f"PREVIEW: would {verb} {settings_path}; other settings keep their values:")
230
+ print("\n".join(hooks))
231
+ if not undo:
232
+ print(DISCLAIMER)
233
+ flags = "--undo --apply" if undo else "--apply"
234
+ print(f"NO_CHANGES: preview only; {'remove' if undo else 'add'} them with: {rerun.replace('{flags}', flags)}")
235
+ return 0
236
+ backup = write(settings_path, before, after)
237
+ print(f"{'REMOVED' if undo else 'INSTALLED'}: Flow guards in {settings_path}")
238
+ if backup:
239
+ print(f"BACKUP: {backup}")
240
+ if not undo:
241
+ print(DISCLAIMER)
242
+ print(f"UNDO: {rerun.replace('{flags}', '--undo --apply')}")
243
+ print("NEXT: restart Claude Code so it loads the changed hooks.")
244
+ return 0
245
+
246
+
247
+ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
248
+ parser = argparse.ArgumentParser(
249
+ description="Add the optional Flow guards to the Claude Code settings.json. Without --apply this only previews."
250
+ )
251
+ parser.add_argument("--apply", action="store_true", help="write the change; the default is a preview")
252
+ parser.add_argument("--undo", action="store_true", help="remove the Flow guards (add --apply to do it)")
253
+ parser.add_argument(
254
+ "--home",
255
+ type=Path,
256
+ help="profile home for ~/.claude; when omitted, CLAUDE_CONFIG_DIR is honoured",
257
+ )
258
+ return parser.parse_args(argv)
259
+
260
+
261
+ def main(argv: list[str] | None = None) -> int:
262
+ args = parse_args(argv)
263
+ if os.name == "nt":
264
+ print("UNSUPPORTED: the Flow guards are POSIX-only in this version.")
265
+ return 3
266
+ home = Path(os.path.abspath(args.home.expanduser())) if args.home else None
267
+ rerun = "python3 scripts/install_guards.py {flags}" + (f" --home {shlex.quote(str(home))}" if home else "")
268
+ try:
269
+ return run(args.apply, args.undo, home, rerun)
270
+ except Refused as refused:
271
+ print(f"REFUSED: {refused}; nothing was changed", file=sys.stderr)
272
+ return 3
273
+ except (OSError, RuntimeError, ValueError) as exc:
274
+ print(f"ERROR: {exc}", file=sys.stderr)
275
+ return 2
276
+
277
+
278
+ if __name__ == "__main__":
279
+ raise SystemExit(main())