@dev-loops/core 1.0.2-pre.0 → 1.0.2-slim.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/package.json +2 -1
- package/src/analysis/diff-analyzer.mjs +85 -137
- package/src/claude/asset-generation.mjs +7 -7
- package/src/claude/hook-decisions.mjs +29 -36
- package/src/config/config.mjs +388 -787
- package/src/github/comment-id-guard.mjs +2 -2
- package/src/github/copilot-helpers.mjs +90 -158
- package/src/loop/bash-command-classify.mjs +34 -49
- package/src/loop/conductor-routing.mjs +15 -23
- package/src/loop/copilot-loop-state.mjs +46 -94
- package/src/loop/gate-carry-forward.mjs +2 -2
- package/src/loop/gate-fanin.mjs +252 -442
- package/src/loop/handoff-envelope.mjs +19 -19
- package/src/loop/issue-refinement-artifact.mjs +158 -252
- package/src/loop/lifecycle-state.mjs +10 -21
- package/src/loop/pr-gate-coordination.mjs +37 -37
- package/src/loop/queue-board-sync.mjs +16 -55
- package/src/loop/review-dispatch-plan.mjs +60 -122
- package/src/loop/review-lineage.mjs +19 -44
- package/src/loop/spec-authority.mjs +39 -69
- package/src/loop/steering.mjs +16 -68
- package/src/projects/list-queue-items.mjs +16 -146
- package/src/projects/move-queue-item.mjs +15 -141
- package/src/projects/projects-access.mjs +202 -0
package/src/loop/steering.mjs
CHANGED
|
@@ -1,24 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Deterministic mid-flight operator steering contract for active dev loops.
|
|
3
3
|
*
|
|
4
|
-
* This module provides:
|
|
5
|
-
* - STEERING_KIND: stable steering kind constants
|
|
6
|
-
* - STEERING_RESULT: acknowledgement/result constants
|
|
7
|
-
* - SAFE_POINT_CATEGORY: safe-point classification constants
|
|
8
|
-
* - normalizeSteeringEvent: validate and canonicalize a raw steering event
|
|
9
|
-
* - normalizeSteeringState: load and validate persisted steering state
|
|
10
|
-
* - createSteeringState: create a fresh steering state for a new run
|
|
11
|
-
* - classifySafePoint: map a copilot loop state to a safe-point category
|
|
12
|
-
* - submitSteering: process a steering event against current run state
|
|
13
|
-
* - promoteQueuedSteering: apply queued steering when the loop reaches a safe point
|
|
14
|
-
* - getEffectiveConstraints: get the current effective steering constraints
|
|
15
|
-
* - resolveEffectiveLoopState: get loop interpretation augmented with active steering
|
|
16
|
-
* - getSteeringStatus: get full inspection output for a run's steering state
|
|
17
|
-
*
|
|
18
4
|
* The proving target for this first implementation slice is the async Copilot
|
|
19
|
-
* review/fix loop (copilot-loop-state.mjs)
|
|
20
|
-
* that loop's state set and
|
|
21
|
-
*
|
|
5
|
+
* review/fix loop (copilot-loop-state.mjs): safe-point rules are defined for
|
|
6
|
+
* that loop's state set, and resolveEffectiveLoopState changes loop behavior
|
|
7
|
+
* when stop_at_next_safe_gate steering is active.
|
|
22
8
|
*/
|
|
23
9
|
|
|
24
10
|
import { normalizeRepoSlug } from "../github/repo-slug.mjs";
|
|
@@ -74,46 +60,30 @@ const VALID_APPLY_MODES = new Set(["immediate", "next_safe_point"]);
|
|
|
74
60
|
// ---------------------------------------------------------------------------
|
|
75
61
|
|
|
76
62
|
/**
|
|
77
|
-
* Map a copilot loop state to a safe-point category
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* IMMEDIATE — between steps / idle / waiting on external state.
|
|
82
|
-
* Steering can be applied right now without risk of splitting a mutation.
|
|
83
|
-
* States: pr_ready_no_feedback, waiting_for_copilot_review,
|
|
84
|
-
* waiting_for_ci, ready_to_rerequest_review
|
|
85
|
-
*
|
|
86
|
-
* NEXT_POINT — actively computing or in a non-interruptible mutation.
|
|
87
|
-
* Applying steering now could produce a half-applied or inconsistent state.
|
|
88
|
-
* Queue the event and promote it when the loop next reaches an IMMEDIATE state.
|
|
89
|
-
* States: pr_draft, unresolved_feedback_present, already_fixed_needs_reply_resolve
|
|
90
|
-
*
|
|
91
|
-
* TERMINAL — run is done, irreversibly failed, or has no active run.
|
|
92
|
-
* Steering is rejected; it would have no effect or could mask a real error.
|
|
93
|
-
* States: no_pr, done, review_request_unavailable, blocked_needs_user_decision
|
|
63
|
+
* Map a copilot loop state to a safe-point category (async Copilot
|
|
64
|
+
* review/fix loop): IMMEDIATE states can apply steering right now; NEXT_POINT
|
|
65
|
+
* states are mid-mutation and queue the event for the next IMMEDIATE state;
|
|
66
|
+
* TERMINAL states reject steering (no active/recoverable run).
|
|
94
67
|
*
|
|
95
68
|
* @param {string} loopState - a copilot loop STATE value
|
|
96
69
|
* @returns {"immediate"|"next_point"|"terminal"}
|
|
97
70
|
*/
|
|
98
71
|
export function classifySafePoint(loopState) {
|
|
99
72
|
switch (loopState) {
|
|
100
|
-
//
|
|
73
|
+
// Idle / waiting on external state.
|
|
101
74
|
case STATE.PR_READY_NO_FEEDBACK:
|
|
102
75
|
case STATE.READY_TO_REREQUEST_REVIEW:
|
|
103
|
-
// Waiting on external state
|
|
104
76
|
case STATE.WAITING_FOR_COPILOT_REVIEW:
|
|
105
77
|
case STATE.WAITING_FOR_CI:
|
|
106
78
|
return SAFE_POINT_CATEGORY.IMMEDIATE;
|
|
107
79
|
|
|
108
|
-
//
|
|
80
|
+
// Actively computing / non-interruptible mutation.
|
|
109
81
|
case STATE.PR_DRAFT:
|
|
110
|
-
// Actively computing — about to apply fixes to resolve feedback
|
|
111
82
|
case STATE.UNRESOLVED_FEEDBACK_PRESENT:
|
|
112
|
-
// In the middle of a non-interruptible mutation (reply/resolve review threads)
|
|
113
83
|
case STATE.ALREADY_FIXED_NEEDS_REPLY_RESOLVE:
|
|
114
84
|
return SAFE_POINT_CATEGORY.NEXT_POINT;
|
|
115
85
|
|
|
116
|
-
// Terminal
|
|
86
|
+
// Terminal: run done, errored, or no active run.
|
|
117
87
|
case STATE.NO_PR:
|
|
118
88
|
case STATE.DONE:
|
|
119
89
|
case STATE.REVIEW_REQUEST_UNAVAILABLE:
|
|
@@ -418,20 +388,9 @@ function hasStopAtNextSafeGate(events) {
|
|
|
418
388
|
|
|
419
389
|
/**
|
|
420
390
|
* Process a steering event against current run state and produce an
|
|
421
|
-
* acknowledgement/result plus updated steering state.
|
|
422
|
-
*
|
|
423
|
-
*
|
|
424
|
-
*
|
|
425
|
-
* Result semantics:
|
|
426
|
-
* - applied_now: event is immediately effective; effectiveStack is updated.
|
|
427
|
-
* - queued_for_safe_point: loop is in a non-safe state; event is queued and
|
|
428
|
-
* will be promoted by promoteQueuedSteering when the loop reaches a safe point.
|
|
429
|
-
* - rejected_unsafe_now: terminal loop state (done/unavailable/no_pr); steering
|
|
430
|
-
* would have no effect or could mask a real issue.
|
|
431
|
-
* - rejected_invalid_or_conflicting: event is malformed, has an out-of-order seq,
|
|
432
|
-
* or exactly duplicates an existing hard_constraint.
|
|
433
|
-
* - needs_human_decision: loop is in blocked_needs_user_decision; human must act
|
|
434
|
-
* before automated steering can be safely applied.
|
|
391
|
+
* acknowledgement/result plus updated steering state. Main entry point for
|
|
392
|
+
* operators submitting mid-flight corrections; see {@link STEERING_RESULT}
|
|
393
|
+
* for the possible outcomes.
|
|
435
394
|
*
|
|
436
395
|
* @param {object} event - normalized steering event (from normalizeSteeringEvent)
|
|
437
396
|
* @param {object} steeringState - current steering state (from normalizeSteeringState)
|
|
@@ -719,7 +678,7 @@ export function getEffectiveConstraints(steeringState) {
|
|
|
719
678
|
* @param {object} snapshot - raw or normalized loop snapshot
|
|
720
679
|
* @param {object} steeringState - current steering state for this run
|
|
721
680
|
* @param {object} [refinementConfig] - interpreter refinement config; pass a config-derived
|
|
722
|
-
* `resolveRefinement(config)` so the base interpretation honors gates.preApproval.requireCi:false
|
|
681
|
+
* `resolveRefinement(config)` so the base interpretation honors gates.preApproval.requireCi:false.
|
|
723
682
|
* @returns {{ state: string, allowedTransitions: string[], nextAction: string, steeringApplied: boolean, pendingStopAtNextSafeGate: boolean, terminalStopAtNextSafeGate: boolean, effectiveConstraints: object }}
|
|
724
683
|
*/
|
|
725
684
|
export function resolveEffectiveLoopState(snapshot, steeringState, refinementConfig) {
|
|
@@ -760,19 +719,8 @@ export function resolveEffectiveLoopState(snapshot, steeringState, refinementCon
|
|
|
760
719
|
// ---------------------------------------------------------------------------
|
|
761
720
|
|
|
762
721
|
/**
|
|
763
|
-
* Get full inspection output for a run's steering state
|
|
764
|
-
*
|
|
765
|
-
* Returns:
|
|
766
|
-
* - runId: the target run
|
|
767
|
-
* - schemaVersion: for migration awareness
|
|
768
|
-
* - eventCount: total events submitted
|
|
769
|
-
* - queuedCount: events waiting for the next safe point
|
|
770
|
-
* - effectiveStackCount: events currently in effect
|
|
771
|
-
* - effectiveConstraints: structured view over the effective stack
|
|
772
|
-
* - latestResult: most recent acknowledgement/result
|
|
773
|
-
* - resultHistory: all historical acknowledgements
|
|
774
|
-
* - history: all submitted events
|
|
775
|
-
* - nextSeq: next expected sequence number
|
|
722
|
+
* Get full inspection output for a run's steering state (event counts,
|
|
723
|
+
* effective constraints, result history, and next expected seq).
|
|
776
724
|
*
|
|
777
725
|
* @param {object} steeringState
|
|
778
726
|
* @returns {object}
|
|
@@ -1,77 +1,14 @@
|
|
|
1
1
|
import { runChild as _runChild } from "../cli/primitives.mjs";
|
|
2
2
|
import { resolveProjectSelector, findProject } from "./resolve-project.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { resolveOwner } from "../github/gh.mjs";
|
|
4
|
+
import { validateProjectsRepo, discoverProjects, listProjectFields, paginateNodes, extractStatus } from "./projects-access.mjs";
|
|
4
5
|
|
|
5
6
|
// ── Validation ───────────────────────────────────────────────────────────
|
|
6
7
|
|
|
7
|
-
const
|
|
8
|
-
const REPO_NAME_RE = /^[a-zA-Z0-9](?:[a-zA-Z0-9_.-]*[a-zA-Z0-9])?$/;
|
|
9
|
-
|
|
10
|
-
function validateRepo(repo) {
|
|
11
|
-
if (!repo || typeof repo !== "string") {
|
|
12
|
-
throw Object.assign(new Error("--repo is required"), { code: "INVALID_REPO" });
|
|
13
|
-
}
|
|
14
|
-
const trimmed = repo.trim();
|
|
15
|
-
if (trimmed !== repo) {
|
|
16
|
-
throw Object.assign(
|
|
17
|
-
new Error(`--repo must not have leading/trailing whitespace, got "${repo}"`),
|
|
18
|
-
{ code: "INVALID_REPO" },
|
|
19
|
-
);
|
|
20
|
-
}
|
|
21
|
-
const slashIdx = repo.indexOf("/");
|
|
22
|
-
if (slashIdx === -1) {
|
|
23
|
-
throw Object.assign(new Error(`--repo must be exactly owner/name, got "${repo}"`), { code: "INVALID_REPO" });
|
|
24
|
-
}
|
|
25
|
-
const owner = repo.slice(0, slashIdx);
|
|
26
|
-
const name = repo.slice(slashIdx + 1);
|
|
27
|
-
if (!owner || !name || !OWNER_RE.test(owner) || !REPO_NAME_RE.test(name)) {
|
|
28
|
-
throw Object.assign(new Error(`--repo must be exactly owner/name, got "${repo}"`), { code: "INVALID_REPO" });
|
|
29
|
-
}
|
|
30
|
-
return repo;
|
|
31
|
-
}
|
|
8
|
+
const validateRepo = validateProjectsRepo;
|
|
32
9
|
|
|
33
10
|
// ── GraphQL fragments ────────────────────────────────────────────────────
|
|
34
11
|
|
|
35
|
-
const LIST_USER_PROJECTS = [
|
|
36
|
-
"query($login:String!, $after:String) {",
|
|
37
|
-
" user(login:$login) {",
|
|
38
|
-
" projectsV2(first:50, after:$after) {",
|
|
39
|
-
" pageInfo { hasNextPage endCursor }",
|
|
40
|
-
" nodes { id number title url }",
|
|
41
|
-
" }",
|
|
42
|
-
" }",
|
|
43
|
-
"}"
|
|
44
|
-
].join("\n");
|
|
45
|
-
|
|
46
|
-
const LIST_ORG_PROJECTS = [
|
|
47
|
-
"query($login:String!, $after:String) {",
|
|
48
|
-
" organization(login:$login) {",
|
|
49
|
-
" projectsV2(first:50, after:$after) {",
|
|
50
|
-
" pageInfo { hasNextPage endCursor }",
|
|
51
|
-
" nodes { id number title url }",
|
|
52
|
-
" }",
|
|
53
|
-
" }",
|
|
54
|
-
"}"
|
|
55
|
-
].join("\n");
|
|
56
|
-
|
|
57
|
-
const GET_PROJECT_FIELDS = [
|
|
58
|
-
"query($projectId:ID!, $after:String) {",
|
|
59
|
-
" node(id:$projectId) {",
|
|
60
|
-
" ... on ProjectV2 {",
|
|
61
|
-
" fields(first:50, after:$after) {",
|
|
62
|
-
" pageInfo { hasNextPage endCursor }",
|
|
63
|
-
" nodes {",
|
|
64
|
-
" ... on ProjectV2SingleSelectField {",
|
|
65
|
-
" id name",
|
|
66
|
-
" options { id name }",
|
|
67
|
-
" }",
|
|
68
|
-
" }",
|
|
69
|
-
" }",
|
|
70
|
-
" }",
|
|
71
|
-
" }",
|
|
72
|
-
"}"
|
|
73
|
-
].join("\n");
|
|
74
|
-
|
|
75
12
|
const GET_PROJECT_ITEMS = [
|
|
76
13
|
"query($projectId:ID!, $after:String) {",
|
|
77
14
|
" node(id:$projectId) {",
|
|
@@ -99,82 +36,22 @@ const GET_PROJECT_ITEMS = [
|
|
|
99
36
|
"}"
|
|
100
37
|
].join("\n");
|
|
101
38
|
|
|
102
|
-
// ── Paginated project listing
|
|
103
|
-
|
|
104
|
-
async function listAllProjects(login, kind, env, runChild) {
|
|
105
|
-
const query = kind === "org" ? LIST_ORG_PROJECTS : LIST_USER_PROJECTS;
|
|
106
|
-
const projects = [];
|
|
107
|
-
let after = null;
|
|
108
|
-
while (true) {
|
|
109
|
-
const vars = { login };
|
|
110
|
-
if (after) vars.after = after;
|
|
111
|
-
const payload = await ghGraphql(query, vars, env, runChild);
|
|
112
|
-
const connection = kind === "org"
|
|
113
|
-
? payload?.data?.organization?.projectsV2
|
|
114
|
-
: payload?.data?.user?.projectsV2;
|
|
115
|
-
const nodes = connection?.nodes ?? [];
|
|
116
|
-
projects.push(...nodes);
|
|
117
|
-
const pageInfo = connection?.pageInfo ?? {};
|
|
118
|
-
if (!pageInfo.hasNextPage) break;
|
|
119
|
-
if (!pageInfo.endCursor) {
|
|
120
|
-
throw Object.assign(
|
|
121
|
-
new Error("Invalid projects list payload: hasNextPage is true but endCursor is missing"),
|
|
122
|
-
{ code: "GH_API_ERROR" },
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
after = pageInfo.endCursor;
|
|
126
|
-
}
|
|
127
|
-
return projects;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// ── Paginated field listing ──────────────────────────────────────────────
|
|
39
|
+
// ── Paginated project + field listing (shared via projects-access) ────────
|
|
131
40
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
let after = null;
|
|
135
|
-
while (true) {
|
|
136
|
-
const vars = { projectId };
|
|
137
|
-
if (after) vars.after = after;
|
|
138
|
-
const payload = await ghGraphql(GET_PROJECT_FIELDS, vars, env, runChild);
|
|
139
|
-
const connection = payload?.data?.node?.fields;
|
|
140
|
-
const nodes = connection?.nodes ?? [];
|
|
141
|
-
fields.push(...nodes);
|
|
142
|
-
const pageInfo = connection?.pageInfo ?? {};
|
|
143
|
-
if (!pageInfo.hasNextPage) break;
|
|
144
|
-
if (!pageInfo.endCursor) {
|
|
145
|
-
throw Object.assign(
|
|
146
|
-
new Error("Invalid fields payload: hasNextPage is true but endCursor is missing"),
|
|
147
|
-
{ code: "GH_API_ERROR" },
|
|
148
|
-
);
|
|
149
|
-
}
|
|
150
|
-
after = pageInfo.endCursor;
|
|
151
|
-
}
|
|
152
|
-
return fields;
|
|
153
|
-
}
|
|
41
|
+
const listAllProjects = discoverProjects;
|
|
42
|
+
const listAllFields = listProjectFields;
|
|
154
43
|
|
|
155
44
|
// ── Paginated item listing ───────────────────────────────────────────────
|
|
156
45
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
items.push(...nodes);
|
|
167
|
-
const pageInfo = connection?.pageInfo ?? {};
|
|
168
|
-
if (!pageInfo.hasNextPage) break;
|
|
169
|
-
if (!pageInfo.endCursor) {
|
|
170
|
-
throw Object.assign(
|
|
171
|
-
new Error("Invalid items payload: hasNextPage is true but endCursor is missing"),
|
|
172
|
-
{ code: "GH_API_ERROR" },
|
|
173
|
-
);
|
|
174
|
-
}
|
|
175
|
-
after = pageInfo.endCursor;
|
|
176
|
-
}
|
|
177
|
-
return items;
|
|
46
|
+
function listAllItems(projectId, env, runChild) {
|
|
47
|
+
return paginateNodes({
|
|
48
|
+
query: GET_PROJECT_ITEMS,
|
|
49
|
+
variables: { projectId },
|
|
50
|
+
selectConnection: (payload) => payload?.data?.node?.items,
|
|
51
|
+
env,
|
|
52
|
+
runChild,
|
|
53
|
+
entity: "items",
|
|
54
|
+
});
|
|
178
55
|
}
|
|
179
56
|
|
|
180
57
|
// ── Exit code classification ────────────────────────────────────────────
|
|
@@ -260,14 +137,7 @@ async function main(args, { env = process.env, runChild } = {}) {
|
|
|
260
137
|
if (!content) continue;
|
|
261
138
|
|
|
262
139
|
// Determine status from field values
|
|
263
|
-
|
|
264
|
-
const fieldValues = item.fieldValues?.nodes ?? [];
|
|
265
|
-
for (const fv of fieldValues) {
|
|
266
|
-
if (fv && fv.field && fv.field.name === "Status") {
|
|
267
|
-
status = fv.name;
|
|
268
|
-
break;
|
|
269
|
-
}
|
|
270
|
-
}
|
|
140
|
+
const status = extractStatus(item);
|
|
271
141
|
|
|
272
142
|
// Filter by column
|
|
273
143
|
if (args.column && status !== args.column) continue;
|
|
@@ -3,74 +3,14 @@ import { runPickupRefinementGate } from "../loop/issue-refinement-artifact.mjs";
|
|
|
3
3
|
import { loadStateColumnMap, LOGICAL_COLUMN } from "../loop/queue-board-sync.mjs";
|
|
4
4
|
import { resolveProjectSelector, findProject, parseItemRef } from "./resolve-project.mjs";
|
|
5
5
|
import { ghGraphql, resolveOwner } from "../github/gh.mjs";
|
|
6
|
+
import { validateProjectsRepo, discoverProjects, listProjectFields, paginateNodes, extractStatus } from "./projects-access.mjs";
|
|
6
7
|
|
|
7
8
|
// ── Validation ───────────────────────────────────────────────────────────
|
|
8
9
|
|
|
9
|
-
const
|
|
10
|
-
const REPO_NAME_RE = /^[a-zA-Z0-9](?:[a-zA-Z0-9_.-]*[a-zA-Z0-9])?$/;
|
|
11
|
-
|
|
12
|
-
function validateRepo(repo) {
|
|
13
|
-
if (!repo || typeof repo !== "string") {
|
|
14
|
-
throw Object.assign(new Error("--repo is required"), { code: "INVALID_REPO" });
|
|
15
|
-
}
|
|
16
|
-
const trimmed = repo.trim();
|
|
17
|
-
if (trimmed !== repo) {
|
|
18
|
-
throw Object.assign(new Error(`--repo must not have leading/trailing whitespace, got "${repo}"`), { code: "INVALID_REPO" });
|
|
19
|
-
}
|
|
20
|
-
const slashIdx = repo.indexOf("/");
|
|
21
|
-
if (slashIdx === -1) {
|
|
22
|
-
throw Object.assign(new Error(`--repo must be exactly owner/name, got "${repo}"`), { code: "INVALID_REPO" });
|
|
23
|
-
}
|
|
24
|
-
const owner = repo.slice(0, slashIdx);
|
|
25
|
-
const name = repo.slice(slashIdx + 1);
|
|
26
|
-
if (!owner || !name || !OWNER_RE.test(owner) || !REPO_NAME_RE.test(name)) {
|
|
27
|
-
throw Object.assign(new Error(`--repo must be exactly owner/name, got "${repo}"`), { code: "INVALID_REPO" });
|
|
28
|
-
}
|
|
29
|
-
return repo;
|
|
30
|
-
}
|
|
10
|
+
const validateRepo = validateProjectsRepo;
|
|
31
11
|
|
|
32
12
|
// ── GraphQL fragments ────────────────────────────────────────────────────
|
|
33
13
|
|
|
34
|
-
const LIST_USER_PROJECTS = [
|
|
35
|
-
"query($login:String!, $after:String) {",
|
|
36
|
-
" user(login:$login) {",
|
|
37
|
-
" projectsV2(first:50, after:$after) {",
|
|
38
|
-
" pageInfo { hasNextPage endCursor }",
|
|
39
|
-
" nodes { id number title url }",
|
|
40
|
-
" }",
|
|
41
|
-
" }",
|
|
42
|
-
"}"
|
|
43
|
-
].join("\n");
|
|
44
|
-
|
|
45
|
-
const LIST_ORG_PROJECTS = [
|
|
46
|
-
"query($login:String!, $after:String) {",
|
|
47
|
-
" organization(login:$login) {",
|
|
48
|
-
" projectsV2(first:50, after:$after) {",
|
|
49
|
-
" pageInfo { hasNextPage endCursor }",
|
|
50
|
-
" nodes { id number title url }",
|
|
51
|
-
" }",
|
|
52
|
-
" }",
|
|
53
|
-
"}"
|
|
54
|
-
].join("\n");
|
|
55
|
-
|
|
56
|
-
const GET_PROJECT_FIELDS = [
|
|
57
|
-
"query($projectId:ID!, $after:String) {",
|
|
58
|
-
" node(id:$projectId) {",
|
|
59
|
-
" ... on ProjectV2 {",
|
|
60
|
-
" fields(first:50, after:$after) {",
|
|
61
|
-
" pageInfo { hasNextPage endCursor }",
|
|
62
|
-
" nodes {",
|
|
63
|
-
" ... on ProjectV2SingleSelectField {",
|
|
64
|
-
" id name",
|
|
65
|
-
" options { id name }",
|
|
66
|
-
" }",
|
|
67
|
-
" }",
|
|
68
|
-
" }",
|
|
69
|
-
" }",
|
|
70
|
-
" }",
|
|
71
|
-
"}"
|
|
72
|
-
].join("\n");
|
|
73
|
-
|
|
74
14
|
const GET_PROJECT_ITEMS_BY_CONTENT = [
|
|
75
15
|
"query($projectId:ID!, $after:String) {",
|
|
76
16
|
" node(id:$projectId) {",
|
|
@@ -108,91 +48,25 @@ const UPDATE_ITEM_FIELD = [
|
|
|
108
48
|
"}"
|
|
109
49
|
].join("\n");
|
|
110
50
|
|
|
111
|
-
// ── Paginated project listing
|
|
112
|
-
|
|
113
|
-
async function listAllProjects(login, kind, env, runChild) {
|
|
114
|
-
const query = kind === "org" ? LIST_ORG_PROJECTS : LIST_USER_PROJECTS;
|
|
115
|
-
const projects = [];
|
|
116
|
-
let after = null;
|
|
117
|
-
while (true) {
|
|
118
|
-
const vars = { login };
|
|
119
|
-
if (after) vars.after = after;
|
|
120
|
-
const payload = await ghGraphql(query, vars, env, runChild);
|
|
121
|
-
const connection = kind === "org"
|
|
122
|
-
? payload?.data?.organization?.projectsV2
|
|
123
|
-
: payload?.data?.user?.projectsV2;
|
|
124
|
-
const nodes = connection?.nodes ?? [];
|
|
125
|
-
projects.push(...nodes);
|
|
126
|
-
const pageInfo = connection?.pageInfo ?? {};
|
|
127
|
-
if (!pageInfo.hasNextPage) break;
|
|
128
|
-
if (!pageInfo.endCursor) {
|
|
129
|
-
throw Object.assign(
|
|
130
|
-
new Error("Invalid projects list payload: hasNextPage is true but endCursor is missing"),
|
|
131
|
-
{ code: "GH_API_ERROR" },
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
after = pageInfo.endCursor;
|
|
135
|
-
}
|
|
136
|
-
return projects;
|
|
137
|
-
}
|
|
51
|
+
// ── Paginated project + field listing (shared via projects-access) ────────
|
|
138
52
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
async function listAllFields(projectId, env, runChild) {
|
|
142
|
-
const fields = [];
|
|
143
|
-
let after = null;
|
|
144
|
-
while (true) {
|
|
145
|
-
const vars = { projectId };
|
|
146
|
-
if (after) vars.after = after;
|
|
147
|
-
const payload = await ghGraphql(GET_PROJECT_FIELDS, vars, env, runChild);
|
|
148
|
-
const connection = payload?.data?.node?.fields;
|
|
149
|
-
const nodes = connection?.nodes ?? [];
|
|
150
|
-
fields.push(...nodes);
|
|
151
|
-
const pageInfo = connection?.pageInfo ?? {};
|
|
152
|
-
if (!pageInfo.hasNextPage) break;
|
|
153
|
-
if (!pageInfo.endCursor) {
|
|
154
|
-
throw Object.assign(
|
|
155
|
-
new Error("Invalid fields payload: hasNextPage is true but endCursor is missing"),
|
|
156
|
-
{ code: "GH_API_ERROR" },
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
after = pageInfo.endCursor;
|
|
160
|
-
}
|
|
161
|
-
return fields;
|
|
162
|
-
}
|
|
53
|
+
const listAllProjects = discoverProjects;
|
|
54
|
+
const listAllFields = listProjectFields;
|
|
163
55
|
|
|
164
56
|
// ── Paginated item listing (position order) ──────────────────────────────
|
|
165
57
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
items.push(...nodes);
|
|
176
|
-
const pageInfo = connection?.pageInfo ?? {};
|
|
177
|
-
if (!pageInfo.hasNextPage) break;
|
|
178
|
-
if (!pageInfo.endCursor) {
|
|
179
|
-
throw Object.assign(
|
|
180
|
-
new Error("Invalid items payload: hasNextPage is true but endCursor is missing"),
|
|
181
|
-
{ code: "GH_API_ERROR" },
|
|
182
|
-
);
|
|
183
|
-
}
|
|
184
|
-
after = pageInfo.endCursor;
|
|
185
|
-
}
|
|
186
|
-
return items;
|
|
58
|
+
function fetchAllItems(projectId, env, runChild) {
|
|
59
|
+
return paginateNodes({
|
|
60
|
+
query: GET_PROJECT_ITEMS_BY_CONTENT,
|
|
61
|
+
variables: { projectId },
|
|
62
|
+
selectConnection: (payload) => payload?.data?.node?.items,
|
|
63
|
+
env,
|
|
64
|
+
runChild,
|
|
65
|
+
entity: "items",
|
|
66
|
+
});
|
|
187
67
|
}
|
|
188
68
|
|
|
189
|
-
|
|
190
|
-
const fvs = node?.fieldValues?.nodes ?? [];
|
|
191
|
-
for (const fv of fvs) {
|
|
192
|
-
if (fv && fv.field && fv.field.name === "Status") return fv.name;
|
|
193
|
-
}
|
|
194
|
-
return null;
|
|
195
|
-
}
|
|
69
|
+
const statusOf = extractStatus;
|
|
196
70
|
|
|
197
71
|
// ── Exit code classification ────────────────────────────────────────────
|
|
198
72
|
|