@attalabs/vinaya 0.8.0 → 0.8.2
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/aeg-root/enforcement.md +2 -2
- package/aeg-root/roles/planner.md +2 -2
- package/aeg-root/roles/reviewer.md +1 -1
- package/aeg-root/skills/brief-authoring/SKILL.md +1 -1
- package/aeg-root/state-machine.md +6 -6
- package/aeg-root/tranche-model.md +1 -1
- package/dist/checks/bin/check-branch-topology.js +21 -5
- package/dist/checks/bin/check-brief-shape.js +21 -5
- package/dist/checks/bin/check-closes-n.js +21 -5
- package/dist/checks/bin/check-coherence.js +200 -10
- package/dist/checks/bin/check-dead-branch-push.js +21 -5
- package/dist/checks/bin/check-dispatch-readiness.js +223 -11
- package/dist/checks/bin/check-doc-coverage-push.js +21 -5
- package/dist/checks/bin/check-doc-coverage.js +55 -10
- package/dist/checks/bin/check-first-push-dispatch.js +224 -12
- package/dist/checks/bin/check-issue-assignment.js +21 -5
- package/dist/checks/bin/check-no-disk-state.js +21 -5
- package/dist/checks/bin/check-reader-resolvable-prose.js +21 -5
- package/dist/checks/bin/check-registry-gates.js +21 -5
- package/dist/checks/bin/check-review-gate.js +21 -5
- package/dist/checks/bin/check-single-plan-pr.js +21 -5
- package/dist/checks/bin/check-test-plan.js +21 -5
- package/dist/index.js +6 -2
- package/package.json +4 -4
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/checks/bin/check-coherence.ts
|
|
4
|
-
import { execFile as execFile4, execFileSync as
|
|
4
|
+
import { execFile as execFile4, execFileSync as execFileSync3 } from "node:child_process";
|
|
5
5
|
import { existsSync, readFileSync } from "node:fs";
|
|
6
|
-
import { join } from "node:path";
|
|
6
|
+
import { join as join2 } from "node:path";
|
|
7
7
|
import { promisify as promisify4 } from "node:util";
|
|
8
8
|
// ../../packages/aeg-core/src/anchored-region.ts
|
|
9
9
|
function maskCode(body) {
|
|
@@ -1221,7 +1221,8 @@ function mapForgeFacts(raw) {
|
|
|
1221
1221
|
reviewDecision: mapReviewDecision(raw.pullRequest?.reviewDecision),
|
|
1222
1222
|
stateReason: mapStateReason(raw.issue.stateReason),
|
|
1223
1223
|
closedAt: raw.issue.closedAt ?? null,
|
|
1224
|
-
mergedAt: raw.pullRequest?.mergedAt ?? null
|
|
1224
|
+
mergedAt: raw.pullRequest?.mergedAt ?? null,
|
|
1225
|
+
closedByActor: raw.closedByActor ?? null
|
|
1225
1226
|
};
|
|
1226
1227
|
}
|
|
1227
1228
|
function mapStateReason(reason) {
|
|
@@ -1314,6 +1315,7 @@ function buildBatchQuery(tranche, tasks) {
|
|
|
1314
1315
|
timelineItems(last: 1, itemTypes: [CLOSED_EVENT]) {
|
|
1315
1316
|
nodes {
|
|
1316
1317
|
... on ClosedEvent {
|
|
1318
|
+
actor { login }
|
|
1317
1319
|
closer {
|
|
1318
1320
|
... on PullRequest {
|
|
1319
1321
|
number
|
|
@@ -1351,7 +1353,8 @@ function extractRawFromResponse(repository, alias) {
|
|
|
1351
1353
|
const issue = repository[`${alias}_issue`];
|
|
1352
1354
|
const ref = repository[`${alias}_ref`];
|
|
1353
1355
|
const prs = repository[`${alias}_prs`];
|
|
1354
|
-
const
|
|
1356
|
+
const timelineNode = issue?.timelineItems?.nodes?.[0];
|
|
1357
|
+
const rawCloser = timelineNode?.closer ?? null;
|
|
1355
1358
|
const closingPr = rawCloser && typeof rawCloser.number === "number" && typeof rawCloser.url === "string" ? rawCloser : null;
|
|
1356
1359
|
const branchPr = prs && prs.nodes.length > 0 && prs.nodes[0] ? prs.nodes[0] : null;
|
|
1357
1360
|
return {
|
|
@@ -1363,7 +1366,8 @@ function extractRawFromResponse(repository, alias) {
|
|
|
1363
1366
|
labels: issue.labels.nodes.map((n) => n.name)
|
|
1364
1367
|
} : null,
|
|
1365
1368
|
refExists: Boolean(ref && ref.name.length > 0),
|
|
1366
|
-
pullRequest: closingPr ?? branchPr
|
|
1369
|
+
pullRequest: closingPr ?? branchPr,
|
|
1370
|
+
closedByActor: timelineNode?.actor?.login ?? null
|
|
1367
1371
|
};
|
|
1368
1372
|
}
|
|
1369
1373
|
function describeError(err) {
|
|
@@ -1501,6 +1505,11 @@ var FORGE_FACT_INPUTS = [
|
|
|
1501
1505
|
fact: "mergedAt",
|
|
1502
1506
|
readsFrom: "PullRequest.mergedAt",
|
|
1503
1507
|
meaning: "Timestamp for the coherence oracle grandfather cutoff. No derivation rule reads it."
|
|
1508
|
+
},
|
|
1509
|
+
{
|
|
1510
|
+
fact: "closedByActor",
|
|
1511
|
+
readsFrom: "ClosedEvent.actor.login (most recent timeline CLOSED_EVENT)",
|
|
1512
|
+
meaning: "GitHub login that hand-closed the Issue. No task-status derivation rule reads it — it is consumed by dispatch-gate.ts/coherence-checks.ts A1 to recognize a hand-closed dependency as resolved (task vinaya-engine-v1 21, #99), not by this status model."
|
|
1504
1513
|
}
|
|
1505
1514
|
];
|
|
1506
1515
|
var DERIVED_STATUSES = [
|
|
@@ -1980,7 +1989,7 @@ function isGrandfathered(isoDate) {
|
|
|
1980
1989
|
return isoDate.slice(0, 10) < COHERENCE_ENFORCED_FROM;
|
|
1981
1990
|
}
|
|
1982
1991
|
var R1_GRANDFATHERED_ISSUES = new Set([279, 280, 281, 282]);
|
|
1983
|
-
function checkA1(entries) {
|
|
1992
|
+
function checkA1(entries, principalAllowlist = PRINCIPAL_ALLOWLIST) {
|
|
1984
1993
|
const failures = [];
|
|
1985
1994
|
for (const e of entries) {
|
|
1986
1995
|
if (!e.facts)
|
|
@@ -1988,6 +1997,9 @@ function checkA1(entries) {
|
|
|
1988
1997
|
if (e.facts.stateReason === "not_planned")
|
|
1989
1998
|
continue;
|
|
1990
1999
|
if (e.facts.issueState === "closed" && e.facts.prState !== "merged") {
|
|
2000
|
+
const handClosed = e.facts.stateReason === "completed" && isPrincipal(e.facts.closedByActor, principalAllowlist);
|
|
2001
|
+
if (handClosed)
|
|
2002
|
+
continue;
|
|
1991
2003
|
failures.push({
|
|
1992
2004
|
issue: e.task.issue,
|
|
1993
2005
|
tranche: e.trancheSlug,
|
|
@@ -2677,9 +2689,13 @@ function parseGlossaryTerms(glossaryContent) {
|
|
|
2677
2689
|
return terms;
|
|
2678
2690
|
}
|
|
2679
2691
|
// ../../packages/aeg-core/src/dispatch-gate.ts
|
|
2692
|
+
function isHandClosedByRecognizedPrincipal(dep, principalAllowlist) {
|
|
2693
|
+
return dep.issueState === "closed" && dep.stateReason === "completed" && isPrincipal(dep.closedByActor ?? null, principalAllowlist);
|
|
2694
|
+
}
|
|
2680
2695
|
function checkDispatchReadiness(input) {
|
|
2681
2696
|
const { trancheSlug, task } = input;
|
|
2682
2697
|
const taskLabel = `task ${task.id} (tranche ${trancheSlug})`;
|
|
2698
|
+
const principalAllowlist = input.principalAllowlist ?? PRINCIPAL_ALLOWLIST;
|
|
2683
2699
|
const blockers = [];
|
|
2684
2700
|
if (task.issue === null) {
|
|
2685
2701
|
blockers.push(`dispatch-gate issue-existence: ${taskLabel} has no Issue (#TBD or blank) in the topology — not dispatchable until the Planner cuts the Issue.`);
|
|
@@ -2690,7 +2706,7 @@ function checkDispatchReadiness(input) {
|
|
|
2690
2706
|
blockers.push(`dispatch-gate rationale: Issue #${input.issue.number} for ${taskLabel} fails the rationale gate (checkIssueRationale) — the Planner must complete the eight-field rationale before this task is dispatchable.`);
|
|
2691
2707
|
}
|
|
2692
2708
|
for (const dep of input.dependsOn) {
|
|
2693
|
-
if (!dep.merged) {
|
|
2709
|
+
if (!dep.merged && !isHandClosedByRecognizedPrincipal(dep, principalAllowlist)) {
|
|
2694
2710
|
const issueStr = dep.issue !== null ? ` (#${dep.issue})` : "";
|
|
2695
2711
|
blockers.push(`dispatch-gate depends-on: ${taskLabel} depends on ${dep.id}${issueStr}, whose PR is not merged yet — not dispatchable, it serializes behind it.`);
|
|
2696
2712
|
}
|
|
@@ -2946,12 +2962,186 @@ function emitCheckError(error) {
|
|
|
2946
2962
|
`);
|
|
2947
2963
|
}
|
|
2948
2964
|
|
|
2965
|
+
// src/lib/config.ts
|
|
2966
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
2967
|
+
import { homedir } from "node:os";
|
|
2968
|
+
import { dirname, join } from "node:path";
|
|
2969
|
+
import { z as z2 } from "zod";
|
|
2970
|
+
var EnvEntrySchema = z2.union([
|
|
2971
|
+
z2.literal(true),
|
|
2972
|
+
z2.object({ optional: z2.literal(true) }),
|
|
2973
|
+
z2.object({ anyOf: z2.array(z2.string()).min(2) }),
|
|
2974
|
+
z2.string()
|
|
2975
|
+
]);
|
|
2976
|
+
var CheckEntrySchema = z2.object({
|
|
2977
|
+
run: z2.string(),
|
|
2978
|
+
scope: z2.enum(["diff", "full"]),
|
|
2979
|
+
include: z2.array(z2.string()).optional(),
|
|
2980
|
+
args: z2.array(z2.string()).optional(),
|
|
2981
|
+
timeoutMs: z2.number().optional(),
|
|
2982
|
+
env: z2.record(z2.string(), EnvEntrySchema).optional(),
|
|
2983
|
+
requiresOpenPr: z2.boolean().optional(),
|
|
2984
|
+
ownWorkflow: z2.boolean().optional()
|
|
2985
|
+
}).superRefine((entry2, ctx) => {
|
|
2986
|
+
if (!entry2.env)
|
|
2987
|
+
return;
|
|
2988
|
+
for (const [key, value] of Object.entries(entry2.env)) {
|
|
2989
|
+
if (typeof value !== "object" || value === null || !("anyOf" in value))
|
|
2990
|
+
continue;
|
|
2991
|
+
const members = value.anyOf;
|
|
2992
|
+
if (new Set(members).size !== members.length) {
|
|
2993
|
+
ctx.addIssue({
|
|
2994
|
+
code: z2.ZodIssueCode.custom,
|
|
2995
|
+
message: `env.${key}.anyOf has duplicate members`,
|
|
2996
|
+
path: ["env", key, "anyOf"]
|
|
2997
|
+
});
|
|
2998
|
+
}
|
|
2999
|
+
if (!members.includes(key)) {
|
|
3000
|
+
ctx.addIssue({
|
|
3001
|
+
code: z2.ZodIssueCode.custom,
|
|
3002
|
+
message: `env.${key}.anyOf must include "${key}" itself as a member`,
|
|
3003
|
+
path: ["env", key, "anyOf"]
|
|
3004
|
+
});
|
|
3005
|
+
}
|
|
3006
|
+
}
|
|
3007
|
+
});
|
|
3008
|
+
var BRIEF_BUILTINS = [
|
|
3009
|
+
"tier",
|
|
3010
|
+
"testPlan",
|
|
3011
|
+
"testPlanExclusivity",
|
|
3012
|
+
"principalPlaceholder",
|
|
3013
|
+
"surfaceMap",
|
|
3014
|
+
"docUpdateList",
|
|
3015
|
+
"worktreeStep0",
|
|
3016
|
+
"stopConditions",
|
|
3017
|
+
"autonomyClause",
|
|
3018
|
+
"project",
|
|
3019
|
+
"for",
|
|
3020
|
+
"closesN",
|
|
3021
|
+
"premiseCoverage",
|
|
3022
|
+
"issueRationale"
|
|
3023
|
+
];
|
|
3024
|
+
var BriefSectionSchema = z2.union([
|
|
3025
|
+
z2.object({ builtin: z2.enum(BRIEF_BUILTINS) }),
|
|
3026
|
+
z2.object({ heading: z2.string().min(1), name: z2.string().optional() }),
|
|
3027
|
+
z2.object({ field: z2.string().min(1), name: z2.string().optional() }),
|
|
3028
|
+
z2.object({ phrase: z2.string().min(1), name: z2.string().optional() })
|
|
3029
|
+
]);
|
|
3030
|
+
var BriefSchemaSchema = z2.object({
|
|
3031
|
+
pr: z2.object({ sections: z2.array(BriefSectionSchema) }).optional(),
|
|
3032
|
+
issue: z2.object({ sections: z2.array(BriefSectionSchema) }).optional()
|
|
3033
|
+
});
|
|
3034
|
+
function isSafeRepoRelPath(p) {
|
|
3035
|
+
if (p.length === 0)
|
|
3036
|
+
return false;
|
|
3037
|
+
if (p.startsWith("/") || /^[a-zA-Z]:[\\/]/.test(p))
|
|
3038
|
+
return false;
|
|
3039
|
+
return p.split(/[\\/]/).every((seg) => seg !== ".." && seg !== "");
|
|
3040
|
+
}
|
|
3041
|
+
var SafeRepoRelPath = z2.string().refine(isSafeRepoRelPath, {
|
|
3042
|
+
message: "must be a repo-root-relative path with no `..` segment or absolute root"
|
|
3043
|
+
});
|
|
3044
|
+
var ManagedBlockRecordSchema = z2.object({
|
|
3045
|
+
path: SafeRepoRelPath,
|
|
3046
|
+
marker: z2.string(),
|
|
3047
|
+
comment: z2.enum(["hash", "html"])
|
|
3048
|
+
});
|
|
3049
|
+
var ManagedManifestSchema = z2.object({
|
|
3050
|
+
version: z2.number().int().positive(),
|
|
3051
|
+
files: z2.array(SafeRepoRelPath),
|
|
3052
|
+
blocks: z2.array(ManagedBlockRecordSchema),
|
|
3053
|
+
labels: z2.array(z2.string())
|
|
3054
|
+
});
|
|
3055
|
+
var VinayaConfigSchema = z2.object({
|
|
3056
|
+
rings: z2.object({
|
|
3057
|
+
ring1_forgeWriteInterception: z2.boolean(),
|
|
3058
|
+
ring2_asyncAudits: z2.boolean()
|
|
3059
|
+
}).optional(),
|
|
3060
|
+
checks: z2.record(z2.string(), CheckEntrySchema).optional(),
|
|
3061
|
+
briefSchema: BriefSchemaSchema.optional(),
|
|
3062
|
+
managed: ManagedManifestSchema.optional(),
|
|
3063
|
+
principals: z2.array(z2.string()).min(1).optional(),
|
|
3064
|
+
ci: z2.object({ setup: z2.string().min(1) }).optional()
|
|
3065
|
+
});
|
|
3066
|
+
var GLOBAL_VINAYA_HOME = join(homedir(), ".vinaya");
|
|
3067
|
+
var GLOBAL_CONFIG_PATH = join(GLOBAL_VINAYA_HOME, "config.json");
|
|
3068
|
+
var LOCAL_CONFIG_FILENAME = "vinaya.config.json";
|
|
3069
|
+
function resolvePrincipalAllowlist(config) {
|
|
3070
|
+
return config?.principals ?? PRINCIPAL_ALLOWLIST;
|
|
3071
|
+
}
|
|
3072
|
+
function trustAnchorRepo() {
|
|
3073
|
+
const wellFormed = (slug) => /^[^/\s]+\/[^/\s]+$/.test(slug) ? slug : null;
|
|
3074
|
+
const fromRunner = process.env.GITHUB_REPOSITORY?.trim();
|
|
3075
|
+
if (fromRunner) {
|
|
3076
|
+
const validated = wellFormed(fromRunner);
|
|
3077
|
+
if (validated)
|
|
3078
|
+
return validated;
|
|
3079
|
+
}
|
|
3080
|
+
try {
|
|
3081
|
+
const url = execFileSync2("git", ["remote", "get-url", "origin"], {
|
|
3082
|
+
encoding: "utf-8",
|
|
3083
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
3084
|
+
}).trim();
|
|
3085
|
+
const m = url.match(/github\.com[:/]([^/]+)\/(.+?)(?:\.git)?\/?$/);
|
|
3086
|
+
return m?.[1] && m[2] ? wellFormed(`${m[1]}/${m[2]}`) : null;
|
|
3087
|
+
} catch {
|
|
3088
|
+
return null;
|
|
3089
|
+
}
|
|
3090
|
+
}
|
|
3091
|
+
function ghFetchTrustAnchorConfig() {
|
|
3092
|
+
const repo = trustAnchorRepo();
|
|
3093
|
+
if (!repo)
|
|
3094
|
+
throw new Error("could not resolve repo identity for the trust-anchor read");
|
|
3095
|
+
return execFileSync2("gh", ["api", `repos/${repo}/contents/${LOCAL_CONFIG_FILENAME}`, "--jq", ".content"], {
|
|
3096
|
+
encoding: "utf-8",
|
|
3097
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
3098
|
+
});
|
|
3099
|
+
}
|
|
3100
|
+
function firstLine(err) {
|
|
3101
|
+
return err?.message?.split(`
|
|
3102
|
+
`)[0] ?? "unknown error";
|
|
3103
|
+
}
|
|
3104
|
+
function isMissingFileError(err) {
|
|
3105
|
+
const stderr = err?.stderr;
|
|
3106
|
+
const haystack = [
|
|
3107
|
+
err?.message ?? "",
|
|
3108
|
+
typeof stderr === "string" ? stderr : stderr?.toString() ?? ""
|
|
3109
|
+
].join(`
|
|
3110
|
+
`);
|
|
3111
|
+
return /\b404\b|not found/i.test(haystack);
|
|
3112
|
+
}
|
|
3113
|
+
function loadTrustAnchorConfig(fetcher = ghFetchTrustAnchorConfig) {
|
|
3114
|
+
const warn = (why) => process.stdout.write(`⚠ could not read \`principals\` from the default branch (${why}) — falling back to vinaya's built-in principal allowlist.
|
|
3115
|
+
`);
|
|
3116
|
+
let base64;
|
|
3117
|
+
try {
|
|
3118
|
+
base64 = fetcher().trim();
|
|
3119
|
+
} catch (err) {
|
|
3120
|
+
if (!isMissingFileError(err))
|
|
3121
|
+
warn(firstLine(err));
|
|
3122
|
+
return null;
|
|
3123
|
+
}
|
|
3124
|
+
if (!base64)
|
|
3125
|
+
return null;
|
|
3126
|
+
try {
|
|
3127
|
+
const raw = Buffer.from(base64, "base64").toString("utf-8");
|
|
3128
|
+
const parsed = VinayaConfigSchema.safeParse(JSON.parse(raw));
|
|
3129
|
+
if (parsed.success)
|
|
3130
|
+
return parsed.data;
|
|
3131
|
+
warn("it did not satisfy the config schema");
|
|
3132
|
+
return null;
|
|
3133
|
+
} catch {
|
|
3134
|
+
warn("it was not readable as JSON");
|
|
3135
|
+
return null;
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
|
|
2949
3139
|
// src/checks/bin/check-coherence.ts
|
|
2950
3140
|
var CHECK_NAME = "coherence";
|
|
2951
3141
|
var execFileAsync4 = promisify4(execFile4);
|
|
2952
3142
|
function git(args) {
|
|
2953
3143
|
try {
|
|
2954
|
-
return
|
|
3144
|
+
return execFileSync3("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
2955
3145
|
} catch {
|
|
2956
3146
|
return "";
|
|
2957
3147
|
}
|
|
@@ -2960,7 +3150,7 @@ function readRegisteredProjectNames() {
|
|
|
2960
3150
|
const root = git(["rev-parse", "--show-toplevel"]);
|
|
2961
3151
|
if (!root)
|
|
2962
3152
|
return [];
|
|
2963
|
-
const path =
|
|
3153
|
+
const path = join2(root, ".vinaya/projects.md");
|
|
2964
3154
|
if (!existsSync(path))
|
|
2965
3155
|
return [];
|
|
2966
3156
|
return parseRegistry(readFileSync(path, "utf8")).map((p) => p.name);
|
|
@@ -3086,7 +3276,7 @@ async function main() {
|
|
|
3086
3276
|
task: t,
|
|
3087
3277
|
facts: snapshot.facts.get(t.id)
|
|
3088
3278
|
}));
|
|
3089
|
-
results.push(checkA1(enrichedEntries));
|
|
3279
|
+
results.push(checkA1(enrichedEntries, resolvePrincipalAllowlist(loadTrustAnchorConfig())));
|
|
3090
3280
|
results.push(checkA3(enrichedEntries));
|
|
3091
3281
|
results.push(checkT1(enrichedEntries));
|
|
3092
3282
|
results.push(checkD1(enrichedEntries, issueToEntry(enrichedEntries), taskToEntry(enrichedEntries, slug)));
|
|
@@ -1218,7 +1218,8 @@ function mapForgeFacts(raw) {
|
|
|
1218
1218
|
reviewDecision: mapReviewDecision(raw.pullRequest?.reviewDecision),
|
|
1219
1219
|
stateReason: mapStateReason(raw.issue.stateReason),
|
|
1220
1220
|
closedAt: raw.issue.closedAt ?? null,
|
|
1221
|
-
mergedAt: raw.pullRequest?.mergedAt ?? null
|
|
1221
|
+
mergedAt: raw.pullRequest?.mergedAt ?? null,
|
|
1222
|
+
closedByActor: raw.closedByActor ?? null
|
|
1222
1223
|
};
|
|
1223
1224
|
}
|
|
1224
1225
|
function mapStateReason(reason) {
|
|
@@ -1311,6 +1312,7 @@ function buildBatchQuery(tranche, tasks) {
|
|
|
1311
1312
|
timelineItems(last: 1, itemTypes: [CLOSED_EVENT]) {
|
|
1312
1313
|
nodes {
|
|
1313
1314
|
... on ClosedEvent {
|
|
1315
|
+
actor { login }
|
|
1314
1316
|
closer {
|
|
1315
1317
|
... on PullRequest {
|
|
1316
1318
|
number
|
|
@@ -1348,7 +1350,8 @@ function extractRawFromResponse(repository, alias) {
|
|
|
1348
1350
|
const issue = repository[`${alias}_issue`];
|
|
1349
1351
|
const ref = repository[`${alias}_ref`];
|
|
1350
1352
|
const prs = repository[`${alias}_prs`];
|
|
1351
|
-
const
|
|
1353
|
+
const timelineNode = issue?.timelineItems?.nodes?.[0];
|
|
1354
|
+
const rawCloser = timelineNode?.closer ?? null;
|
|
1352
1355
|
const closingPr = rawCloser && typeof rawCloser.number === "number" && typeof rawCloser.url === "string" ? rawCloser : null;
|
|
1353
1356
|
const branchPr = prs && prs.nodes.length > 0 && prs.nodes[0] ? prs.nodes[0] : null;
|
|
1354
1357
|
return {
|
|
@@ -1360,7 +1363,8 @@ function extractRawFromResponse(repository, alias) {
|
|
|
1360
1363
|
labels: issue.labels.nodes.map((n) => n.name)
|
|
1361
1364
|
} : null,
|
|
1362
1365
|
refExists: Boolean(ref && ref.name.length > 0),
|
|
1363
|
-
pullRequest: closingPr ?? branchPr
|
|
1366
|
+
pullRequest: closingPr ?? branchPr,
|
|
1367
|
+
closedByActor: timelineNode?.actor?.login ?? null
|
|
1364
1368
|
};
|
|
1365
1369
|
}
|
|
1366
1370
|
function describeError(err) {
|
|
@@ -1498,6 +1502,11 @@ var FORGE_FACT_INPUTS = [
|
|
|
1498
1502
|
fact: "mergedAt",
|
|
1499
1503
|
readsFrom: "PullRequest.mergedAt",
|
|
1500
1504
|
meaning: "Timestamp for the coherence oracle grandfather cutoff. No derivation rule reads it."
|
|
1505
|
+
},
|
|
1506
|
+
{
|
|
1507
|
+
fact: "closedByActor",
|
|
1508
|
+
readsFrom: "ClosedEvent.actor.login (most recent timeline CLOSED_EVENT)",
|
|
1509
|
+
meaning: "GitHub login that hand-closed the Issue. No task-status derivation rule reads it — it is consumed by dispatch-gate.ts/coherence-checks.ts A1 to recognize a hand-closed dependency as resolved (task vinaya-engine-v1 21, #99), not by this status model."
|
|
1501
1510
|
}
|
|
1502
1511
|
];
|
|
1503
1512
|
var DERIVED_STATUSES = [
|
|
@@ -1977,7 +1986,7 @@ function isGrandfathered(isoDate) {
|
|
|
1977
1986
|
return isoDate.slice(0, 10) < COHERENCE_ENFORCED_FROM;
|
|
1978
1987
|
}
|
|
1979
1988
|
var R1_GRANDFATHERED_ISSUES = new Set([279, 280, 281, 282]);
|
|
1980
|
-
function checkA1(entries) {
|
|
1989
|
+
function checkA1(entries, principalAllowlist = PRINCIPAL_ALLOWLIST) {
|
|
1981
1990
|
const failures = [];
|
|
1982
1991
|
for (const e of entries) {
|
|
1983
1992
|
if (!e.facts)
|
|
@@ -1985,6 +1994,9 @@ function checkA1(entries) {
|
|
|
1985
1994
|
if (e.facts.stateReason === "not_planned")
|
|
1986
1995
|
continue;
|
|
1987
1996
|
if (e.facts.issueState === "closed" && e.facts.prState !== "merged") {
|
|
1997
|
+
const handClosed = e.facts.stateReason === "completed" && isPrincipal(e.facts.closedByActor, principalAllowlist);
|
|
1998
|
+
if (handClosed)
|
|
1999
|
+
continue;
|
|
1988
2000
|
failures.push({
|
|
1989
2001
|
issue: e.task.issue,
|
|
1990
2002
|
tranche: e.trancheSlug,
|
|
@@ -2674,9 +2686,13 @@ function parseGlossaryTerms(glossaryContent) {
|
|
|
2674
2686
|
return terms;
|
|
2675
2687
|
}
|
|
2676
2688
|
// ../../packages/aeg-core/src/dispatch-gate.ts
|
|
2689
|
+
function isHandClosedByRecognizedPrincipal(dep, principalAllowlist) {
|
|
2690
|
+
return dep.issueState === "closed" && dep.stateReason === "completed" && isPrincipal(dep.closedByActor ?? null, principalAllowlist);
|
|
2691
|
+
}
|
|
2677
2692
|
function checkDispatchReadiness(input) {
|
|
2678
2693
|
const { trancheSlug, task } = input;
|
|
2679
2694
|
const taskLabel = `task ${task.id} (tranche ${trancheSlug})`;
|
|
2695
|
+
const principalAllowlist = input.principalAllowlist ?? PRINCIPAL_ALLOWLIST;
|
|
2680
2696
|
const blockers = [];
|
|
2681
2697
|
if (task.issue === null) {
|
|
2682
2698
|
blockers.push(`dispatch-gate issue-existence: ${taskLabel} has no Issue (#TBD or blank) in the topology — not dispatchable until the Planner cuts the Issue.`);
|
|
@@ -2687,7 +2703,7 @@ function checkDispatchReadiness(input) {
|
|
|
2687
2703
|
blockers.push(`dispatch-gate rationale: Issue #${input.issue.number} for ${taskLabel} fails the rationale gate (checkIssueRationale) — the Planner must complete the eight-field rationale before this task is dispatchable.`);
|
|
2688
2704
|
}
|
|
2689
2705
|
for (const dep of input.dependsOn) {
|
|
2690
|
-
if (!dep.merged) {
|
|
2706
|
+
if (!dep.merged && !isHandClosedByRecognizedPrincipal(dep, principalAllowlist)) {
|
|
2691
2707
|
const issueStr = dep.issue !== null ? ` (#${dep.issue})` : "";
|
|
2692
2708
|
blockers.push(`dispatch-gate depends-on: ${taskLabel} depends on ${dep.id}${issueStr}, whose PR is not merged yet — not dispatchable, it serializes behind it.`);
|
|
2693
2709
|
}
|