@muggleai/works 5.7.0 → 5.8.1
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/dist/{chunk-LTULYOOW.js → chunk-5PYK7DWB.js} +15 -0
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/dist/plugin/.claude-plugin/plugin.json +1 -1
- package/dist/plugin/.cursor-plugin/plugin.json +1 -1
- package/dist/plugin/agents/test-prepare-runner.md +47 -0
- package/dist/plugin/agents/visual-walkthrough-builder.md +58 -0
- package/dist/plugin/hooks/README.md +11 -2
- package/dist/plugin/hooks/hooks.json +7 -1
- package/dist/plugin/scripts/gc-state.sh +66 -0
- package/dist/plugin/scripts/guard-run-job-object.ps1 +256 -0
- package/dist/plugin/scripts/guard-run.mjs +327 -0
- package/dist/plugin/scripts/guardrail-record-tests.sh +5 -4
- package/dist/plugin/scripts/guardrails.mjs +6 -3
- package/dist/plugin/scripts/pr-watch-guards.sh +46 -0
- package/dist/plugin/skills/CLAUDE.md +39 -36
- package/dist/plugin/skills/_shared/pr-followup-helpers/loop-signature.md +3 -3
- package/dist/plugin/skills/_shared/vcs/CLAUDE.md +2 -1
- package/dist/plugin/skills/_shared/vcs/common/branch-standing.md +19 -0
- package/dist/plugin/skills/_shared/vcs/common/ci-rollup.md +16 -0
- package/dist/plugin/skills/_shared/vcs/common/push-to-branch.md +16 -0
- package/{plugin/skills/_shared/vcs/github → dist/plugin/skills/_shared/vcs/common}/verify-working-tree.md +4 -4
- package/dist/plugin/skills/_shared/vcs/github/pr-edit.md +2 -0
- package/dist/plugin/skills/_shared/vcs/github/reply-line-comment.md +2 -0
- package/dist/plugin/skills/_shared/vcs/github/signed-commits.md +28 -0
- package/dist/plugin/skills/_shared/vcs/github/top-level-comment.md +2 -0
- package/dist/plugin/skills/_shared/vcs/github/unresolved-threads.md +39 -39
- package/dist/plugin/skills/_shared/vcs/github.md +5 -2
- package/dist/plugin/skills/_shared/vcs/gitlab/loop-user-identity.md +1 -1
- package/dist/plugin/skills/_shared/vcs/gitlab/mr-create.md +1 -1
- package/dist/plugin/skills/_shared/vcs/gitlab/mr-discussions.md +1 -1
- package/dist/plugin/skills/_shared/vcs/gitlab/mr-metadata.md +2 -2
- package/dist/plugin/skills/_shared/vcs/gitlab/mr-pipeline.md +1 -1
- package/dist/plugin/skills/_shared/vcs/gitlab/reply-discussion.md +1 -1
- package/dist/plugin/skills/_shared/vcs/gitlab/resolve-discussion.md +1 -1
- package/dist/plugin/skills/_shared/vcs/gitlab/signed-commits.md +15 -0
- package/dist/plugin/skills/_shared/vcs/gitlab/unresolved-discussions.md +2 -2
- package/dist/plugin/skills/_shared/vcs/gitlab.md +6 -3
- package/dist/plugin/skills/_shared/vcs/post-signature.md +31 -0
- package/dist/plugin/skills/do/build.md +58 -56
- package/dist/plugin/skills/do/open-prs/forward.md +85 -84
- package/dist/plugin/skills/do/open-prs/update.md +3 -3
- package/dist/plugin/skills/do/per-comment-replies.md +2 -2
- package/dist/plugin/skills/do/resolve-conflicts.md +59 -58
- package/dist/plugin/skills/do/unit-tests.md +1 -1
- package/dist/plugin/skills/muggle-pr-followup/CLAUDE.md +1 -0
- package/dist/plugin/skills/muggle-pr-followup/SKILL.md +11 -4
- package/dist/plugin/skills/muggle-pr-followup/arm-watcher.md +29 -5
- package/dist/plugin/skills/muggle-pr-followup/bootstrap.md +3 -1
- package/dist/plugin/skills/muggle-pr-followup/contract.md +171 -158
- package/dist/plugin/skills/muggle-pr-followup/evals/evals.json +221 -101
- package/dist/plugin/skills/muggle-pr-followup/output-templates/inline-reply.md +2 -2
- package/dist/plugin/skills/muggle-pr-followup/output-templates/resolve-reminder.md +1 -1
- package/dist/plugin/skills/muggle-pr-followup/reconcile.md +9 -5
- package/dist/plugin/skills/muggle-pr-followup/state-schemas.md +191 -175
- package/dist/plugin/skills/muggle-pr-followup/stop.md +31 -0
- package/dist/plugin/skills/muggle-pr-visual-walkthrough/SKILL.md +41 -227
- package/dist/plugin/skills/muggle-test-prepare/SKILL.md +130 -129
- package/dist/plugin/skills/muggle-test-prepare/steps/start-services.md +2 -2
- package/dist/release-manifest.json +4 -4
- package/package.json +7 -6
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.cursor-plugin/plugin.json +1 -1
- package/plugin/agents/test-prepare-runner.md +47 -0
- package/plugin/agents/visual-walkthrough-builder.md +58 -0
- package/plugin/hooks/README.md +11 -2
- package/plugin/hooks/hooks.json +7 -1
- package/plugin/scripts/gc-state.sh +66 -0
- package/plugin/scripts/guard-run-job-object.ps1 +256 -0
- package/plugin/scripts/guard-run.mjs +327 -0
- package/plugin/scripts/guardrail-record-tests.sh +5 -4
- package/plugin/scripts/guardrails.mjs +6 -3
- package/plugin/scripts/pr-watch-guards.sh +46 -0
- package/plugin/skills/CLAUDE.md +39 -36
- package/plugin/skills/_shared/pr-followup-helpers/loop-signature.md +3 -3
- package/plugin/skills/_shared/vcs/CLAUDE.md +2 -1
- package/plugin/skills/_shared/vcs/common/branch-standing.md +19 -0
- package/plugin/skills/_shared/vcs/common/ci-rollup.md +16 -0
- package/plugin/skills/_shared/vcs/common/push-to-branch.md +16 -0
- package/{dist/plugin/skills/_shared/vcs/github → plugin/skills/_shared/vcs/common}/verify-working-tree.md +4 -4
- package/plugin/skills/_shared/vcs/github/pr-edit.md +2 -0
- package/plugin/skills/_shared/vcs/github/reply-line-comment.md +2 -0
- package/plugin/skills/_shared/vcs/github/signed-commits.md +28 -0
- package/plugin/skills/_shared/vcs/github/top-level-comment.md +2 -0
- package/plugin/skills/_shared/vcs/github/unresolved-threads.md +39 -39
- package/plugin/skills/_shared/vcs/github.md +5 -2
- package/plugin/skills/_shared/vcs/gitlab/loop-user-identity.md +1 -1
- package/plugin/skills/_shared/vcs/gitlab/mr-create.md +1 -1
- package/plugin/skills/_shared/vcs/gitlab/mr-discussions.md +1 -1
- package/plugin/skills/_shared/vcs/gitlab/mr-metadata.md +2 -2
- package/plugin/skills/_shared/vcs/gitlab/mr-pipeline.md +1 -1
- package/plugin/skills/_shared/vcs/gitlab/reply-discussion.md +1 -1
- package/plugin/skills/_shared/vcs/gitlab/resolve-discussion.md +1 -1
- package/plugin/skills/_shared/vcs/gitlab/signed-commits.md +15 -0
- package/plugin/skills/_shared/vcs/gitlab/unresolved-discussions.md +2 -2
- package/plugin/skills/_shared/vcs/gitlab.md +6 -3
- package/plugin/skills/_shared/vcs/post-signature.md +31 -0
- package/plugin/skills/do/build.md +58 -56
- package/plugin/skills/do/open-prs/forward.md +85 -84
- package/plugin/skills/do/open-prs/update.md +3 -3
- package/plugin/skills/do/per-comment-replies.md +2 -2
- package/plugin/skills/do/resolve-conflicts.md +59 -58
- package/plugin/skills/do/unit-tests.md +1 -1
- package/plugin/skills/muggle-pr-followup/CLAUDE.md +1 -0
- package/plugin/skills/muggle-pr-followup/SKILL.md +11 -4
- package/plugin/skills/muggle-pr-followup/arm-watcher.md +29 -5
- package/plugin/skills/muggle-pr-followup/bootstrap.md +3 -1
- package/plugin/skills/muggle-pr-followup/contract.md +171 -158
- package/plugin/skills/muggle-pr-followup/evals/evals.json +221 -101
- package/plugin/skills/muggle-pr-followup/output-templates/inline-reply.md +2 -2
- package/plugin/skills/muggle-pr-followup/output-templates/resolve-reminder.md +1 -1
- package/plugin/skills/muggle-pr-followup/reconcile.md +9 -5
- package/plugin/skills/muggle-pr-followup/state-schemas.md +191 -175
- package/plugin/skills/muggle-pr-followup/stop.md +31 -0
- package/plugin/skills/muggle-pr-visual-walkthrough/SKILL.md +41 -227
- package/plugin/skills/muggle-test-prepare/SKILL.md +130 -129
- package/plugin/skills/muggle-test-prepare/steps/start-services.md +2 -2
- package/dist/plugin/skills/_shared/vcs/github/push-to-branch.md +0 -10
- package/plugin/skills/_shared/vcs/github/push-to-branch.md +0 -10
|
@@ -331,9 +331,24 @@ async function startStdioServer(server) {
|
|
|
331
331
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
332
332
|
watchForParentDeath(shutdown);
|
|
333
333
|
}
|
|
334
|
+
var PARENT_POLL_INTERVAL_MS = 3e4;
|
|
334
335
|
function watchForParentDeath(shutdown) {
|
|
335
336
|
process.stdin.on("end", () => shutdown("stdin-end"));
|
|
336
337
|
process.stdin.on("close", () => shutdown("stdin-close"));
|
|
338
|
+
const parentPid = process.ppid;
|
|
339
|
+
if (!parentPid || parentPid <= 1) return;
|
|
340
|
+
const timer = setInterval(() => {
|
|
341
|
+
if (!parentIsAlive(parentPid)) shutdown("parent-gone");
|
|
342
|
+
}, PARENT_POLL_INTERVAL_MS);
|
|
343
|
+
timer.unref();
|
|
344
|
+
}
|
|
345
|
+
function parentIsAlive(pid) {
|
|
346
|
+
try {
|
|
347
|
+
process.kill(pid, 0);
|
|
348
|
+
return true;
|
|
349
|
+
} catch (err) {
|
|
350
|
+
return err.code === "EPERM";
|
|
351
|
+
}
|
|
337
352
|
}
|
|
338
353
|
|
|
339
354
|
// src/cli/pr-section/render.ts
|
package/dist/cli.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { src_exports as commands, createUnifiedMcpServer, server_exports as server } from './chunk-
|
|
1
|
+
export { src_exports as commands, createUnifiedMcpServer, server_exports as server } from './chunk-5PYK7DWB.js';
|
|
2
2
|
export { createChildLogger, e2e_exports as e2e, getConfig, getLocalQaTools, getLogger, getQaTools, local_exports as localQa, mcp_exports as mcp, e2e_exports as qa, src_exports as shared } from './chunk-LVUJ4E62.js';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "muggle",
|
|
3
3
|
"description": "Run real-browser end-to-end (E2E) acceptance tests on your web app from any AI coding agent. Generate test scripts from plain English, replay them on localhost, capture screenshots, and validate user flows like signup, checkout, and dashboards. Works across Claude Code, Cursor, Codex, and Windsurf.",
|
|
4
|
-
"version": "5.
|
|
4
|
+
"version": "5.8.1",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Muggle AI",
|
|
7
7
|
"email": "support@muggle-ai.com"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "muggle",
|
|
3
3
|
"displayName": "Muggle AI",
|
|
4
4
|
"description": "Ship quality products with AI-powered end-to-end (E2E) acceptance testing that validates your web app like a real user — from Claude Code and Cursor to PR.",
|
|
5
|
-
"version": "5.
|
|
5
|
+
"version": "5.8.1",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Muggle AI",
|
|
8
8
|
"email": "support@muggle-ai.com"
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: test-prepare-runner
|
|
3
|
+
description: "Executes a fully-resolved Muggle Test prepare plan — detects what's listening, verifies env files, fresh-installs stale deps, starts approved services, smoke-tests, and returns the readiness table. Dispatched by the muggle-test-prepare skill after all user decisions are resolved; carries the opus pin so execution never runs below its reliability floor on a cheaper session model."
|
|
4
|
+
model: opus
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Test Prepare Runner
|
|
8
|
+
|
|
9
|
+
You bring a local dev stack to verified readiness for E2E testing: detect what's already listening, verify env files, install stale dependencies, start approved services, smoke-test them, and report the readiness table. Every decision — which services, their directories, start commands and approvals, scope, exclusions, the dev-server URL — arrives resolved in the dispatch prompt from the muggle-test-prepare skill. You have no channel to the user: when the plan is missing a decision you need, return one `needs-input:` line naming it and stop — the dispatching skill resolves it (asking the user if needed) and re-dispatches.
|
|
10
|
+
|
|
11
|
+
## Input contract
|
|
12
|
+
|
|
13
|
+
The dispatch prompt carries the resolved prepare plan:
|
|
14
|
+
|
|
15
|
+
- `services[]` — name, dir, start command, expected port, `external` flag, approval already granted.
|
|
16
|
+
- `testingScope` and `excludedServices[]` (with reasons).
|
|
17
|
+
- The recorded dev-server URL (from the `autoSelectLocalHost` resolution) — never invent or default a host/port; a framework default like `:3000` is not a fallback.
|
|
18
|
+
- Resolved gate values the stages read (`autoRebase` outcome already applied or explicitly skipped upstream).
|
|
19
|
+
|
|
20
|
+
## Stages
|
|
21
|
+
|
|
22
|
+
Run these stage files from the skill, in order, exactly as written — they are the single source of truth for each stage's procedure:
|
|
23
|
+
|
|
24
|
+
1. [`../skills/muggle-test-prepare/steps/check-running.md`](../skills/muggle-test-prepare/steps/check-running.md)
|
|
25
|
+
2. [`../skills/muggle-test-prepare/steps/env-file.md`](../skills/muggle-test-prepare/steps/env-file.md)
|
|
26
|
+
3. [`../skills/muggle-test-prepare/steps/start-commands.md`](../skills/muggle-test-prepare/steps/start-commands.md)
|
|
27
|
+
4. [`../skills/muggle-test-prepare/steps/fresh-install.md`](../skills/muggle-test-prepare/steps/fresh-install.md)
|
|
28
|
+
5. [`../skills/muggle-test-prepare/steps/start-services.md`](../skills/muggle-test-prepare/steps/start-services.md)
|
|
29
|
+
6. [`../skills/muggle-test-prepare/steps/smoke-test.md`](../skills/muggle-test-prepare/steps/smoke-test.md)
|
|
30
|
+
7. [`../skills/muggle-test-prepare/steps/readiness-report.md`](../skills/muggle-test-prepare/steps/readiness-report.md)
|
|
31
|
+
|
|
32
|
+
Where a stage file offers the user a choice, take the branch the plan resolved; where the plan doesn't cover it, return `needs-input:` — never guess, never start anything unapproved.
|
|
33
|
+
|
|
34
|
+
## PID tracking
|
|
35
|
+
|
|
36
|
+
Track every launched process in `/tmp/muggle-test-prepare.json` exactly per the skill's schema (`session_started`, `testing_scope`, `excluded_services`, `services[]` with pid/port/log). Processes the user started independently stay `external: true` and are never killed. Prune dead PIDs silently.
|
|
37
|
+
|
|
38
|
+
## Output contract
|
|
39
|
+
|
|
40
|
+
Return the readiness-report table verbatim as your report, prefixed by one line: `READY` (all services green), `DEGRADED: <which service, why>` (something is up but failed its smoke test after the clean-restart loop), or `needs-input: <decision>`. The dispatcher relays this to its caller — other skills gate on it, so a wrong `READY` is expensive; when in doubt between READY and DEGRADED, pick DEGRADED and say why. `needs-input:` is only for a decision the plan failed to resolve (a missing URL, an unapproved start, an unknown directory) — a service that stays broken after the loop is `DEGRADED` with the diagnosis, never `needs-input:`, even when no further automated fix exists. Repairing the app's own source code is out of scope entirely: a source-level bug surfaced by the smoke test is a `DEGRADED` diagnosis to report, not a decision to escalate.
|
|
41
|
+
|
|
42
|
+
## Guardrails
|
|
43
|
+
|
|
44
|
+
- Privacy boundary as the skill defines it: file contents only inside directories the plan names; never traverse upward past one level.
|
|
45
|
+
- Port-listening is never enough — smoke-test (HTTP + body sniff + log tail) is mandatory before the report.
|
|
46
|
+
- Clean Restart is the first fix in the smoke-test loop; fresh-install notifies, doesn't ask.
|
|
47
|
+
- Never leave an orphan process untracked; never kill an `external` one.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: visual-walkthrough-builder
|
|
3
|
+
description: "Renders the Muggle Test E2E visual walkthrough for a PR — assembles the E2eReport, runs `muggle build-pr-section`, and either posts to the PR (Mode A) or returns the rendered block to the dispatcher (Modes B/C). Dispatched by the muggle-pr-visual-walkthrough skill; carries its sonnet pin so the render runs on sonnet regardless of the session model."
|
|
4
|
+
model: sonnet
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Visual Walkthrough Builder
|
|
8
|
+
|
|
9
|
+
You render and (in Mode A) post the Muggle Test E2E visual walkthrough. The dispatching skill has already resolved the mode, the PR, and user consent. You have no channel to the user: if an input you need is missing, return a single `needs-input:` line naming it and stop — the dispatching skill resolves it and re-dispatches.
|
|
10
|
+
|
|
11
|
+
## Input contract
|
|
12
|
+
|
|
13
|
+
The dispatch prompt carries:
|
|
14
|
+
|
|
15
|
+
- `mode` — `post` (Mode A), `render-for-new-pr` (Mode B), or `embed` (Mode C).
|
|
16
|
+
- `prNumber` + repo — Mode A only, already verified to exist.
|
|
17
|
+
- The `E2eReport` JSON inline, **or** the run identifiers (`projectId`, per-test `runId`/`testCaseId` list) to assemble it from.
|
|
18
|
+
|
|
19
|
+
When assembling from identifiers, follow [`../skills/muggle-pr-visual-walkthrough/e2e-report-assembly.md`](../skills/muggle-pr-visual-walkthrough/e2e-report-assembly.md). The `E2eReport` schema, required fields, and the inconclusive rule live there and in the CLI's Zod schema (`src/cli/pr-section/types.ts`) — a run that couldn't produce pass/fail is `inconclusive` with a `reason`, never dropped.
|
|
20
|
+
|
|
21
|
+
## Render
|
|
22
|
+
|
|
23
|
+
Pipe the report to the CLI; it writes `{"body": "...", "comment": "..." | null}`:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
echo "$REPORT_JSON" | muggle build-pr-section > /tmp/muggle-pr-section.json
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
- Non-zero exit → surface the CLI's stderr verbatim; do not swallow or retry blindly.
|
|
30
|
+
- `comment` is non-null only in the overflow case; the CLI owns fit-vs-overflow.
|
|
31
|
+
|
|
32
|
+
## Deliver
|
|
33
|
+
|
|
34
|
+
**Mode A (`post`)** — post `body` as a PR comment, then `comment` only if non-null. Append the Muggle Works signature to each posted body per [`../skills/_shared/vcs/post-signature.md`](../skills/_shared/vcs/post-signature.md) — this post is the walkthrough's own, so the command it names is `/muggle-pr-visual-walkthrough`:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
sig='🤖 _Posted by `/muggle-pr-visual-walkthrough` · [Muggle Works](https://github.com/multiplex-ai/muggle-ai-works)_'
|
|
38
|
+
{ jq -r '.body' /tmp/muggle-pr-section.json; printf '\n\n%s\n' "$sig"; } | gh pr comment <prNumber> --body-file -
|
|
39
|
+
{ jq -r '.comment' /tmp/muggle-pr-section.json; printf '\n\n%s\n' "$sig"; } | gh pr comment <prNumber> --body-file - # skip when null
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Report back: PR URL + whether an overflow comment was posted.
|
|
43
|
+
|
|
44
|
+
**Modes B/C (`render-for-new-pr` / `embed`)** — do not post, do not touch `gh`. Return the CLI output verbatim as your report:
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
body:
|
|
48
|
+
<body>
|
|
49
|
+
comment:
|
|
50
|
+
<comment or null>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Guardrails
|
|
54
|
+
|
|
55
|
+
- Never hand-write or modify the walkthrough markdown — the CLI is the single source of truth. No custom tables, no added "Verdict" lines, no `Tested on:`/`Project:` footers; the CLI computes the verdict and emits per-test dashboard links.
|
|
56
|
+
- Never invent report fields — missing `projectId`, `viewUrl`, or `screenshotUrl` → `needs-input:`, never a placeholder.
|
|
57
|
+
- Never post the overflow comment when `comment` is null.
|
|
58
|
+
- Never create a PR, never choose a mode — both belong to the dispatcher.
|
|
@@ -31,7 +31,7 @@ Each guardrail is a thin bash wrapper in `../scripts/` registered in `hooks.json
|
|
|
31
31
|
| Hook event | Wrapper | Strength | Condition | Preference | Effect |
|
|
32
32
|
| :--------- | :------ | :------- | :-------- | :--------- | :----- |
|
|
33
33
|
| `PostToolUse` (Bash) | `guardrail-pr-opened.sh` | advise | a `gh pr create`/`gh pr ready` just succeeded | `autoWatchPR` | start a `muggle-pr-followup` watcher on the new PR |
|
|
34
|
-
| `PostToolUse` (Bash + muggle execute/replay MCP tools) | `guardrail-record-tests.sh` | record | a unit-test command passed, an E2E run happened, or an `echo "MUGGLE_E2E_SKIP: <reason>"` marker declared E2E un-runnable | — | set `unitTestsGreen` / `e2eRun` / `e2eSkipped` session state |
|
|
34
|
+
| `PostToolUse` (Bash + muggle execute/replay/skill-emit MCP tools) | `guardrail-record-tests.sh` | record | a unit-test command passed, an E2E run happened (execute/replay call, or the muggle-test skill's own telemetry emit — which registers a clean SKIP verdict too), or an `echo "MUGGLE_E2E_SKIP: <reason>"` marker declared E2E un-runnable | — | set `unitTestsGreen` / `e2eRun` / `e2eSkipped` session state |
|
|
35
35
|
| `PostToolUse` (Bash + Monitor) | `guardrail-pr-terminal.sh` | advise | a PR just went terminal — a `gh pr merge`/`gh pr close` success line or the watch monitor's `TERMINAL pr=N` exit line (never bare `"state":"MERGED"` metadata) | — | record `terminalPending`, direct the post-merge handoff: finalize the watcher slot, tear down per `autoCleanup`, offer next options via AskUserQuestion |
|
|
36
36
|
| `PostToolUse` (AskUserQuestion) | `guardrail-offer-ran.sh` | record | a next-options offer ran while a terminal PR was pending | — | clear `terminalPending` — the only exit for the post-merge Stop gate |
|
|
37
37
|
| `PreToolUse` (Bash) | `guardrail-report-format.sh` | **enforce** | a `gh pr comment\|create\|edit` body reads like an E2E report but lacks the `build-pr-section` sentinel | — | **deny** — render via `muggle build-pr-section` instead |
|
|
@@ -43,4 +43,13 @@ Each guardrail is a thin bash wrapper in `../scripts/` registered in `hooks.json
|
|
|
43
43
|
|
|
44
44
|
`SessionStart` (`scripts/reconcile-stale-watchers.sh`) — a standalone advisory, not part of the `guardrails.mjs` decision tree above.
|
|
45
45
|
|
|
46
|
-
`muggle-pr-followup` watchers are session-only `/loop`
|
|
46
|
+
`muggle-pr-followup` watchers are session-only (a monitor or `/loop` cron); they die with their session, leaving open PRs with no live poller. This is by design — a review is addressed only inside a session that carries the context to address it. The skill's [`reconcile`](../skills/muggle-pr-followup/reconcile.md) procedure recovers them at the next session start — finalizes slots whose PR went terminal, sweeps orphan crons, re-arms silently-stopped open watchers — but re-arming needs Claude tools a shell hook can't call. So this hook nudges rather than acts: it scans `~/.muggle-ai/muggle-do/sessions/*/` for open slots (a `prs.json` with no `result.md`) and, **only when one or more exist**, injects `additionalContext` telling the agent to run `/muggle:muggle-pr-followup reconcile`. Zero open slots → it emits nothing. A pure directory scan (no `gh`, no writes), so it's cheap enough for every session start.
|
|
47
|
+
|
|
48
|
+
## Session-start state GC
|
|
49
|
+
|
|
50
|
+
`SessionStart` (`scripts/gc-state.sh`) — prunes ephemeral state that nothing else garbage-collects, so it doesn't grow without bound (the per-session guardrails files and finalized watcher slots otherwise accumulate one-per-session forever). Collection is keyed on **inactivity, never creation age**, so state a long-lived session still relies on is never deleted out from under it:
|
|
51
|
+
|
|
52
|
+
- `~/.muggle-ai/guardrails/*.json` (one per session) is pruned only after 14 days of **no activity**. An in-use session keeps rewriting its file (`guardrails.mjs` on every guarded tool call) and this hook refreshes it on every resume, so its mtime tracks last activity — a session that runs for months stays live indefinitely, and only one that has genuinely gone quiet for the whole window (i.e. ended) is collected.
|
|
53
|
+
- Finalized watcher slots (`result.md` present) are pruned 30 days after finalize; their `followup.log` is forensic-only. An **open** slot — a PR watched for any length of time — has no `result.md` and is never touched.
|
|
54
|
+
|
|
55
|
+
Both windows are overridable (`MUGGLE_GUARDRAILS_TTL_DAYS`, `MUGGLE_SLOT_TTL_DAYS`). TTL-gated to once per day via a `~/.cache/muggle/state-gc-checked` marker (the current-session refresh runs every start regardless); silent and best-effort, never blocks session start. Never touches an open slot or the current session's own state.
|
|
@@ -15,6 +15,12 @@
|
|
|
15
15
|
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/reconcile-stale-watchers.sh\"",
|
|
16
16
|
"async": false,
|
|
17
17
|
"timeout": 10
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"type": "command",
|
|
21
|
+
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/gc-state.sh\"",
|
|
22
|
+
"async": false,
|
|
23
|
+
"timeout": 10
|
|
18
24
|
}
|
|
19
25
|
]
|
|
20
26
|
}
|
|
@@ -57,7 +63,7 @@
|
|
|
57
63
|
]
|
|
58
64
|
},
|
|
59
65
|
{
|
|
60
|
-
"matcher": "mcp__.*muggle.*(execute|replay)",
|
|
66
|
+
"matcher": "mcp__.*muggle.*(execute|replay|telemetry-skill-emit)",
|
|
61
67
|
"hooks": [
|
|
62
68
|
{
|
|
63
69
|
"type": "command",
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
set -uo pipefail
|
|
4
|
+
|
|
5
|
+
# Prune ephemeral muggle state that nothing else garbage-collects, so it does not
|
|
6
|
+
# grow without bound (observed: hundreds of dead per-session files). Runs from a
|
|
7
|
+
# SessionStart hook, TTL-gated to once/day unless MUGGLE_STATE_GC_FORCE=1. Silent
|
|
8
|
+
# and best-effort: it never blocks session start and never prints context.
|
|
9
|
+
#
|
|
10
|
+
# Collection is keyed on INACTIVITY, never creation age, so a session that lasts
|
|
11
|
+
# months is never collected while it is still in use:
|
|
12
|
+
#
|
|
13
|
+
# guardrails/<session>.json one per Claude session. An in-use session keeps
|
|
14
|
+
# rewriting it (guardrails.mjs on every guarded tool call) and refreshes it
|
|
15
|
+
# on resume (below), so its mtime tracks last activity — only a session gone
|
|
16
|
+
# quiet for the whole window is collected, and by then it has ended.
|
|
17
|
+
# muggle-do/sessions/<slug>/ a PR-follow-up slot. Pruned only once finalized
|
|
18
|
+
# (result.md present — the terminal marker); an OPEN slot, a PR watched for
|
|
19
|
+
# any length of time, has no result.md and is never touched.
|
|
20
|
+
#
|
|
21
|
+
# Age tests use `find -mmin/-mtime` (portable across GNU and BSD) rather than
|
|
22
|
+
# `date -r`, whose file-mtime meaning is GNU-only.
|
|
23
|
+
|
|
24
|
+
home="${HOME}"
|
|
25
|
+
guardrails_dir="${home}/.muggle-ai/guardrails"
|
|
26
|
+
sessions_dir="${home}/.muggle-ai/muggle-do/sessions"
|
|
27
|
+
marker_dir="${home}/.cache/muggle"
|
|
28
|
+
marker="${marker_dir}/state-gc-checked"
|
|
29
|
+
guardrails_ttl_days="${MUGGLE_GUARDRAILS_TTL_DAYS:-14}"
|
|
30
|
+
slot_ttl_days="${MUGGLE_SLOT_TTL_DAYS:-30}"
|
|
31
|
+
|
|
32
|
+
# Refresh this session's own guardrails file before anything else, every start
|
|
33
|
+
# (not behind the daily gate — a resume on an already-swept day must still mark
|
|
34
|
+
# the session live). This is what makes a long-lived or resumed session immune to
|
|
35
|
+
# the inactivity sweep, whatever its age. Filename sanitization mirrors
|
|
36
|
+
# guardrails.mjs fileFor().
|
|
37
|
+
session_id=$(cat 2>/dev/null | sed -n 's/.*"session_id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)
|
|
38
|
+
if [ -n "${session_id}" ]; then
|
|
39
|
+
safe_id="${session_id//[^A-Za-z0-9_-]/_}"
|
|
40
|
+
[ -f "${guardrails_dir}/${safe_id}.json" ] && touch "${guardrails_dir}/${safe_id}.json" 2>/dev/null || true
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
# TTL gate: skip when the marker exists and is younger than 24h (1440 min).
|
|
44
|
+
if [ -z "${MUGGLE_STATE_GC_FORCE:-}" ] && [ -f "$marker" ] \
|
|
45
|
+
&& [ -z "$(find "$marker" -mmin +1440 2>/dev/null)" ]; then
|
|
46
|
+
exit 0
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
if [ -d "$guardrails_dir" ]; then
|
|
50
|
+
find "$guardrails_dir" -maxdepth 1 -type f -name '*.json' \
|
|
51
|
+
-mtime "+${guardrails_ttl_days}" -delete 2>/dev/null || true
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
if [ -d "$sessions_dir" ]; then
|
|
55
|
+
for slot in "$sessions_dir"/*/; do
|
|
56
|
+
[ -d "$slot" ] || continue
|
|
57
|
+
[ -f "${slot}result.md" ] || continue
|
|
58
|
+
if [ -n "$(find "${slot}result.md" -mtime "+${slot_ttl_days}" 2>/dev/null)" ]; then
|
|
59
|
+
rm -rf "$slot" 2>/dev/null || true
|
|
60
|
+
fi
|
|
61
|
+
done
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
mkdir -p "$marker_dir" 2>/dev/null || true
|
|
65
|
+
touch "$marker" 2>/dev/null || true
|
|
66
|
+
exit 0
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# Windows backend for guard-run. Callers never invoke this file: the portable
|
|
2
|
+
# entry point is guard-run.mjs (plain node, runs everywhere), which probes the
|
|
3
|
+
# platform and dispatches to one backend — this Job Object shim on win32,
|
|
4
|
+
# systemd scope / prlimit on linux, ulimit + process group on darwin. So skill
|
|
5
|
+
# instructions stay OS-agnostic by referencing guard-run.mjs only; PowerShell
|
|
6
|
+
# exists solely inside this win32 backend. guard-run.mjs invokes it as:
|
|
7
|
+
#
|
|
8
|
+
# powershell -File guard-run-job-object.ps1 <limit> <kill|persist> <launcherPid> <base64CommandLine>
|
|
9
|
+
#
|
|
10
|
+
# The child is created CREATE_SUSPENDED, assigned to a fresh Job Object, then
|
|
11
|
+
# resumed — so it and every descendant are inside the job from their first
|
|
12
|
+
# instruction. JOB_OBJECT_LIMIT_ACTIVE_PROCESS makes the process past the limit
|
|
13
|
+
# fail at creation in the kernel (nothing to clean up), which is what stops a
|
|
14
|
+
# fork storm: a sweeper that reacts after enumeration always loses that race.
|
|
15
|
+
#
|
|
16
|
+
# kill mode adds JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE — this shim holds the only
|
|
17
|
+
# job handle, so when it exits or is killed the kernel destroys the entire tree
|
|
18
|
+
# — and watches the launcher: if the node launcher dies first, the shim
|
|
19
|
+
# terminates the job so orphans are impossible. persist mode (guard-run
|
|
20
|
+
# --service) keeps only the cap; the tree may outlive the launcher.
|
|
21
|
+
|
|
22
|
+
$ErrorActionPreference = "Stop"
|
|
23
|
+
|
|
24
|
+
if ($args.Count -ne 4) {
|
|
25
|
+
[Console]::Error.WriteLine("usage: guard-run-job-object.ps1 <limit> <kill|persist> <launcherPid> <base64CommandLine>")
|
|
26
|
+
exit 2
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
$activeProcessLimit = 0
|
|
30
|
+
if (-not [int]::TryParse($args[0], [ref]$activeProcessLimit) -or $activeProcessLimit -lt 1) {
|
|
31
|
+
[Console]::Error.WriteLine("guard-run-job-object: limit must be a positive integer")
|
|
32
|
+
exit 2
|
|
33
|
+
}
|
|
34
|
+
if ($args[1] -notin @("kill", "persist")) {
|
|
35
|
+
[Console]::Error.WriteLine("guard-run-job-object: mode must be 'kill' or 'persist'")
|
|
36
|
+
exit 2
|
|
37
|
+
}
|
|
38
|
+
$killOnClose = $args[1] -eq "kill"
|
|
39
|
+
$launcherPid = 0
|
|
40
|
+
if (-not [int]::TryParse($args[2], [ref]$launcherPid)) {
|
|
41
|
+
[Console]::Error.WriteLine("guard-run-job-object: launcherPid must be an integer")
|
|
42
|
+
exit 2
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
$commandLine = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($args[3]))
|
|
46
|
+
} catch {
|
|
47
|
+
[Console]::Error.WriteLine("guard-run-job-object: command line is not valid base64")
|
|
48
|
+
exit 2
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
Add-Type -TypeDefinition @'
|
|
52
|
+
using System;
|
|
53
|
+
using System.Runtime.InteropServices;
|
|
54
|
+
using System.Text;
|
|
55
|
+
|
|
56
|
+
namespace MuggleGuardRun
|
|
57
|
+
{
|
|
58
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
59
|
+
public struct JOBOBJECT_BASIC_LIMIT_INFORMATION
|
|
60
|
+
{
|
|
61
|
+
public long PerProcessUserTimeLimit;
|
|
62
|
+
public long PerJobUserTimeLimit;
|
|
63
|
+
public uint LimitFlags;
|
|
64
|
+
public UIntPtr MinimumWorkingSetSize;
|
|
65
|
+
public UIntPtr MaximumWorkingSetSize;
|
|
66
|
+
public uint ActiveProcessLimit;
|
|
67
|
+
public UIntPtr Affinity;
|
|
68
|
+
public uint PriorityClass;
|
|
69
|
+
public uint SchedulingClass;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
73
|
+
public struct IO_COUNTERS
|
|
74
|
+
{
|
|
75
|
+
public ulong ReadOperationCount;
|
|
76
|
+
public ulong WriteOperationCount;
|
|
77
|
+
public ulong OtherOperationCount;
|
|
78
|
+
public ulong ReadTransferCount;
|
|
79
|
+
public ulong WriteTransferCount;
|
|
80
|
+
public ulong OtherTransferCount;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
84
|
+
public struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION
|
|
85
|
+
{
|
|
86
|
+
public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation;
|
|
87
|
+
public IO_COUNTERS IoInfo;
|
|
88
|
+
public UIntPtr ProcessMemoryLimit;
|
|
89
|
+
public UIntPtr JobMemoryLimit;
|
|
90
|
+
public UIntPtr PeakProcessMemoryUsed;
|
|
91
|
+
public UIntPtr PeakJobMemoryUsed;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
|
95
|
+
public struct STARTUPINFO
|
|
96
|
+
{
|
|
97
|
+
public uint cb;
|
|
98
|
+
public string lpReserved;
|
|
99
|
+
public string lpDesktop;
|
|
100
|
+
public string lpTitle;
|
|
101
|
+
public uint dwX;
|
|
102
|
+
public uint dwY;
|
|
103
|
+
public uint dwXSize;
|
|
104
|
+
public uint dwYSize;
|
|
105
|
+
public uint dwXCountChars;
|
|
106
|
+
public uint dwYCountChars;
|
|
107
|
+
public uint dwFillAttribute;
|
|
108
|
+
public uint dwFlags;
|
|
109
|
+
public ushort wShowWindow;
|
|
110
|
+
public ushort cbReserved2;
|
|
111
|
+
public IntPtr lpReserved2;
|
|
112
|
+
public IntPtr hStdInput;
|
|
113
|
+
public IntPtr hStdOutput;
|
|
114
|
+
public IntPtr hStdError;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
118
|
+
public struct PROCESS_INFORMATION
|
|
119
|
+
{
|
|
120
|
+
public IntPtr hProcess;
|
|
121
|
+
public IntPtr hThread;
|
|
122
|
+
public uint dwProcessId;
|
|
123
|
+
public uint dwThreadId;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
public static class JobObjectLauncher
|
|
127
|
+
{
|
|
128
|
+
const uint JOB_OBJECT_LIMIT_ACTIVE_PROCESS = 0x00000008;
|
|
129
|
+
const uint JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000;
|
|
130
|
+
const int JobObjectExtendedLimitInformation = 9;
|
|
131
|
+
const uint CREATE_SUSPENDED = 0x00000004;
|
|
132
|
+
const uint SYNCHRONIZE = 0x00100000;
|
|
133
|
+
const uint INFINITE = 0xFFFFFFFF;
|
|
134
|
+
const uint WAIT_OBJECT_0 = 0;
|
|
135
|
+
const int LAUNCHER_DEATH_EXIT_CODE = 137;
|
|
136
|
+
|
|
137
|
+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
138
|
+
static extern IntPtr CreateJobObject(IntPtr lpJobAttributes, string lpName);
|
|
139
|
+
|
|
140
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
141
|
+
static extern bool SetInformationJobObject(IntPtr hJob, int jobObjectInfoClass,
|
|
142
|
+
ref JOBOBJECT_EXTENDED_LIMIT_INFORMATION lpJobObjectInfo, uint cbJobObjectInfoLength);
|
|
143
|
+
|
|
144
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
145
|
+
static extern bool AssignProcessToJobObject(IntPtr hJob, IntPtr hProcess);
|
|
146
|
+
|
|
147
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
148
|
+
static extern bool TerminateJobObject(IntPtr hJob, uint uExitCode);
|
|
149
|
+
|
|
150
|
+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
151
|
+
static extern bool CreateProcess(string lpApplicationName, StringBuilder lpCommandLine,
|
|
152
|
+
IntPtr lpProcessAttributes, IntPtr lpThreadAttributes, bool bInheritHandles,
|
|
153
|
+
uint dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory,
|
|
154
|
+
ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation);
|
|
155
|
+
|
|
156
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
157
|
+
static extern uint ResumeThread(IntPtr hThread);
|
|
158
|
+
|
|
159
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
160
|
+
static extern bool TerminateProcess(IntPtr hProcess, uint uExitCode);
|
|
161
|
+
|
|
162
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
163
|
+
static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds);
|
|
164
|
+
|
|
165
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
166
|
+
static extern uint WaitForMultipleObjects(uint nCount, IntPtr[] lpHandles, bool bWaitAll,
|
|
167
|
+
uint dwMilliseconds);
|
|
168
|
+
|
|
169
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
170
|
+
static extern bool GetExitCodeProcess(IntPtr hProcess, out uint lpExitCode);
|
|
171
|
+
|
|
172
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
173
|
+
static extern IntPtr OpenProcess(uint dwDesiredAccess, bool bInheritHandle, uint dwProcessId);
|
|
174
|
+
|
|
175
|
+
static Exception LastError(string operation)
|
|
176
|
+
{
|
|
177
|
+
return new Exception(operation + " failed (Win32 error " +
|
|
178
|
+
Marshal.GetLastWin32Error() + ")");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
public static int Run(string commandLine, int activeProcessLimit, bool killOnClose,
|
|
182
|
+
int launcherPid)
|
|
183
|
+
{
|
|
184
|
+
IntPtr job = CreateJobObject(IntPtr.Zero, null);
|
|
185
|
+
if (job == IntPtr.Zero) throw LastError("CreateJobObject");
|
|
186
|
+
|
|
187
|
+
JOBOBJECT_EXTENDED_LIMIT_INFORMATION limits =
|
|
188
|
+
new JOBOBJECT_EXTENDED_LIMIT_INFORMATION();
|
|
189
|
+
limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_ACTIVE_PROCESS
|
|
190
|
+
| (killOnClose ? JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE : 0);
|
|
191
|
+
limits.BasicLimitInformation.ActiveProcessLimit = (uint)activeProcessLimit;
|
|
192
|
+
uint limitsSize = (uint)Marshal.SizeOf(typeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION));
|
|
193
|
+
if (!SetInformationJobObject(job, JobObjectExtendedLimitInformation, ref limits,
|
|
194
|
+
limitsSize))
|
|
195
|
+
{
|
|
196
|
+
throw LastError("SetInformationJobObject");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
STARTUPINFO startupInfo = new STARTUPINFO();
|
|
200
|
+
startupInfo.cb = (uint)Marshal.SizeOf(typeof(STARTUPINFO));
|
|
201
|
+
PROCESS_INFORMATION processInfo;
|
|
202
|
+
StringBuilder mutableCommandLine = new StringBuilder(commandLine);
|
|
203
|
+
if (!CreateProcess(null, mutableCommandLine, IntPtr.Zero, IntPtr.Zero, true,
|
|
204
|
+
CREATE_SUSPENDED, IntPtr.Zero, null, ref startupInfo, out processInfo))
|
|
205
|
+
{
|
|
206
|
+
throw LastError("CreateProcess for [" + commandLine + "]");
|
|
207
|
+
}
|
|
208
|
+
if (!AssignProcessToJobObject(job, processInfo.hProcess))
|
|
209
|
+
{
|
|
210
|
+
TerminateProcess(processInfo.hProcess, 1);
|
|
211
|
+
throw LastError("AssignProcessToJobObject");
|
|
212
|
+
}
|
|
213
|
+
ResumeThread(processInfo.hThread);
|
|
214
|
+
|
|
215
|
+
if (killOnClose)
|
|
216
|
+
{
|
|
217
|
+
IntPtr launcher = OpenProcess(SYNCHRONIZE, false, (uint)launcherPid);
|
|
218
|
+
if (launcher == IntPtr.Zero)
|
|
219
|
+
{
|
|
220
|
+
// Launcher already gone: the tree is orphaned at birth — reap it.
|
|
221
|
+
TerminateJobObject(job, LAUNCHER_DEATH_EXIT_CODE);
|
|
222
|
+
return LAUNCHER_DEATH_EXIT_CODE;
|
|
223
|
+
}
|
|
224
|
+
IntPtr[] handles = new IntPtr[] { processInfo.hProcess, launcher };
|
|
225
|
+
uint signaled = WaitForMultipleObjects(2, handles, false, INFINITE);
|
|
226
|
+
if (signaled == WAIT_OBJECT_0 + 1)
|
|
227
|
+
{
|
|
228
|
+
TerminateJobObject(job, LAUNCHER_DEATH_EXIT_CODE);
|
|
229
|
+
return LAUNCHER_DEATH_EXIT_CODE;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
else
|
|
233
|
+
{
|
|
234
|
+
WaitForSingleObject(processInfo.hProcess, INFINITE);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
uint exitCode;
|
|
238
|
+
if (!GetExitCodeProcess(processInfo.hProcess, out exitCode))
|
|
239
|
+
{
|
|
240
|
+
throw LastError("GetExitCodeProcess");
|
|
241
|
+
}
|
|
242
|
+
// The job handle is deliberately never closed here: in kill mode this
|
|
243
|
+
// process's death is what closes it, and that close is the kernel's
|
|
244
|
+
// cue to destroy any stragglers.
|
|
245
|
+
return (int)exitCode;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
'@
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
exit ([MuggleGuardRun.JobObjectLauncher]::Run($commandLine, $activeProcessLimit, $killOnClose, $launcherPid))
|
|
253
|
+
} catch {
|
|
254
|
+
[Console]::Error.WriteLine("guard-run-job-object: $($_.Exception.Message)")
|
|
255
|
+
exit 1
|
|
256
|
+
}
|