@tyroneross/build-loop 0.36.0 → 0.43.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.
- package/.agents/plugins/marketplace.json +2 -2
- package/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/rules/build-loop-surface.mdc +12 -11
- package/.cursor/rules/skill-index.mdc +33 -0
- package/AGENTS.md +213 -34
- package/README.md +99 -31
- package/agents/advisor.md +4 -4
- package/agents/alignment-checker.md +2 -2
- package/agents/architecture-scout.md +4 -4
- package/agents/build-orchestrator.md +38 -36
- package/agents/database-assessor.md +11 -5
- package/agents/design-contract-specialist.md +8 -8
- package/agents/fact-checker.md +13 -3
- package/agents/fix-critique.md +2 -2
- package/agents/independent-auditor.md +60 -7
- package/agents/leak-scanner.md +82 -0
- package/agents/overfitting-reviewer.md +2 -2
- package/agents/plan-critic.md +1 -1
- package/agents/promotion-reviewer.md +5 -5
- package/agents/retrospective-synthesizer.md +138 -35
- package/agents/scope-auditor.md +82 -11
- package/agents/security-reviewer.md +56 -2
- package/agents/self-improvement-architect.md +17 -3
- package/agents/transcript-pattern-miner.md +5 -5
- package/agents/ui-validator.md +1 -1
- package/bin/build-loop-debugger.js +143 -0
- package/bin/build-loop-install.js +1 -4
- package/bin/build-loop-load-probe.js +345 -0
- package/codex-skills/build-loop/SKILL.md +28 -6
- package/commands/feedback.md +37 -0
- package/dist/src/interactive-verifier.d.ts +1 -14
- package/dist/src/interactive-verifier.d.ts.map +1 -1
- package/dist/src/interactive-verifier.js +6 -113
- package/dist/src/interactive-verifier.js.map +1 -1
- package/dist/src/quality.d.ts +5 -0
- package/dist/src/quality.d.ts.map +1 -0
- package/dist/src/quality.js +81 -0
- package/dist/src/quality.js.map +1 -0
- package/dist/src/storage.d.ts.map +1 -1
- package/dist/src/storage.js +37 -3
- package/dist/src/storage.js.map +1 -1
- package/docs/agent-surface-policy.md +35 -31
- package/docs/memory-setup.md +19 -0
- package/hooks/git/pre-push +65 -4
- package/hooks/hooks.json +95 -38
- package/hooks/pre-commit +20 -1
- package/hooks/pre-edit-rally-point.sh +10 -3
- package/hooks/session-start-codex-hook-trust.sh +30 -0
- package/hooks/session-start-git-hooks.sh +3 -1
- package/hooks/session-start-rally-point.sh +52 -4
- package/hooks/session-start-worktree-gc.sh +47 -94
- package/hooks/stop-transcript-sweep.sh +173 -0
- package/hooks/test_closeout.sh +14 -2
- package/package.json +8 -7
- package/scripts/README.md +1 -1
- package/scripts/_paths.py +65 -0
- package/scripts/groundwork_exchange.py +1012 -0
- package/scripts/install_memory.py +33 -1
- package/scripts/lessons_index/ingest.py +13 -2
- package/scripts/lessons_index/query.py +36 -13
- package/scripts/memory_context/__init__.py +108 -14
- package/scripts/memory_graph/__init__.py +5 -1
- package/scripts/project_resolver.py +42 -36
- package/scripts/sync_plugin_cache.py +37 -2
- package/skills/agent-rally-point/SKILL.md +46 -0
- package/skills/api-registry-bridge/SKILL.md +1 -1
- package/skills/architecture/dead/SKILL.md +1 -1
- package/skills/architecture/impact/SKILL.md +1 -1
- package/skills/architecture/review/SKILL.md +1 -1
- package/skills/architecture/rules/SKILL.md +3 -3
- package/skills/architecture/scan/SKILL.md +1 -1
- package/skills/architecture/trace/SKILL.md +1 -1
- package/skills/attribution-standard/SKILL.md +6 -6
- package/skills/auto-decision-capture/SKILL.md +31 -2
- package/skills/auto-finding-capture/SKILL.md +28 -1
- package/skills/build-loop/SKILL.md +131 -23
- package/skills/build-loop/fallbacks.md +16 -21
- package/skills/build-loop/phases/ui-validation.md +2 -2
- package/skills/build-loop/references/advisor-dispatch-ladder.md +1 -1
- package/skills/build-loop/references/apple-native-planning.md +1 -1
- package/skills/build-loop/references/autonomous-and-per-commit-modes.md +11 -5
- package/skills/build-loop/references/autonomy-dashboard.md +115 -0
- package/skills/build-loop/references/capability-routing.md +24 -2
- package/skills/build-loop/references/coordination.md +24 -6
- package/skills/build-loop/references/experiment-results-template.md +15 -3
- package/skills/build-loop/references/leadership.md +1 -1
- package/skills/build-loop/references/memory.md +14 -3
- package/skills/build-loop/references/modular-systems-pack.md +8 -0
- package/skills/build-loop/references/output-style.md +86 -0
- package/skills/build-loop/references/phase-1-assess.md +102 -2
- package/skills/build-loop/references/phase-2-plan.md +9 -1
- package/skills/build-loop/references/phase-3-execute.md +5 -2
- package/skills/build-loop/references/phase-4-review.md +85 -8
- package/skills/build-loop/references/phase-5-iterate.md +76 -8
- package/skills/build-loop/references/phase-6-learn.md +10 -17
- package/skills/build-loop/references/privileged-request-broker.md +254 -0
- package/skills/build-loop/references/resource-aware-execution.md +183 -0
- package/skills/build-loop/references/self-recursive-dev.md +2 -2
- package/skills/build-loop/references/status-output-format.md +207 -0
- package/skills/build-loop/references/verify-dispatch.md +56 -2
- package/skills/building-with-deepagents/SKILL.md +1 -1
- package/skills/claim-scope/SKILL.md +185 -0
- package/skills/color-engine/SKILL.md +103 -0
- package/skills/color-engine/_core.py +464 -0
- package/skills/color-engine/color_engine.py +175 -0
- package/skills/cost-rca/SKILL.md +61 -0
- package/skills/data-plane-worktrees/SKILL.md +139 -0
- package/skills/data-plane-worktrees/agents/openai.yaml +4 -0
- package/skills/database-practice/SKILL.md +200 -0
- package/skills/database-practice/references/diagnostic-queries.sql +126 -0
- package/skills/database-practice/references/vector-and-graph-tuning.md +208 -0
- package/skills/database-practice/scripts/db_table_map.py +1244 -0
- package/skills/database-practice/scripts/test_db_table_map.py +514 -0
- package/skills/debug-loop/SKILL.md +36 -6
- package/skills/debugging-memory/SKILL.md +32 -430
- package/skills/debugging-memory/references/pattern-extraction.md +4 -4
- package/skills/debugging-memory/references/search.md +32 -120
- package/skills/debugging-memory/references/store.md +32 -126
- package/skills/debugging-memory/references/subagent-integration.md +1 -1
- package/skills/decision-queue/SKILL.md +251 -0
- package/skills/decision-queue/assets/template.html +1242 -0
- package/skills/decision-queue/references/example-large-queue-batching.md +164 -0
- package/skills/decision-queue/scripts/regen_template_constants.py +160 -0
- package/skills/defenseclaw-bridge/SKILL.md +2 -2
- package/skills/defenseclaw-bridge/references/dc-config-mapping.md +2 -9
- package/skills/drain-proposals/SKILL.md +53 -0
- package/skills/focused-loop-builder/SKILL.md +31 -0
- package/skills/focused-loop-builder/references/spec-format.md +27 -0
- package/skills/handoff/SKILL.md +169 -8
- package/skills/ibr-bridge/SKILL.md +4 -1
- package/skills/knowledge/SKILL.md +26 -14
- package/skills/knowledge/references/review-mode.md +2 -3
- package/skills/knowledge/templates/madr-minimal.md +1 -1
- package/skills/mcp-builder/SKILL.md +1 -1
- package/skills/model-bakeoff/SKILL.md +48 -10
- package/skills/model-tiering/SKILL.md +92 -31
- package/skills/native-ax-driver/SKILL.md +38 -5
- package/skills/native-ax-driver/scripts/native_driver.py +278 -22
- package/skills/native-ax-driver/scripts/test_native_driver.py +227 -0
- package/skills/optimize/SKILL.md +1 -1
- package/skills/plugin-builder/SKILL.md +48 -1
- package/skills/plugin-builder/references/build-loop-phase-guidance.md +3 -4
- package/skills/plugin-builder/references/distribution.md +13 -2
- package/skills/plugin-builder/references/plugin-hygiene-lessons.md +2 -2
- package/skills/plugin-tests/SKILL.md +2 -2
- package/skills/recursive-retrospective/SKILL.md +1 -1
- package/skills/repo-closeout/SKILL.md +17 -0
- package/skills/repo-closeout/agents/openai.yaml +4 -0
- package/skills/repo-maintenance/SKILL.md +179 -0
- package/skills/repo-maintenance/agents/openai.yaml +4 -0
- package/skills/repo-maintenance/references/pre-public-hygiene.md +134 -0
- package/skills/repo-maintenance/references/repository-taxonomy.md +161 -0
- package/skills/repo-maintenance/references/safety-protocol.md +106 -0
- package/skills/repo-maintenance/references/stack-profiles.md +138 -0
- package/skills/repo-maintenance/scripts/audit_repo_maintenance.py +1198 -0
- package/skills/repo-maintenance/scripts/test_audit_repo_maintenance.py +506 -0
- package/skills/repository-intelligence/SKILL.md +189 -0
- package/skills/repository-intelligence/agents/openai.yaml +4 -0
- package/skills/repository-intelligence/references/assessment-rubric.md +88 -0
- package/skills/repository-intelligence/scripts/repository_inventory.py +347 -0
- package/skills/research/SKILL.md +12 -2
- package/skills/root-cause-analysis/SKILL.md +1 -1
- package/skills/runtime-parity-verification/SKILL.md +36 -1
- package/skills/security-methodology/SKILL.md +23 -10
- package/skills/security-methodology/references/agentic-handoff-templates.md +220 -0
- package/skills/security-methodology/references/cross-source-matrix.md +1 -1
- package/skills/security-methodology/references/owasp-agentic-top-10.md +1 -1
- package/skills/security-scan/SKILL.md +55 -15
- package/skills/self-improve/SKILL.md +70 -50
- package/skills/silent-assumptions/SKILL.md +341 -0
- package/skills/silent-assumptions/references/elicitation-detectors.md +342 -0
- package/skills/spec-writing/SKILL.md +128 -24
- package/skills/spec-writing/scripts/check_checklist.py +114 -15
- package/skills/ui-design/SKILL.md +6 -4
- package/skills/ui-design/references/color-engine.md +132 -0
- package/skills/ui-design/references/design-preferences-from-owned-apps.md +8 -8
- package/skills/ui-design/references/ui-guidance-sources.md +1 -1
- package/skills/ui-design/references/universal-design-principles.alt.md +2 -2
- package/plugin-artifacts/codex/.codex-plugin/plugin.json +0 -41
- package/plugin-artifacts/codex/AGENTS.md +0 -560
- package/plugin-artifacts/codex/BUILD-ARTIFACT.md +0 -5
- package/plugin-artifacts/codex/LICENSE +0 -202
- package/plugin-artifacts/codex/README.md +0 -313
- package/plugin-artifacts/codex/assets/build-loop-plugin-icon.png +0 -0
- package/plugin-artifacts/codex/docs/agent-surface-policy.md +0 -63
- package/plugin-artifacts/codex/references/advisor-dispatch-ladder.md +0 -62
- package/plugin-artifacts/codex/references/agent-role-taxonomy.md +0 -135
- package/plugin-artifacts/codex/references/autonomous-and-per-commit-modes.md +0 -161
- package/plugin-artifacts/codex/references/autonomy-config.md +0 -231
- package/plugin-artifacts/codex/references/backlog-system.md +0 -285
- package/plugin-artifacts/codex/references/capability-routing.md +0 -231
- package/plugin-artifacts/codex/references/codex-subagents.md +0 -106
- package/plugin-artifacts/codex/references/coordination-file-template.md +0 -181
- package/plugin-artifacts/codex/references/coordination-rules.md +0 -552
- package/plugin-artifacts/codex/references/dogfood-reload-checkpoint.md +0 -112
- package/plugin-artifacts/codex/references/halt-and-ask-protocol.md +0 -102
- package/plugin-artifacts/codex/references/implementer-envelope-schema.md +0 -302
- package/plugin-artifacts/codex/references/intent-capability-pack.md +0 -257
- package/plugin-artifacts/codex/references/intent-exploration-prompts.md +0 -96
- package/plugin-artifacts/codex/references/leadership.md +0 -72
- package/plugin-artifacts/codex/references/memory-systems.md +0 -261
- package/plugin-artifacts/codex/references/memory.md +0 -313
- package/plugin-artifacts/codex/references/model-tier-mapping.md +0 -296
- package/plugin-artifacts/codex/references/modular-systems-pack.md +0 -96
- package/plugin-artifacts/codex/references/phase-1-assess.md +0 -249
- package/plugin-artifacts/codex/references/phase-2-plan.md +0 -86
- package/plugin-artifacts/codex/references/phase-3-execute.md +0 -49
- package/plugin-artifacts/codex/references/phase-4-review.md +0 -341
- package/plugin-artifacts/codex/references/phase-5-iterate.md +0 -72
- package/plugin-artifacts/codex/references/phase-6-learn.md +0 -58
- package/plugin-artifacts/codex/references/recent-design-structures.md +0 -274
- package/plugin-artifacts/codex/references/research-trigger-policy.md +0 -140
- package/plugin-artifacts/codex/references/runtime-smoke-triggers.md +0 -42
- package/plugin-artifacts/codex/references/self-review.md +0 -234
- package/plugin-artifacts/codex/references/single-writer-commit-protocol.md +0 -90
- package/plugin-artifacts/codex/references/task-capture-policy.md +0 -68
- package/plugin-artifacts/codex/references/ui-io-contract.md +0 -116
- package/plugin-artifacts/codex/references/ui-spotcheck-protocol.md +0 -65
- package/plugin-artifacts/codex/references/verify-dispatch.md +0 -85
- package/plugin-artifacts/codex/skills/build-loop/SKILL.md +0 -381
- package/plugin-artifacts/codex/skills/build-loop/detect-plugins.mjs +0 -82
- package/plugin-artifacts/codex/skills/build-loop/eval-guide.md +0 -65
- package/plugin-artifacts/codex/skills/build-loop/fallbacks.md +0 -549
- package/plugin-artifacts/codex/skills/build-loop/phases/fact-check.md +0 -42
- package/plugin-artifacts/codex/skills/build-loop/phases/ui-validation.md +0 -267
- package/plugin-artifacts/codex/skills/build-loop/references/advisor-dispatch-ladder.md +0 -62
- package/plugin-artifacts/codex/skills/build-loop/references/apple-native-planning.md +0 -439
- package/plugin-artifacts/codex/skills/build-loop/references/autonomous-and-per-commit-modes.md +0 -161
- package/plugin-artifacts/codex/skills/build-loop/references/capability-routing.md +0 -231
- package/plugin-artifacts/codex/skills/build-loop/references/codex-subagents.md +0 -106
- package/plugin-artifacts/codex/skills/build-loop/references/coordination.md +0 -161
- package/plugin-artifacts/codex/skills/build-loop/references/correction-aware-capture.md +0 -177
- package/plugin-artifacts/codex/skills/build-loop/references/experiment-results-template.md +0 -101
- package/plugin-artifacts/codex/skills/build-loop/references/independent-auditor.md +0 -72
- package/plugin-artifacts/codex/skills/build-loop/references/intent-capability-pack.md +0 -257
- package/plugin-artifacts/codex/skills/build-loop/references/intent-exploration-prompts.md +0 -96
- package/plugin-artifacts/codex/skills/build-loop/references/leadership.md +0 -72
- package/plugin-artifacts/codex/skills/build-loop/references/memory.md +0 -313
- package/plugin-artifacts/codex/skills/build-loop/references/modular-systems-pack.md +0 -96
- package/plugin-artifacts/codex/skills/build-loop/references/output-style.md +0 -222
- package/plugin-artifacts/codex/skills/build-loop/references/pay-it-forward-arch.md +0 -98
- package/plugin-artifacts/codex/skills/build-loop/references/phase-1-assess.md +0 -249
- package/plugin-artifacts/codex/skills/build-loop/references/phase-2-plan.md +0 -86
- package/plugin-artifacts/codex/skills/build-loop/references/phase-3-execute.md +0 -49
- package/plugin-artifacts/codex/skills/build-loop/references/phase-4-review.md +0 -341
- package/plugin-artifacts/codex/skills/build-loop/references/phase-5-iterate.md +0 -72
- package/plugin-artifacts/codex/skills/build-loop/references/phase-6-learn.md +0 -58
- package/plugin-artifacts/codex/skills/build-loop/references/recent-design-structures.md +0 -274
- package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/ASSESSMENT.md +0 -85
- package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/STANDALONE_TEST_RUN.md +0 -149
- package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/scenarios/01-simple-bugfix.md +0 -32
- package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/scenarios/02-ui-build-with-iteration.md +0 -48
- package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/scenarios/03-multi-failure-escalation.md +0 -60
- package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/scenarios/04-ui-build-ibr-absent.md +0 -51
- package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/scenarios/05-refactor-navgator-absent.md +0 -71
- package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/scenarios/06-recurring-bug-debugger-absent.md +0 -52
- package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/trace-comparison.md +0 -202
- package/plugin-artifacts/codex/skills/build-loop/references/self-recursive-dev.md +0 -77
- package/plugin-artifacts/codex/skills/build-loop/references/self-review.md +0 -234
- package/plugin-artifacts/codex/skills/build-loop/references/ui-io-contract.md +0 -116
- package/plugin-artifacts/codex/skills/build-loop/references/verify-dispatch.md +0 -85
- package/plugin-artifacts/codex/skills/build-loop/scanners/audit-design-rules.mjs +0 -476
- package/plugin-artifacts/codex/skills/build-loop/scanners/require-visual-evidence.mjs +0 -239
- package/plugin-artifacts/codex/skills/build-loop/templates/backlog-item.md +0 -35
- package/plugin-artifacts/codex/skills/build-loop/templates/codex-worker-prompt.md +0 -100
- package/plugin-artifacts/codex/skills/build-loop/templates/ui-subagent-prompt.md +0 -179
- package/plugin-artifacts/codex/skills/build-loop/templates/ux-fix-plan.md +0 -40
- package/scripts/build_codex_plugin_artifact.py +0 -321
|
@@ -14,10 +14,10 @@ synthetic mouse or keyboard events — the user's hardware cursor is never moved
|
|
|
14
14
|
Usage (from a build-loop run, project cwd):
|
|
15
15
|
|
|
16
16
|
# Resolve a running app to its pid (no AX permission needed for this)
|
|
17
|
-
python3 native_driver.py resolve --app "
|
|
17
|
+
python3 native_driver.py resolve --app "MyApp"
|
|
18
18
|
|
|
19
19
|
# Scan AX tree of running app
|
|
20
|
-
python3 native_driver.py scan --app "
|
|
20
|
+
python3 native_driver.py scan --app "MyApp" --json
|
|
21
21
|
python3 native_driver.py scan --pid 12345 --json
|
|
22
22
|
|
|
23
23
|
# Perform an AX action on a specific element
|
|
@@ -34,6 +34,11 @@ Usage (from a build-loop run, project cwd):
|
|
|
34
34
|
# List running regular GUI apps (no AX permission needed)
|
|
35
35
|
python3 native_driver.py apps
|
|
36
36
|
|
|
37
|
+
# Launch an isolated instance under a private state dir and capture its pid
|
|
38
|
+
# (see SKILL.md "Single-instance PID-scoped verification mode")
|
|
39
|
+
python3 native_driver.py launch --app-path /Applications/MyApp.app \\
|
|
40
|
+
--state-env-var ET_STATE_DIR --state-dir /tmp/myapp-verify --fresh
|
|
41
|
+
|
|
37
42
|
# Analyze a captured AX tree for centered-narrow layout gaps
|
|
38
43
|
python3 native_driver.py analyze-layout --stdin < ax-tree.json
|
|
39
44
|
|
|
@@ -49,6 +54,7 @@ import os
|
|
|
49
54
|
import shutil
|
|
50
55
|
import subprocess
|
|
51
56
|
import sys
|
|
57
|
+
import time
|
|
52
58
|
from pathlib import Path
|
|
53
59
|
|
|
54
60
|
# ─── Paths ────────────────────────────────────────────────────────────────────
|
|
@@ -180,28 +186,33 @@ def cmd_preflight(_args: argparse.Namespace) -> int:
|
|
|
180
186
|
return 2
|
|
181
187
|
|
|
182
188
|
|
|
183
|
-
def
|
|
184
|
-
"""
|
|
189
|
+
def _query_gui_processes() -> list[dict]:
|
|
190
|
+
"""Query System Events for every foreground GUI process: name, pid, bundle id.
|
|
191
|
+
|
|
192
|
+
Raises FileNotFoundError (no osascript) or subprocess.CalledProcessError
|
|
193
|
+
(AppleScript failed) — callers decide how to surface those.
|
|
194
|
+
"""
|
|
195
|
+
# Fetch each property as its own list and concatenate — a record
|
|
196
|
+
# (`{name, unix id, ...} of ...`) or a bound variable holding the filtered
|
|
197
|
+
# collection both raise -1728 on current macOS; the direct per-property form
|
|
198
|
+
# is the reliable one. Restrict to `application processes` (not `every
|
|
199
|
+
# process`): background-only daemons lack a `background only` property and
|
|
200
|
+
# error the filter. Order (names, pids, bundles) matches the flat-thirds
|
|
201
|
+
# parser below.
|
|
202
|
+
proc_set = "(application processes whose background only is false)"
|
|
185
203
|
script = (
|
|
186
|
-
'tell application "System Events" to '
|
|
187
|
-
|
|
188
|
-
"
|
|
204
|
+
'tell application "System Events" to return '
|
|
205
|
+
f"(name of {proc_set}) & "
|
|
206
|
+
f"(unix id of {proc_set}) & "
|
|
207
|
+
f"(bundle identifier of {proc_set})"
|
|
208
|
+
)
|
|
209
|
+
result = subprocess.run(
|
|
210
|
+
["osascript", "-e", script],
|
|
211
|
+
capture_output=True,
|
|
212
|
+
text=True,
|
|
213
|
+
timeout=10,
|
|
214
|
+
check=True,
|
|
189
215
|
)
|
|
190
|
-
try:
|
|
191
|
-
result = subprocess.run(
|
|
192
|
-
["osascript", "-e", script],
|
|
193
|
-
capture_output=True,
|
|
194
|
-
text=True,
|
|
195
|
-
timeout=10,
|
|
196
|
-
check=True,
|
|
197
|
-
)
|
|
198
|
-
except FileNotFoundError:
|
|
199
|
-
print(json.dumps({"error": "osascript missing"}), file=sys.stderr)
|
|
200
|
-
return 1
|
|
201
|
-
except subprocess.CalledProcessError as e:
|
|
202
|
-
print(json.dumps({"error": e.stderr.strip() or "osascript failed"}), file=sys.stderr)
|
|
203
|
-
return 1
|
|
204
|
-
|
|
205
216
|
raw = result.stdout.strip()
|
|
206
217
|
# AppleScript returns a flat list: name1, name2, ..., pid1, pid2, ..., bundle1, ...
|
|
207
218
|
parts = [p.strip() for p in raw.split(",")]
|
|
@@ -215,6 +226,41 @@ def cmd_apps(_args: argparse.Namespace) -> int:
|
|
|
215
226
|
"bundleIdentifier": parts[2 * n + i] if parts[2 * n + i] != "missing value" else None,
|
|
216
227
|
}
|
|
217
228
|
)
|
|
229
|
+
return apps
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _gui_pids(bundle_id: str | None = None) -> set[int]:
|
|
233
|
+
"""Pid set of running GUI processes, optionally filtered to a bundle id.
|
|
234
|
+
|
|
235
|
+
Swallows osascript failures and returns an empty set — callers that need
|
|
236
|
+
to distinguish "no processes" from "query failed" should call
|
|
237
|
+
`_query_gui_processes()` directly instead.
|
|
238
|
+
"""
|
|
239
|
+
try:
|
|
240
|
+
apps = _query_gui_processes()
|
|
241
|
+
except (FileNotFoundError, subprocess.CalledProcessError, subprocess.TimeoutExpired):
|
|
242
|
+
return set()
|
|
243
|
+
pids: set[int] = set()
|
|
244
|
+
for app in apps:
|
|
245
|
+
if app["pid"] is None:
|
|
246
|
+
continue
|
|
247
|
+
if bundle_id is not None and app.get("bundleIdentifier") != bundle_id:
|
|
248
|
+
continue
|
|
249
|
+
pids.add(app["pid"])
|
|
250
|
+
return pids
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def cmd_apps(_args: argparse.Namespace) -> int:
|
|
254
|
+
"""List regular GUI apps with their pid and bundle identifier (no AX needed)."""
|
|
255
|
+
try:
|
|
256
|
+
apps = _query_gui_processes()
|
|
257
|
+
except FileNotFoundError:
|
|
258
|
+
print(json.dumps({"error": "osascript missing"}), file=sys.stderr)
|
|
259
|
+
return 1
|
|
260
|
+
except subprocess.CalledProcessError as e:
|
|
261
|
+
print(json.dumps({"error": e.stderr.strip() or "osascript failed"}), file=sys.stderr)
|
|
262
|
+
return 1
|
|
263
|
+
|
|
218
264
|
print(json.dumps(apps, indent=2))
|
|
219
265
|
return 0
|
|
220
266
|
|
|
@@ -234,6 +280,185 @@ def cmd_resolve(args: argparse.Namespace) -> int:
|
|
|
234
280
|
return 0
|
|
235
281
|
|
|
236
282
|
|
|
283
|
+
def select_new_pid(before: set[int], after: set[int]) -> int | None:
|
|
284
|
+
"""Return the single new pid in `after - before`, or None if 0 or >1.
|
|
285
|
+
|
|
286
|
+
Pure and deterministic — the caller must treat None as "ambiguous, do
|
|
287
|
+
not guess" rather than picking an arbitrary candidate.
|
|
288
|
+
"""
|
|
289
|
+
new = after - before
|
|
290
|
+
if len(new) == 1:
|
|
291
|
+
return next(iter(new))
|
|
292
|
+
return None
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def build_launch_env(base_env: dict, state_env_var: str | None, state_dir: str | None) -> dict:
|
|
296
|
+
"""Return a COPY of base_env with state_env_var=state_dir set, iff both given.
|
|
297
|
+
|
|
298
|
+
Never mutates base_env.
|
|
299
|
+
"""
|
|
300
|
+
env = dict(base_env)
|
|
301
|
+
if state_env_var and state_dir:
|
|
302
|
+
env[state_env_var] = state_dir
|
|
303
|
+
return env
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def build_open_command(
|
|
307
|
+
target: str, *, by_bundle_id: bool, fresh: bool, args: list[str]
|
|
308
|
+
) -> list[str]:
|
|
309
|
+
"""Build the macOS `open` argv for an isolated-instance launch.
|
|
310
|
+
|
|
311
|
+
Always forces a new instance (`-n`). Adds `-F` (ignore saved window/scene
|
|
312
|
+
state) when `fresh` — never wedge into the app's saved state from a prior
|
|
313
|
+
run. Targets by bundle id (`-b`) or by `.app` path. `--args` is only
|
|
314
|
+
appended when there are extra argv to pass through.
|
|
315
|
+
"""
|
|
316
|
+
cmd = ["open", "-n"]
|
|
317
|
+
if fresh:
|
|
318
|
+
cmd.append("-F")
|
|
319
|
+
if by_bundle_id:
|
|
320
|
+
cmd += ["-b", target]
|
|
321
|
+
else:
|
|
322
|
+
cmd.append(target)
|
|
323
|
+
if args:
|
|
324
|
+
cmd.append("--args")
|
|
325
|
+
cmd += list(args)
|
|
326
|
+
return cmd
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _derive_bundle_id(app_path: str) -> str | None:
|
|
330
|
+
"""Best-effort CFBundleIdentifier lookup from an .app's Info.plist.
|
|
331
|
+
|
|
332
|
+
Used only to filter candidate pids and for the launch report — never
|
|
333
|
+
raises; returns None on any failure (missing `defaults`, malformed
|
|
334
|
+
bundle, timeout).
|
|
335
|
+
"""
|
|
336
|
+
info_plist_prefix = str(Path(app_path) / "Contents" / "Info")
|
|
337
|
+
try:
|
|
338
|
+
result = subprocess.run(
|
|
339
|
+
["defaults", "read", info_plist_prefix, "CFBundleIdentifier"],
|
|
340
|
+
capture_output=True,
|
|
341
|
+
text=True,
|
|
342
|
+
timeout=5,
|
|
343
|
+
)
|
|
344
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
345
|
+
return None
|
|
346
|
+
if result.returncode != 0:
|
|
347
|
+
return None
|
|
348
|
+
bundle_id = result.stdout.strip()
|
|
349
|
+
return bundle_id or None
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def cmd_launch(args: argparse.Namespace) -> int:
|
|
353
|
+
"""Launch an isolated app instance under a private state dir and capture its pid.
|
|
354
|
+
|
|
355
|
+
This is the deterministic launch + pid-capture half of the "single-instance
|
|
356
|
+
PID-scoped verification mode" (see SKILL.md). It never assumes which pid
|
|
357
|
+
belongs to the new instance — it snapshots the running GUI pid set before
|
|
358
|
+
launch, launches with `open -n` (always a new instance), and diffs the
|
|
359
|
+
pid set after launch. Ambiguous results (zero or more than one new pid)
|
|
360
|
+
are reported as failure rather than guessed.
|
|
361
|
+
"""
|
|
362
|
+
if bool(args.app_path) == bool(args.bundle_id):
|
|
363
|
+
# The argparse mutually-exclusive required group should prevent this;
|
|
364
|
+
# guard defensively so a programmatic caller gets a clean error too.
|
|
365
|
+
print(
|
|
366
|
+
json.dumps(
|
|
367
|
+
{
|
|
368
|
+
"success": False,
|
|
369
|
+
"pid": None,
|
|
370
|
+
"error": "exactly one of --app-path/--bundle-id required",
|
|
371
|
+
}
|
|
372
|
+
),
|
|
373
|
+
file=sys.stderr,
|
|
374
|
+
)
|
|
375
|
+
return 2
|
|
376
|
+
|
|
377
|
+
if args.bundle_id:
|
|
378
|
+
target = args.bundle_id
|
|
379
|
+
by_bundle_id = True
|
|
380
|
+
bundle_id = args.bundle_id
|
|
381
|
+
else:
|
|
382
|
+
target = str(args.app_path)
|
|
383
|
+
by_bundle_id = False
|
|
384
|
+
bundle_id = _derive_bundle_id(target)
|
|
385
|
+
|
|
386
|
+
def _fail(error: str) -> int:
|
|
387
|
+
print(
|
|
388
|
+
json.dumps(
|
|
389
|
+
{
|
|
390
|
+
"success": False,
|
|
391
|
+
"pid": None,
|
|
392
|
+
"bundle_id": bundle_id,
|
|
393
|
+
"state_dir": args.state_dir,
|
|
394
|
+
"fresh": args.fresh,
|
|
395
|
+
"error": error,
|
|
396
|
+
"next": None,
|
|
397
|
+
}
|
|
398
|
+
)
|
|
399
|
+
)
|
|
400
|
+
return 1
|
|
401
|
+
|
|
402
|
+
# A failed before-snapshot is a HARD launch failure, not an empty baseline.
|
|
403
|
+
# _gui_pids() swallows query errors into an empty set; if we used it here and
|
|
404
|
+
# the osascript query failed while the user had one ambient instance running,
|
|
405
|
+
# the after-poll would surface that ambient pid as "new" and we would drive
|
|
406
|
+
# the user's live window — the exact failure this mode exists to prevent.
|
|
407
|
+
try:
|
|
408
|
+
before = {
|
|
409
|
+
app["pid"]
|
|
410
|
+
for app in _query_gui_processes()
|
|
411
|
+
if app["pid"] is not None
|
|
412
|
+
}
|
|
413
|
+
except (FileNotFoundError, subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:
|
|
414
|
+
return _fail(f"could not snapshot GUI pids before launch: {e}")
|
|
415
|
+
env = build_launch_env(dict(os.environ), args.state_env_var, args.state_dir)
|
|
416
|
+
open_argv = build_open_command(
|
|
417
|
+
target, by_bundle_id=by_bundle_id, fresh=args.fresh, args=args.args or []
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
try:
|
|
421
|
+
proc = subprocess.run(open_argv, env=env, capture_output=True, text=True, timeout=15)
|
|
422
|
+
except (subprocess.TimeoutExpired, FileNotFoundError) as e:
|
|
423
|
+
return _fail(f"`open` failed to run: {e}")
|
|
424
|
+
|
|
425
|
+
if proc.returncode != 0:
|
|
426
|
+
return _fail(proc.stderr.strip() or f"open exited {proc.returncode}")
|
|
427
|
+
|
|
428
|
+
deadline = time.monotonic() + args.timeout
|
|
429
|
+
pid: int | None = None
|
|
430
|
+
while time.monotonic() < deadline:
|
|
431
|
+
after = _gui_pids(bundle_id=bundle_id)
|
|
432
|
+
pid = select_new_pid(before, after)
|
|
433
|
+
if pid is not None:
|
|
434
|
+
break
|
|
435
|
+
time.sleep(0.25)
|
|
436
|
+
|
|
437
|
+
if pid is None:
|
|
438
|
+
return _fail(
|
|
439
|
+
"could not capture a single new pid (zero or ambiguous candidates) "
|
|
440
|
+
"within timeout"
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
print(
|
|
444
|
+
json.dumps(
|
|
445
|
+
{
|
|
446
|
+
"success": True,
|
|
447
|
+
"pid": pid,
|
|
448
|
+
"bundle_id": bundle_id,
|
|
449
|
+
"state_dir": args.state_dir,
|
|
450
|
+
"fresh": args.fresh,
|
|
451
|
+
"error": None,
|
|
452
|
+
"next": (
|
|
453
|
+
f"drive AX scoped to this pid: native_driver.py scan --pid {pid} "
|
|
454
|
+
f"/ action --pid {pid} ..."
|
|
455
|
+
),
|
|
456
|
+
}
|
|
457
|
+
)
|
|
458
|
+
)
|
|
459
|
+
return 0
|
|
460
|
+
|
|
461
|
+
|
|
237
462
|
def cmd_scan(args: argparse.Namespace) -> int:
|
|
238
463
|
binary = ensure_binary()
|
|
239
464
|
cmd: list[str] = [str(binary)]
|
|
@@ -440,6 +665,36 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
440
665
|
p_resolve = sub.add_parser("resolve", help="Resolve an app name to its pid")
|
|
441
666
|
p_resolve.add_argument("--app", required=True)
|
|
442
667
|
|
|
668
|
+
p_launch = sub.add_parser(
|
|
669
|
+
"launch",
|
|
670
|
+
help="Launch an isolated app instance under a private state dir and capture its pid",
|
|
671
|
+
)
|
|
672
|
+
launch_target = p_launch.add_mutually_exclusive_group(required=True)
|
|
673
|
+
launch_target.add_argument("--app-path", help="Path to a .app bundle")
|
|
674
|
+
launch_target.add_argument("--bundle-id", help="Bundle identifier, e.g. com.example.myapp")
|
|
675
|
+
p_launch.add_argument("--state-dir", help="Isolated state directory for this instance")
|
|
676
|
+
p_launch.add_argument(
|
|
677
|
+
"--state-env-var",
|
|
678
|
+
help="Env var name to point at --state-dir, e.g. ET_STATE_DIR",
|
|
679
|
+
)
|
|
680
|
+
p_launch.add_argument(
|
|
681
|
+
"--fresh",
|
|
682
|
+
action="store_true",
|
|
683
|
+
help="Pass -F to `open` (ignore saved window/scene state)",
|
|
684
|
+
)
|
|
685
|
+
p_launch.add_argument(
|
|
686
|
+
"--arg",
|
|
687
|
+
action="append",
|
|
688
|
+
dest="args",
|
|
689
|
+
help="Extra argv passed through to the launched app (repeatable)",
|
|
690
|
+
)
|
|
691
|
+
p_launch.add_argument(
|
|
692
|
+
"--timeout",
|
|
693
|
+
type=float,
|
|
694
|
+
default=10.0,
|
|
695
|
+
help="Seconds to wait for the new pid to appear (default 10.0)",
|
|
696
|
+
)
|
|
697
|
+
|
|
443
698
|
p_scan = sub.add_parser("scan", help="Scan AX tree of running app")
|
|
444
699
|
p_scan.add_argument("--pid", type=int)
|
|
445
700
|
p_scan.add_argument("--app")
|
|
@@ -479,6 +734,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
479
734
|
"preflight": cmd_preflight,
|
|
480
735
|
"apps": cmd_apps,
|
|
481
736
|
"resolve": cmd_resolve,
|
|
737
|
+
"launch": cmd_launch,
|
|
482
738
|
"scan": cmd_scan,
|
|
483
739
|
"analyze-layout": cmd_analyze_layout,
|
|
484
740
|
"action": cmd_action,
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-FileCopyrightText: 2025-2026 Tyrone Ross, Jr <46267523+tyroneross@users.noreply.github.com>
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
"""
|
|
5
|
+
Tests for the pure launch / pid-capture / scope-selection helpers in
|
|
6
|
+
native_driver.py.
|
|
7
|
+
|
|
8
|
+
Honest scope: the live path — `launch` actually invoking macOS `open`,
|
|
9
|
+
snapshotting real GUI process pids via osascript, and driving the Swift AX
|
|
10
|
+
binary against the captured pid — requires a running macOS GUI session and
|
|
11
|
+
Accessibility permission. That path cannot be exercised headlessly and is
|
|
12
|
+
NOT unit-tested here; it is covered by the SKILL.md manual self-test
|
|
13
|
+
("Single-instance PID-scoped verification mode").
|
|
14
|
+
|
|
15
|
+
What IS tested here is the pure, deterministic logic that decides which pid
|
|
16
|
+
belongs to the freshly-launched instance and how the `open` invocation is
|
|
17
|
+
built — the part that must never guess. These functions take no OS action
|
|
18
|
+
and have no side effects, so they run anywhere pytest runs.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import shutil
|
|
25
|
+
|
|
26
|
+
import pytest
|
|
27
|
+
|
|
28
|
+
import native_driver
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ─── select_new_pid ────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_select_new_pid_single_new_pid():
|
|
35
|
+
assert native_driver.select_new_pid({1, 2}, {1, 2, 3}) == 3
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_select_new_pid_identical_sets_returns_none():
|
|
39
|
+
assert native_driver.select_new_pid({1, 2}, {1, 2}) is None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_select_new_pid_two_new_pids_is_ambiguous():
|
|
43
|
+
assert native_driver.select_new_pid({1, 2}, {1, 2, 3, 4}) is None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_select_new_pid_empty_before_one_after():
|
|
47
|
+
assert native_driver.select_new_pid(set(), {42}) == 42
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_select_new_pid_empty_before_empty_after():
|
|
51
|
+
assert native_driver.select_new_pid(set(), set()) is None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_select_new_pid_pid_disappeared_is_not_new():
|
|
55
|
+
# before has a pid that's gone in after; no new pid appeared.
|
|
56
|
+
assert native_driver.select_new_pid({1, 2, 3}, {1, 2}) is None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ─── build_launch_env ──────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_build_launch_env_sets_key_when_both_given():
|
|
63
|
+
base = {"PATH": "/usr/bin"}
|
|
64
|
+
env = native_driver.build_launch_env(base, "ET_STATE_DIR", "/tmp/state")
|
|
65
|
+
assert env["ET_STATE_DIR"] == "/tmp/state"
|
|
66
|
+
assert env["PATH"] == "/usr/bin"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_build_launch_env_does_not_mutate_input():
|
|
70
|
+
base = {"PATH": "/usr/bin"}
|
|
71
|
+
native_driver.build_launch_env(base, "ET_STATE_DIR", "/tmp/state")
|
|
72
|
+
assert base == {"PATH": "/usr/bin"}
|
|
73
|
+
assert "ET_STATE_DIR" not in base
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_build_launch_env_no_state_env_var_returns_unchanged_copy():
|
|
77
|
+
base = {"PATH": "/usr/bin"}
|
|
78
|
+
env = native_driver.build_launch_env(base, None, "/tmp/state")
|
|
79
|
+
assert env == base
|
|
80
|
+
assert env is not base
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_build_launch_env_no_state_dir_returns_unchanged_copy():
|
|
84
|
+
base = {"PATH": "/usr/bin"}
|
|
85
|
+
env = native_driver.build_launch_env(base, "ET_STATE_DIR", None)
|
|
86
|
+
assert env == base
|
|
87
|
+
assert env is not base
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_build_launch_env_both_none_returns_unchanged_copy():
|
|
91
|
+
base = {"PATH": "/usr/bin"}
|
|
92
|
+
env = native_driver.build_launch_env(base, None, None)
|
|
93
|
+
assert env == base
|
|
94
|
+
assert env is not base
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# ─── build_open_command ────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_build_open_command_always_includes_force_new_instance():
|
|
101
|
+
cmd = native_driver.build_open_command(
|
|
102
|
+
"/Applications/MyApp.app", by_bundle_id=False, fresh=False, args=[]
|
|
103
|
+
)
|
|
104
|
+
assert "-n" in cmd
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_build_open_command_fresh_true_includes_dash_F():
|
|
108
|
+
cmd = native_driver.build_open_command(
|
|
109
|
+
"/Applications/MyApp.app", by_bundle_id=False, fresh=True, args=[]
|
|
110
|
+
)
|
|
111
|
+
assert "-F" in cmd
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_build_open_command_fresh_false_excludes_dash_F():
|
|
115
|
+
cmd = native_driver.build_open_command(
|
|
116
|
+
"/Applications/MyApp.app", by_bundle_id=False, fresh=False, args=[]
|
|
117
|
+
)
|
|
118
|
+
assert "-F" not in cmd
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_build_open_command_by_bundle_id_uses_dash_b():
|
|
122
|
+
cmd = native_driver.build_open_command(
|
|
123
|
+
"com.example.myapp", by_bundle_id=True, fresh=False, args=[]
|
|
124
|
+
)
|
|
125
|
+
assert "-b" in cmd
|
|
126
|
+
assert cmd[cmd.index("-b") + 1] == "com.example.myapp"
|
|
127
|
+
assert "com.example.myapp" not in cmd[: cmd.index("-b")]
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_build_open_command_by_app_path_uses_raw_path():
|
|
131
|
+
cmd = native_driver.build_open_command(
|
|
132
|
+
"/Applications/MyApp.app", by_bundle_id=False, fresh=False, args=[]
|
|
133
|
+
)
|
|
134
|
+
assert "-b" not in cmd
|
|
135
|
+
assert "/Applications/MyApp.app" in cmd
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_build_open_command_appends_args_when_nonempty():
|
|
139
|
+
cmd = native_driver.build_open_command(
|
|
140
|
+
"/Applications/MyApp.app",
|
|
141
|
+
by_bundle_id=False,
|
|
142
|
+
fresh=False,
|
|
143
|
+
args=["--flag", "value"],
|
|
144
|
+
)
|
|
145
|
+
assert "--args" in cmd
|
|
146
|
+
idx = cmd.index("--args")
|
|
147
|
+
assert cmd[idx + 1 :] == ["--flag", "value"]
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_build_open_command_no_args_omits_dash_dash_args():
|
|
151
|
+
cmd = native_driver.build_open_command(
|
|
152
|
+
"/Applications/MyApp.app", by_bundle_id=False, fresh=False, args=[]
|
|
153
|
+
)
|
|
154
|
+
assert "--args" not in cmd
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def test_build_open_command_full_shape_fresh_bundle_with_args():
|
|
158
|
+
cmd = native_driver.build_open_command(
|
|
159
|
+
"com.example.myapp",
|
|
160
|
+
by_bundle_id=True,
|
|
161
|
+
fresh=True,
|
|
162
|
+
args=["--seed", "123"],
|
|
163
|
+
)
|
|
164
|
+
assert cmd == ["open", "-n", "-F", "-b", "com.example.myapp", "--args", "--seed", "123"]
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# ─── cmd_launch before-snapshot safety (auditor finding f1) ─────────────────
|
|
168
|
+
#
|
|
169
|
+
# The one path where this mode could hand back the USER's pid: if the
|
|
170
|
+
# before-snapshot query fails, an empty baseline makes a lone ambient pid look
|
|
171
|
+
# "new". cmd_launch must treat a failed before-snapshot as a HARD launch
|
|
172
|
+
# failure and NEVER invoke `open`.
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def test_cmd_launch_hard_fails_when_before_snapshot_query_errors(monkeypatch, capsys):
|
|
176
|
+
import argparse
|
|
177
|
+
import subprocess as _subprocess
|
|
178
|
+
|
|
179
|
+
def _raise(*_a, **_k):
|
|
180
|
+
raise _subprocess.CalledProcessError(1, ["osascript"], stderr="boom")
|
|
181
|
+
|
|
182
|
+
monkeypatch.setattr(native_driver, "_query_gui_processes", _raise)
|
|
183
|
+
|
|
184
|
+
# If `open` (or any subprocess) is invoked, the guard failed — fail loudly.
|
|
185
|
+
def _forbidden(*_a, **_k):
|
|
186
|
+
raise AssertionError("cmd_launch invoked a subprocess despite a failed before-snapshot")
|
|
187
|
+
|
|
188
|
+
monkeypatch.setattr(native_driver.subprocess, "run", _forbidden)
|
|
189
|
+
|
|
190
|
+
args = argparse.Namespace(
|
|
191
|
+
app_path=None,
|
|
192
|
+
bundle_id="com.example.myapp",
|
|
193
|
+
state_dir=None,
|
|
194
|
+
state_env_var=None,
|
|
195
|
+
fresh=False,
|
|
196
|
+
args=[],
|
|
197
|
+
timeout=1.0,
|
|
198
|
+
)
|
|
199
|
+
rc = native_driver.cmd_launch(args)
|
|
200
|
+
out = capsys.readouterr().out
|
|
201
|
+
assert rc == 1
|
|
202
|
+
payload = json.loads(out)
|
|
203
|
+
assert payload["success"] is False
|
|
204
|
+
assert "snapshot" in payload["error"].lower()
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@pytest.mark.skipif(
|
|
208
|
+
shutil.which("osascript") is None,
|
|
209
|
+
reason="live osascript smoke — requires a macOS GUI session; skipped headless",
|
|
210
|
+
)
|
|
211
|
+
def test_query_gui_processes_live_smoke():
|
|
212
|
+
"""Invoke the REAL osascript query, not a mock.
|
|
213
|
+
|
|
214
|
+
The mocked unit tests above cannot validate the AppleScript STRING — which
|
|
215
|
+
is exactly how a malformed multi-property query (`{name, unix id, bundle
|
|
216
|
+
identifier} of every process whose ...`) shipped and raised -1728 at
|
|
217
|
+
runtime (2026-07-19), leaving `_gui_pids()` empty and the PID-scoped launch
|
|
218
|
+
non-functional while every unit test stayed green. This asserts the query
|
|
219
|
+
actually parses and returns a plausible process list on a real macOS
|
|
220
|
+
session; it skips cleanly in headless CI.
|
|
221
|
+
"""
|
|
222
|
+
apps = native_driver._query_gui_processes()
|
|
223
|
+
assert isinstance(apps, list)
|
|
224
|
+
assert apps, "no GUI processes returned — the AppleScript query is likely malformed"
|
|
225
|
+
for a in apps:
|
|
226
|
+
assert {"name", "pid", "bundleIdentifier"} <= set(a)
|
|
227
|
+
assert any(a["pid"] for a in apps), "no real pids captured — launch pid-capture would fail"
|
package/skills/optimize/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: optimize
|
|
3
3
|
description: Use when the user says "optimize this", "optimization", "make X faster", "reduce <metric>", or "speed up my app". Runs a Design of Experiments test matrix — up to 6 variables in one pass — to find which variable actually moves the number. Falls back to one-at-a-time loop for single variables.
|
|
4
|
-
user-invocable:
|
|
4
|
+
user-invocable: false
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
<!-- SPDX-FileCopyrightText: 2025-2026 Tyrone Ross, Jr <46267523+tyroneross@users.noreply.github.com> | SPDX-License-Identifier: Apache-2.0 -->
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: plugin-builder
|
|
3
|
-
description: Use when the user asks to create
|
|
3
|
+
description: Create, scaffold, or migrate a Claude Code plugin — manifest, directory layout, hooks, agents, distribution. Use when the user asks to create/build/scaffold/convert a plugin, or needs plugin.json/layout guidance. Not for building the MCP server itself once the shell exists (use `mcp-builder`; pair the two).
|
|
4
4
|
user-invocable: false
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -327,3 +327,50 @@ Core references (load as needed — don't pre-load all):
|
|
|
327
327
|
Related skills:
|
|
328
328
|
- **`mcp-builder`** — MCP server implementation companion
|
|
329
329
|
- **`skill-builder`** — SKILL.md authoring (for skills shipped inside a plugin)
|
|
330
|
+
|
|
331
|
+
## Distribution & versioning — DETECT, don't mandate
|
|
332
|
+
|
|
333
|
+
Before touching any `version` field, run the detector:
|
|
334
|
+
|
|
335
|
+
```bash
|
|
336
|
+
python3 scripts/detect_plugin_distribution.py <repo> --hub <fleet marketplace.json>
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
The install SOURCE decides the policy (auto-SHA vs semver) — a blanket rule gets it wrong.
|
|
340
|
+
What is NOT negotiable is *consistency*: the version state must agree across `plugin.json`,
|
|
341
|
+
the codex manifest, and the marketplace entry, or one silently masks the others. Enforce that
|
|
342
|
+
invariant in CI with either of two equivalent options:
|
|
343
|
+
|
|
344
|
+
**Option A — local check (default; no external dependency).** `detect_plugin_distribution.py`
|
|
345
|
+
itself is advisory-only (always exits 0, prints a recommendation) — it is not a gate as
|
|
346
|
+
shipped. Vendor the script into the generated repo and wrap its `--json` output in a CI step
|
|
347
|
+
that fails on the `consistent` field:
|
|
348
|
+
|
|
349
|
+
```yaml
|
|
350
|
+
jobs:
|
|
351
|
+
verify-manifests:
|
|
352
|
+
runs-on: ubuntu-latest
|
|
353
|
+
steps:
|
|
354
|
+
- uses: actions/checkout@v4
|
|
355
|
+
- run: |
|
|
356
|
+
python3 scripts/detect_plugin_distribution.py . --json > /tmp/dist.json
|
|
357
|
+
python3 -c "import json,sys; d=json.load(open('/tmp/dist.json')); sys.exit(0 if d['consistent'] else 1)"
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
(Adjust the script path to wherever `detect_plugin_distribution.py` lands in the generated
|
|
361
|
+
repo — vendor the script itself, not just the CI call.)
|
|
362
|
+
|
|
363
|
+
**Option B — reuse the maintainer's shared workflow (opt-in, explicit dependency).** build-loop's
|
|
364
|
+
own CI calls a reusable workflow hosted in the maintainer's `tyroneross/RossLabs-AI-Toolkit` repo
|
|
365
|
+
at the `plugin-ci-v1` tag. This is convenient but puts the generated repo's CI at the mercy of a
|
|
366
|
+
third-party org: if that repo goes private, is renamed, or the tag is force-moved/deleted, the
|
|
367
|
+
generated repo's CI breaks with no local fallback. Only wire this in if the user explicitly wants
|
|
368
|
+
to track the maintainer's shared workflow and understands that risk:
|
|
369
|
+
|
|
370
|
+
```yaml
|
|
371
|
+
jobs:
|
|
372
|
+
verify-manifests:
|
|
373
|
+
uses: tyroneross/RossLabs-AI-Toolkit/.github/workflows/verify-plugin-manifests.yml@plugin-ci-v1
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
Full decision table + the failure that motivated it: `references/plugin-distribution.md`.
|
|
@@ -73,10 +73,10 @@ The rename is a full migration, not a metadata change. Partial renames produce p
|
|
|
73
73
|
## Source-of-truth flow
|
|
74
74
|
|
|
75
75
|
```
|
|
76
|
-
|
|
76
|
+
<your-local-checkout>/<plugin-repo>/ ← author-owned, edit here, commit
|
|
77
77
|
│
|
|
78
78
|
▼ (git push)
|
|
79
|
-
GitHub:
|
|
79
|
+
GitHub: <your-github-org>/<marketplace>/ ← marketplace pulls from here
|
|
80
80
|
│
|
|
81
81
|
▼ (plugin sync)
|
|
82
82
|
~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/ ← generated, never edit long-term
|
|
@@ -90,5 +90,4 @@ Build-loop must edit the source repo, not the cache. Cache edits survive until t
|
|
|
90
90
|
## References
|
|
91
91
|
|
|
92
92
|
- `~/.claude/skills/plugin-builder/references/plugin-hygiene-lessons.md` — full incident log
|
|
93
|
-
-
|
|
94
|
-
- `~/.claude/projects/-Users-tyroneross/memory/feedback_rosslabs_toolkit_sync.md` — marketplace rename sync
|
|
93
|
+
- Per-project Claude Code memory (`feedback_hook_design.md`, `feedback_rosslabs_toolkit_sync.md`) — hook design rules and marketplace-rename-sync notes, when present. These live under a project-slug directory in `~/.claude/projects/` derived from your home path (glob `~/.claude/projects/-Users-*/memory/` to find yours); they are optional, personal notes, not part of this plugin.
|