mandrel 1.73.0 → 1.75.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/README.md CHANGED
@@ -33,7 +33,7 @@ It then shows a **two-option prompt**:
33
33
 
34
34
  1. **Configure now** — runs `node .agents/scripts/bootstrap.js`, forwarding any
35
35
  passthrough flags unchanged, to wire the project and GitHub side (creates the
36
- GitHub repo + Projects board).
36
+ GitHub repo; board decoration and Issue Forms are opt-in — see below).
37
37
  2. **Just the files** — stops after materialization and prints a re-run hint
38
38
  (`mandrel init`) so you can configure later.
39
39
 
@@ -100,9 +100,13 @@ The bootstrap pipeline, in order:
100
100
  `.claude/commands/` tree so every `/<command>` loads), wires the system
101
101
  prompt (see below), gitignores derived artefacts, and runs the
102
102
  quality-gates installer.
103
- 4. **GitHub-side mutations.** Creates the label taxonomy, Project V2
104
- fields, branch protection, and merge-method settings. Skipped with
105
- `--skip-github`.
103
+ 4. **GitHub-side mutations.** Creates the label taxonomy, branch protection,
104
+ and merge-method settings. Skipped with `--skip-github`. Two additional
105
+ mutations are **opt-in** (prompted y/N, defaulting No, or passed as flags):
106
+ - `--with-project-board` — provision the Projects V2 Status field and
107
+ custom fields on an existing board.
108
+ - `--with-issue-forms` — generate `.github/ISSUE_TEMPLATE/story.yml` and
109
+ `epic.yml` from the ticket-body schema.
106
110
 
107
111
  The bootstrap is idempotent — safe to re-run; an already-configured
108
112
  clone produces zero file mutations.
@@ -184,7 +188,7 @@ Reverses a recorded install using the install ledger
184
188
  (`.agents/.install-manifest.json`). Each ledger entry is a
185
189
  mutation-manifest record; uninstall walks reversible entries and undoes
186
190
  exactly what the install applied, without touching pre-existing operator
187
- content. GitHub-side state (labels, branch protection, Projects board)
191
+ content. GitHub-side state (labels, branch protection, project board fields)
188
192
  requires manual reversal and is surfaced as a follow-up checklist.
189
193
 
190
194
  ```bash
@@ -76,6 +76,15 @@ environment variables that override project defaults. The config resolver
76
76
  deep-merges `.agentrc.local.json` over `.agentrc.json` (local wins; absent
77
77
  local file is a no-op). Do not modify these local files unless requested.
78
78
 
79
+ **Durable slash commands.** Any `.md` file placed at
80
+ `.agents/local/workflows/<name>.md` is automatically projected into
81
+ `.claude/commands/<name>.md` by `sync-claude-commands.js`, making it
82
+ invocable as `/<name>`. Because the entire `.agents/local/` subtree is
83
+ exempt from `mandrel sync`'s prune pass, these commands survive
84
+ `npm install`, `mandrel sync`, and `mandrel update` with no manual
85
+ re-sync. Core payload commands of the same basename always win (the
86
+ local copy is ignored with a `shadowed` warning).
87
+
79
88
  ### F. Modular Global Rules
80
89
 
81
90
  Before writing code or documentation, verify if any domain-agnostic rules
@@ -43,7 +43,6 @@ import { Logger } from './lib/Logger.js';
43
43
  import {
44
44
  LABEL_TAXONOMY,
45
45
  PROJECT_FIELD_DEFS,
46
- PROJECT_VIEW_DEFS,
47
46
  STATUS_FIELD_OPTIONS,
48
47
  } from './lib/label-taxonomy.js';
49
48
  import { createProvider } from './lib/provider-factory.js';
@@ -154,25 +153,6 @@ async function ensureStatusField(provider, log) {
154
153
  }
155
154
  }
156
155
 
157
- async function ensureViews(provider, log) {
158
- try {
159
- const views = await provider.ensureProjectViews(PROJECT_VIEW_DEFS);
160
- if (views.unavailable) {
161
- log(
162
- `[Bootstrap] Projects V2 Views unavailable — skipped ${views.skipped.join(', ')}.${views.error ? ` (${views.error})` : ''} ${PROJECTS_DOC_POINTER}`,
163
- );
164
- } else {
165
- log(
166
- `[Bootstrap] Views — created: ${views.created.length}, skipped: ${views.skipped.length}`,
167
- );
168
- }
169
- return views;
170
- } catch (err) {
171
- log(`[Bootstrap] Views provisioning failed: ${err.message}`);
172
- return { created: [], skipped: [], unavailable: false };
173
- }
174
- }
175
-
176
156
  /**
177
157
  * Audit the project's built-in workflows and, when explicitly opted-in
178
158
  * via `--reap-conflicting-workflows`, delete the ones that race against
@@ -283,10 +263,15 @@ async function ensureProjectFields(provider, project, log) {
283
263
  * github?: object,
284
264
  * baseBranch?: string,
285
265
  * githubAdminApproved?: boolean,
266
+ * withProjectBoard?: boolean,
286
267
  * isTTY?: boolean,
287
268
  * }} [opts] - `githubAdminApproved` MUST be `true` for any GitHub mutation to
288
269
  * occur; any other value (absent / `false`) is treated as "not approved"
289
270
  * and the run is a verified no-op.
271
+ * `withProjectBoard` (default `false`) — opt-in for Projects V2 board,
272
+ * Status field, custom fields, and workflow audit. When absent or `false`,
273
+ * the board decoration is skipped and only labels + branch protection +
274
+ * merge methods are provisioned.
290
275
  */
291
276
  export async function runBootstrap(config, opts = {}) {
292
277
  // Explicit opt-in gate (Story #3526). Default-deny: absent or non-`true`
@@ -316,36 +301,46 @@ export async function runBootstrap(config, opts = {}) {
316
301
  log('[Bootstrap] API access verified.');
317
302
 
318
303
  const labels = await ensureLabels(provider, log);
319
- const project = await resolveProject(provider, providerConfig, log);
320
304
 
321
- const projectReady = !project.skipped && project.projectNumber;
305
+ // Board decoration (Projects V2 board, Status field, custom fields, workflow
306
+ // audit) is opt-in and defaults OFF. Minimal install = labels only. Gate is
307
+ // `opts.withProjectBoard === true`; absent or false skips all board work.
308
+ // ColumnSync already soft-noops when projectNumber is unset (column-sync.js:19-23).
309
+ const projectBoard = opts.withProjectBoard === true;
310
+ let project = { projectNumber: null, created: false, skipped: true };
322
311
  let statusField = { status: 'skipped', added: [] };
323
- let views = { created: [], skipped: [], unavailable: false };
324
312
  let fields = { created: [], skipped: [] };
313
+ let workflowAudit = {
314
+ skipped: true,
315
+ reason: 'board-decoration-not-opted-in',
316
+ };
325
317
 
326
- if (projectReady) {
327
- statusField = await ensureStatusField(provider, log);
328
- views = await ensureViews(provider, log);
329
- fields = await ensureProjectFields(provider, project, log);
330
- } else {
331
- log('[Bootstrap] No active project — skipping legacy project-field setup.');
332
- }
333
-
334
- // Story #2845 audit project workflows for the ones that race against
335
- // the orchestrator's ColumnSync writes (notably `Pull request merged`
336
- // and `Pull request linked to issue`, which both rewrite Status as a
337
- // side-effect of auto-merge). When `--reap-conflicting-workflows` is
338
- // set, also delete the offenders via `deleteProjectV2Workflow` (the
339
- // only programmatic action GraphQL exposes today — `enabled` is
340
- // read-only).
341
- const workflowAudit = projectReady
342
- ? await auditAndOptionallyReapWorkflows(
318
+ if (projectBoard) {
319
+ project = await resolveProject(provider, providerConfig, log);
320
+ const projectReady = !project.skipped && project.projectNumber;
321
+ if (projectReady) {
322
+ statusField = await ensureStatusField(provider, log);
323
+ fields = await ensureProjectFields(provider, project, log);
324
+ // Story #2845 — audit project workflows for the ones that race against
325
+ // the orchestrator's ColumnSync writes (notably `Pull request merged`
326
+ // and `Pull request linked to issue`, which both rewrite Status as a
327
+ // side-effect of auto-merge). When `--reap-conflicting-workflows` is
328
+ // set, also delete the offenders via `deleteProjectV2Workflow` (the
329
+ // only programmatic action GraphQL exposes today — `enabled` is
330
+ // read-only).
331
+ workflowAudit = await auditAndOptionallyReapWorkflows(
343
332
  provider,
344
333
  project.projectNumber,
345
334
  opts.reapConflictingWorkflows === true,
346
335
  log,
347
- )
348
- : { skipped: true, reason: 'no-project' };
336
+ );
337
+ } else {
338
+ log('[Bootstrap] No active project — skipping project-field setup.');
339
+ workflowAudit = { skipped: true, reason: 'no-project' };
340
+ }
341
+ } else {
342
+ log('[Bootstrap] Project board decoration skipped (opt-in not set).');
343
+ }
349
344
 
350
345
  // Consumer-facing bootstrap promotes the framework's CI-gates-only
351
346
  // stance: branch protection with enforce_admins + 0-approval-count and
@@ -405,7 +400,6 @@ export async function runBootstrap(config, opts = {}) {
405
400
  fields,
406
401
  project,
407
402
  statusField,
408
- views,
409
403
  workflowAudit,
410
404
  branchProtection,
411
405
  mergeMethods,
@@ -490,6 +484,10 @@ async function main() {
490
484
  // invocation never silently reconfigures branch protection or merge methods.
491
485
  const githubAdminApproved =
492
486
  assumeYes || process.argv.includes('--approve-github-admin');
487
+ // Story #4234 — Board decoration is opt-in (default off). Pass
488
+ // `--with-project-board` to also provision the Projects V2 board, Status
489
+ // field, and custom fields.
490
+ const withProjectBoard = process.argv.includes('--with-project-board');
493
491
 
494
492
  try {
495
493
  const result = await runBootstrap(config, {
@@ -499,6 +497,7 @@ async function main() {
499
497
  assumeNo,
500
498
  reapConflictingWorkflows,
501
499
  githubAdminApproved,
500
+ withProjectBoard,
502
501
  });
503
502
  // A non-approved run returns the skip envelope (no full result shape);
504
503
  // the skip line is already logged inside runBootstrap, so render the
@@ -37,8 +37,16 @@
37
37
  * (labels, Projects V2, branch protection, merge
38
38
  * methods) without accepting every other default.
39
39
  * --skip-github Skip the GitHub-side bootstrap entirely
40
- * --skip-quality Skip the quality-gates bootstrap
40
+ * --with-quality Opt-in: install local quality gates (pre-commit
41
+ * hook + quality:preview/watch scripts). Off by
42
+ * default — prompted y/N.
41
43
  * --dry-run Collect info and print the plan; change nothing
44
+ * --with-project-board Opt-in: provision the Projects V2 Status field
45
+ * and custom fields. Off by default — the project
46
+ * board object is still created when a project
47
+ * name is supplied, but decoration is skipped.
48
+ * --with-issue-forms Opt-in: generate .github/ISSUE_TEMPLATE/story.yml
49
+ * and epic.yml. Off by default.
42
50
  * --reap-conflicting-workflows Delete Projects V2 built-in workflows that
43
51
  * race against the orchestrator (destructive)
44
52
  * --help Print this help
@@ -97,8 +105,14 @@ Flags:
97
105
  (labels, Projects V2, branch protection, merge
98
106
  methods) without accepting every other default.
99
107
  --skip-github Skip the GitHub-side bootstrap entirely
100
- --skip-quality Skip the quality-gates bootstrap
108
+ --with-quality Opt-in: install local quality gates (pre-commit
109
+ hook + quality:preview/watch scripts).
110
+ (default: off — prompted y/N).
101
111
  --dry-run Collect info and print the plan; change nothing
112
+ --with-project-board Opt-in: provision the Projects V2 Status field
113
+ and custom fields (default: off — prompted y/N).
114
+ --with-issue-forms Opt-in: generate .github/ISSUE_TEMPLATE/story.yml
115
+ and epic.yml (default: off — prompted y/N).
102
116
  --reap-conflicting-workflows Delete Projects V2 built-in workflows that
103
117
  race against the orchestrator (destructive)
104
118
  --help Print this help
@@ -157,16 +171,24 @@ export function resolveOwnerForPicker(defaults, flags, env = process.env) {
157
171
  return null;
158
172
  }
159
173
 
160
- /** Ask a yes/no question. Non-interactive runs auto-accept (return true). */
161
- async function confirmYesNo(message, interactive) {
162
- if (!interactive) return true;
174
+ /**
175
+ * Ask a yes/no question. `defaultAnswer` controls the default when the
176
+ * operator presses Enter without typing (true = Y/n, false = y/N). In
177
+ * non-interactive mode the default is returned immediately.
178
+ */
179
+ async function confirmYesNo(message, interactive, defaultAnswer = true) {
180
+ if (!interactive) return defaultAnswer;
163
181
  const rl = readline.createInterface({
164
182
  input: process.stdin,
165
183
  output: process.stdout,
166
184
  });
167
185
  try {
168
- const raw = (await rl.question(`${message} [Y/n]: `)).trim().toLowerCase();
169
- return raw === '' || raw === 'y' || raw === 'yes';
186
+ const hint = defaultAnswer ? '[Y/n]' : '[y/N]';
187
+ const raw = (await rl.question(`${message} ${hint}: `))
188
+ .trim()
189
+ .toLowerCase();
190
+ if (raw === '') return defaultAnswer;
191
+ return raw === 'y' || raw === 'yes';
170
192
  } finally {
171
193
  rl.close();
172
194
  }
@@ -688,6 +710,9 @@ async function runGithubBootstrap(answers, opts) {
688
710
  // interactive operator confirmation, `--assume-yes`, or
689
711
  // `--approve-github-admin`. Default-deny at the boundary gate when absent.
690
712
  githubAdminApproved: opts.githubAdminApproved === true,
713
+ // Opt-in: provision Status field + custom fields on the project board.
714
+ // Default off — prompted y/N during collect/confirm or via --with-project-board.
715
+ withProjectBoard: opts.withProjectBoard === true,
691
716
  // Opt-in: delete the Projects V2 built-in workflows that race against the
692
717
  // orchestrator's ColumnSync (e.g. "Pull request merged"). Off by default.
693
718
  reapConflictingWorkflows: Boolean(opts.reapConflictingWorkflows),
@@ -1011,7 +1036,48 @@ export async function collectAndConfirm(state) {
1011
1036
  return { ok: false, exit: 1 };
1012
1037
  }
1013
1038
  }
1014
- return { ok: true, payload: { answers, creation } };
1039
+
1040
+ // Opt-in: board decoration (Status field, custom fields). Default off.
1041
+ // Dry-run halts immediately after this step — resolve without prompting.
1042
+ let withProjectBoard = Boolean(state.flags['with-project-board']);
1043
+ if (!state.flags['dry-run'] && !withProjectBoard) {
1044
+ withProjectBoard = await confirmYesNo(
1045
+ 'Set up project board fields (Status, custom)?',
1046
+ state.interactive,
1047
+ false,
1048
+ );
1049
+ }
1050
+
1051
+ // Opt-in: GitHub Issue Form templates. Default off.
1052
+ let withIssueForms = Boolean(state.flags['with-issue-forms']);
1053
+ if (!state.flags['dry-run'] && !withIssueForms) {
1054
+ withIssueForms = await confirmYesNo(
1055
+ 'Generate GitHub Issue Form templates?',
1056
+ state.interactive,
1057
+ false,
1058
+ );
1059
+ }
1060
+
1061
+ // Opt-in: local quality gates. Default off.
1062
+ let withQuality = Boolean(state.flags['with-quality']);
1063
+ if (!state.flags['dry-run'] && !withQuality) {
1064
+ withQuality = await confirmYesNo(
1065
+ 'Install local quality gates (pre-commit hook + quality:preview/watch scripts)?',
1066
+ state.interactive,
1067
+ false,
1068
+ );
1069
+ }
1070
+
1071
+ return {
1072
+ ok: true,
1073
+ payload: {
1074
+ answers,
1075
+ creation,
1076
+ withProjectBoard,
1077
+ withIssueForms,
1078
+ withQuality,
1079
+ },
1080
+ };
1015
1081
  }
1016
1082
  }
1017
1083
 
@@ -1157,7 +1223,8 @@ export async function executeBootstrap(state) {
1157
1223
  agentRoot: state.agentRoot,
1158
1224
  answers: state.answers,
1159
1225
  approvedGroups,
1160
- skipQuality: Boolean(state.flags['skip-quality']),
1226
+ withQuality: state.withQuality === true,
1227
+ withIssueForms: state.withIssueForms === true,
1161
1228
  });
1162
1229
  return { ok: true, payload: { report, approvedGroups } };
1163
1230
  }
@@ -1210,6 +1277,7 @@ export async function executeGithubBootstrap(state) {
1210
1277
  state.report.github = await runGithubBootstrap(state.answers, {
1211
1278
  assumeYes: state.assumeYes,
1212
1279
  githubAdminApproved: state.githubAdminApproved === true,
1280
+ withProjectBoard: state.withProjectBoard === true,
1213
1281
  reapConflictingWorkflows: Boolean(
1214
1282
  state.flags['reap-conflicting-workflows'],
1215
1283
  ),
@@ -1232,7 +1300,7 @@ export function recordLedger(state) {
1232
1300
  const manifestCtx = {
1233
1301
  answers: state.answers,
1234
1302
  skipGithub: Boolean(state.flags['skip-github']),
1235
- skipQuality: Boolean(state.flags['skip-quality']),
1303
+ withQuality: state.withQuality === true,
1236
1304
  };
1237
1305
  const entries = buildMutationManifest(manifestCtx).filter((e) =>
1238
1306
  appliedGroups.has(e.phaseGroup),
@@ -1384,7 +1452,7 @@ export async function main(argv = process.argv.slice(2), deps = {}) {
1384
1452
  `\n[Bootstrap] GitHub bootstrap failed: ${githubError}. ` +
1385
1453
  'Project-side setup (labels are GitHub-side; the local .agentrc.json / ' +
1386
1454
  'quality-gate / workflow files that were applied are recorded in the ' +
1387
- 'install ledger) completed, but the GitHub label/board/views/protection ' +
1455
+ 'install ledger) completed, but the GitHub label/board/protection ' +
1388
1456
  'setup did not. Resolve the cause above (commonly `gh auth login` or a ' +
1389
1457
  'missing repo/project scope) and re-run `mandrel bootstrap` — the run is ' +
1390
1458
  'idempotent and will skip what already succeeded.',
@@ -94,14 +94,14 @@ export const MANIFEST_ENTRY_FIELDS = Object.freeze([
94
94
  * platform; remote targets are scoped to the resolved `owner/repo` slug.
95
95
  *
96
96
  * The `github-admin` group is omitted when `ctx.skipGithub` is set, and the
97
- * `quality-gates` group is omitted when `ctx.skipQuality` is set, so the
98
- * preview reflects the same flags the executing pipeline honours.
97
+ * `quality-gates` group is included only when `ctx.withQuality` is true, so
98
+ * the preview reflects the same flags the executing pipeline honours.
99
99
  *
100
100
  * @param {object} [ctx]
101
101
  * @param {{ owner?: string, repo?: string }} [ctx.answers] — scopes the
102
102
  * `github-admin` targets to the `owner/repo` slug.
103
103
  * @param {boolean} [ctx.skipGithub] — omit the `github-admin` group.
104
- * @param {boolean} [ctx.skipQuality] — omit the `quality-gates` group.
104
+ * @param {boolean} [ctx.withQuality] — include the `quality-gates` group.
105
105
  * @returns {MutationManifestEntry[]}
106
106
  */
107
107
  export function buildMutationManifest(ctx = {}) {
@@ -189,8 +189,8 @@ export function buildMutationManifest(ctx = {}) {
189
189
 
190
190
  // --- quality-gates ----------------------------------------------------
191
191
  // Stabilized quality-gate surface (husky pre-commit, quality npm
192
- // scripts, .agentrc quality defaults).
193
- if (!ctx.skipQuality) {
192
+ // scripts, .agentrc quality defaults). Included only when opted in.
193
+ if (ctx.withQuality) {
194
194
  entries.push(
195
195
  {
196
196
  phaseGroup: PHASE_GROUPS.QUALITY_GATES,
@@ -724,7 +724,10 @@ export const BOOTSTRAP_PHASES = Object.freeze([
724
724
  {
725
725
  name: 'issueForms',
726
726
  phaseGroup: PHASE_GROUPS.REPO_CONFIG,
727
- run: (ctx) => ensureIssueFormsPhase(ctx),
727
+ run: (ctx) =>
728
+ ctx.withIssueForms === true
729
+ ? ensureIssueFormsPhase(ctx)
730
+ : { skipped: true, reason: 'issue-forms-not-opted-in' },
728
731
  },
729
732
  {
730
733
  name: 'sync',
@@ -742,9 +745,9 @@ export const BOOTSTRAP_PHASES = Object.freeze([
742
745
  name: 'quality',
743
746
  phaseGroup: PHASE_GROUPS.QUALITY_GATES,
744
747
  run: (ctx) =>
745
- ctx.skipQuality
746
- ? { skipped: true }
747
- : applyQualityBootstrap({ projectRoot: ctx.projectRoot }),
748
+ ctx.withQuality === true
749
+ ? applyQualityBootstrap({ projectRoot: ctx.projectRoot })
750
+ : { skipped: true, reason: 'quality-not-opted-in' },
748
751
  },
749
752
  {
750
753
  name: 'winPerf',
@@ -844,7 +847,7 @@ export async function runPhases(phases, ctx) {
844
847
  * @param {Set<string>} [ctx.approvedGroups] — when present, only phases
845
848
  * whose `phaseGroup` is in this set execute (the consent-first gate from
846
849
  * Story #3524); always-run infrastructure phases ignore it.
847
- * @param {boolean} [ctx.skipQuality]
850
+ * @param {boolean} [ctx.withQuality]
848
851
  * @param {boolean} [ctx.skipGithub]
849
852
  * @param {boolean} [ctx.skipInstall]
850
853
  * @param {boolean} [ctx.quiet]
@@ -41,7 +41,7 @@ export const KNOWN_FLAGS = Object.freeze({
41
41
  'assume-yes',
42
42
  'approve-github-admin',
43
43
  'skip-github',
44
- 'skip-quality',
44
+ 'with-quality',
45
45
  'help',
46
46
  'dry-run',
47
47
  'reap-conflicting-workflows',
@@ -57,12 +57,6 @@ export function printSummary(result) {
57
57
  Logger.info(`Fields skipped: ${result.fields.skipped.length}`);
58
58
  Logger.info(`Project: ${formatProjectSummary(result.project)}`);
59
59
  Logger.info(`Status field: ${result.statusField.status}`);
60
- const unavailableSuffix = result.views.unavailable
61
- ? ' (mutation unavailable)'
62
- : '';
63
- Logger.info(
64
- `Views — created: ${result.views.created.length}, skipped: ${result.views.skipped.length}${unavailableSuffix}`,
65
- );
66
60
  Logger.info(
67
61
  `Workflow audit: ${formatWorkflowAuditSummary(result.workflowAudit)}`,
68
62
  );
@@ -36,6 +36,8 @@
36
36
  * {@link reapConflictingWorkflows}.
37
37
  */
38
38
 
39
+ import { resolveProjectMeta } from '../orchestration/project-meta-resolver.js';
40
+
39
41
  /**
40
42
  * Workflows that **must not** be enabled when the orchestrator owns
41
43
  * the Status column. Each entry writes Status as a side-effect of an
@@ -207,14 +209,23 @@ export async function reapConflictingWorkflows(args) {
207
209
  }
208
210
 
209
211
  /**
210
- * Resolve a Project v2 node id from a project number against the viewer
211
- * scope. Used by the bootstrap CLI to convert the resolver's
212
- * `projectNumber` into the node id required by
213
- * {@link auditProjectWorkflows}. Returns `null` when the viewer cannot
214
- * see the project (e.g. missing scope, project not under viewer) so the
215
- * caller can degrade gracefully.
212
+ * Resolve a Project v2 node id from a project number. Used by the
213
+ * bootstrap CLI to convert the resolver's `projectNumber` into the node
214
+ * id required by {@link auditProjectWorkflows}.
215
+ *
216
+ * Walks the shared owner-type ladder `organization(login:$owner)`
217
+ * `user(login:$owner)` `viewer` — via {@link resolveProjectMeta}, so an
218
+ * **org-owned** board resolves here the same way it does for `ColumnSync`
219
+ * (Story #4237). The owner login is read from `provider.projectOwner`
220
+ * (explicit board owner) and falls back to `provider.owner` (the repo
221
+ * owner) so org boards resolve even when no separate `projectOwner` is
222
+ * configured. Returns `null` when no owner scope can see the project
223
+ * (e.g. missing scope) so the caller can degrade gracefully.
216
224
  *
217
- * @param {{ provider: { graphql: Function }, projectNumber: number }} args
225
+ * @param {{
226
+ * provider: { graphql: Function, owner?: string|null, projectOwner?: string|null },
227
+ * projectNumber: number,
228
+ * }} args
218
229
  * @returns {Promise<string|null>}
219
230
  */
220
231
  export async function resolveProjectIdByNumber(args) {
@@ -230,11 +241,13 @@ export async function resolveProjectIdByNumber(args) {
230
241
  );
231
242
  }
232
243
  try {
233
- const data = await provider.graphql(
234
- `query($n: Int!) { viewer { projectV2(number: $n) { id } } }`,
235
- { n: projectNumber },
236
- );
237
- return data?.viewer?.projectV2?.id ?? null;
244
+ const project = await resolveProjectMeta({
245
+ provider,
246
+ owner: provider.projectOwner ?? provider.owner ?? null,
247
+ projectNumber,
248
+ projectFields: 'id',
249
+ });
250
+ return project?.id ?? null;
238
251
  } catch {
239
252
  return null;
240
253
  }
@@ -156,40 +156,3 @@ export const PROJECT_FIELD_DEFS = [
156
156
  * @type {string[]}
157
157
  */
158
158
  export const STATUS_FIELD_OPTIONS = ['Todo', 'In Progress', 'Done'];
159
-
160
- /**
161
- * Default Projects V2 saved Views. Filter strings follow GitHub's Projects
162
- * search syntax (`label:`, `status:`, `assignee:`). Each is grouped by the
163
- * Status field to match the board's columnar layout.
164
- *
165
- * GitHub's GraphQL surface does not expose a public `createProjectV2View`
166
- * mutation, so bootstrap creates these via the REST Projects V2 views
167
- * endpoint best-effort; when the endpoint is unavailable the views must be
168
- * configured manually in the GitHub Projects UI.
169
- *
170
- * @type {Array<{ name: string, filter: string, groupBy: string,
171
- * layout?: 'table'|'board'|'roadmap' }>}
172
- */
173
- export const PROJECT_VIEW_DEFS = [
174
- {
175
- name: 'Mandrel Board',
176
- filter: '',
177
- groupBy: 'Status',
178
- layout: 'board',
179
- },
180
- {
181
- name: 'Epic Roadmap',
182
- filter: 'label:type::epic',
183
- groupBy: 'Status',
184
- },
185
- {
186
- name: 'Active Stories',
187
- filter: 'label:type::story -status:Done',
188
- groupBy: 'Status',
189
- },
190
- {
191
- name: 'My Queue',
192
- filter: 'assignee:@me',
193
- groupBy: 'Status',
194
- },
195
- ];
@@ -62,23 +62,22 @@ function formatMissingList(missing) {
62
62
  }
63
63
 
64
64
  /** Prompt text shown only on a TTY when asking to scaffold. */
65
- const SCAFFOLD_PROMPT = '\nCreate placeholders? [Y/n]: ';
65
+ const SCAFFOLD_PROMPT = '\nCreate placeholders? [y/N]: ';
66
66
 
67
67
  /**
68
68
  * Async y/N read from stdin via `node:readline` (mirrors the prompt mechanism
69
69
  * in `bootstrap.js`). Returns on Enter and never blocks waiting for EOF the way
70
70
  * `fs.readFileSync(0)` did — that EOF-blocking read hung `mandrel init` on an
71
- * interactive TTY. Yes is the default (`[Y/n]`): a bare Enter — or anything but
72
- * an explicit `n`/`no` — resolves to `true` (create the placeholders), since the
73
- * missing docs are known-needed and the stubs carry a `MANDREL:STUB` marker the
74
- * `/plan` preflight still flags until they are fleshed out. A read error
75
- * resolves to `false` so a genuine I/O failure never writes unattended. The
76
- * prompt text is written by the caller via `stdout`, so the question string
77
- * passed here is empty.
71
+ * interactive TTY. No is the default (`[y/N]`): only an explicit `y`/`yes`
72
+ * resolves to `true` (create the placeholders). A bare Enter or any other input
73
+ * declines, matching the same default-off policy as `--with-issue-forms`. A
74
+ * read error resolves to `false` so a genuine I/O failure never writes
75
+ * unattended. The prompt text is written by the caller via `stdout`, so the
76
+ * question string passed here is empty.
78
77
  *
79
78
  * `terminal: false` is **load-bearing**: with terminal mode on (the default
80
79
  * when stdout is a TTY) readline emits cursor-control escapes
81
- * (`\x1b[1G\x1b[0J`) that erase the `Create placeholders? [Y/n]:` prompt already
80
+ * (`\x1b[1G\x1b[0J`) that erase the `Create placeholders? [y/N]:` prompt already
82
81
  * written via the caller's `stdout`, leaving the operator staring at a blank,
83
82
  * dead-looking line. Disabling terminal mode preserves the pre-written prompt
84
83
  * and reads the line via the TTY's cooked-mode echo. `createInterface` is
@@ -97,7 +96,7 @@ export async function readConfirm({
97
96
  });
98
97
  try {
99
98
  const answer = (await rl.question('')).trim().toLowerCase();
100
- return answer !== 'n' && answer !== 'no';
99
+ return answer === 'y' || answer === 'yes';
101
100
  } catch {
102
101
  return false;
103
102
  } finally {