mandrel 1.63.0 → 1.64.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/.agents/scripts/agents-bootstrap-github.js +40 -48
- package/.agents/scripts/bootstrap.js +74 -60
- package/.agents/scripts/lib/bootstrap/branch-protection.js +8 -8
- package/.agents/scripts/lib/bootstrap/gh-preflight.js +3 -3
- package/.agents/scripts/lib/bootstrap/hitl-confirm.js +2 -2
- package/.agents/scripts/lib/bootstrap/merge-methods.js +7 -7
- package/.agents/scripts/lib/bootstrap/preflight.js +18 -15
- package/.agents/scripts/lib/bootstrap/project-bootstrap.js +5 -5
- package/.agents/scripts/lib/bootstrap/prompt.js +5 -1
- package/.agents/scripts/lib/detect-package-manager.js +2 -2
- package/.agents/scripts/lib/onboard/init-tail.js +60 -69
- package/.agents/scripts/providers/github/tickets.js +1 -1
- package/.agents/workflows/helpers/deliver-stories.md +24 -2
- package/.agents/workflows/helpers/single-story-deliver.md +84 -1
- package/docs/CHANGELOG.md +15 -0
- package/lib/cli/init.js +66 -21
- package/lib/cli/sync.js +3 -3
- package/package.json +1 -1
- package/.agents/scripts/lib/onboard/detect-stack.js +0 -300
|
@@ -195,13 +195,13 @@ function runGit(args, cwd) {
|
|
|
195
195
|
* `GhExecError` — so a bare "gh exited with code 1" is actually diagnosable.
|
|
196
196
|
*/
|
|
197
197
|
function logGhError(label, err) {
|
|
198
|
-
Logger.error(`[
|
|
198
|
+
Logger.error(`[Bootstrap] ${label} failed: ${err.message}`);
|
|
199
199
|
if (err.stderr)
|
|
200
|
-
Logger.error(`[
|
|
200
|
+
Logger.error(`[Bootstrap] gh stderr: ${String(err.stderr).trim()}`);
|
|
201
201
|
if (err.stdout)
|
|
202
|
-
Logger.error(`[
|
|
202
|
+
Logger.error(`[Bootstrap] gh stdout: ${String(err.stdout).trim()}`);
|
|
203
203
|
if (Array.isArray(err.args)) {
|
|
204
|
-
Logger.error(`[
|
|
204
|
+
Logger.error(`[Bootstrap] gh args: ${err.args.join(' ')}`);
|
|
205
205
|
}
|
|
206
206
|
}
|
|
207
207
|
|
|
@@ -249,7 +249,7 @@ function ensureGitInitialized(state) {
|
|
|
249
249
|
initialized = true;
|
|
250
250
|
state.gitInitialized = true;
|
|
251
251
|
Logger.info(
|
|
252
|
-
`[
|
|
252
|
+
`[Bootstrap] Initialized git repo (branch ${branch}) at ${cwd}.`,
|
|
253
253
|
);
|
|
254
254
|
}
|
|
255
255
|
|
|
@@ -281,7 +281,7 @@ function ensureGitInitialized(state) {
|
|
|
281
281
|
return { ok: false, error: commit.stderr || 'git commit failed' };
|
|
282
282
|
}
|
|
283
283
|
committed = true;
|
|
284
|
-
Logger.info('[
|
|
284
|
+
Logger.info('[Bootstrap] Created initial commit.');
|
|
285
285
|
}
|
|
286
286
|
return { ok: true, initialized, committed };
|
|
287
287
|
}
|
|
@@ -303,21 +303,21 @@ async function ensureGitRemote(state, execImpl = exec) {
|
|
|
303
303
|
if (runGit(['remote', 'get-url', 'origin'], cwd).ok) return;
|
|
304
304
|
if (!(await repoExists(owner, repo, execImpl))) {
|
|
305
305
|
Logger.warn(
|
|
306
|
-
`[
|
|
306
|
+
`[Bootstrap] No 'origin' remote and ${owner}/${repo} does not exist on GitHub — skipping remote wiring.`,
|
|
307
307
|
);
|
|
308
308
|
return;
|
|
309
309
|
}
|
|
310
310
|
const url = `https://github.com/${owner}/${repo}.git`;
|
|
311
311
|
const add = runGit(['remote', 'add', 'origin', url], cwd);
|
|
312
312
|
if (!add.ok) {
|
|
313
|
-
Logger.warn(`[
|
|
313
|
+
Logger.warn(`[Bootstrap] Could not add 'origin' remote: ${add.stderr}`);
|
|
314
314
|
return;
|
|
315
315
|
}
|
|
316
|
-
Logger.info(`[
|
|
316
|
+
Logger.info(`[Bootstrap] Wired 'origin' → ${url}.`);
|
|
317
317
|
const push = runGit(['push', '-u', 'origin', branch], cwd);
|
|
318
318
|
if (!push.ok) {
|
|
319
319
|
Logger.warn(
|
|
320
|
-
`[
|
|
320
|
+
`[Bootstrap] 'origin' is set but push of '${branch}' failed (resolve manually, e.g. \`git pull --rebase origin ${branch}\`): ${push.stderr}`,
|
|
321
321
|
);
|
|
322
322
|
}
|
|
323
323
|
}
|
|
@@ -359,11 +359,11 @@ async function ensureProjectLinked(state, execImpl = exec) {
|
|
|
359
359
|
args: ['project', 'link', pn, '--owner', owner, '--repo', repo],
|
|
360
360
|
});
|
|
361
361
|
Logger.info(
|
|
362
|
-
`[
|
|
362
|
+
`[Bootstrap] Linked repo ${owner}/${repo} to Project V2 #${pn}.`,
|
|
363
363
|
);
|
|
364
364
|
} catch (err) {
|
|
365
365
|
Logger.warn(
|
|
366
|
-
`[
|
|
366
|
+
`[Bootstrap] Could not link repo ${owner}/${repo} to Project V2 #${pn} (continuing): ${err.message}`,
|
|
367
367
|
);
|
|
368
368
|
}
|
|
369
369
|
}
|
|
@@ -413,7 +413,7 @@ async function createGithubRepo(state, execImpl = exec) {
|
|
|
413
413
|
],
|
|
414
414
|
});
|
|
415
415
|
Logger.info(
|
|
416
|
-
`[
|
|
416
|
+
`[Bootstrap] Created GitHub repo ${slug} (${visibility}) and pushed.`,
|
|
417
417
|
);
|
|
418
418
|
}
|
|
419
419
|
|
|
@@ -480,7 +480,7 @@ async function createGithubProject(state, execImpl = exec) {
|
|
|
480
480
|
if (Number.isInteger(existing)) {
|
|
481
481
|
state.answers.projectNumber = String(existing);
|
|
482
482
|
Logger.info(
|
|
483
|
-
`[
|
|
483
|
+
`[Bootstrap] Reusing existing GitHub Project V2 "${title}" (#${existing}) — no duplicate created.`,
|
|
484
484
|
);
|
|
485
485
|
return existing;
|
|
486
486
|
}
|
|
@@ -510,7 +510,7 @@ async function createGithubProject(state, execImpl = exec) {
|
|
|
510
510
|
);
|
|
511
511
|
}
|
|
512
512
|
state.answers.projectNumber = String(number);
|
|
513
|
-
Logger.info(`[
|
|
513
|
+
Logger.info(`[Bootstrap] Created GitHub Project V2 "${title}" (#${number}).`);
|
|
514
514
|
return number;
|
|
515
515
|
}
|
|
516
516
|
|
|
@@ -539,7 +539,7 @@ export function buildQuestions(defaults, flags, env = process.env, lists = {}) {
|
|
|
539
539
|
key: 'owner',
|
|
540
540
|
flag: 'owner',
|
|
541
541
|
env: 'GH_OWNER',
|
|
542
|
-
message: '
|
|
542
|
+
message: '\n\nGitHub repo owner',
|
|
543
543
|
default: defaults.owner,
|
|
544
544
|
required: true,
|
|
545
545
|
validate: (v) =>
|
|
@@ -549,7 +549,8 @@ export function buildQuestions(defaults, flags, env = process.env, lists = {}) {
|
|
|
549
549
|
key: 'operatorHandle',
|
|
550
550
|
flag: 'operator-handle',
|
|
551
551
|
env: 'GH_OPERATOR_HANDLE',
|
|
552
|
-
message:
|
|
552
|
+
message:
|
|
553
|
+
'GitHub username/handle without preceding@ (default: same as owner)',
|
|
553
554
|
// Default tracks the repo owner; resolved post-collect if left blank.
|
|
554
555
|
default: defaults.owner,
|
|
555
556
|
required: false,
|
|
@@ -562,8 +563,9 @@ export function buildQuestions(defaults, flags, env = process.env, lists = {}) {
|
|
|
562
563
|
key: 'repo',
|
|
563
564
|
flag: 'repo',
|
|
564
565
|
env: 'GH_REPO',
|
|
565
|
-
message:
|
|
566
|
-
|
|
566
|
+
message: 'New GitHub repo name',
|
|
567
|
+
pickerMessage:
|
|
568
|
+
'GitHub repo name - Select existing or press ENTER to create',
|
|
567
569
|
default: defaults.repo,
|
|
568
570
|
required: true,
|
|
569
571
|
picker: {
|
|
@@ -590,8 +592,9 @@ export function buildQuestions(defaults, flags, env = process.env, lists = {}) {
|
|
|
590
592
|
key: 'projectNumber',
|
|
591
593
|
flag: 'project-number',
|
|
592
594
|
env: 'GH_PROJECT_NUMBER',
|
|
593
|
-
message:
|
|
594
|
-
|
|
595
|
+
message: 'New GitHub Project V2 name',
|
|
596
|
+
pickerMessage:
|
|
597
|
+
'GitHub Project V2 name - Select existing or press ENTER to create',
|
|
595
598
|
// Prefer the already-stored numeric project number (an
|
|
596
599
|
// already-provisioned project on a re-run) so `--assume-yes` resolves a
|
|
597
600
|
// numeric answer that `detectCreation` treats as existing — never a
|
|
@@ -750,7 +753,7 @@ export function parseAndValidate(argv, opts = {}) {
|
|
|
750
753
|
// `--assume-yes` path did.)
|
|
751
754
|
if (!interactive && !assumeYes && !approveGithubAdmin) {
|
|
752
755
|
Logger.error(
|
|
753
|
-
'[
|
|
756
|
+
'[Bootstrap] non-TTY run requires --assume-yes or --approve-github-admin ' +
|
|
754
757
|
'(no operator is present to confirm the GitHub-admin mutations).',
|
|
755
758
|
);
|
|
756
759
|
return { ok: false, exit: 1 };
|
|
@@ -760,7 +763,7 @@ export function parseAndValidate(argv, opts = {}) {
|
|
|
760
763
|
const githubAdminApproved = interactive || assumeYes || approveGithubAdmin;
|
|
761
764
|
if (resolveRepoVisibility(flags) === null) {
|
|
762
765
|
Logger.error(
|
|
763
|
-
`[
|
|
766
|
+
`[Bootstrap] invalid --visibility "${flags.visibility}". ` +
|
|
764
767
|
`Expected one of: ${REPO_VISIBILITIES.join(', ')}.`,
|
|
765
768
|
);
|
|
766
769
|
return { ok: false, exit: 1 };
|
|
@@ -783,11 +786,12 @@ export function prepareContext(state, opts = {}) {
|
|
|
783
786
|
const defaults = inferDefaults(projectRoot);
|
|
784
787
|
const silentAccept = resolveSilentAccept(defaults, state.flags);
|
|
785
788
|
|
|
786
|
-
Logger.info('[
|
|
787
|
-
Logger.info(
|
|
788
|
-
Logger.info(`
|
|
789
|
-
Logger.info(`
|
|
790
|
-
Logger.info(`
|
|
789
|
+
Logger.info('[\n');
|
|
790
|
+
Logger.info('[Bootstrap] Checking existing GitHub values:');
|
|
791
|
+
Logger.info(` GitHub Repo Owner ${defaults.owner ?? '(unknown)'}`);
|
|
792
|
+
Logger.info(` GitHub Repo Name ${defaults.repo ?? '(unknown)'}`);
|
|
793
|
+
Logger.info(` Base Branch ${defaults.baseBranch ?? '(unknown)'}`);
|
|
794
|
+
Logger.info(` GitHub Username ${defaults.operatorHandle ?? '(unknown)'}`);
|
|
791
795
|
|
|
792
796
|
return {
|
|
793
797
|
ok: true,
|
|
@@ -811,24 +815,34 @@ export async function runPreflightPhase(state, opts = {}) {
|
|
|
811
815
|
|
|
812
816
|
for (const check of result.checks) {
|
|
813
817
|
if (check.ok) {
|
|
818
|
+
// A non-fatal informational check (it carries `gitInitialized`) shows a
|
|
819
|
+
// glyph reflecting the real state rather than its always-true gate pass:
|
|
820
|
+
// ✓ when the git repo exists, ✗ when it does not (bootstrap initialises
|
|
821
|
+
// it in a later phase regardless — the ✗ never aborts the run).
|
|
822
|
+
const glyph =
|
|
823
|
+
typeof check.gitInitialized === 'boolean' && !check.gitInitialized
|
|
824
|
+
? '✗'
|
|
825
|
+
: '✓';
|
|
814
826
|
Logger.info(
|
|
815
|
-
`[
|
|
827
|
+
`[Bootstrap] ${glyph} ${check.name}${check.detail ? ` — ${check.detail}` : ''}`,
|
|
816
828
|
);
|
|
817
829
|
} else {
|
|
818
|
-
Logger.error(`[
|
|
830
|
+
Logger.error(`[Bootstrap] ✗ ${check.name}: ${check.remedy}`);
|
|
819
831
|
}
|
|
820
832
|
}
|
|
821
833
|
|
|
822
834
|
if (!result.ok) {
|
|
823
835
|
Logger.error(
|
|
824
|
-
'[
|
|
836
|
+
'[Bootstrap] Preflight failed. Resolve the issues above and re-run.',
|
|
825
837
|
);
|
|
826
838
|
return { ok: false, exit: 1 };
|
|
827
839
|
}
|
|
828
840
|
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
841
|
+
// The git-repo state is already reported by the (non-fatal) "Local git
|
|
842
|
+
// initialized" check above — both derive from the same
|
|
843
|
+
// `git rev-parse --is-inside-work-tree` probe — so there is no separate
|
|
844
|
+
// "git initialized" line here (it would duplicate, and contradict, that
|
|
845
|
+
// check). The boolean is still threaded through the payload for later phases.
|
|
832
846
|
return {
|
|
833
847
|
ok: true,
|
|
834
848
|
payload: { preflight: result, gitInitialized: result.gitInitialized },
|
|
@@ -844,15 +858,15 @@ function renderAnswerSummary(
|
|
|
844
858
|
visibility,
|
|
845
859
|
) {
|
|
846
860
|
const newRepoNote = creation.newRepo
|
|
847
|
-
? `
|
|
861
|
+
? ` will be created as ${visibility}`
|
|
848
862
|
: '';
|
|
849
863
|
const lines = [
|
|
850
|
-
'
|
|
864
|
+
'=== Review choices ===',
|
|
851
865
|
` Repo owner ${answers.owner}`,
|
|
852
866
|
` Username/handle ${answers.operatorHandle || '(none)'}`,
|
|
853
867
|
` Repo name ${answers.repo}${newRepoNote}`,
|
|
854
868
|
` Base branch ${answers.baseBranch}`,
|
|
855
|
-
` Project V2 name ${project.name}${creation.newProject ? '
|
|
869
|
+
` Project V2 name ${project.name}${creation.newProject ? ' will be created' : ''}`,
|
|
856
870
|
` Project V2 # ${project.number}`,
|
|
857
871
|
` Local git ${gitInitialized ? 'initialized' : 'will be initialized'}`,
|
|
858
872
|
];
|
|
@@ -943,7 +957,7 @@ export async function collectAndConfirm(state) {
|
|
|
943
957
|
});
|
|
944
958
|
if (missing.length > 0) {
|
|
945
959
|
Logger.error(
|
|
946
|
-
`[
|
|
960
|
+
`[Bootstrap] missing required answers: ${missing.join(', ')}`,
|
|
947
961
|
);
|
|
948
962
|
return { ok: false, exit: 1 };
|
|
949
963
|
}
|
|
@@ -968,7 +982,7 @@ export async function collectAndConfirm(state) {
|
|
|
968
982
|
);
|
|
969
983
|
const correct = await confirmYesNo('Is this correct?', state.interactive);
|
|
970
984
|
if (!correct) {
|
|
971
|
-
Logger.info('[
|
|
985
|
+
Logger.info('[Bootstrap] Okay — let’s try again.');
|
|
972
986
|
// Re-prompt everything on the next pass (drop silent-accept).
|
|
973
987
|
silentAccept = [];
|
|
974
988
|
continue;
|
|
@@ -982,7 +996,7 @@ export async function collectAndConfirm(state) {
|
|
|
982
996
|
);
|
|
983
997
|
if (!approved) {
|
|
984
998
|
Logger.error(
|
|
985
|
-
'[
|
|
999
|
+
'[Bootstrap] Creation declined — cannot continue without the repo/project. Exiting.',
|
|
986
1000
|
);
|
|
987
1001
|
return { ok: false, exit: 1 };
|
|
988
1002
|
}
|
|
@@ -1025,7 +1039,7 @@ function renderDryRunPlan(state) {
|
|
|
1025
1039
|
export function dryRunPlan(state) {
|
|
1026
1040
|
if (!state.flags['dry-run']) return { ok: true, payload: {} };
|
|
1027
1041
|
Logger.info(
|
|
1028
|
-
'[
|
|
1042
|
+
'[Bootstrap] --dry-run: no files, GitHub settings, or labels will be changed.',
|
|
1029
1043
|
);
|
|
1030
1044
|
Logger.info(renderDryRunPlan(state));
|
|
1031
1045
|
return { ok: false, exit: 0 };
|
|
@@ -1062,18 +1076,18 @@ export async function provisionResources(state, deps = {}) {
|
|
|
1062
1076
|
// 1. Local git — initialize + first commit when missing (idempotent).
|
|
1063
1077
|
const git = ensureGitInitialized(state);
|
|
1064
1078
|
if (!git.ok) {
|
|
1065
|
-
Logger.error(`[
|
|
1079
|
+
Logger.error(`[Bootstrap] git initialization failed: ${git.error}`);
|
|
1066
1080
|
return { ok: false, exit: 1 };
|
|
1067
1081
|
}
|
|
1068
1082
|
if (!git.initialized && !git.committed) {
|
|
1069
|
-
Logger.info('[
|
|
1083
|
+
Logger.info('[Bootstrap] git already initialized — leaving as-is.');
|
|
1070
1084
|
}
|
|
1071
1085
|
|
|
1072
1086
|
const { newRepo, newProject } = state.creation;
|
|
1073
1087
|
if (skipGithub) {
|
|
1074
1088
|
if (newRepo || newProject) {
|
|
1075
1089
|
Logger.info(
|
|
1076
|
-
'[
|
|
1090
|
+
'[Bootstrap] --skip-github set; not creating the GitHub repo/project.',
|
|
1077
1091
|
);
|
|
1078
1092
|
}
|
|
1079
1093
|
return { ok: true, payload: {} };
|
|
@@ -1108,7 +1122,7 @@ export async function provisionResources(state, deps = {}) {
|
|
|
1108
1122
|
}
|
|
1109
1123
|
|
|
1110
1124
|
if (!newRepo && !newProject) {
|
|
1111
|
-
Logger.info('[
|
|
1125
|
+
Logger.info('[Bootstrap] No new GitHub resources needed.');
|
|
1112
1126
|
}
|
|
1113
1127
|
|
|
1114
1128
|
// 4. Link the repo to the project board so issues/PRs surface on it
|
|
@@ -1125,7 +1139,7 @@ export async function provisionResources(state, deps = {}) {
|
|
|
1125
1139
|
*/
|
|
1126
1140
|
export async function executeBootstrap(state) {
|
|
1127
1141
|
Logger.info(
|
|
1128
|
-
`[
|
|
1142
|
+
`[Bootstrap] Starting project bootstrap at ${state.projectRoot} (owner=${state.answers.owner} repo=${state.answers.repo} base=${state.answers.baseBranch})`,
|
|
1129
1143
|
);
|
|
1130
1144
|
const approvedGroups = new Set(Object.values(PHASE_GROUPS));
|
|
1131
1145
|
const report = await applyProjectBootstrap({
|
|
@@ -1158,7 +1172,7 @@ export function persistProjectNumber(state) {
|
|
|
1158
1172
|
config = JSON.parse(fs.readFileSync(target, 'utf8'));
|
|
1159
1173
|
} catch (err) {
|
|
1160
1174
|
Logger.error(
|
|
1161
|
-
`[
|
|
1175
|
+
`[Bootstrap] Could not read ${target} to store projectNumber: ${err.message}`,
|
|
1162
1176
|
);
|
|
1163
1177
|
return { ok: true, payload: {} };
|
|
1164
1178
|
}
|
|
@@ -1172,14 +1186,14 @@ export function persistProjectNumber(state) {
|
|
|
1172
1186
|
}
|
|
1173
1187
|
config.github.projectNumber = Number(pn);
|
|
1174
1188
|
fs.writeFileSync(target, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
|
|
1175
|
-
Logger.info(`[
|
|
1189
|
+
Logger.info(`[Bootstrap] Stored github.projectNumber=${pn} in .agentrc.json`);
|
|
1176
1190
|
return { ok: true, payload: {} };
|
|
1177
1191
|
}
|
|
1178
1192
|
|
|
1179
1193
|
/** Step 6b — GitHub-side bootstrap. Honours `--skip-github`. */
|
|
1180
1194
|
export async function executeGithubBootstrap(state) {
|
|
1181
1195
|
if (state.flags['skip-github']) {
|
|
1182
|
-
Logger.info('[
|
|
1196
|
+
Logger.info('[Bootstrap] --skip-github set; skipping GitHub bootstrap.');
|
|
1183
1197
|
return { ok: true, payload: {} };
|
|
1184
1198
|
}
|
|
1185
1199
|
try {
|
|
@@ -1274,7 +1288,7 @@ export async function offerCommitPush(state, deps = {}) {
|
|
|
1274
1288
|
// Non-interactive (--assume-yes / no TTY): never push unprompted. Print the
|
|
1275
1289
|
// exact commands and leave the working tree untouched.
|
|
1276
1290
|
if (!state.interactive) {
|
|
1277
|
-
Logger.info(`\n[
|
|
1291
|
+
Logger.info(`\n[Bootstrap] ${instructions}`);
|
|
1278
1292
|
return { ok: true, payload: { commitPush: { action: 'instructed' } } };
|
|
1279
1293
|
}
|
|
1280
1294
|
|
|
@@ -1283,14 +1297,14 @@ export async function offerCommitPush(state, deps = {}) {
|
|
|
1283
1297
|
state.interactive,
|
|
1284
1298
|
);
|
|
1285
1299
|
if (!accepted) {
|
|
1286
|
-
Logger.info(`\n[
|
|
1300
|
+
Logger.info(`\n[Bootstrap] ${instructions}`);
|
|
1287
1301
|
return { ok: true, payload: { commitPush: { action: 'declined' } } };
|
|
1288
1302
|
}
|
|
1289
1303
|
|
|
1290
1304
|
const staged = stageBootstrapFiles({ projectRoot: cwd, runGit: runGitImpl });
|
|
1291
1305
|
if (!staged.ok) {
|
|
1292
|
-
Logger.warn(`[
|
|
1293
|
-
Logger.info(`\n[
|
|
1306
|
+
Logger.warn(`[Bootstrap] Could not stage the wiring: ${staged.error}`);
|
|
1307
|
+
Logger.info(`\n[Bootstrap] ${instructions}`);
|
|
1294
1308
|
return { ok: true, payload: { commitPush: { action: 'stage-failed' } } };
|
|
1295
1309
|
}
|
|
1296
1310
|
const commit = runGitImpl(
|
|
@@ -1300,20 +1314,20 @@ export async function offerCommitPush(state, deps = {}) {
|
|
|
1300
1314
|
if (!commit.ok) {
|
|
1301
1315
|
// A "nothing to commit" exit is benign — the wiring is already committed.
|
|
1302
1316
|
Logger.warn(
|
|
1303
|
-
`[
|
|
1317
|
+
`[Bootstrap] git commit did not create a commit (already committed?): ${commit.stderr || commit.stdout}`,
|
|
1304
1318
|
);
|
|
1305
|
-
Logger.info(`\n[
|
|
1319
|
+
Logger.info(`\n[Bootstrap] ${instructions}`);
|
|
1306
1320
|
return { ok: true, payload: { commitPush: { action: 'commit-skipped' } } };
|
|
1307
1321
|
}
|
|
1308
|
-
Logger.info('[
|
|
1322
|
+
Logger.info('[Bootstrap] Committed the Mandrel wiring.');
|
|
1309
1323
|
const push = runGitImpl(['push', '-u', 'origin', branch], cwd);
|
|
1310
1324
|
if (!push.ok) {
|
|
1311
1325
|
Logger.warn(
|
|
1312
|
-
`[
|
|
1326
|
+
`[Bootstrap] Commit landed but push of '${branch}' failed (push it manually with \`git push -u origin ${branch}\`): ${push.stderr}`,
|
|
1313
1327
|
);
|
|
1314
1328
|
return { ok: true, payload: { commitPush: { action: 'push-failed' } } };
|
|
1315
1329
|
}
|
|
1316
|
-
Logger.info(`[
|
|
1330
|
+
Logger.info(`[Bootstrap] Pushed '${branch}' to origin.`);
|
|
1317
1331
|
return { ok: true, payload: { commitPush: { action: 'committed-pushed' } } };
|
|
1318
1332
|
}
|
|
1319
1333
|
|
|
@@ -1357,7 +1371,7 @@ export async function main(argv = process.argv.slice(2), deps = {}) {
|
|
|
1357
1371
|
const githubError = result.state?.report?.github?.error;
|
|
1358
1372
|
if (githubError) {
|
|
1359
1373
|
Logger.error(
|
|
1360
|
-
`\n[
|
|
1374
|
+
`\n[Bootstrap] GitHub bootstrap failed: ${githubError}. ` +
|
|
1361
1375
|
'Project-side setup (labels are GitHub-side; the local .agentrc.json / ' +
|
|
1362
1376
|
'quality-gate / workflow files that were applied are recorded in the ' +
|
|
1363
1377
|
'install ledger) completed, but the GitHub label/board/views/protection ' +
|
|
@@ -1368,7 +1382,7 @@ export async function main(argv = process.argv.slice(2), deps = {}) {
|
|
|
1368
1382
|
return 1;
|
|
1369
1383
|
}
|
|
1370
1384
|
|
|
1371
|
-
Logger.info('\n[
|
|
1385
|
+
Logger.info('\n[Bootstrap] Done.');
|
|
1372
1386
|
return 0;
|
|
1373
1387
|
}
|
|
1374
1388
|
|
|
@@ -126,7 +126,7 @@ export async function applyBranchProtection({
|
|
|
126
126
|
|
|
127
127
|
if (enforce === false) {
|
|
128
128
|
log(
|
|
129
|
-
`[
|
|
129
|
+
`[Bootstrap] Branch protection on '${baseBranch}': skipped (github.branchProtection.enforce=false).`,
|
|
130
130
|
);
|
|
131
131
|
return { status: 'skipped', reason: 'opt-out' };
|
|
132
132
|
}
|
|
@@ -136,7 +136,7 @@ export async function applyBranchProtection({
|
|
|
136
136
|
.filter((n) => typeof n === 'string' && n.length > 0);
|
|
137
137
|
if (checkNames.length === 0) {
|
|
138
138
|
log(
|
|
139
|
-
`[
|
|
139
|
+
`[Bootstrap] Branch protection on '${baseBranch}': skipped (no github.branchProtection.requiredChecks configured).`,
|
|
140
140
|
);
|
|
141
141
|
return { status: 'skipped', reason: 'no-checks' };
|
|
142
142
|
}
|
|
@@ -152,12 +152,12 @@ export async function applyBranchProtection({
|
|
|
152
152
|
exists = await provider.branchExists(baseBranch);
|
|
153
153
|
} catch (err) {
|
|
154
154
|
log(
|
|
155
|
-
`[
|
|
155
|
+
`[Bootstrap] Branch protection on '${baseBranch}': existence probe failed — ${err.message}. Proceeding with the write attempt.`,
|
|
156
156
|
);
|
|
157
157
|
}
|
|
158
158
|
if (!exists) {
|
|
159
159
|
log(
|
|
160
|
-
`[
|
|
160
|
+
`[Bootstrap] Branch protection on '${baseBranch}': skipped (base branch does not exist on the remote — push an initial commit first).`,
|
|
161
161
|
);
|
|
162
162
|
return { status: 'skipped', reason: 'no-base-branch' };
|
|
163
163
|
}
|
|
@@ -169,7 +169,7 @@ export async function applyBranchProtection({
|
|
|
169
169
|
current = probe?.enabled ? (probe.raw ?? null) : null;
|
|
170
170
|
} catch (err) {
|
|
171
171
|
log(
|
|
172
|
-
`[
|
|
172
|
+
`[Bootstrap] Branch protection on '${baseBranch}': read failed — ${err.message}. Proceeding as if no rule exists.`,
|
|
173
173
|
);
|
|
174
174
|
}
|
|
175
175
|
|
|
@@ -193,7 +193,7 @@ export async function applyBranchProtection({
|
|
|
193
193
|
: false;
|
|
194
194
|
if (!approved) {
|
|
195
195
|
log(
|
|
196
|
-
`[
|
|
196
|
+
`[Bootstrap] Branch protection on '${baseBranch}': diverges from framework stance; HITL declined / non-TTY — leaving the operator's rule untouched.`,
|
|
197
197
|
);
|
|
198
198
|
return { status: 'skipped', reason: 'hitl-declined', diff };
|
|
199
199
|
}
|
|
@@ -210,12 +210,12 @@ export async function applyBranchProtection({
|
|
|
210
210
|
? ` (added: ${result.added.join(', ')})`
|
|
211
211
|
: ' (all required checks already present)';
|
|
212
212
|
log(
|
|
213
|
-
`[
|
|
213
|
+
`[Bootstrap] Branch protection on '${baseBranch}': ${verb} rule${addedSuffix}.`,
|
|
214
214
|
);
|
|
215
215
|
return { status: result.created ? 'created' : 'merged', ...result };
|
|
216
216
|
} catch (err) {
|
|
217
217
|
log(
|
|
218
|
-
`[
|
|
218
|
+
`[Bootstrap] Branch protection on '${baseBranch}': failed — ${err.message}. Proceeding without it.`,
|
|
219
219
|
);
|
|
220
220
|
return { status: 'failed', reason: err.message };
|
|
221
221
|
}
|
|
@@ -253,16 +253,16 @@ export async function checkProjectScopes(opts = {}) {
|
|
|
253
253
|
function classifyProjectScopes(scopeLine) {
|
|
254
254
|
if (!scopeLine) {
|
|
255
255
|
return {
|
|
256
|
-
name: '
|
|
256
|
+
name: 'GitHub Projects V2 access',
|
|
257
257
|
ok: true,
|
|
258
258
|
detail: GH_SCOPES_UNREADABLE_NOTE,
|
|
259
259
|
};
|
|
260
260
|
}
|
|
261
261
|
if (/\bproject\b/i.test(scopeLine[1])) {
|
|
262
|
-
return { name: '
|
|
262
|
+
return { name: 'GitHub Projects V2 access', ok: true };
|
|
263
263
|
}
|
|
264
264
|
return {
|
|
265
|
-
name: '
|
|
265
|
+
name: 'GitHub Projects V2 access',
|
|
266
266
|
ok: true,
|
|
267
267
|
detail: GH_PROJECT_SCOPE_NOTE,
|
|
268
268
|
};
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
import { createInterface } from 'node:readline';
|
|
27
27
|
|
|
28
28
|
const ABORT_MESSAGE =
|
|
29
|
-
'[
|
|
29
|
+
'[Bootstrap] aborting: no TTY available for HITL confirm (opt in with --approve-github-admin for GitHub-admin mutations, or --assume-yes to accept every phase group)';
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* @param {object} args
|
|
@@ -61,7 +61,7 @@ export async function confirm({ summary, current, proposed }, opts = {}) {
|
|
|
61
61
|
// Render the diff. Single-line summary, then a JSON block so the
|
|
62
62
|
// operator can pipe the prompt to a logger and still recover the
|
|
63
63
|
// structured shape.
|
|
64
|
-
stdout.write(`\
|
|
64
|
+
stdout.write(`\nHITL confirm: ${summary}\n`);
|
|
65
65
|
stdout.write(
|
|
66
66
|
` current: ${JSON.stringify(current ?? null, null, 2)
|
|
67
67
|
.split('\n')
|
|
@@ -74,13 +74,13 @@ export async function applyMergeMethods({
|
|
|
74
74
|
try {
|
|
75
75
|
current = (await provider.getMergeMethods()) ?? {};
|
|
76
76
|
} catch (err) {
|
|
77
|
-
log(`[
|
|
77
|
+
log(`[Bootstrap] Merge methods: read failed — ${err.message}.`);
|
|
78
78
|
return { status: 'failed', reason: err.message };
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
const diff = diffMergeMethods(current, target);
|
|
82
82
|
if (!diff) {
|
|
83
|
-
log('[
|
|
83
|
+
log('[Bootstrap] Merge methods: already at target stance (no-op).');
|
|
84
84
|
return { status: 'unchanged' };
|
|
85
85
|
}
|
|
86
86
|
|
|
@@ -94,8 +94,8 @@ export async function applyMergeMethods({
|
|
|
94
94
|
});
|
|
95
95
|
if (!approved) {
|
|
96
96
|
log(
|
|
97
|
-
'[
|
|
98
|
-
'
|
|
97
|
+
'[Bootstrap] Merge methods: HITL declined — leaving operator settings untouched\n\n' +
|
|
98
|
+
'Note: auto-merge will remain disabled until the merge-method ' +
|
|
99
99
|
'settings match the framework stance (allow_squash_merge: true, ' +
|
|
100
100
|
'allow_auto_merge: true, delete_branch_on_merge: true).',
|
|
101
101
|
);
|
|
@@ -105,7 +105,7 @@ export async function applyMergeMethods({
|
|
|
105
105
|
// Non-TTY: no operator present to confirm. Default-apply the framework
|
|
106
106
|
// stance and log explicitly so the consequence is never silent.
|
|
107
107
|
log(
|
|
108
|
-
'[
|
|
108
|
+
'[Bootstrap] Merge methods: non-TTY — applying framework stance automatically ' +
|
|
109
109
|
'(allow_squash_merge, allow_auto_merge, delete_branch_on_merge). ' +
|
|
110
110
|
'To opt out, pass a hitlConfirm gate or set github.mergeMethods overrides in .agentrc.json.',
|
|
111
111
|
);
|
|
@@ -114,10 +114,10 @@ export async function applyMergeMethods({
|
|
|
114
114
|
|
|
115
115
|
try {
|
|
116
116
|
const result = await provider.setMergeMethods(target);
|
|
117
|
-
log(`[
|
|
117
|
+
log(`[Bootstrap] Merge methods: patched (${result.patched.join(', ')}).`);
|
|
118
118
|
return { status: 'patched', ...result, diff };
|
|
119
119
|
} catch (err) {
|
|
120
|
-
log(`[
|
|
120
|
+
log(`[Bootstrap] Merge methods: PATCH failed — ${err.message}.`);
|
|
121
121
|
return { status: 'failed', reason: err.message };
|
|
122
122
|
}
|
|
123
123
|
}
|
|
@@ -59,8 +59,8 @@ function defaultGitRunner(args) {
|
|
|
59
59
|
*/
|
|
60
60
|
function checkNode(nodeCheck) {
|
|
61
61
|
const result = nodeCheck();
|
|
62
|
-
if (result.ok) return { name: '
|
|
63
|
-
return { name: '
|
|
62
|
+
if (result.ok) return { name: 'Node version', ok: true };
|
|
63
|
+
return { name: 'Node version', ok: false, remedy: NODE_REMEDY(result) };
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
/**
|
|
@@ -73,19 +73,19 @@ function checkNode(nodeCheck) {
|
|
|
73
73
|
function checkGitAvailable(gitRunner) {
|
|
74
74
|
const result = gitRunner(['--version']);
|
|
75
75
|
if (result.error?.code === 'ENOENT') {
|
|
76
|
-
return { name: '
|
|
76
|
+
return { name: 'Git installed', ok: false, remedy: GIT_INSTALL_HINT };
|
|
77
77
|
}
|
|
78
78
|
if (result.status !== 0) {
|
|
79
79
|
const snippet = (result.stderr || '').trim().slice(0, 200);
|
|
80
80
|
return {
|
|
81
|
-
name: '
|
|
81
|
+
name: 'Git installed',
|
|
82
82
|
ok: false,
|
|
83
83
|
remedy: `git --version failed (exit ${result.status})${
|
|
84
84
|
snippet ? `: ${snippet}` : ''
|
|
85
85
|
}. ${GIT_INSTALL_HINT}`,
|
|
86
86
|
};
|
|
87
87
|
}
|
|
88
|
-
return { name: '
|
|
88
|
+
return { name: 'Git installed', ok: true };
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
/**
|
|
@@ -99,9 +99,13 @@ function checkGitAvailable(gitRunner) {
|
|
|
99
99
|
function checkInsideWorkTree(gitRunner) {
|
|
100
100
|
const result = gitRunner(['rev-parse', '--is-inside-work-tree']);
|
|
101
101
|
if (result.status === 0 && result.stdout.trim() === 'true') {
|
|
102
|
-
return { name: 'git
|
|
102
|
+
return { name: 'Local git initialized', ok: true };
|
|
103
103
|
}
|
|
104
|
-
return {
|
|
104
|
+
return {
|
|
105
|
+
name: 'Local git initialized',
|
|
106
|
+
ok: false,
|
|
107
|
+
remedy: GIT_WORKTREE_HINT,
|
|
108
|
+
};
|
|
105
109
|
}
|
|
106
110
|
|
|
107
111
|
/**
|
|
@@ -115,9 +119,9 @@ function checkInsideWorkTree(gitRunner) {
|
|
|
115
119
|
async function checkGh(gh) {
|
|
116
120
|
try {
|
|
117
121
|
await gh();
|
|
118
|
-
return { name: '
|
|
122
|
+
return { name: 'GitHub CLI', ok: true };
|
|
119
123
|
} catch (err) {
|
|
120
|
-
return { name: '
|
|
124
|
+
return { name: 'GitHub CLI', ok: false, remedy: err.message };
|
|
121
125
|
}
|
|
122
126
|
}
|
|
123
127
|
|
|
@@ -170,15 +174,14 @@ export async function runPreflight(opts = {}) {
|
|
|
170
174
|
checks.push(workTree);
|
|
171
175
|
} else {
|
|
172
176
|
// Non-fatal detection: record whether we are inside a git repo but
|
|
173
|
-
// never fail the gate on it —
|
|
174
|
-
//
|
|
177
|
+
// never fail the gate on it — bootstrap initialises git in a later
|
|
178
|
+
// phase. `ok` stays true so the gate passes; the rendered glyph is
|
|
179
|
+
// driven off `gitInitialized` (✓ when a repo exists, ✗ when not) so the
|
|
180
|
+
// operator sees the real state without the run aborting.
|
|
175
181
|
checks.push({
|
|
176
|
-
name: 'git
|
|
182
|
+
name: 'Local git initialized',
|
|
177
183
|
ok: true,
|
|
178
184
|
gitInitialized,
|
|
179
|
-
detail: gitInitialized
|
|
180
|
-
? 'inside a git work tree'
|
|
181
|
-
: 'not a git repo yet — will be collected in later steps',
|
|
182
185
|
});
|
|
183
186
|
}
|
|
184
187
|
}
|