@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.
Files changed (130) hide show
  1. package/README.md +97 -78
  2. package/dist/cli/commands/arch.js +4 -4
  3. package/dist/cli/commands/check-imports.js +1 -1
  4. package/dist/cli/commands/onboarding.js +5 -5
  5. package/dist/cli/commands/perimeter.js +2 -2
  6. package/dist/cli/commands/runtime.js +1 -1
  7. package/dist/cli/commands/scan.js +2 -2
  8. package/dist/cli/commands/teacher.js +1 -1
  9. package/dist/cli/commands/verify.js +3 -3
  10. package/dist/cli/lib/architecture-credential.js +1 -1
  11. package/dist/cli/lib/architecture-pointer.js +1 -1
  12. package/dist/cli/lib/ast-discovery.js +1 -1
  13. package/dist/cli/lib/audience-output.js +7 -7
  14. package/dist/cli/lib/cli-io.js +1 -1
  15. package/dist/cli/lib/config-namespace.d.ts +18 -0
  16. package/dist/cli/lib/config-namespace.d.ts.map +1 -0
  17. package/dist/cli/lib/config-namespace.js +39 -0
  18. package/dist/cli/lib/config-namespace.js.map +1 -0
  19. package/dist/cli/lib/constants.d.ts +6 -5
  20. package/dist/cli/lib/constants.d.ts.map +1 -1
  21. package/dist/cli/lib/constants.js +6 -5
  22. package/dist/cli/lib/constants.js.map +1 -1
  23. package/dist/cli/lib/context-feed-store.d.ts +1 -1
  24. package/dist/cli/lib/context-feed-store.js +1 -1
  25. package/dist/cli/lib/dirty-missions.js +1 -1
  26. package/dist/cli/lib/doctor-core.d.ts.map +1 -1
  27. package/dist/cli/lib/doctor-core.js +8 -7
  28. package/dist/cli/lib/doctor-core.js.map +1 -1
  29. package/dist/cli/lib/doctor-integration.js +1 -1
  30. package/dist/cli/lib/doctor-substrate.js +4 -4
  31. package/dist/cli/lib/errors.d.ts.map +1 -1
  32. package/dist/cli/lib/errors.js +2 -1
  33. package/dist/cli/lib/errors.js.map +1 -1
  34. package/dist/cli/lib/file-merge-gxt.js +2 -2
  35. package/dist/cli/lib/fix-hints.js +9 -9
  36. package/dist/cli/lib/git-metrics.js +1 -1
  37. package/dist/cli/lib/git-proof.d.ts +1 -1
  38. package/dist/cli/lib/git-proof.d.ts.map +1 -1
  39. package/dist/cli/lib/git-proof.js +3 -2
  40. package/dist/cli/lib/git-proof.js.map +1 -1
  41. package/dist/cli/lib/init-interactive.js +2 -2
  42. package/dist/cli/lib/init-plan.js +6 -6
  43. package/dist/cli/lib/init-tutorial.js +10 -10
  44. package/dist/cli/lib/kpi-engine.js +3 -3
  45. package/dist/cli/lib/kpi-scan.js +3 -3
  46. package/dist/cli/lib/legislate.d.ts.map +1 -1
  47. package/dist/cli/lib/legislate.js +11 -5
  48. package/dist/cli/lib/legislate.js.map +1 -1
  49. package/dist/cli/lib/manifest.js +2 -2
  50. package/dist/cli/lib/manifest.js.map +1 -1
  51. package/dist/cli/lib/mcp-governance.d.ts.map +1 -1
  52. package/dist/cli/lib/mcp-governance.js +4 -3
  53. package/dist/cli/lib/mcp-governance.js.map +1 -1
  54. package/dist/cli/lib/mcp-tools-register.js +4 -4
  55. package/dist/cli/lib/missions/formatter.js +1 -1
  56. package/dist/cli/lib/missions/parser.d.ts.map +1 -1
  57. package/dist/cli/lib/missions/parser.js +7 -5
  58. package/dist/cli/lib/missions/parser.js.map +1 -1
  59. package/dist/cli/lib/missions/validator.js +1 -1
  60. package/dist/cli/lib/msn-allocate.js +2 -2
  61. package/dist/cli/lib/onboarding-flow.js +5 -5
  62. package/dist/cli/lib/perimeter.js +1 -1
  63. package/dist/cli/lib/register-proposals.js +1 -1
  64. package/dist/cli/lib/runtime-env.d.ts.map +1 -1
  65. package/dist/cli/lib/runtime-env.js +7 -4
  66. package/dist/cli/lib/runtime-env.js.map +1 -1
  67. package/dist/cli/lib/skill-key.d.ts +12 -0
  68. package/dist/cli/lib/skill-key.d.ts.map +1 -0
  69. package/dist/cli/lib/skill-key.js +28 -0
  70. package/dist/cli/lib/skill-key.js.map +1 -0
  71. package/dist/cli/lib/staged-tmvc-guard.js +1 -1
  72. package/dist/cli/lib/start-orchestration.js +5 -5
  73. package/dist/cli/lib/status-report.js +3 -3
  74. package/dist/cli/lib/substrate-version.js +2 -2
  75. package/dist/cli/lib/surgeons/typescript-workspace.d.ts +1 -1
  76. package/dist/cli/lib/surgeons/typescript-workspace.js +3 -3
  77. package/dist/cli/lib/teacher-identity.d.ts +1 -2
  78. package/dist/cli/lib/teacher-identity.d.ts.map +1 -1
  79. package/dist/cli/lib/teacher-identity.js +11 -15
  80. package/dist/cli/lib/teacher-identity.js.map +1 -1
  81. package/dist/cli/lib/tmvc-path.d.ts.map +1 -1
  82. package/dist/cli/lib/tmvc-path.js +5 -2
  83. package/dist/cli/lib/tmvc-path.js.map +1 -1
  84. package/dist/cli/lib/types.d.ts +1 -1
  85. package/dist/cli/lib/upgrade-apply.js +5 -5
  86. package/dist/cli/lib/upgrade-plan.d.ts +1 -1
  87. package/dist/cli/lib/upgrade-plan.js +4 -4
  88. package/dist/cli/lib/verify-failure-normalize.js +1 -1
  89. package/dist/cli/lib/verify-hints.js +9 -9
  90. package/dist/cli/lib/verify-run.d.ts.map +1 -1
  91. package/dist/cli/lib/verify-run.js +3 -2
  92. package/dist/cli/lib/verify-run.js.map +1 -1
  93. package/dist/cli/lib/version.gen.d.ts +1 -1
  94. package/dist/cli/lib/version.gen.js +1 -1
  95. package/dist/cli/program-core.js +2 -2
  96. package/package.json +9 -4
  97. package/templates/.cursor/hooks/gxt-before-shell.sh +2 -2
  98. package/templates/.cursor/hooks/gxt-session-start.sh +5 -5
  99. package/templates/.cursor/mcp.json +1 -1
  100. package/templates/.cursor/rules/opengantry-gxt-substrate.mdc +4 -4
  101. package/templates/.gitagent/config.json +15 -0
  102. package/templates/.gitagent/foreman/MANIFEST.json +2 -2
  103. package/templates/.gitagent/foreman/SUBSTRATE.version.json +1 -1
  104. package/templates/.gitagent/foreman/TEACHER.allowlist.example +1 -1
  105. package/templates/.gitagent/missions/README.md +3 -3
  106. package/templates/.gitagent/teacher/ARCHITECTURE-ACCESS.md +8 -8
  107. package/templates/.gitagent/teacher/KPI-REPORT.schema.yaml +1 -1
  108. package/templates/.gitagent/teacher/MISSION-ARCHITECT.md +5 -5
  109. package/templates/.gitagent/teacher/MISSION.schema.yaml +2 -2
  110. package/templates/.gitagent/teacher/RULES.md +2 -2
  111. package/templates/.githooks/pre-commit +4 -4
  112. package/templates/.githooks/pre-push +6 -6
  113. package/templates/.github/workflows/gxt-validate.yml +14 -13
  114. package/templates/AGENTS.md +1 -1
  115. package/templates/integrations/asset-catalog.json +2 -1
  116. package/templates/integrations/compatibility.json +1 -1
  117. package/templates/integrations/recipes/_footer.md +6 -6
  118. package/templates/integrations/recipes/_preamble.md +9 -9
  119. package/templates/integrations/recipes/aider.md +1 -1
  120. package/templates/integrations/recipes/antigravity.md +1 -1
  121. package/templates/integrations/recipes/claude-code.md +1 -1
  122. package/templates/integrations/recipes/codex-cli.md +2 -2
  123. package/templates/integrations/recipes/cursor.md +3 -3
  124. package/templates/scripts/dev-validate.sh +1 -1
  125. package/templates/scripts/gxt-manifest-lib.mjs +418 -3
  126. package/templates/scripts/gxt-resolve-mission.sh +2 -2
  127. package/templates/scripts/gxt-runtime-env.sh +4 -4
  128. package/templates/scripts/validate-gxt.sh +5 -1
  129. package/templates/scripts/verify-pr-missions.sh +12 -6
  130. 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` — `gapman mcp serve`.
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: `gapman verify --mission … --fix`.
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: `gapman runtime exec --mission .gitagent/missions/MSN-0001.<slug>.yaml -- cursor agent "<task>"`
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 + gapman stack before push/PR.
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 dependency).
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.replace(/\\/g, "/"));
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> [repoRoot]",
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
  }
@@ -14,8 +14,8 @@ if [ -n "$explicit" ]; then
14
14
  exit 0
15
15
  fi
16
16
 
17
- if [ -n "${GAPMAN_MISSION:-}" ]; then
18
- printf '%s\n' "$GAPMAN_MISSION"
17
+ if [ -n "${GANTRY_MISSION:-}" ]; then
18
+ printf '%s\n' "$GANTRY_MISSION"
19
19
  exit 0
20
20
  fi
21
21
 
@@ -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 GAPMAN_MISSION=.gitagent/missions/<file>.yaml
18
+ export GANTRY_MISSION=.gitagent/missions/<file>.yaml
19
19
  EOF
20
20
  exit 1
21
21
  fi
22
22
 
23
- if command -v gapman >/dev/null 2>&1; then
24
- eval "$(gapman runtime env --mission "$MISSION")"
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: gapman not found (npm ci && npm run build)" >&2
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 gapman verify --break-glass --reason \"...\" with GXT_BYPASS_SECRET, push refs/notes/gxt-bypass" >&2
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 gapman verify on changed missions.
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 gapman >/dev/null 2>&1; then
61
- GAPMAN=(gapman)
60
+ elif command -v gantry >/dev/null 2>&1; then
61
+ GAPMAN=(gantry)
62
62
  else
63
- echo "verify-pr-missions: build gapman first (npm run build)" >&2
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: gapman legislate \"<intent>\" --msn MSN-NNNN --skill-key gapman && include mission YAML in this PR" >&2
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: gapman verify --mission ${mission}" >&2
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: gapman
1
+ # Skill: gantry
2
2
 
3
- Manifest key `gapman`. **gapman** CLI and tests under `src/cli/` per [`.gitagent/foreman/MANIFEST.json`](../.gitagent/foreman/MANIFEST.json).
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.