@julioborges/gantry 0.1.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 (46) hide show
  1. package/.agents/skills/gantry/SKILL.md +166 -0
  2. package/.agents/skills/gantry/capabilities/claude-code.json +15 -0
  3. package/.agents/skills/gantry/capabilities/codex.json +14 -0
  4. package/.agents/skills/gantry/capabilities/opencode.json +15 -0
  5. package/.agents/skills/gantry/dashboard/static/app.js +100 -0
  6. package/.agents/skills/gantry/dashboard/static/index.html +16 -0
  7. package/.agents/skills/gantry/dashboard/static/style.css +74 -0
  8. package/.agents/skills/gantry/hooks/claude-code.settings.json +56 -0
  9. package/.agents/skills/gantry/hooks/codex.hooks.json +4 -0
  10. package/.agents/skills/gantry/hooks/git/pre-commit +77 -0
  11. package/.agents/skills/gantry/hooks/git/pre-push +123 -0
  12. package/.agents/skills/gantry/hooks/git/skipscan.py +88 -0
  13. package/.agents/skills/gantry/hooks/opencode.plugin.js +44 -0
  14. package/.agents/skills/gantry/reference/plan-workflow.md +383 -0
  15. package/.agents/skills/gantry/reference/round-workflow.md +755 -0
  16. package/.agents/skills/gantry/schemas/critic.json +93 -0
  17. package/.agents/skills/gantry/schemas/implementer.json +52 -0
  18. package/.agents/skills/gantry/schemas/learner.json +35 -0
  19. package/.agents/skills/gantry/schemas/plan-critic.json +39 -0
  20. package/.agents/skills/gantry/schemas/planner.json +64 -0
  21. package/.agents/skills/gantry/schemas/requirement-critic.json +48 -0
  22. package/.agents/skills/gantry/schemas/reviewer.json +52 -0
  23. package/.agents/skills/gantry/scripts/acceptance.py +66 -0
  24. package/.agents/skills/gantry/scripts/budget.py +162 -0
  25. package/.agents/skills/gantry/scripts/cleanup.py +186 -0
  26. package/.agents/skills/gantry/scripts/common.py +361 -0
  27. package/.agents/skills/gantry/scripts/dashboard.py +233 -0
  28. package/.agents/skills/gantry/scripts/frontier.py +192 -0
  29. package/.agents/skills/gantry/scripts/gates.py +401 -0
  30. package/.agents/skills/gantry/scripts/guard.py +568 -0
  31. package/.agents/skills/gantry/scripts/learner.py +99 -0
  32. package/.agents/skills/gantry/scripts/result.py +104 -0
  33. package/.agents/skills/gantry/scripts/roadmap.py +212 -0
  34. package/.agents/skills/gantry/scripts/runlog.py +491 -0
  35. package/.agents/skills/gantry/scripts/setup.py +139 -0
  36. package/.agents/skills/gantry/scripts/spec.py +252 -0
  37. package/.agents/skills/gantry/templates/issue.md +32 -0
  38. package/.agents/skills/gantry/templates/prd.md +26 -0
  39. package/.agents/skills/gantry/templates/spec.md +48 -0
  40. package/.agents/skills/gantry-dashboard/SKILL.md +55 -0
  41. package/.agents/skills/gantry-setup/SKILL.md +30 -0
  42. package/LICENSE +201 -0
  43. package/README.md +437 -0
  44. package/bin/gantry.mjs +45 -0
  45. package/package.json +36 -0
  46. package/scripts/ensure-npm-author.mjs +29 -0
@@ -0,0 +1,166 @@
1
+ ---
2
+ name: gantry
3
+ description: Harness-neutral agentic SDLC workflow. Resolves ready Issues deterministically, plans only to operator approval, implements with TDD, reviews against standards and Spec, and accepts delivery only after adversarial verification.
4
+ argument-hint: <spec-slug | spec#NN | wave:N | frontier | all | "free-text goal"> [--limit N] [--budget N]
5
+ ---
6
+
7
+ # Gantry
8
+
9
+ Gantry is a harness-neutral skill pack, not an execution engine. Workflow scripts decide readiness,
10
+ acceptance, gates and roadmap state; agents plan, implement, review and refute; the operator makes
11
+ approval decisions in the host harness.
12
+
13
+ ```
14
+ preflight → models → frontier.py → planned round loop
15
+ └→ spec.py --check → Requirement Critic → research → draft plan → critique
16
+ → STOP for operator approval
17
+ round: implement (TDD) → review (standards + Spec) → Critic → serial integration → roadmap.py done
18
+ ```
19
+
20
+ ## Resolve the portable runtime
21
+
22
+ Before a Run, resolve these values once and pass them as `args` to every reference workflow:
23
+
24
+ 1. `skillDir` is this `gantry` directory as an absolute real path. A harness-specific symlink resolves
25
+ through `realpath`; never assume a fixed installation directory.
26
+ 2. `repoRoot` is the enclosing Git repository or worktree (`common.repo_root()`).
27
+ 3. `policy` is `common.resolve_policy(repoRoot)`: Gantry's sparse defaults overlaid by
28
+ `<repoRoot>/.gantry/config.json` when present. A missing repository policy is valid.
29
+ 4. `paths` comes from `common.resolve_workflow_paths(repoRoot, scopeSlug)`. Prompts receive paths, not
30
+ repository-specific literals.
31
+
32
+ Run the standard-library workflow scripts as `python3 <skillDir>/scripts/<script>.py`. `common.py` provides
33
+ the shared Markdown parser and policy resolution; its `--json` path prints the resolved portable runtime.
34
+ Every script provides `--help`, and data-producing paths support `--json`.
35
+
36
+ ## Workflow rules
37
+
38
+ - Preflight refuses a dirty worktree, offers a dedicated Run worktree before creating anything, resolves
39
+ the effective policy, checks `roadmap.py check`, and asks models for Plan, Implement, Review and Critic.
40
+ Preflight also resolves `unitId` (`runlog.py unit-id --cwd <repoRoot> --json`) and a fresh `runId`, then
41
+ queries `runlog.py inflight <unitId> --json`. Every match names an Issue still `ready-for-agent`, its
42
+ phase and its preserved worktree; preflight offers the operator continuation there before doing anything
43
+ else. `runlog.py inflight` reports only `run`, `issue`, `phase`, `worktree`, `repositoryRoot`,
44
+ `policyHash`, `tier` and `staleAfterSeconds` — it does not report `correctionsSpent` or `branch`, so
45
+ preflight derives them before building `args.priorRun` by invoking the shipped query
46
+ `runlog.py corrections <unitId> <match.run> <match.issue> --json`, which reads the match's own Run log
47
+ (`~/.gantry/state/<unitId>/runs/<match.run>.jsonl`, or `--state-root` when overridden) and returns
48
+ `correctionsSpent` as the sum of two counts: (1) the `run.resumed.data.correctionsSpent` recorded in
49
+ that same Run log, but only when that same `run.resumed` event's `data.issue` also equals `match.issue`
50
+ — `0` when the log has no `run.resumed` event, or when its `run.resumed` names a different Issue, since
51
+ the base is per-Issue and must never be lent to another Issue that happens to share the Run log; plus
52
+ (2) the number of `refutation` events in that log whose `issue` equals `match.issue` and that are each
53
+ followed, later in the log, by a `phase.started` event for `Implement` on that same Issue — i.e. only
54
+ refutations whose correction pass actually started count toward the spent budget. Neither this
55
+ derivation rule nor `correctionsSpent` itself is ever computed by prose or by test code: this shipped
56
+ command is the single implementation, and it fails closed (`runlog error: no Run log for <runId>`,
57
+ exit 1) when no Run log exists for the requested Run ID, rather than silently reporting `0`.
58
+ `branch` is optional: when omitted, `reference/round-workflow.md` derives it itself from
59
+ `common.issue_branch` and verifies it with `git branch --show-current` in the preserved worktree
60
+ (see `implementationLocation`). Only explicit acceptance carries the derived match forward as
61
+ `args.priorRun` (`run`, `worktree`, `issue`, `correctionsSpent`, `policyHash`, and `branch` when known)
62
+ into `reference/round-workflow.md`, which appends `run.resumed` naming the prior Run and worktree
63
+ instead of starting a fresh worktree, and resumes the spent correction count instead of resetting it.
64
+ When the prior Run's `policyHash` differs from the effective policy resolved for this Run,
65
+ `reference/round-workflow.md` appends `policy.changed` with the new hash so the drift is recorded
66
+ before any Issue work resumes. The Run log is read only to offer that continuation and to derive
67
+ `correctionsSpent`; it never decides readiness or completion — `frontier.py`, Issue `Status:` lines
68
+ and `roadmap.py` do.
69
+ - `reference/round-workflow.md` appends every recorded-Run lifecycle event through `runlog.py append
70
+ <unitId> <runId>` when the caller supplies both. A Run spans one or more rounds, each a separate
71
+ invocation of this workflow sharing the same `runId`/`unitId`: only the first round (`args.isFirstRound`
72
+ not explicitly `false`) appends `run.started` (with the repository root, a policy hash, the harness
73
+ tier and the effective `staleAfterSeconds` in `data`) and, when resuming, `run.resumed` and
74
+ `policy.changed`; every subsequent round of the same Run passes `args.isFirstRound = false` so these
75
+ three events are never appended again — a Run log accepts only one `run.started` and rejects a
76
+ duplicate. Every round, first or not, then appends `round.started`, one `phase.started` /
77
+ `phase.finished` pair per phase that actually runs (Implement, Review and Critic per Issue, plus the
78
+ optional Learner phase on the last round when it finds recurring evidence), one
79
+ `subagent.started` / `subagent.stopped` pair per fresh agent carrying its role result,
80
+ `review.finding` after the Reviewer returns, `refutation` on every non-accepted Critic verdict,
81
+ `issue.blocked` when the correction ceiling is spent without acceptance, `issue.done` on successful
82
+ integration, `run.cancelled` on the first red post-merge gate, and `round.finished`. Only the last round
83
+ of a Run (`args.isLastRound = true`) appends `run.finished`, once, and only after the optional Learner
84
+ phase (see below) has already run and recorded its own `phase.started`/`subagent.started`/
85
+ `subagent.stopped`/`phase.finished` events — `run.finished` remains the final event of a completed Run,
86
+ never followed by a subagent.
87
+ The Critic's `subagent.stopped` carries a projection of its verdict, never the verdict unchanged: its
88
+ `gateResult` is a real `gates.py --json` payload, and `runlog.py append` fails loudly on any
89
+ `command`/`output`-tokenized field at any depth (its own rule), which a real `gates[].command` and
90
+ `gates[].output_tail` are. `reference/round-workflow.md`'s `projectCriticResult` keeps `complete`,
91
+ `criteria`, `gatesVerdict`, `gateFailures`, `refutations`, `requiredFixes` and `decisionsForOperator`
92
+ as returned, and narrows `gateResult` to only its `verdict` and `requirements` — dropping `gates`
93
+ entirely — so a genuine gates run never fails the append and the Run log still records only the
94
+ Critic's role result and reasoning, never command output. Every other role's `subagent.stopped` still
95
+ carries its result unprojected, and `runlog.py` still rejects it if it ever carries prohibited data.
96
+ `issue.blocked` records only a Run-log fact: the Issue's `Status:` line stays `ready-for-agent` so
97
+ `frontier.py` keeps offering it, and only `roadmap.py done` after Critic acceptance ever changes an
98
+ Issue's authoritative status. Omitting `runId` or `unitId` disables recording entirely and leaves the
99
+ round behaviorally identical, so a harness with no resolved Run log keeps working.
100
+ - Before any agent works in a worktree, the round workflow marks it with the Run — `runlog.py mark
101
+ <runId> --cwd <worktree>`, stored in that worktree's own git directory — and clears it with
102
+ `runlog.py unmark` when the Run ends, never between rounds. The tracked git hooks
103
+ (`hooks/git/pre-commit`, `hooks/git/pre-push`) and `guard.py` resolve the Run in one order:
104
+ `GANTRY_RUN_ID` when the caller exports it, then the marker, and a harness session ID only when
105
+ nothing else names a Run and its Run log already exists — a Claude Code or OpenCode session ID is
106
+ a session, not a Run, and no Run log is ever keyed by it. That is what makes a denial inside a Run
107
+ always recorded as `hook.denied`; git keeps one git directory per worktree, so concurrent
108
+ worktrees of one execution unit never attribute a denial to each other's Run. Because each round
109
+ is a separate invocation, the Run's end enumerates `git worktree list --porcelain` and clears
110
+ every marker naming that Run, so a worktree marked by an earlier round is never left behind.
111
+ - Use `frontier.py --scope <scope> --json` as the only authority for dependency rounds. Exit 1 for a
112
+ cyclic or dangling blocker graph. Parked `draft`, `blocked`, and `needs-operator` Issues are reported
113
+ and skipped.
114
+ - Before slicing, `spec.py --check` validates the Spec's structure and then the read-only Requirement
115
+ Critic (Critic model) assesses ambiguity, coherence, verifiability and non-goal coverage. A blocking
116
+ finding stops the run, quotes the finding, and tells the operator to amend the Spec; the Critic never
117
+ edits it. Neither structural validation nor Requirement Review approves planning — only explicit
118
+ operator approval does.
119
+ - Planning creates draft Issues and never edits `ROADMAP.md`. Present drafts and the critic verdict, then
120
+ stop. Only explicit operator approval permits `roadmap.py status <ref> ready-for-agent`, followed by
121
+ `roadmap.py waves` and `roadmap.py check`.
122
+ - Each Issue follows `reference/round-workflow.md`: a fresh TDD implementer, a reviewer on both standards
123
+ and Spec axes, one review fix pass, then a fresh adversarial Critic. The Critic alone can establish a
124
+ complete delivery. Its refutation consumes at most the correction budget.
125
+ - A multi-Issue round uses one isolated worktree and branch per implementer. Integrate accepted branches
126
+ serially, run gates after every merge, and stop on a failed integration gate. Create and identify each
127
+ Issue branch through the `git.issueBranch` policy template (default
128
+ `{prefix}{spec}-{number:02d}`), rendered by `common.issue_branch(policy, issue)`.
129
+ - Only after Critic acceptance, green gates and a clean worktree may the orchestrator run
130
+ `roadmap.py done <ref>`. Never hand-edit Issue status, criteria checkboxes or the roadmap.
131
+ - At the end of a Run, execute `python3 <skillDir>/scripts/cleanup.py --plan --json` from the Run worktree
132
+ and present its JSON output as the actual, read-only cleanup plan for the operator's authorization.
133
+ Only after explicit Cleanup Authorization may the workflow pass that unchanged JSON to
134
+ `cleanup.py --yes --plan-file <authorized-plan.json>`; it revalidates the plan against the repository
135
+ state and refuses any divergence. The workflow never executes `cleanup.py --yes` automatically.
136
+ - After the last round, the optional Learner (`reference/round-workflow.md`) reads only the refutation
137
+ and review-finding events already recorded in the Run log and drafts a lesson candidate for each
138
+ problem that recurred across Issues or attempts. The final Run report lists every lesson candidate,
139
+ with its evidence and proposed target, as an operator decision: the workflow never writes a candidate
140
+ into `AGENTS.md`, `CONTEXT.md`, a template or policy on its own. Pass `args.isLastRound = true` and
141
+ `args.learnerRunLogs` only for that final frontier round; `learnerRunLogs` is the current Run's own
142
+ Run-log path(s), normally `~/.gantry/state/<unit-id>/runs/<run-id>.jsonl`. When the Learner actually
143
+ runs (recurring evidence found), it is recorded exactly like the Implement, Review and Critic phases —
144
+ `phase.started`, `subagent.started`, `subagent.stopped` and `phase.finished` — appended after
145
+ `round.finished` and before `run.finished`, so `run.finished` stays the last event of a completed Run.
146
+ A skipped Learner phase (no logs, or nothing recurring) records none of those four events.
147
+ - After the last round (including the Learner phase), the final `roadmap.py check` and frontier reporting, offer to open a draft pull request from the Run branch to the configured target branch (`policy.git.target`). Generate an English PR body containing each completed Issue's authoritative criteria and Critic evidence. Ask the operator before calling `gh`. Open one draft pull request only on explicit confirmation. If `gh` is unavailable or the operator declines, report the Run branch and configured target as the handoff instead. Never merge, never auto-create a pull request, never create per-Issue pull requests, and never observe provider state. The final English Run report must state the capability-file support tier (`tier`).
148
+
149
+ ## Harness-neutral execution
150
+
151
+ Use the host harness to ask the operator and spawn agents. Where native workflow scripts, structured
152
+ outputs, parallel agents or worktree isolation are available, use them. Otherwise execute the same
153
+ prompts from the reference files manually and validate their JSON-shaped results before advancing.
154
+ The deterministic scripts and workflow semantics stay identical in every harness. The host's command
155
+ runner contract is `runCommand(command, { cwd, input })`: `input`, when supplied, must be written to the
156
+ command's stdin, not appended to the command line. Every recorded Run event goes through
157
+ `runlog.py append <unitId> <runId>` this way, with the event JSON as `input` — a harness that implements
158
+ `runCommand` without stdin support breaks every recorded round, not just this one.
159
+
160
+ ## References
161
+
162
+ - `reference/plan-workflow.md` — structural validation, Requirement Critic, research, draft, plan
163
+ critic and mandatory operator stop.
164
+ - `reference/round-workflow.md` — TDD implementation, two-axis review, adversarial Critic and serial
165
+ integration contract.
166
+ - `templates/` — default Spec, PRD and Issue structures.
@@ -0,0 +1,15 @@
1
+ {
2
+ "tier": "reference",
3
+ "hooks": true,
4
+ "structured_output": true,
5
+ "worktree_isolation": true,
6
+ "per_role_model": true,
7
+ "parallel_round": true,
8
+ "skills_path": ".agents/skills",
9
+ "hook_events": ["PreToolUse", "PostToolUse", "SubagentStart", "SubagentStop", "PreCompact"],
10
+ "payload_fields": ["tool_name", "tool_input", "session_id", "transcript_path"],
11
+ "models": {
12
+ "claude-opus-4-5": {"contextWindow": 200000},
13
+ "claude-sonnet-4-5": {"contextWindow": 200000}
14
+ }
15
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "tier": "compatible",
3
+ "hooks": false,
4
+ "structured_output": false,
5
+ "worktree_isolation": false,
6
+ "per_role_model": false,
7
+ "parallel_round": false,
8
+ "skills_path": ".agents/skills",
9
+ "hook_events": [],
10
+ "payload_fields": [],
11
+ "models": {
12
+ "gpt-5.2-codex": {"contextWindow": 272000}
13
+ }
14
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "tier": "supported",
3
+ "hooks": true,
4
+ "structured_output": false,
5
+ "worktree_isolation": false,
6
+ "per_role_model": true,
7
+ "parallel_round": false,
8
+ "skills_path": ".agents/skills",
9
+ "hook_events": ["tool.execute.before", "session.compacted"],
10
+ "payload_fields": ["tool", "args", "sessionID"],
11
+ "models": {
12
+ "anthropic/claude-opus-4-5": {"contextWindow": 200000},
13
+ "openai/gpt-5.2-codex": {"contextWindow": 272000}
14
+ }
15
+ }
@@ -0,0 +1,100 @@
1
+ // Read-only Gantry dashboard renderer. This file makes no request that can mutate
2
+ // state: it only polls GET /api/state and rebuilds the swimlane markup from the response.
3
+ (function () {
4
+ "use strict";
5
+
6
+ const POLL_INTERVAL_MS = 1000;
7
+
8
+ function badge(label, extraClass) {
9
+ const span = document.createElement("span");
10
+ span.className = "badge" + (extraClass ? " " + extraClass : "");
11
+ span.textContent = label;
12
+ return span;
13
+ }
14
+
15
+ function renderCard(issue) {
16
+ const card = document.createElement("div");
17
+ card.className = "card";
18
+
19
+ const title = document.createElement("strong");
20
+ title.textContent = issue.issue;
21
+ card.appendChild(title);
22
+ card.appendChild(document.createElement("br"));
23
+
24
+ if (issue.branch) card.appendChild(badge("branch: " + issue.branch));
25
+ if (issue.worktree) card.appendChild(badge("worktree: " + issue.worktree));
26
+ Object.keys(issue.models || {}).forEach(function (role) {
27
+ card.appendChild(badge(role + ": " + issue.models[role]));
28
+ });
29
+ if (issue.correctionBudget) {
30
+ card.appendChild(
31
+ badge("corrections: " + issue.correctionBudget.used + "/" + issue.correctionBudget.ceiling)
32
+ );
33
+ }
34
+ if (typeof issue.elapsedPhaseSeconds === "number") {
35
+ card.appendChild(badge("elapsed: " + issue.elapsedPhaseSeconds + "s"));
36
+ }
37
+ if (issue.operatorWaiting) {
38
+ card.appendChild(badge("awaiting operator", "waiting"));
39
+ }
40
+ return card;
41
+ }
42
+
43
+ function renderRun(run, columns) {
44
+ const section = document.createElement("section");
45
+ section.className = "swimlane" + (run.stale ? " stale" : "");
46
+
47
+ const heading = document.createElement("h2");
48
+ heading.textContent =
49
+ run.repositoryRoot + " — " + run.run + (run.stale ? " (stale)" : "") + " [tier: " + run.tier + "]";
50
+ section.appendChild(heading);
51
+
52
+ if (run.compactionAt) {
53
+ section.appendChild(badge("compacted at " + run.compactionAt, "compaction"));
54
+ }
55
+
56
+ const columnsEl = document.createElement("div");
57
+ columnsEl.className = "columns";
58
+
59
+ columns.forEach(function (columnName) {
60
+ const columnEl = document.createElement("div");
61
+ columnEl.className = "column";
62
+ const title = document.createElement("h3");
63
+ title.textContent = columnName;
64
+ columnEl.appendChild(title);
65
+ run.issues
66
+ .filter(function (issue) {
67
+ return issue.column === columnName;
68
+ })
69
+ .forEach(function (issue) {
70
+ columnEl.appendChild(renderCard(issue));
71
+ });
72
+ columnsEl.appendChild(columnEl);
73
+ });
74
+
75
+ section.appendChild(columnsEl);
76
+ return section;
77
+ }
78
+
79
+ function render(state) {
80
+ const root = document.getElementById("swimlanes");
81
+ root.textContent = "";
82
+ state.runs.forEach(function (run) {
83
+ root.appendChild(renderRun(run, state.columns));
84
+ });
85
+ }
86
+
87
+ function poll() {
88
+ fetch("/api/state", { cache: "no-store" })
89
+ .then(function (response) {
90
+ return response.json();
91
+ })
92
+ .then(render)
93
+ .catch(function () {
94
+ // A transient fetch failure leaves the previous render in place.
95
+ });
96
+ }
97
+
98
+ poll();
99
+ setInterval(poll, POLL_INTERVAL_MS);
100
+ })();
@@ -0,0 +1,16 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>Gantry Dashboard</title>
6
+ <link rel="stylesheet" href="/style.css" />
7
+ </head>
8
+ <body>
9
+ <header>
10
+ <h1>Gantry Dashboard</h1>
11
+ <p>Read-only view of every Run. Nothing on this page can change an Issue, a policy, a Run log, a branch or a worktree.</p>
12
+ </header>
13
+ <main id="swimlanes"></main>
14
+ <script src="/app.js"></script>
15
+ </body>
16
+ </html>
@@ -0,0 +1,74 @@
1
+ :root {
2
+ color-scheme: light dark;
3
+ --border: #8888;
4
+ }
5
+
6
+ body {
7
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
8
+ margin: 1.5rem;
9
+ }
10
+
11
+ header p {
12
+ opacity: 0.75;
13
+ }
14
+
15
+ .swimlane {
16
+ border: 1px solid var(--border);
17
+ border-radius: 0.5rem;
18
+ margin-bottom: 1.5rem;
19
+ padding: 0.75rem;
20
+ }
21
+
22
+ .swimlane.stale {
23
+ border-color: #c0392b;
24
+ }
25
+
26
+ .swimlane h2 {
27
+ margin: 0 0 0.5rem 0;
28
+ font-size: 1rem;
29
+ }
30
+
31
+ .columns {
32
+ display: grid;
33
+ grid-template-columns: repeat(8, minmax(8rem, 1fr));
34
+ gap: 0.5rem;
35
+ }
36
+
37
+ .column {
38
+ border: 1px dashed var(--border);
39
+ border-radius: 0.375rem;
40
+ padding: 0.5rem;
41
+ min-height: 4rem;
42
+ }
43
+
44
+ .column h3 {
45
+ margin: 0 0 0.5rem 0;
46
+ font-size: 0.8rem;
47
+ text-transform: uppercase;
48
+ opacity: 0.7;
49
+ }
50
+
51
+ .card {
52
+ border: 1px solid var(--border);
53
+ border-radius: 0.25rem;
54
+ padding: 0.35rem;
55
+ margin-bottom: 0.35rem;
56
+ font-size: 0.8rem;
57
+ }
58
+
59
+ .badge {
60
+ display: inline-block;
61
+ border-radius: 0.25rem;
62
+ padding: 0 0.25rem;
63
+ margin-right: 0.25rem;
64
+ font-size: 0.7rem;
65
+ border: 1px solid var(--border);
66
+ }
67
+
68
+ .badge.waiting {
69
+ border-color: #e67e22;
70
+ }
71
+
72
+ .badge.compaction {
73
+ border-color: #8e44ad;
74
+ }
@@ -0,0 +1,56 @@
1
+ {
2
+ "hooks": {
3
+ "PreToolUse": [
4
+ {
5
+ "matcher": "*",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "python3 \"$CLAUDE_PROJECT_DIR/.agents/skills/gantry/scripts/guard.py\" PreToolUse --cwd \"$CLAUDE_PROJECT_DIR\""
10
+ }
11
+ ]
12
+ }
13
+ ],
14
+ "PostToolUse": [
15
+ {
16
+ "matcher": "*",
17
+ "hooks": [
18
+ {
19
+ "type": "command",
20
+ "command": "python3 \"$CLAUDE_PROJECT_DIR/.agents/skills/gantry/scripts/guard.py\" PostToolUse --cwd \"$CLAUDE_PROJECT_DIR\""
21
+ }
22
+ ]
23
+ }
24
+ ],
25
+ "SubagentStart": [
26
+ {
27
+ "hooks": [
28
+ {
29
+ "type": "command",
30
+ "command": "python3 \"$CLAUDE_PROJECT_DIR/.agents/skills/gantry/scripts/guard.py\" SubagentStart --cwd \"$CLAUDE_PROJECT_DIR\""
31
+ }
32
+ ]
33
+ }
34
+ ],
35
+ "SubagentStop": [
36
+ {
37
+ "hooks": [
38
+ {
39
+ "type": "command",
40
+ "command": "python3 \"$CLAUDE_PROJECT_DIR/.agents/skills/gantry/scripts/guard.py\" SubagentStop --cwd \"$CLAUDE_PROJECT_DIR\""
41
+ }
42
+ ]
43
+ }
44
+ ],
45
+ "PreCompact": [
46
+ {
47
+ "hooks": [
48
+ {
49
+ "type": "command",
50
+ "command": "python3 \"$CLAUDE_PROJECT_DIR/.agents/skills/gantry/scripts/guard.py\" PreCompact --cwd \"$CLAUDE_PROJECT_DIR\""
51
+ }
52
+ ]
53
+ }
54
+ ]
55
+ }
56
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "_note": "Codex has no hook events yet (see ../capabilities/codex.json: hooks=false, hook_events=[]). This file is the same guard.py wiring the other two harnesses use -- 'python3 <skillDir>/scripts/guard.py <event>' with the event payload on stdin -- ready to activate entry by entry the day Codex ships a hook event. Until then it grants guard.py no authority over Codex, matching the compatible tier: every protected rule stays enforced only through the prompts and the Critic.",
3
+ "hooks": {}
4
+ }
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env python3
2
+ """Git pre-commit hook: reject a commit whose staged diff introduces a test-skip pattern.
3
+
4
+ Per `docs/adr/0005-git-hooks-enforce-git-rules.md`, test-skip detection lives here
5
+ instead of in `guard.py`'s Bash-command parsing. This hook reads `git diff --cached
6
+ --unified=0`, which -- because the subprocess call inherits this hook's environment,
7
+ including whatever `GIT_INDEX_FILE` git itself points at for a pathspec or
8
+ `-i`/`--include` commit -- reflects exactly the content this commit is about to
9
+ write, regardless of how it reached the index: `-a`/`--all`, a pathspec argument,
10
+ `-i`/`--include`, a prior `git stage` (an alias for `git add`), or a plain prior
11
+ `git add`. Git resolves all of that staging semantics itself before invoking this
12
+ hook; no Bash-command parsing is involved, so there is no spelling left to evade.
13
+
14
+ Activated by `core.hooksPath` pointing at this directory
15
+ (`.agents/skills/gantry/hooks/git/`); `gantry-setup` writes that config into the
16
+ operator's repository (`gantry-migration#10`) -- this hook only proves itself in
17
+ temporary repositories where a test sets `core.hooksPath` directly.
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import datetime
22
+ import sys
23
+ from pathlib import Path
24
+
25
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
26
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent / "scripts"))
27
+
28
+ import runlog # noqa: E402
29
+ import skipscan # noqa: E402
30
+
31
+
32
+ def now_iso() -> str:
33
+ return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
34
+
35
+
36
+ def record_denied(cwd: Path, path: str) -> None:
37
+ """Append the `hook.denied` event for this denial; a logging failure never changes the decision.
38
+
39
+ The Run is resolved by `runlog.resolve_hook_run`: `GANTRY_RUN_ID` when the caller
40
+ exported it, otherwise this worktree's own current-Run marker, which the round workflow
41
+ writes before any agent works there. A denial inside a Run is therefore recorded whether
42
+ or not the shell that invoked `git` carried any Gantry environment.
43
+ """
44
+ run_id, state_root_value = runlog.resolve_hook_run(cwd)
45
+ if not run_id:
46
+ return
47
+ try:
48
+ root = runlog.state_root(state_root_value)
49
+ unit = runlog.unit_id(cwd)
50
+ log_path = runlog.run_log_path(root, unit, run_id)
51
+ if not log_path.exists():
52
+ return
53
+ event = runlog.validate_event(
54
+ {
55
+ "ts": now_iso(),
56
+ "run": run_id,
57
+ "event": "hook.denied",
58
+ "data": {"rule": "no-test-skip-commit", "path": path},
59
+ }
60
+ )
61
+ runlog.append_event(log_path, event)
62
+ except (runlog.EventError, OSError, ValueError):
63
+ return
64
+
65
+
66
+ def main() -> int:
67
+ matched_file = skipscan.staged_skip_match()
68
+ if not matched_file:
69
+ return 0
70
+ message = f"deny: no-test-skip-commit {matched_file}"
71
+ print(message, file=sys.stderr)
72
+ record_denied(Path.cwd(), matched_file)
73
+ return 1
74
+
75
+
76
+ if __name__ == "__main__":
77
+ sys.exit(main())