@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.
- package/.agents/skills/gantry/SKILL.md +166 -0
- package/.agents/skills/gantry/capabilities/claude-code.json +15 -0
- package/.agents/skills/gantry/capabilities/codex.json +14 -0
- package/.agents/skills/gantry/capabilities/opencode.json +15 -0
- package/.agents/skills/gantry/dashboard/static/app.js +100 -0
- package/.agents/skills/gantry/dashboard/static/index.html +16 -0
- package/.agents/skills/gantry/dashboard/static/style.css +74 -0
- package/.agents/skills/gantry/hooks/claude-code.settings.json +56 -0
- package/.agents/skills/gantry/hooks/codex.hooks.json +4 -0
- package/.agents/skills/gantry/hooks/git/pre-commit +77 -0
- package/.agents/skills/gantry/hooks/git/pre-push +123 -0
- package/.agents/skills/gantry/hooks/git/skipscan.py +88 -0
- package/.agents/skills/gantry/hooks/opencode.plugin.js +44 -0
- package/.agents/skills/gantry/reference/plan-workflow.md +383 -0
- package/.agents/skills/gantry/reference/round-workflow.md +755 -0
- package/.agents/skills/gantry/schemas/critic.json +93 -0
- package/.agents/skills/gantry/schemas/implementer.json +52 -0
- package/.agents/skills/gantry/schemas/learner.json +35 -0
- package/.agents/skills/gantry/schemas/plan-critic.json +39 -0
- package/.agents/skills/gantry/schemas/planner.json +64 -0
- package/.agents/skills/gantry/schemas/requirement-critic.json +48 -0
- package/.agents/skills/gantry/schemas/reviewer.json +52 -0
- package/.agents/skills/gantry/scripts/acceptance.py +66 -0
- package/.agents/skills/gantry/scripts/budget.py +162 -0
- package/.agents/skills/gantry/scripts/cleanup.py +186 -0
- package/.agents/skills/gantry/scripts/common.py +361 -0
- package/.agents/skills/gantry/scripts/dashboard.py +233 -0
- package/.agents/skills/gantry/scripts/frontier.py +192 -0
- package/.agents/skills/gantry/scripts/gates.py +401 -0
- package/.agents/skills/gantry/scripts/guard.py +568 -0
- package/.agents/skills/gantry/scripts/learner.py +99 -0
- package/.agents/skills/gantry/scripts/result.py +104 -0
- package/.agents/skills/gantry/scripts/roadmap.py +212 -0
- package/.agents/skills/gantry/scripts/runlog.py +491 -0
- package/.agents/skills/gantry/scripts/setup.py +139 -0
- package/.agents/skills/gantry/scripts/spec.py +252 -0
- package/.agents/skills/gantry/templates/issue.md +32 -0
- package/.agents/skills/gantry/templates/prd.md +26 -0
- package/.agents/skills/gantry/templates/spec.md +48 -0
- package/.agents/skills/gantry-dashboard/SKILL.md +55 -0
- package/.agents/skills/gantry-setup/SKILL.md +30 -0
- package/LICENSE +201 -0
- package/README.md +437 -0
- package/bin/gantry.mjs +45 -0
- package/package.json +36 -0
- 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())
|