@jeger-ai/opengantry 2.2.2 → 2.2.4
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/README.md +97 -78
- package/dist/cli/commands/arch.js +4 -4
- package/dist/cli/commands/check-imports.js +1 -1
- package/dist/cli/commands/onboarding.js +5 -5
- package/dist/cli/commands/perimeter.js +2 -2
- package/dist/cli/commands/runtime.js +1 -1
- package/dist/cli/commands/scan.js +2 -2
- package/dist/cli/commands/teacher.js +1 -1
- package/dist/cli/commands/verify.js +3 -3
- package/dist/cli/lib/architecture-credential.js +1 -1
- package/dist/cli/lib/architecture-pointer.js +1 -1
- package/dist/cli/lib/ast-discovery.js +1 -1
- package/dist/cli/lib/audience-output.js +7 -7
- package/dist/cli/lib/cli-io.js +1 -1
- package/dist/cli/lib/config-namespace.d.ts +18 -0
- package/dist/cli/lib/config-namespace.d.ts.map +1 -0
- package/dist/cli/lib/config-namespace.js +39 -0
- package/dist/cli/lib/config-namespace.js.map +1 -0
- package/dist/cli/lib/constants.d.ts +6 -5
- package/dist/cli/lib/constants.d.ts.map +1 -1
- package/dist/cli/lib/constants.js +6 -5
- package/dist/cli/lib/constants.js.map +1 -1
- package/dist/cli/lib/context-feed-store.d.ts +1 -1
- package/dist/cli/lib/context-feed-store.js +1 -1
- package/dist/cli/lib/dirty-missions.js +1 -1
- package/dist/cli/lib/doctor-core.d.ts.map +1 -1
- package/dist/cli/lib/doctor-core.js +8 -7
- package/dist/cli/lib/doctor-core.js.map +1 -1
- package/dist/cli/lib/doctor-integration.js +1 -1
- package/dist/cli/lib/doctor-substrate.js +4 -4
- package/dist/cli/lib/errors.d.ts.map +1 -1
- package/dist/cli/lib/errors.js +2 -1
- package/dist/cli/lib/errors.js.map +1 -1
- package/dist/cli/lib/file-merge-gxt.js +2 -2
- package/dist/cli/lib/fix-hints.js +9 -9
- package/dist/cli/lib/git-metrics.js +1 -1
- package/dist/cli/lib/git-proof.d.ts +1 -1
- package/dist/cli/lib/git-proof.d.ts.map +1 -1
- package/dist/cli/lib/git-proof.js +3 -2
- package/dist/cli/lib/git-proof.js.map +1 -1
- package/dist/cli/lib/init-interactive.js +2 -2
- package/dist/cli/lib/init-plan.js +6 -6
- package/dist/cli/lib/init-tutorial.js +10 -10
- package/dist/cli/lib/kpi-engine.js +3 -3
- package/dist/cli/lib/kpi-scan.js +3 -3
- package/dist/cli/lib/legislate.d.ts.map +1 -1
- package/dist/cli/lib/legislate.js +11 -5
- package/dist/cli/lib/legislate.js.map +1 -1
- package/dist/cli/lib/manifest.js +2 -2
- package/dist/cli/lib/manifest.js.map +1 -1
- package/dist/cli/lib/mcp-governance.d.ts.map +1 -1
- package/dist/cli/lib/mcp-governance.js +4 -3
- package/dist/cli/lib/mcp-governance.js.map +1 -1
- package/dist/cli/lib/mcp-tools-register.js +4 -4
- package/dist/cli/lib/missions/formatter.js +1 -1
- package/dist/cli/lib/missions/parser.d.ts.map +1 -1
- package/dist/cli/lib/missions/parser.js +7 -5
- package/dist/cli/lib/missions/parser.js.map +1 -1
- package/dist/cli/lib/missions/validator.js +1 -1
- package/dist/cli/lib/msn-allocate.js +2 -2
- package/dist/cli/lib/onboarding-flow.js +5 -5
- package/dist/cli/lib/perimeter.js +1 -1
- package/dist/cli/lib/register-proposals.js +1 -1
- package/dist/cli/lib/runtime-env.d.ts.map +1 -1
- package/dist/cli/lib/runtime-env.js +7 -4
- package/dist/cli/lib/runtime-env.js.map +1 -1
- package/dist/cli/lib/skill-key.d.ts +12 -0
- package/dist/cli/lib/skill-key.d.ts.map +1 -0
- package/dist/cli/lib/skill-key.js +28 -0
- package/dist/cli/lib/skill-key.js.map +1 -0
- package/dist/cli/lib/staged-tmvc-guard.js +1 -1
- package/dist/cli/lib/start-orchestration.js +5 -5
- package/dist/cli/lib/status-report.js +3 -3
- package/dist/cli/lib/substrate-version.js +2 -2
- package/dist/cli/lib/surgeons/typescript-workspace.d.ts +1 -1
- package/dist/cli/lib/surgeons/typescript-workspace.js +3 -3
- package/dist/cli/lib/teacher-identity.d.ts +1 -2
- package/dist/cli/lib/teacher-identity.d.ts.map +1 -1
- package/dist/cli/lib/teacher-identity.js +11 -15
- package/dist/cli/lib/teacher-identity.js.map +1 -1
- package/dist/cli/lib/tmvc-path.d.ts.map +1 -1
- package/dist/cli/lib/tmvc-path.js +5 -2
- package/dist/cli/lib/tmvc-path.js.map +1 -1
- package/dist/cli/lib/types.d.ts +1 -1
- package/dist/cli/lib/upgrade-apply.js +5 -5
- package/dist/cli/lib/upgrade-plan.d.ts +1 -1
- package/dist/cli/lib/upgrade-plan.js +4 -4
- package/dist/cli/lib/verify-failure-normalize.js +1 -1
- package/dist/cli/lib/verify-hints.js +9 -9
- package/dist/cli/lib/verify-run.d.ts.map +1 -1
- package/dist/cli/lib/verify-run.js +3 -2
- package/dist/cli/lib/verify-run.js.map +1 -1
- package/dist/cli/lib/version.gen.d.ts +1 -1
- package/dist/cli/lib/version.gen.js +1 -1
- package/dist/cli/program-core.js +2 -2
- package/package.json +9 -4
- package/templates/.cursor/hooks/gxt-before-shell.sh +2 -2
- package/templates/.cursor/hooks/gxt-session-start.sh +5 -5
- package/templates/.cursor/mcp.json +1 -1
- package/templates/.cursor/rules/opengantry-gxt-substrate.mdc +4 -4
- package/templates/.gitagent/config.json +15 -0
- package/templates/.gitagent/foreman/MANIFEST.json +2 -2
- package/templates/.gitagent/foreman/SUBSTRATE.version.json +1 -1
- package/templates/.gitagent/foreman/TEACHER.allowlist.example +1 -1
- package/templates/.gitagent/missions/README.md +3 -3
- package/templates/.gitagent/teacher/ARCHITECTURE-ACCESS.md +8 -8
- package/templates/.gitagent/teacher/KPI-REPORT.schema.yaml +1 -1
- package/templates/.gitagent/teacher/MISSION-ARCHITECT.md +5 -5
- package/templates/.gitagent/teacher/MISSION.schema.yaml +2 -2
- package/templates/.gitagent/teacher/RULES.md +2 -2
- package/templates/.githooks/pre-commit +4 -4
- package/templates/.githooks/pre-push +6 -6
- package/templates/.github/workflows/gxt-validate.yml +14 -13
- package/templates/AGENTS.md +1 -1
- package/templates/integrations/asset-catalog.json +2 -1
- package/templates/integrations/compatibility.json +1 -1
- package/templates/integrations/recipes/_footer.md +6 -6
- package/templates/integrations/recipes/_preamble.md +9 -9
- package/templates/integrations/recipes/aider.md +1 -1
- package/templates/integrations/recipes/antigravity.md +1 -1
- package/templates/integrations/recipes/claude-code.md +1 -1
- package/templates/integrations/recipes/codex-cli.md +2 -2
- package/templates/integrations/recipes/cursor.md +3 -3
- package/templates/scripts/dev-validate.sh +1 -1
- package/templates/scripts/gxt-manifest-lib.mjs +418 -3
- package/templates/scripts/gxt-resolve-mission.sh +2 -2
- package/templates/scripts/gxt-runtime-env.sh +4 -4
- package/templates/scripts/validate-gxt.sh +5 -1
- package/templates/scripts/verify-pr-missions.sh +12 -6
- package/templates/skills/{gapman.md → gantry.md} +2 -2
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
### Cursor
|
|
2
2
|
|
|
3
|
-
- **Context injection:** `.cursor/rules/opengantry-gxt-substrate.mdc` (`alwaysApply: true`); `.cursor/hooks.json` — `sessionStart` mission scope + `beforeShellExecution` fallback guard; `.cursor/mcp.json` — `
|
|
3
|
+
- **Context injection:** `.cursor/rules/opengantry-gxt-substrate.mdc` (`alwaysApply: true`); `.cursor/hooks.json` — `sessionStart` mission scope + `beforeShellExecution` fallback guard; `.cursor/mcp.json` — `gantry mcp serve`.
|
|
4
4
|
- **Mission Architect:** `/gantry` macro (do not use `/plan` — Cursor native Plan Mode); implicit activation when user asks to **write/edit code** with no pinned mission. Follow `.gitagent/teacher/MISSION-ARCHITECT.md`.
|
|
5
5
|
- **MCP legislation:** `gxt_draft_legislation` → semantic chat approval → `gxt_execute_legislation` → Teacher commit → `gxt_check_signature` → `gxt_pin_mission`. Or **`gxt_start_orchestration`** for goal-first CLI parity.
|
|
6
|
-
- **Verify remediation:** `gxt_verify` returns `error_code`, `fix_hints`, `next_actions` on failure; CLI equivalent: `
|
|
6
|
+
- **Verify remediation:** `gxt_verify` returns `error_code`, `fix_hints`, `next_actions` on failure; CLI equivalent: `gantry verify --mission … --fix`.
|
|
7
7
|
- **Session bootstrap:**
|
|
8
8
|
|
|
9
9
|
```bash
|
|
@@ -14,6 +14,6 @@ source scripts/gxt-runtime-env.sh
|
|
|
14
14
|
- **Enforcement:** Advisory for Agent edits; MCP two-step gate for legislation; shell hook fallback for non-MCP paths; use `runtime exec` for headless CLI/SDK runs.
|
|
15
15
|
- **Gotcha:** Enable hooks **and** MCP in Cursor Settings; restart if they do not load (**Output → Hooks**).
|
|
16
16
|
|
|
17
|
-
Headless: `
|
|
17
|
+
Headless: `gantry runtime exec --mission .gitagent/missions/MSN-0001.<slug>.yaml -- cursor agent "<task>"`
|
|
18
18
|
|
|
19
19
|
Vendor docs: https://cursor.com/docs/agent/hooks
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# OpenGantry dogfood: run the full local GXT +
|
|
2
|
+
# OpenGantry dogfood: run the full local GXT + gantry stack before push/PR.
|
|
3
3
|
# Usage: ./scripts/dev-validate.sh [base-ref] [head-ref]
|
|
4
4
|
# base-ref defaults to origin/main (falls back to HEAD~1)
|
|
5
5
|
# head-ref defaults to HEAD
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Node-only MANIFEST helpers for validate-gxt.sh (no jq
|
|
3
|
+
* Node-only MANIFEST + trusted automation policy helpers for validate-gxt.sh (no jq).
|
|
4
4
|
* Usage:
|
|
5
5
|
* node scripts/gxt-manifest-lib.mjs prefixes [repoRoot]
|
|
6
6
|
* node scripts/gxt-manifest-lib.mjs validate-manifest [repoRoot]
|
|
7
7
|
* node scripts/gxt-manifest-lib.mjs validate-bypass-note (JSON on stdin)
|
|
8
|
+
* node scripts/gxt-manifest-lib.mjs eval-commit <repoRoot> <commitSha>
|
|
9
|
+
* node scripts/gxt-manifest-lib.mjs eval-range <repoRoot> <baseSha> <headSha>
|
|
8
10
|
*/
|
|
9
11
|
import fs from "node:fs";
|
|
10
12
|
import path from "node:path";
|
|
13
|
+
import { execFileSync } from "node:child_process";
|
|
14
|
+
import { createRequire } from "node:module";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
11
16
|
|
|
12
17
|
const FIXED_MSN_PREFIXES = [
|
|
13
18
|
".gitagent/",
|
|
@@ -16,11 +21,38 @@ const FIXED_MSN_PREFIXES = [
|
|
|
16
21
|
".github/workflows/gxt-validate.yml",
|
|
17
22
|
];
|
|
18
23
|
|
|
24
|
+
const HARD_MAX_NET_LOC = 5;
|
|
25
|
+
const ALLOWED_STRUCTURAL_CHANGES = new Set(["workflow_version_pin"]);
|
|
26
|
+
const CONFIG_REL = ".gitagent/config.json";
|
|
27
|
+
|
|
19
28
|
export function repoRootFromArg(arg) {
|
|
20
29
|
if (arg?.trim()) return path.resolve(arg.trim());
|
|
21
30
|
return process.cwd();
|
|
22
31
|
}
|
|
23
32
|
|
|
33
|
+
function git(repoRoot, args) {
|
|
34
|
+
return execFileSync("git", args, { cwd: repoRoot, encoding: "utf8" }).trimEnd();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizePath(p) {
|
|
38
|
+
return p.replace(/\\/g, "/");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function resolveYamlParser(repoRoot) {
|
|
42
|
+
const candidates = [
|
|
43
|
+
path.join(repoRoot, "node_modules/yaml"),
|
|
44
|
+
path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "node_modules/yaml"),
|
|
45
|
+
];
|
|
46
|
+
for (const dir of candidates) {
|
|
47
|
+
const pkg = path.join(dir, "package.json");
|
|
48
|
+
if (fs.existsSync(pkg)) {
|
|
49
|
+
const req = createRequire(pkg);
|
|
50
|
+
return req("yaml");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
throw new Error("gxt-manifest-lib: yaml package not found (run npm ci)");
|
|
54
|
+
}
|
|
55
|
+
|
|
24
56
|
export function readManifest(repoRoot) {
|
|
25
57
|
const rel = ".gitagent/foreman/MANIFEST.json";
|
|
26
58
|
const abs = path.join(repoRoot, rel);
|
|
@@ -36,6 +68,81 @@ export function readManifest(repoRoot) {
|
|
|
36
68
|
return parsed;
|
|
37
69
|
}
|
|
38
70
|
|
|
71
|
+
export function loadTrustedAutomationRules(repoRoot) {
|
|
72
|
+
const abs = path.join(repoRoot, CONFIG_REL);
|
|
73
|
+
if (!fs.existsSync(abs)) {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
let parsed;
|
|
77
|
+
try {
|
|
78
|
+
parsed = JSON.parse(fs.readFileSync(abs, "utf8"));
|
|
79
|
+
} catch (e) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`gxt-manifest-lib: invalid JSON in ${CONFIG_REL}: ${e instanceof Error ? e.message : String(e)}`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
const block = parsed?.trusted_automation;
|
|
85
|
+
if (!block) {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
if (!Array.isArray(block.rules) || block.rules.length === 0) {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
const rules = [];
|
|
92
|
+
for (const [i, rule] of block.rules.entries()) {
|
|
93
|
+
if (!rule || typeof rule !== "object") {
|
|
94
|
+
throw new Error(`gxt-manifest-lib: trusted_automation.rules[${i}] must be an object`);
|
|
95
|
+
}
|
|
96
|
+
if (typeof rule.id !== "string" || !rule.id.trim()) {
|
|
97
|
+
throw new Error(`gxt-manifest-lib: trusted_automation.rules[${i}] missing id`);
|
|
98
|
+
}
|
|
99
|
+
if (!Array.isArray(rule.allowed_actors) || rule.allowed_actors.length === 0) {
|
|
100
|
+
throw new Error(`gxt-manifest-lib: rule ${rule.id} requires non-empty allowed_actors`);
|
|
101
|
+
}
|
|
102
|
+
if (!Array.isArray(rule.allowed_paths) || rule.allowed_paths.length === 0) {
|
|
103
|
+
throw new Error(`gxt-manifest-lib: rule ${rule.id} requires non-empty allowed_paths`);
|
|
104
|
+
}
|
|
105
|
+
if (!Array.isArray(rule.allowed_structural_changes) || rule.allowed_structural_changes.length === 0) {
|
|
106
|
+
throw new Error(`gxt-manifest-lib: rule ${rule.id} requires allowed_structural_changes`);
|
|
107
|
+
}
|
|
108
|
+
for (const change of rule.allowed_structural_changes) {
|
|
109
|
+
if (!ALLOWED_STRUCTURAL_CHANGES.has(change)) {
|
|
110
|
+
throw new Error(`gxt-manifest-lib: rule ${rule.id} has unsupported structural change: ${change}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (typeof rule.max_net_loc !== "number" || !Number.isInteger(rule.max_net_loc) || rule.max_net_loc < 1) {
|
|
114
|
+
throw new Error(`gxt-manifest-lib: rule ${rule.id} max_net_loc must be a positive integer`);
|
|
115
|
+
}
|
|
116
|
+
if (rule.max_net_loc > HARD_MAX_NET_LOC) {
|
|
117
|
+
throw new Error(`gxt-manifest-lib: rule ${rule.id} max_net_loc exceeds hard cap ${HARD_MAX_NET_LOC}`);
|
|
118
|
+
}
|
|
119
|
+
rules.push({
|
|
120
|
+
id: rule.id,
|
|
121
|
+
allowed_actors: rule.allowed_actors.map((a) => String(a).trim().toLowerCase()),
|
|
122
|
+
allowed_paths: rule.allowed_paths.map((p) => normalizePath(String(p))),
|
|
123
|
+
allowed_structural_changes: new Set(rule.allowed_structural_changes),
|
|
124
|
+
max_net_loc: rule.max_net_loc,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return rules;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function matchGlob(pattern, filePath) {
|
|
131
|
+
const normalized = normalizePath(filePath);
|
|
132
|
+
const re = new RegExp(
|
|
133
|
+
`^${normalizePath(pattern)
|
|
134
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
135
|
+
.replace(/\*\*/g, "<<<GLOBSTAR>>>")
|
|
136
|
+
.replace(/\*/g, "[^/]*")
|
|
137
|
+
.replace(/<<<GLOBSTAR>>>/g, ".*")}$`,
|
|
138
|
+
);
|
|
139
|
+
return re.test(normalized);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function pathMatchesAllowed(filePath, allowedPaths) {
|
|
143
|
+
return allowedPaths.some((pattern) => matchGlob(pattern, filePath));
|
|
144
|
+
}
|
|
145
|
+
|
|
39
146
|
export function listMsnEnforcedPrefixes(repoRoot) {
|
|
40
147
|
const manifest = readManifest(repoRoot);
|
|
41
148
|
const skills = manifest.skills;
|
|
@@ -49,11 +156,22 @@ export function listMsnEnforcedPrefixes(repoRoot) {
|
|
|
49
156
|
const out = new Set();
|
|
50
157
|
for (const p of [...FIXED_MSN_PREFIXES, ...roots]) {
|
|
51
158
|
if (typeof p !== "string" || !p.trim()) continue;
|
|
52
|
-
out.add(p
|
|
159
|
+
out.add(normalizePath(p));
|
|
53
160
|
}
|
|
54
161
|
return [...out];
|
|
55
162
|
}
|
|
56
163
|
|
|
164
|
+
export function isMsnEnforcedPath(filePath, prefixes) {
|
|
165
|
+
const p = normalizePath(filePath);
|
|
166
|
+
for (const prefix of prefixes) {
|
|
167
|
+
if (!prefix) continue;
|
|
168
|
+
if (p === prefix) return true;
|
|
169
|
+
if (prefix.endsWith("/") && p.startsWith(prefix)) return true;
|
|
170
|
+
if (!prefix.endsWith("/") && p.startsWith(`${prefix}/`)) return true;
|
|
171
|
+
}
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
|
|
57
175
|
export function validateManifestStructure(repoRoot) {
|
|
58
176
|
const m = readManifest(repoRoot);
|
|
59
177
|
if (typeof m.schema_version !== "string" || m.schema_version.length === 0) {
|
|
@@ -101,6 +219,274 @@ export function validateBypassNoteJson(raw) {
|
|
|
101
219
|
);
|
|
102
220
|
}
|
|
103
221
|
|
|
222
|
+
function parseNumstat(output) {
|
|
223
|
+
let total = 0;
|
|
224
|
+
for (const line of output.split("\n")) {
|
|
225
|
+
if (!line.trim()) continue;
|
|
226
|
+
const [adds, dels] = line.split("\t");
|
|
227
|
+
if (adds === "-" || dels === "-") continue;
|
|
228
|
+
total += Number(adds) + Number(dels);
|
|
229
|
+
}
|
|
230
|
+
return total;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function gitNumstat(repoRoot, args) {
|
|
234
|
+
try {
|
|
235
|
+
return parseNumstat(git(repoRoot, args));
|
|
236
|
+
} catch {
|
|
237
|
+
return 0;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function commitChangedFiles(repoRoot, commitSha) {
|
|
242
|
+
const out = git(repoRoot, ["diff-tree", "--no-commit-id", "--name-only", "-r", commitSha]);
|
|
243
|
+
return out ? out.split("\n").filter(Boolean).map(normalizePath) : [];
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function rangeChangedFiles(repoRoot, baseSha, headSha) {
|
|
247
|
+
const out = git(repoRoot, ["diff", "--name-only", "--diff-filter=ACMRT", `${baseSha}...${headSha}`]);
|
|
248
|
+
return out ? out.split("\n").filter(Boolean).map(normalizePath) : [];
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function commitAuthorEmail(repoRoot, commitSha) {
|
|
252
|
+
return git(repoRoot, ["log", "-1", "--format=%ae", commitSha]).toLowerCase();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function objectKeysEqual(a, b) {
|
|
256
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
257
|
+
return Array.isArray(a) && Array.isArray(b) && a.length === b.length;
|
|
258
|
+
}
|
|
259
|
+
if (a === null || b === null || typeof a !== "object" || typeof b !== "object") {
|
|
260
|
+
return typeof a === typeof b;
|
|
261
|
+
}
|
|
262
|
+
const keysA = Object.keys(a).sort();
|
|
263
|
+
const keysB = Object.keys(b).sort();
|
|
264
|
+
return keysA.length === keysB.length && keysA.every((k, i) => k === keysB[i]);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function collectStructuralDiffs(before, after, keyPath = []) {
|
|
268
|
+
const diffs = [];
|
|
269
|
+
if (Array.isArray(before) && Array.isArray(after)) {
|
|
270
|
+
for (let i = 0; i < before.length; i++) {
|
|
271
|
+
diffs.push(...collectStructuralDiffs(before[i], after[i], [...keyPath, String(i)]));
|
|
272
|
+
}
|
|
273
|
+
return diffs;
|
|
274
|
+
}
|
|
275
|
+
if (before !== null && after !== null && typeof before === "object" && typeof after === "object") {
|
|
276
|
+
const keys = Object.keys(before);
|
|
277
|
+
for (const key of keys) {
|
|
278
|
+
const childPath = [...keyPath, key];
|
|
279
|
+
const bv = before[key];
|
|
280
|
+
const av = after[key];
|
|
281
|
+
if (bv !== null && av !== null && typeof bv === "object" && typeof av === "object") {
|
|
282
|
+
diffs.push(...collectStructuralDiffs(bv, av, childPath));
|
|
283
|
+
} else if (bv !== av) {
|
|
284
|
+
diffs.push({ path: childPath, before: bv, after: av });
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return diffs;
|
|
288
|
+
}
|
|
289
|
+
if (before !== after) {
|
|
290
|
+
diffs.push({ path: keyPath, before, after });
|
|
291
|
+
}
|
|
292
|
+
return diffs;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function isWorkflowVersionPinChange(before, after) {
|
|
296
|
+
if (typeof before !== "string" || typeof after !== "string") return false;
|
|
297
|
+
const usesRe = /^([^@]+@)(.+)$/;
|
|
298
|
+
const bm = before.match(usesRe);
|
|
299
|
+
const am = after.match(usesRe);
|
|
300
|
+
return Boolean(bm && am && bm[1] === am[1] && bm[2] !== am[2]);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function structuresMatch(before, after) {
|
|
304
|
+
if (Array.isArray(before) && Array.isArray(after)) {
|
|
305
|
+
if (before.length !== after.length) return false;
|
|
306
|
+
return before.every((item, i) => structuresMatch(item, after[i]));
|
|
307
|
+
}
|
|
308
|
+
if (before !== null && after !== null && typeof before === "object" && typeof after === "object") {
|
|
309
|
+
if (!objectKeysEqual(before, after)) return false;
|
|
310
|
+
return Object.keys(before).every((k) => structuresMatch(before[k], after[k]));
|
|
311
|
+
}
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function workflowVersionPinOnly(beforeContent, afterContent, yaml) {
|
|
316
|
+
let beforeDoc;
|
|
317
|
+
let afterDoc;
|
|
318
|
+
try {
|
|
319
|
+
beforeDoc = yaml.parse(beforeContent);
|
|
320
|
+
afterDoc = yaml.parse(afterContent);
|
|
321
|
+
} catch {
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
if (!structuresMatch(beforeDoc, afterDoc)) {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
const diffs = collectStructuralDiffs(beforeDoc, afterDoc);
|
|
328
|
+
if (diffs.length === 0) return true;
|
|
329
|
+
return diffs.every((d) => {
|
|
330
|
+
const lastKey = d.path[d.path.length - 1];
|
|
331
|
+
return lastKey === "uses" && isWorkflowVersionPinChange(d.before, d.after);
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function readBlobAt(repoRoot, commitSha, filePath) {
|
|
336
|
+
try {
|
|
337
|
+
return git(repoRoot, ["show", `${commitSha}:${filePath}`]);
|
|
338
|
+
} catch {
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function validateStructuralChanges(repoRoot, files, rule, baseSha, headSha, mode) {
|
|
344
|
+
if (!rule.allowed_structural_changes.has("workflow_version_pin")) {
|
|
345
|
+
return { ok: false, reason: `rule ${rule.id}: no matching structural change policy` };
|
|
346
|
+
}
|
|
347
|
+
const yaml = resolveYamlParser(repoRoot);
|
|
348
|
+
const workflowFiles = files.filter((f) => /\.ya?ml$/i.test(f));
|
|
349
|
+
for (const file of workflowFiles) {
|
|
350
|
+
let beforeContent;
|
|
351
|
+
let afterContent;
|
|
352
|
+
if (mode === "commit") {
|
|
353
|
+
const parent = git(repoRoot, ["rev-parse", `${headSha}^`]);
|
|
354
|
+
beforeContent = readBlobAt(repoRoot, parent, file);
|
|
355
|
+
afterContent = readBlobAt(repoRoot, headSha, file);
|
|
356
|
+
} else {
|
|
357
|
+
beforeContent = readBlobAt(repoRoot, baseSha, file);
|
|
358
|
+
afterContent = readBlobAt(repoRoot, headSha, file);
|
|
359
|
+
}
|
|
360
|
+
if (afterContent === null) continue;
|
|
361
|
+
if (beforeContent === null) {
|
|
362
|
+
return { ok: false, reason: `rule ${rule.id}: new file not allowed (${file})` };
|
|
363
|
+
}
|
|
364
|
+
if (!workflowVersionPinOnly(beforeContent, afterContent, yaml)) {
|
|
365
|
+
return { ok: false, reason: `rule ${rule.id}: structural change outside workflow_version_pin (${file})` };
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return { ok: true, reason: "" };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function findMatchingRule(rules, actorEmail) {
|
|
372
|
+
const actor = actorEmail.toLowerCase();
|
|
373
|
+
return rules.filter((rule) => rule.allowed_actors.includes(actor));
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function evaluateAgainstRule(repoRoot, rule, files, actorEmail, netLoc, structuralCtx) {
|
|
377
|
+
if (!rule.allowed_actors.includes(actorEmail.toLowerCase())) {
|
|
378
|
+
return { eligible: false, reason: `actor ${actorEmail} not in rule ${rule.id} allowed_actors` };
|
|
379
|
+
}
|
|
380
|
+
if (files.length === 0) {
|
|
381
|
+
return { eligible: false, reason: "no changed files" };
|
|
382
|
+
}
|
|
383
|
+
for (const file of files) {
|
|
384
|
+
if (!pathMatchesAllowed(file, rule.allowed_paths)) {
|
|
385
|
+
return { eligible: false, reason: `path ${file} outside rule ${rule.id} allowed_paths` };
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (netLoc > rule.max_net_loc) {
|
|
389
|
+
return {
|
|
390
|
+
eligible: false,
|
|
391
|
+
reason: `net_loc ${netLoc} exceeds rule ${rule.id} max_net_loc ${rule.max_net_loc}`,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
const structural = validateStructuralChanges(
|
|
395
|
+
repoRoot,
|
|
396
|
+
files,
|
|
397
|
+
rule,
|
|
398
|
+
structuralCtx.baseSha,
|
|
399
|
+
structuralCtx.headSha,
|
|
400
|
+
structuralCtx.mode,
|
|
401
|
+
);
|
|
402
|
+
if (!structural.ok) {
|
|
403
|
+
return { eligible: false, reason: structural.reason };
|
|
404
|
+
}
|
|
405
|
+
return {
|
|
406
|
+
eligible: true,
|
|
407
|
+
reason: `TRUSTED-AUTOMATION-OK: rule=${rule.id} actor=${actorEmail} paths=${files.join(",")} net_loc=${netLoc}`,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Evaluate a single commit for trusted automation eligibility.
|
|
413
|
+
* @returns {{ eligible: boolean, reason: string }}
|
|
414
|
+
*/
|
|
415
|
+
export function evaluateTrustedAutomationCommit(repoRoot, commitSha) {
|
|
416
|
+
const rules = loadTrustedAutomationRules(repoRoot);
|
|
417
|
+
if (rules.length === 0) {
|
|
418
|
+
return { eligible: false, reason: "no trusted_automation policy configured" };
|
|
419
|
+
}
|
|
420
|
+
const prefixes = listMsnEnforcedPrefixes(repoRoot);
|
|
421
|
+
const files = commitChangedFiles(repoRoot, commitSha).filter((f) => isMsnEnforcedPath(f, prefixes));
|
|
422
|
+
if (files.length === 0) {
|
|
423
|
+
return { eligible: false, reason: "commit does not touch MSN-enforced paths" };
|
|
424
|
+
}
|
|
425
|
+
const actorEmail = commitAuthorEmail(repoRoot, commitSha);
|
|
426
|
+
const matchingRules = findMatchingRule(rules, actorEmail);
|
|
427
|
+
if (matchingRules.length === 0) {
|
|
428
|
+
return { eligible: false, reason: `actor ${actorEmail} not covered by trusted_automation policy` };
|
|
429
|
+
}
|
|
430
|
+
const netLoc = gitNumstat(repoRoot, ["show", "--numstat", "--format=", commitSha]);
|
|
431
|
+
let lastReason = `commit ${commitSha} failed trusted_automation policy checks`;
|
|
432
|
+
for (const rule of matchingRules) {
|
|
433
|
+
const result = evaluateAgainstRule(repoRoot, rule, files, actorEmail, netLoc, {
|
|
434
|
+
mode: "commit",
|
|
435
|
+
baseSha: null,
|
|
436
|
+
headSha: commitSha,
|
|
437
|
+
});
|
|
438
|
+
if (result.eligible) return result;
|
|
439
|
+
lastReason = result.reason;
|
|
440
|
+
}
|
|
441
|
+
return { eligible: false, reason: lastReason };
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Evaluate a PR commit range for trusted automation eligibility.
|
|
446
|
+
* @returns {{ eligible: boolean, reason: string }}
|
|
447
|
+
*/
|
|
448
|
+
export function evaluateTrustedAutomationRange(repoRoot, baseSha, headSha) {
|
|
449
|
+
const rules = loadTrustedAutomationRules(repoRoot);
|
|
450
|
+
if (rules.length === 0) {
|
|
451
|
+
return { eligible: false, reason: "no trusted_automation policy configured" };
|
|
452
|
+
}
|
|
453
|
+
const prefixes = listMsnEnforcedPrefixes(repoRoot);
|
|
454
|
+
const allFiles = rangeChangedFiles(repoRoot, baseSha, headSha);
|
|
455
|
+
const enforcedFiles = allFiles.filter((f) => isMsnEnforcedPath(f, prefixes));
|
|
456
|
+
if (enforcedFiles.length === 0) {
|
|
457
|
+
return { eligible: false, reason: "range does not touch MSN-enforced paths" };
|
|
458
|
+
}
|
|
459
|
+
const commits = git(repoRoot, ["rev-list", "--no-merges", `${baseSha}..${headSha}`])
|
|
460
|
+
.split("\n")
|
|
461
|
+
.filter(Boolean);
|
|
462
|
+
for (const commit of commits) {
|
|
463
|
+
const files = commitChangedFiles(repoRoot, commit).filter((f) => isMsnEnforcedPath(f, prefixes));
|
|
464
|
+
if (files.length === 0) continue;
|
|
465
|
+
const actorEmail = commitAuthorEmail(repoRoot, commit);
|
|
466
|
+
const commitResult = evaluateTrustedAutomationCommit(repoRoot, commit);
|
|
467
|
+
if (!commitResult.eligible) {
|
|
468
|
+
return {
|
|
469
|
+
eligible: false,
|
|
470
|
+
reason: `commit ${commit.slice(0, 7)} failed: ${commitResult.reason}`,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
const netLoc = gitNumstat(repoRoot, ["diff", "--numstat", `${baseSha}...${headSha}`]);
|
|
475
|
+
const actorEmail = commits.length > 0 ? commitAuthorEmail(repoRoot, commits[commits.length - 1]) : "";
|
|
476
|
+
const matchingRules = findMatchingRule(rules, actorEmail);
|
|
477
|
+
let lastReason = "PR diff failed trusted_automation policy checks";
|
|
478
|
+
for (const rule of matchingRules) {
|
|
479
|
+
const result = evaluateAgainstRule(repoRoot, rule, enforcedFiles, actorEmail, netLoc, {
|
|
480
|
+
mode: "range",
|
|
481
|
+
baseSha,
|
|
482
|
+
headSha,
|
|
483
|
+
});
|
|
484
|
+
if (result.eligible) return result;
|
|
485
|
+
lastReason = result.reason;
|
|
486
|
+
}
|
|
487
|
+
return { eligible: false, reason: lastReason };
|
|
488
|
+
}
|
|
489
|
+
|
|
104
490
|
function main() {
|
|
105
491
|
const cmd = process.argv[2];
|
|
106
492
|
const repoRoot = repoRootFromArg(process.argv[3]);
|
|
@@ -122,9 +508,38 @@ function main() {
|
|
|
122
508
|
const stdin = fs.readFileSync(0, "utf8");
|
|
123
509
|
process.exit(validateBypassNoteJson(stdin) ? 0 : 1);
|
|
124
510
|
}
|
|
511
|
+
case "eval-commit": {
|
|
512
|
+
const commitSha = process.argv[4];
|
|
513
|
+
if (!commitSha) {
|
|
514
|
+
console.error("Usage: gxt-manifest-lib.mjs eval-commit <repoRoot> <commitSha>");
|
|
515
|
+
process.exit(2);
|
|
516
|
+
}
|
|
517
|
+
const result = evaluateTrustedAutomationCommit(repoRoot, commitSha);
|
|
518
|
+
if (result.eligible) {
|
|
519
|
+
console.error(result.reason);
|
|
520
|
+
process.exit(0);
|
|
521
|
+
}
|
|
522
|
+
console.error(`TRUSTED-AUTOMATION-DENY: ${result.reason}`);
|
|
523
|
+
process.exit(1);
|
|
524
|
+
}
|
|
525
|
+
case "eval-range": {
|
|
526
|
+
const baseSha = process.argv[4];
|
|
527
|
+
const headSha = process.argv[5];
|
|
528
|
+
if (!baseSha || !headSha) {
|
|
529
|
+
console.error("Usage: gxt-manifest-lib.mjs eval-range <repoRoot> <baseSha> <headSha>");
|
|
530
|
+
process.exit(2);
|
|
531
|
+
}
|
|
532
|
+
const result = evaluateTrustedAutomationRange(repoRoot, baseSha, headSha);
|
|
533
|
+
if (result.eligible) {
|
|
534
|
+
console.error(result.reason);
|
|
535
|
+
process.exit(0);
|
|
536
|
+
}
|
|
537
|
+
console.error(`TRUSTED-AUTOMATION-DENY: ${result.reason}`);
|
|
538
|
+
process.exit(1);
|
|
539
|
+
}
|
|
125
540
|
default:
|
|
126
541
|
console.error(
|
|
127
|
-
"Usage: gxt-manifest-lib.mjs <prefixes|validate-manifest|validate-bypass-note> [
|
|
542
|
+
"Usage: gxt-manifest-lib.mjs <prefixes|validate-manifest|validate-bypass-note|eval-commit|eval-range> [args]",
|
|
128
543
|
);
|
|
129
544
|
process.exit(2);
|
|
130
545
|
}
|
|
@@ -15,17 +15,17 @@ if ! MISSION="$(scripts/gxt-resolve-mission.sh "${1:-}" 2>/dev/null)"; then
|
|
|
15
15
|
gxt-runtime-env: no mission file resolved.
|
|
16
16
|
scripts/gxt-pin-mission.sh .gitagent/missions/<file>.yaml
|
|
17
17
|
source scripts/gxt-runtime-env.sh .gitagent/missions/<file>.yaml
|
|
18
|
-
export
|
|
18
|
+
export GANTRY_MISSION=.gitagent/missions/<file>.yaml
|
|
19
19
|
EOF
|
|
20
20
|
exit 1
|
|
21
21
|
fi
|
|
22
22
|
|
|
23
|
-
if command -v
|
|
24
|
-
eval "$(
|
|
23
|
+
if command -v gantry >/dev/null 2>&1; then
|
|
24
|
+
eval "$(gantry runtime env --mission "$MISSION")"
|
|
25
25
|
elif [ -f "dist/cli/index.js" ]; then
|
|
26
26
|
eval "$(node dist/cli/index.js runtime env --mission "$MISSION")"
|
|
27
27
|
else
|
|
28
|
-
echo "gxt-runtime-env:
|
|
28
|
+
echo "gxt-runtime-env: gantry not found (npm ci && npm run build)" >&2
|
|
29
29
|
exit 1
|
|
30
30
|
fi
|
|
31
31
|
|
|
@@ -120,9 +120,13 @@ cmd_msn() {
|
|
|
120
120
|
if commit_has_gxt_bypass_note "$commit"; then
|
|
121
121
|
continue
|
|
122
122
|
fi
|
|
123
|
+
if eval_out="$(node "$GXT_MANIFEST_LIB" eval-commit "$ROOT" "$commit" 2>&1)"; then
|
|
124
|
+
echo "$eval_out" >&2
|
|
125
|
+
continue
|
|
126
|
+
fi
|
|
123
127
|
echo "MSN check FAILED: commit $commit touches MSN-enforced paths but subject does not start with [MSN-NNNN] and has no gxt-bypass git note" >&2
|
|
124
128
|
echo " subject: $subject" >&2
|
|
125
|
-
echo " hint: run
|
|
129
|
+
echo " hint: run gantry verify --break-glass --reason \"...\" with GXT_BYPASS_SECRET, push refs/notes/gxt-bypass" >&2
|
|
126
130
|
echo " touched paths (sample):" >&2
|
|
127
131
|
git diff-tree --no-commit-id --name-only -r "$commit" | head -20 >&2
|
|
128
132
|
exit 1
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# PR / branch mission verify: mission purity, triple-dot file diff, full
|
|
2
|
+
# PR / branch mission verify: mission purity, triple-dot file diff, full gantry verify on changed missions.
|
|
3
3
|
# Usage:
|
|
4
4
|
# BASE_SHA=<base> HEAD_SHA=<head> ./scripts/verify-pr-missions.sh
|
|
5
5
|
# ./scripts/verify-pr-missions.sh <base-ref> <head-ref> # rev-parse refs to SHAs
|
|
@@ -57,10 +57,10 @@ fi
|
|
|
57
57
|
|
|
58
58
|
if [[ -f dist/cli/index.js ]]; then
|
|
59
59
|
GAPMAN=(node dist/cli/index.js)
|
|
60
|
-
elif command -v
|
|
61
|
-
GAPMAN=(
|
|
60
|
+
elif command -v gantry >/dev/null 2>&1; then
|
|
61
|
+
GAPMAN=(gantry)
|
|
62
62
|
else
|
|
63
|
-
echo "verify-pr-missions: build
|
|
63
|
+
echo "verify-pr-missions: build gantry first (npm run build)" >&2
|
|
64
64
|
exit 1
|
|
65
65
|
fi
|
|
66
66
|
|
|
@@ -109,8 +109,14 @@ mapfile -t CHANGED_MISSIONS < <(
|
|
|
109
109
|
)
|
|
110
110
|
|
|
111
111
|
if [[ "$needs_mission" -eq 1 && ${#CHANGED_MISSIONS[@]} -eq 0 ]]; then
|
|
112
|
+
if eval_out="$(node "$GXT_MANIFEST_LIB" eval-range "$ROOT" "$BASE_SHA" "$HEAD_SHA" 2>&1)"; then
|
|
113
|
+
echo "$eval_out" >&2
|
|
114
|
+
echo "verify-pr-missions: trusted automation policy satisfied — mission file not required"
|
|
115
|
+
exit 0
|
|
116
|
+
fi
|
|
112
117
|
echo "verify-pr-missions FAILED: diff touches MSN-enforced paths but no mission file under ${MISSIONS_PREFIX}" >&2
|
|
113
|
-
echo " Fix:
|
|
118
|
+
echo " Fix: gantry legislate \"<intent>\" --msn MSN-NNNN --skill-key gantry && include mission YAML in this PR" >&2
|
|
119
|
+
echo " Or: declare eligible automation in .gitagent/config.json trusted_automation (fail-closed by default)" >&2
|
|
114
120
|
exit 1
|
|
115
121
|
fi
|
|
116
122
|
|
|
@@ -134,7 +140,7 @@ if [[ ${#UNIQUE_MSNS[@]} -eq 1 ]]; then
|
|
|
134
140
|
fi
|
|
135
141
|
|
|
136
142
|
for mission in "${CHANGED_MISSIONS[@]}"; do
|
|
137
|
-
echo "verify-pr-missions:
|
|
143
|
+
echo "verify-pr-missions: gantry verify --mission ${mission}" >&2
|
|
138
144
|
"${GAPMAN[@]}" verify --mission "$mission" --audience verifier || exit 1
|
|
139
145
|
done
|
|
140
146
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
# Skill:
|
|
1
|
+
# Skill: gantry
|
|
2
2
|
|
|
3
|
-
Manifest key `
|
|
3
|
+
Manifest key `gantry`. **gantry** CLI and tests under `src/cli/` per [`.gitagent/foreman/MANIFEST.json`](../.gitagent/foreman/MANIFEST.json).
|
|
4
4
|
|
|
5
5
|
Substrate paths (`.gitagent/`, hooks, workflows) require Teacher legislation and **`substrate`** or explicit mission scope — do not edit Foreman law without a mission.
|