@deftai/directive-core 0.105.0 → 0.107.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/dist/authz/classify.js +591 -34
- package/dist/check/gate-lists.js +2 -0
- package/dist/check/named-cause.js +1 -1
- package/dist/design-critique/exclusive-chip.d.ts +33 -0
- package/dist/design-critique/exclusive-chip.js +68 -0
- package/dist/design-critique/parent-audit.d.ts +59 -0
- package/dist/design-critique/parent-audit.js +121 -0
- package/dist/doctor/checks.d.ts +6 -0
- package/dist/doctor/checks.js +35 -0
- package/dist/forward-coverage/diff-coverage.d.ts +39 -0
- package/dist/forward-coverage/diff-coverage.js +112 -0
- package/dist/forward-coverage/diff-lines.d.ts +19 -0
- package/dist/forward-coverage/diff-lines.js +173 -0
- package/dist/forward-coverage/evaluate.d.ts +24 -9
- package/dist/forward-coverage/evaluate.js +125 -19
- package/dist/hooks/dest-form.d.ts +46 -0
- package/dist/hooks/dest-form.js +575 -0
- package/dist/hooks/dispatcher.d.ts +17 -1
- package/dist/hooks/dispatcher.js +82 -11
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +1 -0
- package/dist/init-deposit/agent-hooks.d.ts +1 -1
- package/dist/init-deposit/agent-hooks.js +1 -1
- package/dist/intake/clause-derivation.d.ts +9 -0
- package/dist/intake/clause-derivation.js +30 -3
- package/dist/intake/github-auth-modes-cli.js +14 -2
- package/dist/intake/github-auth-modes.d.ts +54 -9
- package/dist/intake/github-auth-modes.js +326 -123
- package/dist/lifecycle/completed-tracked-on-delivery.d.ts +26 -0
- package/dist/lifecycle/completed-tracked-on-delivery.js +37 -14
- package/dist/lifecycle/completed-write-guard.d.ts +48 -0
- package/dist/lifecycle/completed-write-guard.js +373 -0
- package/dist/lifecycle/index.d.ts +1 -0
- package/dist/lifecycle/index.js +1 -0
- package/dist/lifecycle-visible/evaluate.d.ts +98 -0
- package/dist/lifecycle-visible/evaluate.js +710 -0
- package/dist/lifecycle-visible/index.d.ts +2 -0
- package/dist/lifecycle-visible/index.js +2 -0
- package/dist/literal-acceptance/capture.d.ts +16 -0
- package/dist/literal-acceptance/capture.js +78 -10
- package/dist/literal-acceptance/evaluate.js +25 -3
- package/dist/literal-acceptance/index.d.ts +2 -2
- package/dist/literal-acceptance/index.js +2 -2
- package/dist/literal-acceptance/run.js +9 -3
- package/dist/literal-acceptance/types.d.ts +6 -0
- package/dist/literal-acceptance/types.js +6 -0
- package/dist/orchestration/judgment-policy.d.ts +13 -0
- package/dist/orchestration/judgment-policy.js +15 -0
- package/dist/orchestration/probe-session.js +13 -8
- package/dist/orchestration/verify-judgment-gates.d.ts +4 -0
- package/dist/orchestration/verify-judgment-gates.js +43 -11
- package/dist/policy/ceremony-dial.js +7 -7
- package/dist/policy/host-hooks.d.ts +20 -0
- package/dist/policy/host-hooks.js +121 -2
- package/dist/policy/org-force-on-migration.js +2 -1
- package/dist/policy/plan-extensions.d.ts +1 -1
- package/dist/policy/plan-extensions.js +2 -0
- package/dist/policy/product-signal.js +6 -7
- package/dist/policy/require-human-merge.js +8 -6
- package/dist/policy/resolve.d.ts +7 -2
- package/dist/policy/resolve.js +25 -8
- package/dist/policy/runtime-authority.d.ts +18 -0
- package/dist/policy/runtime-authority.js +11 -0
- package/dist/policy/value-feedback.js +7 -8
- package/dist/policy/write-fence.js +3 -0
- package/dist/preflight-cache/evaluate.d.ts +12 -0
- package/dist/preflight-cache/evaluate.js +24 -4
- package/dist/preflight-cache/index.d.ts +1 -1
- package/dist/preflight-cache/index.js +1 -1
- package/dist/product-first-done-gate/evaluate.d.ts +4 -0
- package/dist/product-first-done-gate/evaluate.js +82 -10
- package/dist/release/closed-verb-gate.d.ts +12 -0
- package/dist/release/closed-verb-gate.js +51 -0
- package/dist/release/index.d.ts +1 -0
- package/dist/release/index.js +1 -0
- package/dist/release/pipeline.js +7 -0
- package/dist/release/types.d.ts +12 -0
- package/dist/render/constants.d.ts +2 -1
- package/dist/render/constants.js +2 -1
- package/dist/render/framework-commands.js +4 -0
- package/dist/render/spec-validate.js +23 -12
- package/dist/run-summary/types.d.ts +4 -0
- package/dist/scm/design-critique-chip.d.ts +37 -0
- package/dist/scm/design-critique-chip.js +171 -0
- package/dist/scm/index.d.ts +1 -0
- package/dist/scm/index.js +1 -0
- package/dist/scm/main.d.ts +3 -0
- package/dist/scm/main.js +15 -1
- package/dist/scm/readiness-cli.d.ts +2 -0
- package/dist/scm/readiness-cli.js +42 -1
- package/dist/scm/readiness.d.ts +2 -1
- package/dist/scm/readiness.js +7 -1
- package/dist/scope/decompose.js +2 -1
- package/dist/scope/delivery-evidence.d.ts +27 -1
- package/dist/scope/index.d.ts +1 -0
- package/dist/scope/index.js +1 -0
- package/dist/scope/lifecycle-write.d.ts +24 -0
- package/dist/scope/lifecycle-write.js +64 -0
- package/dist/scope/main.js +18 -6
- package/dist/scope/promote-from-issue.js +5 -5
- package/dist/scope/transition.js +90 -11
- package/dist/scope-provenance/evaluate.js +8 -4
- package/dist/session/ac-pass-banking.d.ts +5 -0
- package/dist/session/ac-pass-banking.js +3 -2
- package/dist/session/git.d.ts +18 -0
- package/dist/session/git.js +81 -1
- package/dist/session/occupancy.d.ts +2 -1
- package/dist/session/occupancy.js +11 -4
- package/dist/session/process-cost.d.ts +32 -10
- package/dist/session/process-cost.js +189 -1
- package/dist/session/product-state-hash.js +41 -24
- package/dist/session/ritual-entrypoint.js +16 -1
- package/dist/session/session-start.d.ts +18 -0
- package/dist/session/session-start.js +49 -2
- package/dist/session/verify-session-ritual.d.ts +46 -4
- package/dist/session/verify-session-ritual.js +118 -10
- package/dist/swarm/finalize-cohort-cli.d.ts +7 -2
- package/dist/swarm/finalize-cohort-cli.js +165 -35
- package/dist/swarm/finalize-cohort.d.ts +1 -0
- package/dist/swarm/finalize-cohort.js +14 -2
- package/dist/swarm/launch.d.ts +69 -0
- package/dist/swarm/launch.js +191 -1
- package/dist/swarm/routing-set-cli.js +2 -0
- package/dist/swarm/routing-verify.d.ts +1 -1
- package/dist/swarm/routing-verify.js +2 -2
- package/dist/triage/actions/index.d.ts +4 -0
- package/dist/triage/actions/index.js +6 -0
- package/dist/triage/bootstrap/index.js +6 -6
- package/dist/triage/evaluate/evaluate.d.ts +11 -0
- package/dist/triage/evaluate/evaluate.js +169 -0
- package/dist/triage/evaluate/github.d.ts +16 -0
- package/dist/triage/evaluate/github.js +153 -0
- package/dist/triage/evaluate/index.d.ts +9 -0
- package/dist/triage/evaluate/index.js +8 -0
- package/dist/triage/evaluate/paths.d.ts +7 -0
- package/dist/triage/evaluate/paths.js +25 -0
- package/dist/triage/evaluate/sink.d.ts +5 -0
- package/dist/triage/evaluate/sink.js +52 -0
- package/dist/triage/evaluate/types.d.ts +93 -0
- package/dist/triage/evaluate/types.js +13 -0
- package/dist/triage/evaluate/validity.d.ts +8 -0
- package/dist/triage/evaluate/validity.js +87 -0
- package/dist/triage/evaluate/value.d.ts +12 -0
- package/dist/triage/evaluate/value.js +33 -0
- package/dist/triage/evaluate/wip-census.d.ts +5 -0
- package/dist/triage/evaluate/wip-census.js +39 -0
- package/dist/triage/evaluate/worktrees.d.ts +8 -0
- package/dist/triage/evaluate/worktrees.js +68 -0
- package/dist/triage/evaluate/xbrief-refs.d.ts +8 -0
- package/dist/triage/evaluate/xbrief-refs.js +60 -0
- package/dist/triage/help/registry-data.d.ts +22 -8
- package/dist/triage/help/registry-data.js +54 -6
- package/dist/triage/index.d.ts +1 -0
- package/dist/triage/index.js +1 -0
- package/dist/triage/welcome/writers.d.ts +1 -1
- package/dist/triage/welcome/writers.js +10 -5
- package/dist/value/feedback-file.d.ts +17 -2
- package/dist/value/feedback-file.js +80 -6
- package/dist/value/readback.d.ts +4 -1
- package/dist/value/readback.js +51 -12
- package/dist/vbrief-reconcile/labels.d.ts +1 -0
- package/dist/vbrief-reconcile/labels.js +30 -0
- package/dist/vbrief-validate/schema.d.ts +4 -0
- package/dist/vbrief-validate/schema.js +1 -1
- package/dist/vbrief-validation/story-quality.d.ts +7 -0
- package/dist/vbrief-validation/story-quality.js +8 -1
- package/dist/verify-env/agent-hook-readiness.d.ts +2 -2
- package/dist/verify-env/agent-hook-readiness.js +12 -8
- package/dist/verify-env/agent-hooks-live-probe.d.ts +5 -1
- package/dist/verify-env/agent-hooks-live-probe.js +26 -5
- package/dist/verify-env/agent-hooks.js +3 -4
- package/dist/xbrief-migrate/agents-header.js +69 -7
- package/package.json +7 -3
|
@@ -7,12 +7,18 @@ export const KNOWN_GITHUB_AUTH_MODES = new Set([
|
|
|
7
7
|
GITHUB_AUTH_MODE_INJECTED_TOKEN,
|
|
8
8
|
GITHUB_AUTH_MODE_HOST_GH,
|
|
9
9
|
]);
|
|
10
|
-
export const
|
|
10
|
+
export const PRINCIPAL_KIND_USER = "user";
|
|
11
11
|
export const FAILURE_MISSING_INJECTED_TOKEN = "missing_injected_token";
|
|
12
12
|
export const FAILURE_GH_AUTH = "gh_auth_failed";
|
|
13
13
|
export const FAILURE_API_UNREACHABLE = "api_unreachable";
|
|
14
14
|
export const FAILURE_REPO_ACCESS = "repo_access_denied";
|
|
15
15
|
export const FAILURE_INVALID_MODE = "invalid_auth_mode";
|
|
16
|
+
export const FAILURE_MISSING_EXPECTED_PRINCIPAL = "missing_expected_principal";
|
|
17
|
+
export const FAILURE_PRINCIPAL_MISMATCH = "principal_mismatch";
|
|
18
|
+
export const FAILURE_MISSING_TARGET_REPO = "missing_target_repo";
|
|
19
|
+
export const FAILURE_INSTALLATION_IDENTITY_UNVERIFIABLE = "installation_identity_unverifiable";
|
|
20
|
+
export const ENV_EXPECTED_GITHUB_LOGIN = "DEFT_EXPECTED_GITHUB_LOGIN";
|
|
21
|
+
export const INSTALLATION_IDENTITY_ISSUE_URL = "https://github.com/deftai/directive/issues/3693";
|
|
16
22
|
const INJECTED_TOKEN_ENV_VARS = ["GH_TOKEN", "GITHUB_TOKEN", "GH_ENTERPRISE_TOKEN"];
|
|
17
23
|
const SANDBOX_REMEDIATION = "Remediation options for worker sandbox GitHub auth failures:\n" +
|
|
18
24
|
" - Run the GitHub step with full-access execution\n" +
|
|
@@ -22,6 +28,15 @@ const REPO_ACCESS_REMEDIATION = "Remediation options for repo-access failures:\n
|
|
|
22
28
|
" - Confirm the worker credential can read the target repository\n" +
|
|
23
29
|
" - Run the GitHub step with full-access execution if host gh has access\n" +
|
|
24
30
|
" - Use injected-token handoff scoped to the required repository";
|
|
31
|
+
const PRINCIPAL_REMEDIATION = "Remediation for GitHub worker-principal failures:\n" +
|
|
32
|
+
" - User-bearing credential: pass expectedPrincipal.login or set DEFT_EXPECTED_GITHUB_LOGIN\n" +
|
|
33
|
+
" - Do not treat a /user 403 on an installation token as API unreachability\n" +
|
|
34
|
+
" - GitHub App installation identity cannot be verified from the token; see #3693";
|
|
35
|
+
const INSTALLATION_IDENTITY_REMEDIATION = "A GitHub App installation token cannot prove which App it belongs to.\n" +
|
|
36
|
+
`Tracked in ${INSTALLATION_IDENTITY_ISSUE_URL}. Do not treat a /user 403 as API unreachability.`;
|
|
37
|
+
const TARGET_REPO_REMEDIATION = "Remediation for missing target repository:\n" +
|
|
38
|
+
" - Pass repo as owner/repo, or set GH_REPO / GITHUB_REPOSITORY, or run inside a git checkout with origin\n" +
|
|
39
|
+
" - Do not fall back to a hard-coded public default";
|
|
25
40
|
export function findInjectedToken(environ) {
|
|
26
41
|
for (const name of INJECTED_TOKEN_ENV_VARS) {
|
|
27
42
|
const value = environ[name]?.trim() ?? "";
|
|
@@ -63,6 +78,113 @@ function defaultRunGh(args, environ) {
|
|
|
63
78
|
return { args: [binary, ...args], returncode: 1, stdout: "", stderr: message };
|
|
64
79
|
}
|
|
65
80
|
}
|
|
81
|
+
export function defaultReadGitOriginUrl(cwd) {
|
|
82
|
+
try {
|
|
83
|
+
const result = spawnSync("git", ["remote", "get-url", "origin"], {
|
|
84
|
+
cwd,
|
|
85
|
+
encoding: "utf8",
|
|
86
|
+
timeout: 10_000,
|
|
87
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
88
|
+
});
|
|
89
|
+
if ((result.status ?? 1) !== 0) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
const url = (typeof result.stdout === "string" ? result.stdout : "").trim();
|
|
93
|
+
return url.length > 0 ? url : null;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
export function parseOwnerRepoSlug(input) {
|
|
100
|
+
const raw = input.trim();
|
|
101
|
+
if (raw.length === 0) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
const direct = raw.match(/^([A-Za-z0-9._-]+)\/([A-Za-z0-9._-]+)$/);
|
|
105
|
+
if (direct) {
|
|
106
|
+
return `${direct[1]}/${direct[2]}`;
|
|
107
|
+
}
|
|
108
|
+
const stripped = raw.replace(/\.git$/i, "");
|
|
109
|
+
// SCP form including GitHub Enterprise: git@host:owner/repo
|
|
110
|
+
if (!stripped.includes("://")) {
|
|
111
|
+
const scp = stripped.match(/:([A-Za-z0-9._-]+)\/([A-Za-z0-9._-]+)$/);
|
|
112
|
+
if (scp) {
|
|
113
|
+
return `${scp[1]}/${scp[2]}`;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const fromUrl = stripped.match(/[:/]([A-Za-z0-9._-]+)\/([A-Za-z0-9._-]+)$/);
|
|
117
|
+
if (fromUrl) {
|
|
118
|
+
return `${fromUrl[1]}/${fromUrl[2]}`;
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
export function deriveValidationRepo(options) {
|
|
123
|
+
if (options.repo !== undefined && options.repo !== null) {
|
|
124
|
+
const parsed = parseOwnerRepoSlug(options.repo);
|
|
125
|
+
if (parsed === null) {
|
|
126
|
+
return {
|
|
127
|
+
ok: false,
|
|
128
|
+
detail: `invalid repository slug: ${JSON.stringify(options.repo)} (expected owner/repo)`,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
return { ok: true, repo: parsed };
|
|
132
|
+
}
|
|
133
|
+
const env = options.environ ?? {};
|
|
134
|
+
const fromEnv = (env.GH_REPO ?? env.GITHUB_REPOSITORY ?? "").trim();
|
|
135
|
+
if (fromEnv.length > 0) {
|
|
136
|
+
const parsed = parseOwnerRepoSlug(fromEnv);
|
|
137
|
+
if (parsed === null) {
|
|
138
|
+
return {
|
|
139
|
+
ok: false,
|
|
140
|
+
detail: `invalid repository slug from GH_REPO/GITHUB_REPOSITORY: ${JSON.stringify(fromEnv)}`,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
return { ok: true, repo: parsed };
|
|
144
|
+
}
|
|
145
|
+
const remote = options.gitRemoteUrl !== undefined && options.gitRemoteUrl !== null
|
|
146
|
+
? options.gitRemoteUrl
|
|
147
|
+
: (options.readGitRemote ?? defaultReadGitOriginUrl)(options.cwd);
|
|
148
|
+
if (typeof remote === "string" && remote.trim().length > 0) {
|
|
149
|
+
const parsed = parseOwnerRepoSlug(remote);
|
|
150
|
+
if (parsed === null) {
|
|
151
|
+
return {
|
|
152
|
+
ok: false,
|
|
153
|
+
detail: `invalid git origin URL for repository derivation: ${JSON.stringify(remote.trim())}`,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
return { ok: true, repo: parsed };
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
ok: false,
|
|
160
|
+
detail: "cannot derive target repository; pass repo as owner/repo, set GH_REPO or GITHUB_REPOSITORY, or run inside a git checkout with origin",
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
export function resolveExpectedGithubWorkerPrincipal(environ, explicit) {
|
|
164
|
+
if (explicit === null) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
if (explicit !== undefined) {
|
|
168
|
+
return normalizeExpectedPrincipal(explicit);
|
|
169
|
+
}
|
|
170
|
+
const login = environ[ENV_EXPECTED_GITHUB_LOGIN]?.trim() ?? "";
|
|
171
|
+
if (login.length > 0) {
|
|
172
|
+
return { kind: PRINCIPAL_KIND_USER, login };
|
|
173
|
+
}
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
function normalizeExpectedPrincipal(principal) {
|
|
177
|
+
if (principal.kind !== PRINCIPAL_KIND_USER) {
|
|
178
|
+
return {
|
|
179
|
+
error: `unknown expected principal kind ${pyRepr(principal.kind)}`,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
const login = principal.login.trim();
|
|
183
|
+
if (login.length === 0) {
|
|
184
|
+
return { error: "user principal requires a non-empty login" };
|
|
185
|
+
}
|
|
186
|
+
return { kind: PRINCIPAL_KIND_USER, login };
|
|
187
|
+
}
|
|
66
188
|
function splitRepo(repo) {
|
|
67
189
|
const idx = repo.indexOf("/");
|
|
68
190
|
if (idx <= 0 || idx >= repo.length - 1) {
|
|
@@ -76,13 +198,30 @@ function sandboxRemediation(runtimeMode, failureKind) {
|
|
|
76
198
|
}
|
|
77
199
|
if (failureKind === FAILURE_GH_AUTH ||
|
|
78
200
|
failureKind === FAILURE_API_UNREACHABLE ||
|
|
79
|
-
failureKind === FAILURE_REPO_ACCESS
|
|
201
|
+
failureKind === FAILURE_REPO_ACCESS ||
|
|
202
|
+
failureKind === FAILURE_MISSING_EXPECTED_PRINCIPAL ||
|
|
203
|
+
failureKind === FAILURE_PRINCIPAL_MISMATCH ||
|
|
204
|
+
failureKind === FAILURE_MISSING_TARGET_REPO ||
|
|
205
|
+
failureKind === FAILURE_INSTALLATION_IDENTITY_UNVERIFIABLE) {
|
|
80
206
|
return SANDBOX_REMEDIATION;
|
|
81
207
|
}
|
|
82
208
|
return null;
|
|
83
209
|
}
|
|
84
|
-
function
|
|
85
|
-
|
|
210
|
+
function extraRemediation(failureKind) {
|
|
211
|
+
if (failureKind === FAILURE_REPO_ACCESS) {
|
|
212
|
+
return REPO_ACCESS_REMEDIATION;
|
|
213
|
+
}
|
|
214
|
+
if (failureKind === FAILURE_INSTALLATION_IDENTITY_UNVERIFIABLE) {
|
|
215
|
+
return INSTALLATION_IDENTITY_REMEDIATION;
|
|
216
|
+
}
|
|
217
|
+
if (failureKind === FAILURE_MISSING_EXPECTED_PRINCIPAL ||
|
|
218
|
+
failureKind === FAILURE_PRINCIPAL_MISMATCH) {
|
|
219
|
+
return PRINCIPAL_REMEDIATION;
|
|
220
|
+
}
|
|
221
|
+
if (failureKind === FAILURE_MISSING_TARGET_REPO) {
|
|
222
|
+
return TARGET_REPO_REMEDIATION;
|
|
223
|
+
}
|
|
224
|
+
return null;
|
|
86
225
|
}
|
|
87
226
|
function mergeRemediation(runtimeMode, failureKind) {
|
|
88
227
|
const parts = [];
|
|
@@ -90,9 +229,9 @@ function mergeRemediation(runtimeMode, failureKind) {
|
|
|
90
229
|
if (sandbox !== null) {
|
|
91
230
|
parts.push(sandbox);
|
|
92
231
|
}
|
|
93
|
-
const
|
|
94
|
-
if (
|
|
95
|
-
parts.push(
|
|
232
|
+
const extra = extraRemediation(failureKind);
|
|
233
|
+
if (extra !== null && !parts.includes(extra)) {
|
|
234
|
+
parts.push(extra);
|
|
96
235
|
}
|
|
97
236
|
return parts.length > 0 ? parts.join("\n\n") : null;
|
|
98
237
|
}
|
|
@@ -118,147 +257,193 @@ function parseLogin(stdout) {
|
|
|
118
257
|
}
|
|
119
258
|
return null;
|
|
120
259
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
remediation: null,
|
|
134
|
-
login: null,
|
|
135
|
-
};
|
|
260
|
+
function combinedGhText(proc) {
|
|
261
|
+
return `${proc.stdout}\n${proc.stderr}`;
|
|
262
|
+
}
|
|
263
|
+
export function isInstallationUserEndpointInapplicable(proc) {
|
|
264
|
+
const blob = combinedGhText(proc).toLowerCase();
|
|
265
|
+
return (blob.includes("resource not accessible by integration") ||
|
|
266
|
+
blob.includes("not accessible by integration"));
|
|
267
|
+
}
|
|
268
|
+
function clipFailureText(text, max = 240) {
|
|
269
|
+
const compact = text.replace(/\s+/g, " ").trim();
|
|
270
|
+
if (compact.length === 0) {
|
|
271
|
+
return "";
|
|
136
272
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
return {
|
|
140
|
-
ok: false,
|
|
141
|
-
githubAuthMode: GITHUB_AUTH_MODE_INJECTED_TOKEN,
|
|
142
|
-
runtimeMode,
|
|
143
|
-
failureKind: FAILURE_GH_AUTH,
|
|
144
|
-
detail: "injected token present but gh auth status failed in worker",
|
|
145
|
-
remediation: mergeRemediation(runtimeMode, FAILURE_GH_AUTH),
|
|
146
|
-
login: null,
|
|
147
|
-
};
|
|
273
|
+
if (compact.length <= max) {
|
|
274
|
+
return compact;
|
|
148
275
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
276
|
+
return `${compact.slice(0, max - 3)}...`;
|
|
277
|
+
}
|
|
278
|
+
function parseGhApiMessage(proc) {
|
|
279
|
+
for (const chunk of [proc.stdout, proc.stderr]) {
|
|
280
|
+
const text = chunk.trim();
|
|
281
|
+
if (text.length === 0) {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
try {
|
|
285
|
+
const payload = JSON.parse(text);
|
|
286
|
+
if (payload !== null && typeof payload === "object" && !Array.isArray(payload)) {
|
|
287
|
+
const message = payload.message;
|
|
288
|
+
if (typeof message === "string" && message.length > 0) {
|
|
289
|
+
const status = payload.status;
|
|
290
|
+
if (typeof status === "string" || typeof status === "number") {
|
|
291
|
+
return `${message} (HTTP ${status})`;
|
|
292
|
+
}
|
|
293
|
+
return message;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
catch {
|
|
298
|
+
// fall through to raw text
|
|
299
|
+
}
|
|
160
300
|
}
|
|
161
|
-
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
function isNetworkUnreachableText(text) {
|
|
304
|
+
return /timed out|timeout|econnrefused|enotfound|eai_again|network is unreachable|could not resolve host/i.test(text);
|
|
305
|
+
}
|
|
306
|
+
export function formatUserApiFailureDetail(mode, proc) {
|
|
307
|
+
const parsed = parseGhApiMessage(proc);
|
|
308
|
+
const raw = clipFailureText(combinedGhText(proc));
|
|
309
|
+
const cause = parsed ?? (raw.length > 0 ? raw : `exit ${proc.returncode}`);
|
|
310
|
+
const prefix = mode === GITHUB_AUTH_MODE_INJECTED_TOKEN
|
|
311
|
+
? "injected token present but GitHub /user failed"
|
|
312
|
+
: "gh auth status passed but GitHub /user failed";
|
|
313
|
+
if (isNetworkUnreachableText(cause) || isNetworkUnreachableText(raw)) {
|
|
314
|
+
return `${prefix}: GitHub API is unreachable (${cause})`;
|
|
315
|
+
}
|
|
316
|
+
return `${prefix}: ${cause}`;
|
|
317
|
+
}
|
|
318
|
+
function emptyResult(mode, runtimeMode, failureKind, detail, options = {}) {
|
|
319
|
+
const ok = options.ok ?? false;
|
|
320
|
+
return {
|
|
321
|
+
ok,
|
|
322
|
+
githubAuthMode: mode,
|
|
323
|
+
runtimeMode,
|
|
324
|
+
failureKind: ok ? null : failureKind,
|
|
325
|
+
detail,
|
|
326
|
+
remediation: ok ? null : mergeRemediation(runtimeMode, failureKind ?? ""),
|
|
327
|
+
login: options.login ?? null,
|
|
328
|
+
principal: options.principal ?? null,
|
|
329
|
+
validationRepo: options.validationRepo ?? null,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
function loginsMatch(expected, observed) {
|
|
333
|
+
return expected.localeCompare(observed, undefined, { sensitivity: "accent" }) === 0;
|
|
334
|
+
}
|
|
335
|
+
function failClosedInstallationCredential(mode, runtimeMode, repo) {
|
|
336
|
+
return emptyResult(mode, runtimeMode, FAILURE_INSTALLATION_IDENTITY_UNVERIFIABLE, "GitHub /user is inapplicable to a GitHub App installation credential (no authenticated user). " +
|
|
337
|
+
"The API is reachable. An installation token cannot disclose which App it belongs to, " +
|
|
338
|
+
`so identity cannot be verified. Deferred to ${INSTALLATION_IDENTITY_ISSUE_URL}.`, { validationRepo: repo });
|
|
339
|
+
}
|
|
340
|
+
function checkTargetRepoAccess(mode, runtimeMode, runner, environ, repo, login, principal) {
|
|
162
341
|
const [owner, name] = splitRepo(repo);
|
|
163
342
|
const repoApi = runner(["api", `repos/${owner}/${name}`], environ);
|
|
164
343
|
if (repoApi.returncode !== 0) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
failureKind: FAILURE_REPO_ACCESS,
|
|
170
|
-
detail: `injected token can reach GitHub API but cannot access ${repo}`,
|
|
171
|
-
remediation: mergeRemediation(runtimeMode, FAILURE_REPO_ACCESS),
|
|
344
|
+
const detail = mode === GITHUB_AUTH_MODE_INJECTED_TOKEN
|
|
345
|
+
? `injected token can reach GitHub API but cannot access ${repo}`
|
|
346
|
+
: `GitHub API reachable but repository access failed for ${repo}`;
|
|
347
|
+
return emptyResult(mode, runtimeMode, FAILURE_REPO_ACCESS, detail, {
|
|
172
348
|
login,
|
|
173
|
-
|
|
349
|
+
principal,
|
|
350
|
+
validationRepo: repo,
|
|
351
|
+
});
|
|
174
352
|
}
|
|
175
|
-
|
|
353
|
+
const okDetail = mode === GITHUB_AUTH_MODE_INJECTED_TOKEN
|
|
354
|
+
? "injected-token mode validated in worker environment"
|
|
355
|
+
: "host-gh mode validated in worker environment";
|
|
356
|
+
return emptyResult(mode, runtimeMode, null, okDetail, {
|
|
176
357
|
ok: true,
|
|
177
|
-
githubAuthMode: GITHUB_AUTH_MODE_INJECTED_TOKEN,
|
|
178
|
-
runtimeMode,
|
|
179
|
-
failureKind: null,
|
|
180
|
-
detail: "injected-token mode validated in worker environment",
|
|
181
|
-
remediation: null,
|
|
182
358
|
login,
|
|
183
|
-
|
|
359
|
+
principal,
|
|
360
|
+
validationRepo: repo,
|
|
361
|
+
});
|
|
184
362
|
}
|
|
185
|
-
|
|
363
|
+
function validateAfterAuth(mode, environ, options, runtimeMode, runner) {
|
|
364
|
+
const derived = deriveValidationRepo({
|
|
365
|
+
repo: options.repo,
|
|
366
|
+
environ,
|
|
367
|
+
gitRemoteUrl: options.gitRemoteUrl,
|
|
368
|
+
readGitRemote: options.readGitRemote,
|
|
369
|
+
cwd: options.cwd,
|
|
370
|
+
});
|
|
371
|
+
if (!derived.ok) {
|
|
372
|
+
return emptyResult(mode, runtimeMode, FAILURE_MISSING_TARGET_REPO, derived.detail);
|
|
373
|
+
}
|
|
374
|
+
const repo = derived.repo;
|
|
375
|
+
const expectedPrincipal = resolveExpectedGithubWorkerPrincipal(environ, options.expectedPrincipal);
|
|
376
|
+
if (expectedPrincipal !== null && "error" in expectedPrincipal && options.expectedPrincipal) {
|
|
377
|
+
return emptyResult(mode, runtimeMode, FAILURE_MISSING_EXPECTED_PRINCIPAL, expectedPrincipal.error, {
|
|
378
|
+
validationRepo: repo,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
const userApi = runner(["api", "user"], environ);
|
|
382
|
+
if (userApi.returncode !== 0) {
|
|
383
|
+
if (isInstallationUserEndpointInapplicable(userApi)) {
|
|
384
|
+
return failClosedInstallationCredential(mode, runtimeMode, repo);
|
|
385
|
+
}
|
|
386
|
+
return emptyResult(mode, runtimeMode, FAILURE_API_UNREACHABLE, formatUserApiFailureDetail(mode, userApi), { validationRepo: repo });
|
|
387
|
+
}
|
|
388
|
+
const login = parseLogin(userApi.stdout);
|
|
389
|
+
if (login === null) {
|
|
390
|
+
return emptyResult(mode, runtimeMode, FAILURE_PRINCIPAL_MISMATCH, "GitHub /user succeeded but returned no login", { validationRepo: repo });
|
|
391
|
+
}
|
|
392
|
+
if (expectedPrincipal !== null && "error" in expectedPrincipal) {
|
|
393
|
+
return emptyResult(mode, runtimeMode, FAILURE_MISSING_EXPECTED_PRINCIPAL, expectedPrincipal.error, {
|
|
394
|
+
login,
|
|
395
|
+
validationRepo: repo,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
if (expectedPrincipal !== null) {
|
|
399
|
+
if (!loginsMatch(expectedPrincipal.login, login)) {
|
|
400
|
+
return emptyResult(mode, runtimeMode, FAILURE_PRINCIPAL_MISMATCH, `identity mismatch: expected ${expectedPrincipal.login}, observed ${login}`, { login, validationRepo: repo });
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
const principal = { kind: PRINCIPAL_KIND_USER, login };
|
|
404
|
+
return checkTargetRepoAccess(mode, runtimeMode, runner, environ, repo, login, principal);
|
|
405
|
+
}
|
|
406
|
+
export function validateInjectedTokenMode(environ, options = {}) {
|
|
186
407
|
const runner = options.runGh ?? defaultRunGh;
|
|
187
|
-
const repo = options.repo ?? DEFAULT_VALIDATION_REPO;
|
|
188
408
|
const runtimeMode = options.runtimeMode ?? null;
|
|
409
|
+
const tokenPresent = findInjectedToken(environ) !== null;
|
|
410
|
+
if (!tokenPresent) {
|
|
411
|
+
return emptyResult(GITHUB_AUTH_MODE_INJECTED_TOKEN, runtimeMode, FAILURE_MISSING_INJECTED_TOKEN, "injected-token mode requires GH_TOKEN, GITHUB_TOKEN, or GH_ENTERPRISE_TOKEN; host gh credential store is not used");
|
|
412
|
+
}
|
|
189
413
|
const authStatus = runner(["auth", "status"], environ);
|
|
190
414
|
if (authStatus.returncode !== 0) {
|
|
191
|
-
return
|
|
192
|
-
ok: false,
|
|
193
|
-
githubAuthMode: GITHUB_AUTH_MODE_HOST_GH,
|
|
194
|
-
runtimeMode,
|
|
195
|
-
failureKind: FAILURE_GH_AUTH,
|
|
196
|
-
detail: "gh auth status failed in worker environment",
|
|
197
|
-
remediation: mergeRemediation(runtimeMode, FAILURE_GH_AUTH),
|
|
198
|
-
login: null,
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
const userApi = runner(["api", "user", "--jq", ".login"], environ);
|
|
202
|
-
if (userApi.returncode !== 0) {
|
|
203
|
-
return {
|
|
204
|
-
ok: false,
|
|
205
|
-
githubAuthMode: GITHUB_AUTH_MODE_HOST_GH,
|
|
206
|
-
runtimeMode,
|
|
207
|
-
failureKind: FAILURE_API_UNREACHABLE,
|
|
208
|
-
detail: "gh auth status passed but GitHub API is unreachable",
|
|
209
|
-
remediation: mergeRemediation(runtimeMode, FAILURE_API_UNREACHABLE),
|
|
210
|
-
login: null,
|
|
211
|
-
};
|
|
415
|
+
return emptyResult(GITHUB_AUTH_MODE_INJECTED_TOKEN, runtimeMode, FAILURE_GH_AUTH, "injected token present but gh auth status failed in worker");
|
|
212
416
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
detail: `GitHub API reachable but repository access failed for ${repo}`,
|
|
222
|
-
remediation: mergeRemediation(runtimeMode, FAILURE_REPO_ACCESS),
|
|
223
|
-
login: parseLogin(userApi.stdout),
|
|
224
|
-
};
|
|
417
|
+
return validateAfterAuth(GITHUB_AUTH_MODE_INJECTED_TOKEN, environ, options, runtimeMode, runner);
|
|
418
|
+
}
|
|
419
|
+
export function validateHostGhMode(environ, options = {}) {
|
|
420
|
+
const runner = options.runGh ?? defaultRunGh;
|
|
421
|
+
const runtimeMode = options.runtimeMode ?? null;
|
|
422
|
+
const authStatus = runner(["auth", "status"], environ);
|
|
423
|
+
if (authStatus.returncode !== 0) {
|
|
424
|
+
return emptyResult(GITHUB_AUTH_MODE_HOST_GH, runtimeMode, FAILURE_GH_AUTH, "gh auth status failed in worker environment");
|
|
225
425
|
}
|
|
226
|
-
return
|
|
227
|
-
ok: true,
|
|
228
|
-
githubAuthMode: GITHUB_AUTH_MODE_HOST_GH,
|
|
229
|
-
runtimeMode,
|
|
230
|
-
failureKind: null,
|
|
231
|
-
detail: "host-gh mode validated in worker environment",
|
|
232
|
-
remediation: null,
|
|
233
|
-
login: parseLogin(userApi.stdout),
|
|
234
|
-
};
|
|
426
|
+
return validateAfterAuth(GITHUB_AUTH_MODE_HOST_GH, environ, options, runtimeMode, runner);
|
|
235
427
|
}
|
|
236
428
|
export function validateGithubAuth(githubAuthMode, options = {}) {
|
|
237
429
|
const env = options.environ ?? process.env;
|
|
238
430
|
const runtimeMode = options.runtimeReport?.runtimeMode ?? null;
|
|
239
431
|
if (!KNOWN_GITHUB_AUTH_MODES.has(githubAuthMode)) {
|
|
240
|
-
return {
|
|
241
|
-
ok: false,
|
|
242
|
-
githubAuthMode,
|
|
243
|
-
runtimeMode,
|
|
244
|
-
failureKind: FAILURE_INVALID_MODE,
|
|
245
|
-
detail: `unknown github_auth_mode ${pyRepr(githubAuthMode)}; expected one of ${pyRepr([...KNOWN_GITHUB_AUTH_MODES].sort())}`,
|
|
246
|
-
remediation: null,
|
|
247
|
-
login: null,
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
if (githubAuthMode === GITHUB_AUTH_MODE_INJECTED_TOKEN) {
|
|
251
|
-
return validateInjectedTokenMode(env, {
|
|
252
|
-
repo: options.repo,
|
|
253
|
-
runtimeMode,
|
|
254
|
-
runGh: options.runGh,
|
|
255
|
-
});
|
|
432
|
+
return emptyResult(githubAuthMode, runtimeMode, FAILURE_INVALID_MODE, `unknown github_auth_mode ${pyRepr(githubAuthMode)}; expected one of ${pyRepr([...KNOWN_GITHUB_AUTH_MODES].sort())}`);
|
|
256
433
|
}
|
|
257
|
-
|
|
434
|
+
const shared = {
|
|
258
435
|
repo: options.repo,
|
|
259
436
|
runtimeMode,
|
|
260
437
|
runGh: options.runGh,
|
|
261
|
-
|
|
438
|
+
expectedPrincipal: options.expectedPrincipal,
|
|
439
|
+
gitRemoteUrl: options.gitRemoteUrl,
|
|
440
|
+
readGitRemote: options.readGitRemote,
|
|
441
|
+
cwd: options.cwd,
|
|
442
|
+
};
|
|
443
|
+
if (githubAuthMode === GITHUB_AUTH_MODE_INJECTED_TOKEN) {
|
|
444
|
+
return validateInjectedTokenMode(env, shared);
|
|
445
|
+
}
|
|
446
|
+
return validateHostGhMode(env, shared);
|
|
262
447
|
}
|
|
263
448
|
export function validateGithubAuthForWorker(githubAuthMode = null, options = {}) {
|
|
264
449
|
const report = options.runtimeReport ?? getPlatformCapabilities();
|
|
@@ -274,12 +459,30 @@ export function resultToDict(result) {
|
|
|
274
459
|
detail: result.detail,
|
|
275
460
|
remediation: result.remediation,
|
|
276
461
|
login: result.login,
|
|
462
|
+
principal_kind: result.principal?.kind ?? null,
|
|
463
|
+
validation_repo: result.validationRepo,
|
|
277
464
|
};
|
|
278
465
|
}
|
|
466
|
+
function expectedPrincipalFromCliArgs(args) {
|
|
467
|
+
if (args.expectedPrincipal !== undefined) {
|
|
468
|
+
return args.expectedPrincipal ?? undefined;
|
|
469
|
+
}
|
|
470
|
+
const login = args.expectedLogin?.trim() ?? "";
|
|
471
|
+
if (login.length > 0) {
|
|
472
|
+
return { kind: PRINCIPAL_KIND_USER, login };
|
|
473
|
+
}
|
|
474
|
+
return undefined;
|
|
475
|
+
}
|
|
279
476
|
export function githubAuthModesMain(args) {
|
|
477
|
+
const fromFlags = expectedPrincipalFromCliArgs(args);
|
|
478
|
+
if (fromFlags !== undefined && "error" in fromFlags) {
|
|
479
|
+
process.stderr.write(`${fromFlags.error}\n`);
|
|
480
|
+
return 2;
|
|
481
|
+
}
|
|
280
482
|
const result = validateGithubAuthForWorker(args.githubAuthMode ?? null, {
|
|
281
|
-
repo: args.repo
|
|
483
|
+
repo: args.repo,
|
|
282
484
|
runGh: args.runGh,
|
|
485
|
+
expectedPrincipal: fromFlags,
|
|
283
486
|
});
|
|
284
487
|
if (args.json) {
|
|
285
488
|
process.stdout.write(`${JSON.stringify(resultToDict(result), null, 2)}\n`);
|
|
@@ -35,7 +35,33 @@ export interface EvaluateCompletedTrackedOptions {
|
|
|
35
35
|
readonly runGit?: GitRunner;
|
|
36
36
|
/** Test seam: override issue state resolution. */
|
|
37
37
|
readonly resolveIssueState?: (ref: IssueRef) => "open" | "closed" | null;
|
|
38
|
+
/**
|
|
39
|
+
* Progress hook (#3673). Fired after tip listing and after the batch
|
|
40
|
+
* blob read so a CLI can announce an up-front count only when elapsed
|
|
41
|
+
* time crosses a measured threshold.
|
|
42
|
+
*/
|
|
43
|
+
readonly onProgress?: (event: CompletedTrackedProgress) => void;
|
|
44
|
+
/** Test seam: override wall clock for progress elapsedMs. */
|
|
45
|
+
readonly now?: () => number;
|
|
46
|
+
}
|
|
47
|
+
/** Default silence window so a ~2s batch read does not emit progress noise. */
|
|
48
|
+
export declare const COMPLETED_TRACKED_PROGRESS_THRESHOLD_MS = 3000;
|
|
49
|
+
/**
|
|
50
|
+
* Up-front count floor (#3673 Greptile P2). Listing is cheap; the batch
|
|
51
|
+
* read is the remaining stall. Announce the blob count after listing
|
|
52
|
+
* (before `cat-file --batch`) when the corpus is large enough that a
|
|
53
|
+
* silent wait is the old DONE-path failure mode. Fixture-sized runs stay
|
|
54
|
+
* quiet unless the duration threshold is also crossed.
|
|
55
|
+
*/
|
|
56
|
+
export declare const COMPLETED_TRACKED_PROGRESS_MIN_BLOBS = 32;
|
|
57
|
+
export interface CompletedTrackedProgress {
|
|
58
|
+
readonly phase: "listed" | "read";
|
|
59
|
+
readonly terminalCount: number;
|
|
60
|
+
readonly nonterminalCount: number;
|
|
61
|
+
readonly elapsedMs: number;
|
|
38
62
|
}
|
|
63
|
+
export declare function shouldAnnounceProgress(elapsedMs: number, thresholdMs?: number): boolean;
|
|
64
|
+
export declare function shouldAnnounceUpFrontCount(terminalCount: number, nonterminalCount: number, minBlobs?: number): boolean;
|
|
39
65
|
/**
|
|
40
66
|
* Resolve the git tip to inspect for tracked completed/cancelled land.
|
|
41
67
|
* Prefers origin/<deliveryBranch> when present.
|
|
@@ -16,7 +16,7 @@ import { hasArtifactSuffix, LEGACY_ARTIFACT_DIR, MIGRATED_ARTIFACT_DIR, resolveL
|
|
|
16
16
|
import { collectGithubRefs } from "../orphan-active/refs.js";
|
|
17
17
|
import { resolveDeliveryBranch } from "../policy/delivery-branch.js";
|
|
18
18
|
import { defaultRunGh } from "../pr-protected-issues/gh.js";
|
|
19
|
-
import { defaultGitRunner } from "../session/git.js";
|
|
19
|
+
import { defaultGitRunner, showBlobsBatch } from "../session/git.js";
|
|
20
20
|
import { CACHE_DIR_NAME, CACHE_SOURCE_GITHUB_ISSUE } from "../triage/queue/constants.js";
|
|
21
21
|
import { resolveRepo } from "../triage/queue/repo.js";
|
|
22
22
|
/** Lifecycle folders scanned for local "known origin" xBRIEFs. */
|
|
@@ -31,6 +31,22 @@ export const LOCAL_ORIGIN_FOLDERS = [
|
|
|
31
31
|
export const TIP_NONTERMINAL_FOLDERS = ["proposed", "pending", "active"];
|
|
32
32
|
/** Terminal tip folders that satisfy the land rule. */
|
|
33
33
|
export const TIP_TERMINAL_FOLDERS = ["completed", "cancelled"];
|
|
34
|
+
/** Default silence window so a ~2s batch read does not emit progress noise. */
|
|
35
|
+
export const COMPLETED_TRACKED_PROGRESS_THRESHOLD_MS = 3_000;
|
|
36
|
+
/**
|
|
37
|
+
* Up-front count floor (#3673 Greptile P2). Listing is cheap; the batch
|
|
38
|
+
* read is the remaining stall. Announce the blob count after listing
|
|
39
|
+
* (before `cat-file --batch`) when the corpus is large enough that a
|
|
40
|
+
* silent wait is the old DONE-path failure mode. Fixture-sized runs stay
|
|
41
|
+
* quiet unless the duration threshold is also crossed.
|
|
42
|
+
*/
|
|
43
|
+
export const COMPLETED_TRACKED_PROGRESS_MIN_BLOBS = 32;
|
|
44
|
+
export function shouldAnnounceProgress(elapsedMs, thresholdMs = COMPLETED_TRACKED_PROGRESS_THRESHOLD_MS) {
|
|
45
|
+
return elapsedMs >= thresholdMs;
|
|
46
|
+
}
|
|
47
|
+
export function shouldAnnounceUpFrontCount(terminalCount, nonterminalCount, minBlobs = COMPLETED_TRACKED_PROGRESS_MIN_BLOBS) {
|
|
48
|
+
return terminalCount + nonterminalCount >= minBlobs;
|
|
49
|
+
}
|
|
34
50
|
function readJson(path) {
|
|
35
51
|
try {
|
|
36
52
|
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
@@ -223,13 +239,6 @@ function listTreePaths(projectRoot, tip, prefixes, runGit) {
|
|
|
223
239
|
.map((line) => line.trim().replace(/\\/g, "/"))
|
|
224
240
|
.filter((line) => line.length > 0 && hasArtifactSuffix(line));
|
|
225
241
|
}
|
|
226
|
-
function showBlob(projectRoot, tip, path, runGit) {
|
|
227
|
-
const result = runGit(projectRoot, ["show", `${tip}:${path}`]);
|
|
228
|
-
if (result.code !== 0) {
|
|
229
|
-
return null;
|
|
230
|
-
}
|
|
231
|
-
return result.stdout;
|
|
232
|
-
}
|
|
233
242
|
function terminalPrefixes() {
|
|
234
243
|
const out = [];
|
|
235
244
|
for (const root of [MIGRATED_ARTIFACT_DIR, LEGACY_ARTIFACT_DIR]) {
|
|
@@ -248,11 +257,11 @@ function nonterminalPrefixes() {
|
|
|
248
257
|
}
|
|
249
258
|
return out;
|
|
250
259
|
}
|
|
251
|
-
function
|
|
260
|
+
function issuesFromBlobBodies(paths, bodies, defaultRepo, originPrefix) {
|
|
252
261
|
const hits = [];
|
|
253
262
|
for (const path of paths) {
|
|
254
|
-
const body =
|
|
255
|
-
if (body === null) {
|
|
263
|
+
const body = bodies.get(path);
|
|
264
|
+
if (body === undefined || body === null) {
|
|
256
265
|
continue;
|
|
257
266
|
}
|
|
258
267
|
let parsed;
|
|
@@ -375,17 +384,31 @@ export function evaluateCompletedTracked(projectRoot, options = {}) {
|
|
|
375
384
|
tip: null,
|
|
376
385
|
};
|
|
377
386
|
}
|
|
387
|
+
const startedAt = (options.now ?? Date.now)();
|
|
388
|
+
const emitProgress = (phase, terminalCount, nonterminalCount) => {
|
|
389
|
+
options.onProgress?.({
|
|
390
|
+
phase,
|
|
391
|
+
terminalCount,
|
|
392
|
+
nonterminalCount,
|
|
393
|
+
elapsedMs: (options.now ?? Date.now)() - startedAt,
|
|
394
|
+
});
|
|
395
|
+
};
|
|
378
396
|
const localHits = scanLocalOrigins(root, defaultRepo);
|
|
379
397
|
const tipNonterminalPaths = listTreePaths(root, tip, nonterminalPrefixes(), runGit);
|
|
380
|
-
const tipNonterminalHits = issuesFromBlobPaths(root, tip, tipNonterminalPaths, defaultRepo, runGit, `tip:${tip}`);
|
|
381
|
-
let originMap = mergeOrigins([...localHits, ...tipNonterminalHits]);
|
|
382
398
|
// Delivery tip only — the land invariant is post-merge tip truth (#3264 AC).
|
|
383
399
|
// Lifecycle PRs that commit completed/ on a feature branch are not expected
|
|
384
400
|
// to satisfy this gate until merge; run the verb with --tip HEAD when
|
|
385
401
|
// validating an in-flight land PR. The gate is a standalone verify verb
|
|
386
402
|
// (not wired into check:consumer) so product PRs are not deadlocked.
|
|
387
403
|
const tipTerminalPaths = listTreePaths(root, tip, terminalPrefixes(), runGit);
|
|
388
|
-
|
|
404
|
+
emitProgress("listed", tipTerminalPaths.length, tipNonterminalPaths.length);
|
|
405
|
+
// One process for every tip artifact. Issue identity still comes from
|
|
406
|
+
// parsed plan.references / plan.metadata["x-tracking"] — never the path.
|
|
407
|
+
const tipBodies = showBlobsBatch(root, tip, [...tipNonterminalPaths, ...tipTerminalPaths], runGit);
|
|
408
|
+
emitProgress("read", tipTerminalPaths.length, tipNonterminalPaths.length);
|
|
409
|
+
const tipNonterminalHits = issuesFromBlobBodies(tipNonterminalPaths, tipBodies, defaultRepo, `tip:${tip}`);
|
|
410
|
+
let originMap = mergeOrigins([...localHits, ...tipNonterminalHits]);
|
|
411
|
+
const tipTerminalHits = issuesFromBlobBodies(tipTerminalPaths, tipBodies, defaultRepo, `tip:${tip}`);
|
|
389
412
|
const landedKeys = new Set(tipTerminalHits.map((h) => issueKey(h.issue)));
|
|
390
413
|
const issueFilter = options.issue ?? null;
|
|
391
414
|
if (issueFilter !== null) {
|