github-router 0.3.206 → 0.3.211

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.
@@ -4563,7 +4563,7 @@ function authorMatchesBot(authorLogin, botLogin) {
4563
4563
  const authorKey = agentKeyForLogin(authorLogin);
4564
4564
  return authorKey !== null && authorKey === agentKeyForLogin(botLogin);
4565
4565
  }
4566
- function asRecord$7(value) {
4566
+ function asRecord$8(value) {
4567
4567
  return typeof value === "object" && value !== null ? value : void 0;
4568
4568
  }
4569
4569
  function stringValue$2(value) {
@@ -4573,7 +4573,7 @@ async function readJsonObject(response) {
4573
4573
  const text = await response.text();
4574
4574
  if (!text.trim()) return {};
4575
4575
  try {
4576
- return asRecord$7(JSON.parse(text)) ?? {};
4576
+ return asRecord$8(JSON.parse(text)) ?? {};
4577
4577
  } catch (err) {
4578
4578
  throw new AgentError("UPSTREAM", "GitHub API returned invalid JSON", { cause: err });
4579
4579
  }
@@ -5139,7 +5139,7 @@ function capiHeaders() {
5139
5139
  "user-agent": "github-router-first-mate"
5140
5140
  };
5141
5141
  }
5142
- function asRecord$6(value) {
5142
+ function asRecord$7(value) {
5143
5143
  return typeof value === "object" && value !== null ? value : void 0;
5144
5144
  }
5145
5145
  function truncateHead(text) {
@@ -5238,25 +5238,25 @@ function parseSessionLog(body) {
5238
5238
  if (!payload || payload === "[DONE]") continue;
5239
5239
  let chunk;
5240
5240
  try {
5241
- chunk = asRecord$6(JSON.parse(payload));
5241
+ chunk = asRecord$7(JSON.parse(payload));
5242
5242
  } catch {
5243
5243
  continue;
5244
5244
  }
5245
5245
  if (!chunk || chunk.object !== "chat.completion.chunk") continue;
5246
5246
  const choices = Array.isArray(chunk.choices) ? chunk.choices : [];
5247
5247
  for (const choiceValue of choices) {
5248
- const choice = asRecord$6(choiceValue);
5248
+ const choice = asRecord$7(choiceValue);
5249
5249
  if (!choice) continue;
5250
- const delta = asRecord$6(choice.delta) ?? {};
5250
+ const delta = asRecord$7(choice.delta) ?? {};
5251
5251
  if (typeof delta.content === "string") content = capped(content, delta.content);
5252
5252
  if (typeof delta.reasoning_text === "string") reasoning = capped(reasoning, delta.reasoning_text);
5253
5253
  if (choice.finish_reason === "stop") finished = true;
5254
5254
  const toolCallDeltas = Array.isArray(delta.tool_calls) ? delta.tool_calls : [];
5255
5255
  for (const tcValue of toolCallDeltas) {
5256
- const call = asRecord$6(tcValue);
5256
+ const call = asRecord$7(tcValue);
5257
5257
  if (!call) continue;
5258
5258
  const index = typeof call.index === "number" ? call.index : toolCalls.size;
5259
- const fn = asRecord$6(call.function) ?? {};
5259
+ const fn = asRecord$7(call.function) ?? {};
5260
5260
  const existing = toolCalls.get(index) ?? { args: "" };
5261
5261
  if (typeof fn.name === "string" && fn.name.length > 0) {
5262
5262
  existing.name = fn.name;
@@ -5271,7 +5271,7 @@ function parseSessionLog(body) {
5271
5271
  for (const call of toolCalls.values()) {
5272
5272
  if (call.name !== "report_progress") continue;
5273
5273
  try {
5274
- const parsed = asRecord$6(JSON.parse(call.args));
5274
+ const parsed = asRecord$7(JSON.parse(call.args));
5275
5275
  const desc = parsed?.prDescription ?? parsed?.pr_description;
5276
5276
  if (typeof desc === "string" && desc.length > planDescription.length) planDescription = desc;
5277
5277
  } catch {}
@@ -5335,7 +5335,6 @@ async function getSessionLog(sessionId, signal) {
5335
5335
  const AGENT_TASKS_API_VERSION = "2026-03-10";
5336
5336
  const LOG_EXCERPT_LIMIT = 4e3;
5337
5337
  const TRUNCATED_MARKER = "…[truncated]…";
5338
- const FOLLOW_UP_TASK_PATH_SUFFIX = "";
5339
5338
  const CANCEL_TASK_PATH_SUFFIX = "/cancel";
5340
5339
  function segment$1(value) {
5341
5340
  return encodeURIComponent(String(value));
@@ -5346,7 +5345,7 @@ function repoTasksPath(repo) {
5346
5345
  function taskPath(repo, taskId) {
5347
5346
  return `${repoTasksPath(repo)}/${segment$1(taskId)}`;
5348
5347
  }
5349
- function asRecord$5(value) {
5348
+ function asRecord$6(value) {
5350
5349
  return typeof value === "object" && value !== null ? value : void 0;
5351
5350
  }
5352
5351
  function stringField(record, keys) {
@@ -5364,10 +5363,10 @@ function numberField(record, keys) {
5364
5363
  function collectText(record) {
5365
5364
  if (!record) return [];
5366
5365
  const nested = [
5367
- asRecord$5(record.task),
5368
- asRecord$5(record.session),
5369
- asRecord$5(record.progress),
5370
- asRecord$5(record.result)
5366
+ asRecord$6(record.task),
5367
+ asRecord$6(record.session),
5368
+ asRecord$6(record.progress),
5369
+ asRecord$6(record.result)
5371
5370
  ];
5372
5371
  const textKeys = [
5373
5372
  "session_log",
@@ -5405,7 +5404,7 @@ function taskPrUrl(record) {
5405
5404
  "pullRequestUrl"
5406
5405
  ]);
5407
5406
  if (direct) return direct;
5408
- return stringField(asRecord$5(record?.pull_request) ?? asRecord$5(record?.pullRequest), ["html_url", "url"]);
5407
+ return stringField(asRecord$6(record?.pull_request) ?? asRecord$6(record?.pullRequest), ["html_url", "url"]);
5409
5408
  }
5410
5409
  function taskPrNumber$1(record) {
5411
5410
  const direct = numberField(record, [
@@ -5415,13 +5414,13 @@ function taskPrNumber$1(record) {
5415
5414
  "pull_request_number"
5416
5415
  ]);
5417
5416
  if (direct !== void 0) return direct;
5418
- return numberField(asRecord$5(record?.pull_request) ?? asRecord$5(record?.pullRequest), ["number"]) ?? void 0;
5417
+ return numberField(asRecord$6(record?.pull_request) ?? asRecord$6(record?.pullRequest), ["number"]) ?? void 0;
5419
5418
  }
5420
5419
  function latestSessionId(record) {
5421
5420
  const sessions$1 = record?.sessions;
5422
5421
  if (!Array.isArray(sessions$1) || sessions$1.length === 0) return void 0;
5423
5422
  for (let i = sessions$1.length - 1; i >= 0; i -= 1) {
5424
- const id = stringField(asRecord$5(sessions$1[i]), [
5423
+ const id = stringField(asRecord$6(sessions$1[i]), [
5425
5424
  "id",
5426
5425
  "session_id",
5427
5426
  "sessionId"
@@ -5450,7 +5449,7 @@ async function startTask(repo, input) {
5450
5449
  };
5451
5450
  }
5452
5451
  async function getTask(repo, taskId) {
5453
- const record = asRecord$5(await ghRest("GET", taskPath(repo, taskId), { apiVersion: AGENT_TASKS_API_VERSION }));
5452
+ const record = asRecord$6(await ghRest("GET", taskPath(repo, taskId), { apiVersion: AGENT_TASKS_API_VERSION }));
5454
5453
  const sessionId = latestSessionId(record);
5455
5454
  const sessionLog = sessionId ? await getSessionLog(sessionId) : null;
5456
5455
  const fallbackText = collectText(record).join("\n\n");
@@ -5469,12 +5468,37 @@ async function getTask(repo, taskId) {
5469
5468
  ...sessionLog?.branch ? { branch: sessionLog.branch } : {}
5470
5469
  };
5471
5470
  }
5472
- async function followUpTask(repo, taskId, prompt) {
5473
- await ghRest("POST", `${taskPath(repo, taskId)}${FOLLOW_UP_TASK_PATH_SUFFIX}`, {
5471
+ /**
5472
+ * Continue an existing cloud-agent task by re-POSTing to the tasks endpoint with
5473
+ * `head_ref` set to the agent's existing branch. There is NO follow-up/steer
5474
+ * endpoint on the Agent-Tasks preview API; the documented way to give a running
5475
+ * or blocked task new input (e.g. the answer to its plan-mode question) is to
5476
+ * start a fresh session bound to the same branch — GitHub commits to `head_ref`
5477
+ * instead of creating a new branch. Returns the NEW task/session id so the
5478
+ * caller can re-point observation at it. Best-effort: the API is public preview
5479
+ * and pre-PR (branch-only) continue is inferred, not documented-guaranteed.
5480
+ */
5481
+ async function continueTaskOnBranch(repo, input) {
5482
+ const body = {
5483
+ prompt: input.prompt,
5484
+ head_ref: input.headRef
5485
+ };
5486
+ if (input.baseRef !== void 0) body.base_ref = input.baseRef;
5487
+ if (input.model !== void 0) body.model = input.model;
5488
+ const response = await ghRest("POST", repoTasksPath(repo), {
5474
5489
  apiVersion: AGENT_TASKS_API_VERSION,
5475
- body: { prompt }
5490
+ body,
5491
+ retry: false,
5492
+ ...input.idempotencyKey ? { headers: { "Idempotency-Key": input.idempotencyKey } } : {}
5476
5493
  });
5477
- return { ok: true };
5494
+ return {
5495
+ taskId: stringField(response, [
5496
+ "task_id",
5497
+ "taskId",
5498
+ "id"
5499
+ ]) ?? "",
5500
+ state: stringField(response, ["state", "status"]) ?? "unknown"
5501
+ };
5478
5502
  }
5479
5503
  async function cancelTask(repo, taskId) {
5480
5504
  await ghRest("POST", `${taskPath(repo, taskId)}${CANCEL_TASK_PATH_SUFFIX}`, { apiVersion: AGENT_TASKS_API_VERSION });
@@ -5875,53 +5899,53 @@ const DECISION_STATUSES = new Set([
5875
5899
  function decisionsPath() {
5876
5900
  return nodePath.join(PATHS.FIRST_MATE_DIR, "decisions.json");
5877
5901
  }
5878
- function asRecord$4(value) {
5902
+ function asRecord$5(value) {
5879
5903
  return typeof value === "object" && value !== null ? value : void 0;
5880
5904
  }
5881
5905
  function isOneOf$1(value, allowed) {
5882
5906
  return typeof value === "string" && allowed.has(value);
5883
5907
  }
5884
- function isFiniteNumber$2(value) {
5908
+ function isFiniteNumber$3(value) {
5885
5909
  return typeof value === "number" && Number.isFinite(value);
5886
5910
  }
5887
5911
  function isPositiveInteger(value) {
5888
5912
  return typeof value === "number" && Number.isInteger(value) && value > 0;
5889
5913
  }
5890
- function isNonNegativeInteger$3(value) {
5914
+ function isNonNegativeInteger$4(value) {
5891
5915
  return typeof value === "number" && Number.isInteger(value) && value >= 0;
5892
5916
  }
5893
- function isOptionalString$2(value) {
5917
+ function isOptionalString$3(value) {
5894
5918
  return value === void 0 || typeof value === "string";
5895
5919
  }
5896
5920
  function isOptionalStringOrNull$1(value) {
5897
5921
  return value === void 0 || value === null || typeof value === "string";
5898
5922
  }
5899
5923
  function isOptionalFiniteNumber$2(value) {
5900
- return value === void 0 || isFiniteNumber$2(value);
5924
+ return value === void 0 || isFiniteNumber$3(value);
5901
5925
  }
5902
- function isOptionalStringArray(value) {
5926
+ function isOptionalStringArray$1(value) {
5903
5927
  return value === void 0 || Array.isArray(value) && value.every((entry) => typeof entry === "string");
5904
5928
  }
5905
5929
  function isRepoRef$2(value) {
5906
- const repo = asRecord$4(value);
5930
+ const repo = asRecord$5(value);
5907
5931
  return repo !== void 0 && typeof repo.owner === "string" && repo.owner.length > 0 && typeof repo.name === "string" && repo.name.length > 0;
5908
5932
  }
5909
5933
  function isOptionRefs(value) {
5910
5934
  return value === void 0 || Array.isArray(value) && value.every((option) => {
5911
- const row = asRecord$4(option);
5935
+ const row = asRecord$5(option);
5912
5936
  return row !== void 0 && typeof row.id === "string";
5913
5937
  });
5914
5938
  }
5915
5939
  function isApprovalRecord(value) {
5916
- const approval = asRecord$4(value);
5917
- return approval !== void 0 && typeof approval.decisionId === "string" && approval.decisionId.length > 0 && isRepoRef$2(approval.repo) && isPositiveInteger(approval.pr) && typeof approval.headSha === "string" && approval.headSha.length > 0 && isOptionalString$2(approval.baseSha) && isOptionalString$2(approval.diffDigest) && isOptionalStringArray(approval.requiredCheckIds) && isOptionalString$2(approval.floorRunId) && approval.status === "approved" && typeof approval.consumed === "boolean" && isFiniteNumber$2(approval.createdMs) && isOptionalFiniteNumber$2(approval.consumedMs);
5940
+ const approval = asRecord$5(value);
5941
+ return approval !== void 0 && typeof approval.decisionId === "string" && approval.decisionId.length > 0 && isRepoRef$2(approval.repo) && isPositiveInteger(approval.pr) && typeof approval.headSha === "string" && approval.headSha.length > 0 && isOptionalString$3(approval.baseSha) && isOptionalString$3(approval.diffDigest) && isOptionalStringArray$1(approval.requiredCheckIds) && isOptionalString$3(approval.floorRunId) && approval.status === "approved" && typeof approval.consumed === "boolean" && isFiniteNumber$3(approval.createdMs) && isOptionalFiniteNumber$2(approval.consumedMs);
5918
5942
  }
5919
5943
  function isOptionalApprovalRecord(value) {
5920
5944
  return value === void 0 || isApprovalRecord(value);
5921
5945
  }
5922
5946
  function isDecisionRecord(value) {
5923
- const record = asRecord$4(value);
5924
- return record !== void 0 && typeof record.decisionId === "string" && record.decisionId.length > 0 && typeof record.decisionKey === "string" && record.decisionKey.length > 0 && typeof record.type === "string" && record.type.length > 0 && isOneOf$1(record.status, DECISION_STATUSES) && isOptionalString$2(record.packetId) && typeof record.inputFingerprint === "string" && record.inputFingerprint.length > 0 && isOptionRefs(record.options) && isOptionalStringOrNull$1(record.chosenOptionId) && isOptionalStringOrNull$1(record.resolvedBy) && isFiniteNumber$2(record.createdMs) && isOptionalFiniteNumber$2(record.resolvedMs) && isOptionalApprovalRecord(record.approval);
5947
+ const record = asRecord$5(value);
5948
+ return record !== void 0 && typeof record.decisionId === "string" && record.decisionId.length > 0 && typeof record.decisionKey === "string" && record.decisionKey.length > 0 && typeof record.type === "string" && record.type.length > 0 && isOneOf$1(record.status, DECISION_STATUSES) && isOptionalString$3(record.packetId) && typeof record.inputFingerprint === "string" && record.inputFingerprint.length > 0 && isOptionRefs(record.options) && isOptionalStringOrNull$1(record.chosenOptionId) && isOptionalStringOrNull$1(record.resolvedBy) && isFiniteNumber$3(record.createdMs) && isOptionalFiniteNumber$2(record.resolvedMs) && isOptionalApprovalRecord(record.approval);
5925
5949
  }
5926
5950
  function parseDecisions(raw) {
5927
5951
  if (raw === void 0) return {
@@ -5929,12 +5953,12 @@ function parseDecisions(raw) {
5929
5953
  decisions: []
5930
5954
  };
5931
5955
  try {
5932
- const parsed = asRecord$4(JSON.parse(raw));
5956
+ const parsed = asRecord$5(JSON.parse(raw));
5933
5957
  if (!parsed || parsed.version !== DECISIONS_VERSION || !Array.isArray(parsed.decisions)) return {
5934
5958
  rev: 0,
5935
5959
  decisions: []
5936
5960
  };
5937
- const rev = isNonNegativeInteger$3(parsed.rev) ? parsed.rev : 0;
5961
+ const rev = isNonNegativeInteger$4(parsed.rev) ? parsed.rev : 0;
5938
5962
  const cleaned = parsed.decisions.filter(isDecisionRecord);
5939
5963
  if (cleaned.length !== parsed.decisions.length) consola.debug(`first-mate decisions dropped ${parsed.decisions.length - cleaned.length} corrupt decision(s)`);
5940
5964
  return {
@@ -6488,25 +6512,25 @@ function sanitizeSegment$1(value) {
6488
6512
  function repoLedgerPath(repo) {
6489
6513
  return nodePath.join(PATHS.FIRST_MATE_DIR, `${sanitizeSegment$1(repo.owner)}__${sanitizeSegment$1(repo.name)}.json`);
6490
6514
  }
6491
- function asRecord$3(value) {
6515
+ function asRecord$4(value) {
6492
6516
  return typeof value === "object" && value !== null ? value : void 0;
6493
6517
  }
6494
6518
  function isOneOf(value, allowed) {
6495
6519
  return typeof value === "string" && allowed.has(value);
6496
6520
  }
6497
- function isFiniteNumber$1(value) {
6521
+ function isFiniteNumber$2(value) {
6498
6522
  return typeof value === "number" && Number.isFinite(value);
6499
6523
  }
6500
- function isNonNegativeInteger$2(value) {
6524
+ function isNonNegativeInteger$3(value) {
6501
6525
  return typeof value === "number" && Number.isInteger(value) && value >= 0;
6502
6526
  }
6503
6527
  function isIssueNumberOrNull(value) {
6504
- return value === null || isNonNegativeInteger$2(value);
6528
+ return value === null || isNonNegativeInteger$3(value);
6505
6529
  }
6506
6530
  function isStringOrNull(value) {
6507
6531
  return value === null || typeof value === "string";
6508
6532
  }
6509
- function isOptionalString$1(value) {
6533
+ function isOptionalString$2(value) {
6510
6534
  return value === void 0 || typeof value === "string";
6511
6535
  }
6512
6536
  function isOptionalStringOrNull(value) {
@@ -6516,10 +6540,10 @@ function isOptionalBoolean(value) {
6516
6540
  return value === void 0 || typeof value === "boolean";
6517
6541
  }
6518
6542
  function isOptionalFiniteNumber$1(value) {
6519
- return value === void 0 || isFiniteNumber$1(value);
6543
+ return value === void 0 || isFiniteNumber$2(value);
6520
6544
  }
6521
6545
  function isRepoRef$1(value) {
6522
- const repo = asRecord$3(value);
6546
+ const repo = asRecord$4(value);
6523
6547
  return repo !== void 0 && typeof repo.owner === "string" && repo.owner.length > 0 && typeof repo.name === "string" && repo.name.length > 0;
6524
6548
  }
6525
6549
  function isStringArray(value) {
@@ -6527,13 +6551,13 @@ function isStringArray(value) {
6527
6551
  }
6528
6552
  function isLastSteer(value) {
6529
6553
  if (value === void 0) return true;
6530
- const steer = asRecord$3(value);
6531
- return steer !== void 0 && isOptionalString$1(steer.cursor) && isOptionalString$1(steer.sha) && isFiniteNumber$1(steer.atMs);
6554
+ const steer = asRecord$4(value);
6555
+ return steer !== void 0 && isOptionalString$2(steer.cursor) && isOptionalString$2(steer.sha) && isFiniteNumber$2(steer.atMs);
6532
6556
  }
6533
6557
  function isUnitRow(value) {
6534
- const row = asRecord$3(value);
6558
+ const row = asRecord$4(value);
6535
6559
  if (!row) return false;
6536
- return typeof row.missionId === "string" && row.missionId.length > 0 && isRepoRef$1(row.repo) && isIssueNumberOrNull(row.issue) && isIssueNumberOrNull(row.pr) && isStringOrNull(row.taskId) && isOneOf(row.agent, AGENTS) && typeof row.botLogin === "string" && isOneOf(row.dispatchMode, DISPATCH_MODES) && isOptionalString$1(row.model) && isOneOf(row.provider, PROVIDER_STATES$2) && isOneOf(row.phase, PHASES) && isOneOf(row.artifact, ARTIFACTS) && isOneOf(row.validation, VALIDATIONS) && isNonNegativeInteger$2(row.retries) && isStringArray(row.dependsOn) && typeof row.title === "string" && isLastSteer(row.lastSteer) && (row.cancelledBy === void 0 || row.cancelledBy === "controller" || row.cancelledBy === "external") && isOptionalStringOrNull(row.bakeoffGroupId) && isOptionalStringOrNull(row.blockingDecisionId) && isOptionalBoolean(row.verifierAssigned) && (row.implementerLab === void 0 || isOneOf(row.implementerLab, AGENTS)) && isOptionalStringOrNull(row.branch) && isOptionalStringOrNull(row.headSha) && isOptionalStringOrNull(row.baseSha) && isOptionalFiniteNumber$1(row.lastCheckedMs) && isOptionalBoolean(row.terminal);
6560
+ return typeof row.missionId === "string" && row.missionId.length > 0 && isRepoRef$1(row.repo) && isIssueNumberOrNull(row.issue) && isIssueNumberOrNull(row.pr) && isStringOrNull(row.taskId) && isOneOf(row.agent, AGENTS) && typeof row.botLogin === "string" && isOneOf(row.dispatchMode, DISPATCH_MODES) && isOptionalString$2(row.model) && isOneOf(row.provider, PROVIDER_STATES$2) && isOneOf(row.phase, PHASES) && isOneOf(row.artifact, ARTIFACTS) && isOneOf(row.validation, VALIDATIONS) && isNonNegativeInteger$3(row.retries) && isStringArray(row.dependsOn) && typeof row.title === "string" && isLastSteer(row.lastSteer) && (row.cancelledBy === void 0 || row.cancelledBy === "controller" || row.cancelledBy === "external") && isOptionalStringOrNull(row.bakeoffGroupId) && isOptionalStringOrNull(row.blockingDecisionId) && isOptionalBoolean(row.verifierAssigned) && (row.implementerLab === void 0 || isOneOf(row.implementerLab, AGENTS)) && isOptionalStringOrNull(row.branch) && isOptionalStringOrNull(row.headSha) && isOptionalStringOrNull(row.baseSha) && isOptionalFiniteNumber$1(row.lastCheckedMs) && isOptionalBoolean(row.terminal);
6537
6561
  }
6538
6562
  function sameUnitHandle(a, b) {
6539
6563
  return b.id != null && a.id === b.id || b.issue !== null && a.issue === b.issue || b.taskId !== null && a.taskId === b.taskId;
@@ -6543,12 +6567,12 @@ function terminalTimestamp(row) {
6543
6567
  }
6544
6568
  function parseLedgerRaw(raw) {
6545
6569
  try {
6546
- const parsed = asRecord$3(JSON.parse(raw));
6570
+ const parsed = asRecord$4(JSON.parse(raw));
6547
6571
  if (!parsed || parsed.version !== LEDGER_VERSION || !Array.isArray(parsed.units)) return {
6548
6572
  rev: 0,
6549
6573
  units: []
6550
6574
  };
6551
- const rev = isNonNegativeInteger$2(parsed.rev) ? parsed.rev : 0;
6575
+ const rev = isNonNegativeInteger$3(parsed.rev) ? parsed.rev : 0;
6552
6576
  const cleaned = parsed.units.filter(isUnitRow);
6553
6577
  if (cleaned.length !== parsed.units.length) consola.debug(`first-mate ledger dropped ${parsed.units.length - cleaned.length} corrupt unit(s)`);
6554
6578
  return {
@@ -6644,31 +6668,31 @@ const REGISTRY_VERSION = 1;
6644
6668
  function registryPath() {
6645
6669
  return nodePath.join(PATHS.FIRST_MATE_DIR, "missions.json");
6646
6670
  }
6647
- function asRecord$2(value) {
6671
+ function asRecord$3(value) {
6648
6672
  return typeof value === "object" && value !== null ? value : void 0;
6649
6673
  }
6650
- function isFiniteNumber(value) {
6674
+ function isFiniteNumber$1(value) {
6651
6675
  return typeof value === "number" && Number.isFinite(value);
6652
6676
  }
6653
- function isNonNegativeInteger$1(value) {
6677
+ function isNonNegativeInteger$2(value) {
6654
6678
  return typeof value === "number" && Number.isInteger(value) && value >= 0;
6655
6679
  }
6656
- function isOptionalString(value) {
6680
+ function isOptionalString$1(value) {
6657
6681
  return value === void 0 || typeof value === "string";
6658
6682
  }
6659
6683
  function isOptionalFiniteNumber(value) {
6660
- return value === void 0 || isFiniteNumber(value);
6684
+ return value === void 0 || isFiniteNumber$1(value);
6661
6685
  }
6662
6686
  function isOptionalPositiveInteger(value) {
6663
6687
  return value === void 0 || Number.isInteger(value) && value >= 1;
6664
6688
  }
6665
6689
  function isRepoRef(value) {
6666
- const repo = asRecord$2(value);
6690
+ const repo = asRecord$3(value);
6667
6691
  return repo !== void 0 && typeof repo.owner === "string" && repo.owner.length > 0 && typeof repo.name === "string" && repo.name.length > 0;
6668
6692
  }
6669
6693
  function isMission(value) {
6670
- const mission = asRecord$2(value);
6671
- return mission !== void 0 && typeof mission.id === "string" && mission.id.length > 0 && typeof mission.goal === "string" && typeof mission.acceptanceCriteria === "string" && isOptionalString(mission.houseRules) && isOptionalFiniteNumber(mission.priority) && isOptionalString(mission.defaultModel) && (mission.planGate === void 0 || mission.planGate === "hard" || mission.planGate === "soft") && isOptionalPositiveInteger(mission.maxFixCycles) && isOptionalPositiveInteger(mission.maxCopilotComments) && (mission.ciRequired === void 0 || typeof mission.ciRequired === "boolean") && (mission.everDecomposed === void 0 || typeof mission.everDecomposed === "boolean") && Array.isArray(mission.repos) && mission.repos.every(isRepoRef) && (mission.status === "active" || mission.status === "done" || mission.status === "abandoned") && isFiniteNumber(mission.createdMs) && isFiniteNumber(mission.updatedMs);
6694
+ const mission = asRecord$3(value);
6695
+ return mission !== void 0 && typeof mission.id === "string" && mission.id.length > 0 && typeof mission.goal === "string" && typeof mission.acceptanceCriteria === "string" && isOptionalString$1(mission.houseRules) && isOptionalFiniteNumber(mission.priority) && isOptionalString$1(mission.defaultModel) && (mission.planGate === void 0 || mission.planGate === "hard" || mission.planGate === "soft") && isOptionalPositiveInteger(mission.maxFixCycles) && isOptionalPositiveInteger(mission.maxCopilotComments) && isOptionalPositiveInteger(mission.maxConcurrentBuilds) && (mission.ciRequired === void 0 || typeof mission.ciRequired === "boolean") && (mission.everDecomposed === void 0 || typeof mission.everDecomposed === "boolean") && (mission.noProgressWakes === void 0 || isNonNegativeInteger$2(mission.noProgressWakes)) && isOptionalFiniteNumber(mission.stallSinceMs) && isOptionalString$1(mission.stallEscalatedFingerprint) && Array.isArray(mission.repos) && mission.repos.every(isRepoRef) && (mission.status === "active" || mission.status === "done" || mission.status === "abandoned") && isFiniteNumber$1(mission.createdMs) && isFiniteNumber$1(mission.updatedMs);
6672
6696
  }
6673
6697
  function parseRegistry(raw) {
6674
6698
  if (raw === void 0) return {
@@ -6676,12 +6700,12 @@ function parseRegistry(raw) {
6676
6700
  missions: []
6677
6701
  };
6678
6702
  try {
6679
- const parsed = asRecord$2(JSON.parse(raw));
6703
+ const parsed = asRecord$3(JSON.parse(raw));
6680
6704
  if (!parsed || parsed.version !== REGISTRY_VERSION || !Array.isArray(parsed.missions)) return {
6681
6705
  rev: 0,
6682
6706
  missions: []
6683
6707
  };
6684
- const rev = isNonNegativeInteger$1(parsed.rev) ? parsed.rev : 0;
6708
+ const rev = isNonNegativeInteger$2(parsed.rev) ? parsed.rev : 0;
6685
6709
  const cleaned = parsed.missions.filter(isMission);
6686
6710
  if (cleaned.length !== parsed.missions.length) consola.debug(`first-mate registry dropped ${parsed.missions.length - cleaned.length} corrupt mission(s)`);
6687
6711
  return {
@@ -7205,6 +7229,7 @@ var Outbox = class {
7205
7229
 
7206
7230
  //#endregion
7207
7231
  //#region src/lib/first-mate/state-machine.ts
7232
+ const PLAN_RETRY_CAP = 2;
7208
7233
  function classify(observed, row) {
7209
7234
  const events$1 = [];
7210
7235
  const provider = observed.provider;
@@ -7279,10 +7304,13 @@ function nextAction(state$1, row, policy) {
7279
7304
  kind: "escalate_human",
7280
7305
  reason: "the agent opened multiple pull requests for one unit"
7281
7306
  };
7282
- if (state$1.provider === "failed" || state$1.provider === "timed_out") return {
7283
- kind: "escalate_human",
7284
- reason: `cloud agent task ${state$1.provider}`
7285
- };
7307
+ if ((state$1.provider === "failed" || state$1.provider === "timed_out") && state$1.artifact !== "pr_open") {
7308
+ if (state$1.phase === "plan" && state$1.artifact === "no_pr" && row.dispatchMode === "plan" && (row.planRetries ?? 0) < PLAN_RETRY_CAP) return { kind: "retry_plan" };
7309
+ return {
7310
+ kind: "escalate_human",
7311
+ reason: `cloud agent task ${state$1.provider}`
7312
+ };
7313
+ }
7286
7314
  if (state$1.provider === "waiting_for_user") {
7287
7315
  if (row.blockingDecisionId) return { kind: "noop" };
7288
7316
  return {
@@ -7394,7 +7422,7 @@ const defaultDeps = {
7394
7422
  readDecisions,
7395
7423
  markAnswered,
7396
7424
  startTask,
7397
- followUpTask,
7425
+ continueTaskOnBranch,
7398
7426
  cancelTask,
7399
7427
  createIssue,
7400
7428
  resolveAgentActor,
@@ -7443,6 +7471,8 @@ function unitHandle(unit) {
7443
7471
  * identity (the solo operator's account may BE the router PAT).
7444
7472
  */
7445
7473
  const FM_REVIEW_SENTINEL = "first-mate-review:";
7474
+ const STALL_WAKES = 6;
7475
+ const STALL_MS = 30 * 6e4;
7446
7476
  function stampReviewSentinel(unit, body) {
7447
7477
  return `<!-- ${FM_REVIEW_SENTINEL}${unit.id ?? unitHandle(unit)} -->\n\n${body}`;
7448
7478
  }
@@ -7457,7 +7487,7 @@ function requestIdFor(unit, kind) {
7457
7487
  function humanRequestBase(unit, type) {
7458
7488
  return `${unit.missionId}:${unitHandle(unit)}:${type}`;
7459
7489
  }
7460
- function asRecord$1(value) {
7490
+ function asRecord$2(value) {
7461
7491
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
7462
7492
  }
7463
7493
  function stringValue(value) {
@@ -7564,6 +7594,70 @@ function primaryObservedPr(observed) {
7564
7594
  function isTerminalProvider(provider) {
7565
7595
  return provider === "completed" || provider === "failed" || provider === "timed_out" || provider === "cancelled";
7566
7596
  }
7597
+ /** Portal-only progress identity. Provider state, phase, logs, and comments are excluded. */
7598
+ function verifiedProgressFact(unit, observed) {
7599
+ const primary = primaryObservedPr(observed);
7600
+ const merged = observed.prs.some((pr) => pr.state.toUpperCase() === "MERGED" || pr.merged === true);
7601
+ const contentHead = observed.changedFiles !== void 0 && observed.changedFiles > 0 && primary?.headSha ? `${primary.number}@${primary.headSha}` : null;
7602
+ const ci = observed.ci?.rollup ?? "none";
7603
+ const verifier = observed.verifierReviewed === true ? primary?.headSha ?? unit.headSha ?? "present" : null;
7604
+ const floor = observed.floor ?? (unit.validation === "floor_passed" ? "passed" : null);
7605
+ const artifact = merged || unit.artifact === "pr_merged" ? "pr_merged" : null;
7606
+ const fingerprint = [
7607
+ `content=${contentHead ?? "none"}`,
7608
+ `ci=${ci}`,
7609
+ `verifier=${verifier ?? "none"}`,
7610
+ `floor=${floor ?? "none"}`,
7611
+ `artifact=${artifact ?? "none"}`
7612
+ ].join("|");
7613
+ if (artifact) return {
7614
+ fingerprint,
7615
+ kind: "merged"
7616
+ };
7617
+ if (floor === "passed") return {
7618
+ fingerprint,
7619
+ kind: "floor_passed"
7620
+ };
7621
+ if (verifier) return {
7622
+ fingerprint,
7623
+ kind: "verifier_review"
7624
+ };
7625
+ if (ci === "passing") return {
7626
+ fingerprint,
7627
+ kind: "ci_passed"
7628
+ };
7629
+ if (contentHead) return {
7630
+ fingerprint,
7631
+ kind: "content_head"
7632
+ };
7633
+ return null;
7634
+ }
7635
+ function noProgressReason(unit, observed) {
7636
+ if (unit.blockingDecisionId) return "waiting_for_user unanswered";
7637
+ const primary = primaryObservedPr(observed);
7638
+ if (primary) {
7639
+ const ci = observed.ci?.rollup ?? "none";
7640
+ return `PR #${primary.number} open, CI ${ci}, head unchanged`;
7641
+ }
7642
+ const since = unit.verifiedProgress?.atMs ?? unit.lastSteer?.atMs;
7643
+ return `provider=${observed.provider}, no content-bearing head${since ? ` since ${new Date(since).toISOString()}` : " observed"}`;
7644
+ }
7645
+ function recordVerifiedProgress(unit, observed, now = Date.now()) {
7646
+ const fact = verifiedProgressFact(unit, observed);
7647
+ if (fact !== null && fact.fingerprint !== unit.verifiedProgress?.fingerprint) {
7648
+ unit.verifiedProgress = {
7649
+ ...fact,
7650
+ atMs: now
7651
+ };
7652
+ unit.noProgressWakes = 0;
7653
+ delete unit.lastNoProgressReason;
7654
+ return true;
7655
+ }
7656
+ if (fact?.kind === "merged" || unit.artifact === "pr_merged") return false;
7657
+ unit.noProgressWakes = (unit.noProgressWakes ?? 0) + 1;
7658
+ unit.lastNoProgressReason = noProgressReason(unit, observed);
7659
+ return false;
7660
+ }
7567
7661
  function updateUnitFromObservedPrs(unit, observed) {
7568
7662
  if (observed.prs.length === 1) {
7569
7663
  const pr = observed.prs[0];
@@ -7725,7 +7819,7 @@ async function applyModelAnswer(answer, units, missions, deps, applied, needsHum
7725
7819
  return;
7726
7820
  }
7727
7821
  const { unit, kind } = target;
7728
- const verdict = asRecord$1(answer.verdict) ?? {};
7822
+ const verdict = asRecord$2(answer.verdict) ?? {};
7729
7823
  const repo = agentRepo(unit.repo);
7730
7824
  if (kind === "review_plan") {
7731
7825
  if (unit.provider !== "completed") {
@@ -7736,8 +7830,8 @@ async function applyModelAnswer(answer, units, missions, deps, applied, needsHum
7736
7830
  const mission = missions.find((entry) => entry.id === unit.missionId);
7737
7831
  if (decision === "approve") {
7738
7832
  if (mission !== void 0) {
7739
- if (hasActiveBuildUnit(unit.missionId, units)) {
7740
- applied.push(`deferred build dispatch for ${unit.missionId}:${unitHandle(unit)}: another build is active`);
7833
+ if (!canDispatchBuild(unit, mission, units)) {
7834
+ applied.push(`deferred build dispatch for ${unit.missionId}:${unitHandle(unit)}: concurrency cap or overlapping file scope with an active build`);
7741
7835
  return;
7742
7836
  }
7743
7837
  const model = resolveCloudAgentModel(unit.model ?? mission.defaultModel);
@@ -7849,14 +7943,40 @@ async function applyModelAnswer(answer, units, missions, deps, applied, needsHum
7849
7943
  applied.push(`sent fix instruction for ${unit.missionId}:${unitHandle(unit)}`);
7850
7944
  } else if (kind === "answer_agent_question") {
7851
7945
  const answerText = stringValue(verdict.answer);
7852
- if (answerText !== void 0 && unit.pr !== null) {
7853
- await assertFenceHeld("agent-question comment");
7854
- await deps.postComment(repo, unit.pr, answerText);
7855
- unit.lastSteer = {
7856
- sha: unit.headSha ?? void 0,
7857
- atMs: Date.now()
7858
- };
7859
- applied.push(`answered agent question for ${unit.missionId}:${unitHandle(unit)}`);
7946
+ if (answerText !== void 0) {
7947
+ if (unit.pr !== null) {
7948
+ const currentHead = unit.headSha ?? void 0;
7949
+ if (!(unit.answerMentionSha != null && currentHead != null && unit.answerMentionSha === currentHead)) {
7950
+ await assertFenceHeld("agent-question mention");
7951
+ await deps.mentionCopilot(repo, unit.pr, answerText);
7952
+ unit.answerMentionSha = currentHead ?? null;
7953
+ unit.lastSteer = {
7954
+ sha: currentHead,
7955
+ atMs: Date.now()
7956
+ };
7957
+ applied.push(`answered agent question via @copilot for ${unit.missionId}:${unitHandle(unit)}`);
7958
+ }
7959
+ } else if (unit.branch != null && unit.branch.length > 0) {
7960
+ const mission = missions.find((entry) => entry.id === unit.missionId);
7961
+ let model;
7962
+ try {
7963
+ model = resolveCloudAgentModel(unit.model ?? mission?.defaultModel);
7964
+ } catch {
7965
+ model = void 0;
7966
+ }
7967
+ await assertFenceHeld("agent-question task-continue");
7968
+ const task = await deps.continueTaskOnBranch(repo, {
7969
+ headRef: unit.branch,
7970
+ baseRef: unit.baseRef ?? void 0,
7971
+ prompt: answerText,
7972
+ model,
7973
+ idempotencyKey: `continue:${unit.id ?? unitHandle(unit)}:${unit.taskId ?? "none"}`
7974
+ });
7975
+ unit.taskId = task.taskId;
7976
+ unit.provider = providerState(task.state, "in_progress");
7977
+ unit.lastSteer = { atMs: Date.now() };
7978
+ applied.push(`answered agent question via task-continue for ${unit.missionId}:${unitHandle(unit)}`);
7979
+ }
7860
7980
  }
7861
7981
  } else if (kind === "judge_review") {
7862
7982
  if (!(unit.verifierAssigned === true && (unit.validation === "review_pending" || unit.validation === "ci_passed" || unit.validation === "no_ci" || unit.validation === "floor_pending"))) {
@@ -7959,7 +8079,7 @@ function asAgentKey(value) {
7959
8079
  function validRawUnitIndices(rawUnits) {
7960
8080
  const indices = /* @__PURE__ */ new Set();
7961
8081
  for (let index = 0; index < rawUnits.length; index += 1) {
7962
- const spec = asRecord$1(rawUnits[index]);
8082
+ const spec = asRecord$2(rawUnits[index]);
7963
8083
  const title = spec === void 0 ? void 0 : stringValue(spec.title);
7964
8084
  if (title !== void 0 && title.length > 0) indices.add(index);
7965
8085
  }
@@ -7973,7 +8093,7 @@ function hasDependsOnCycle(rawUnits) {
7973
8093
  const validIndices = validRawUnitIndices(rawUnits);
7974
8094
  const graph = /* @__PURE__ */ new Map();
7975
8095
  for (const index of validIndices) {
7976
- const spec = asRecord$1(rawUnits[index]) ?? {};
8096
+ const spec = asRecord$2(rawUnits[index]) ?? {};
7977
8097
  if ((Array.isArray(spec.dependsOn) ? spec.dependsOn : []).some((idx) => idx === index)) return true;
7978
8098
  graph.set(index, dependsOnIndices$1(spec, index, validIndices));
7979
8099
  }
@@ -7991,6 +8111,11 @@ function hasDependsOnCycle(rawUnits) {
7991
8111
  for (const index of graph.keys()) if (visit(index)) return true;
7992
8112
  return false;
7993
8113
  }
8114
+ /** Parse a unit spec's `fileScopes` into a clean string[] (non-string/blank dropped). */
8115
+ function parseFileScopes(raw) {
8116
+ if (!Array.isArray(raw)) return [];
8117
+ return raw.filter((s) => typeof s === "string" && s.trim().length > 0).map((s) => s.trim());
8118
+ }
7994
8119
  /**
7995
8120
  * Turn a model `decompose` answer into queued units. This is the mission→units
7996
8121
  * step: `start_mission` only registers the mission; `advance` emits one
@@ -8005,7 +8130,7 @@ async function addUnitsToMission(mission, rawUnits, deps, existingUnits = []) {
8005
8130
  const idByRawIndex = /* @__PURE__ */ new Map();
8006
8131
  const specs = [];
8007
8132
  for (let rawIndex = 0; rawIndex < rawUnits.length; rawIndex += 1) {
8008
- const spec = asRecord$1(rawUnits[rawIndex]) ?? {};
8133
+ const spec = asRecord$2(rawUnits[rawIndex]) ?? {};
8009
8134
  const title = stringValue(spec.title);
8010
8135
  if (title === void 0 || title.length === 0) continue;
8011
8136
  const repo = parseRepoRef$1(stringValue(spec.repo)) ?? mission.repos[0];
@@ -8035,6 +8160,7 @@ async function addUnitsToMission(mission, rawUnits, deps, existingUnits = []) {
8035
8160
  let created = 0;
8036
8161
  for (const { rawIndex, id, spec, title, repo, goalHash } of specs) {
8037
8162
  const dependsOn = dependsOnIndices$1(spec, rawIndex, validIndices).map((idx) => idByRawIndex.get(idx)).filter((id$1) => id$1 !== void 0);
8163
+ const fileScopes = parseFileScopes(spec.fileScopes);
8038
8164
  const unit = {
8039
8165
  id,
8040
8166
  missionId: mission.id,
@@ -8053,7 +8179,8 @@ async function addUnitsToMission(mission, rawUnits, deps, existingUnits = []) {
8053
8179
  retries: 0,
8054
8180
  goalHash,
8055
8181
  dependsOn,
8056
- title
8182
+ title,
8183
+ ...fileScopes.length > 0 ? { fileScopes } : {}
8057
8184
  };
8058
8185
  await deps.upsertUnit(repo, unit);
8059
8186
  created += 1;
@@ -8069,7 +8196,7 @@ async function applyDecomposeAnswer(answer, missions, deps, applied) {
8069
8196
  const missionId = answer.requestId.slice(10);
8070
8197
  const mission = missions.find((m) => m.id === missionId);
8071
8198
  if (mission === void 0 || mission.status !== "active") return;
8072
- const verdict = asRecord$1(answer.verdict) ?? {};
8199
+ const verdict = asRecord$2(answer.verdict) ?? {};
8073
8200
  const created = await addUnitsToMission(mission, Array.isArray(verdict.units) ? verdict.units : [], deps, await deps.loadAllUnits(mission.id));
8074
8201
  if (created > 0) applied.push(`decomposed ${missionId} into ${created} unit(s)`);
8075
8202
  }
@@ -8124,6 +8251,42 @@ async function maybeMergeWithApproval(unit, observed, evidence, deps, applied) {
8124
8251
  await deps.upsertUnit(unit.repo, unit);
8125
8252
  return true;
8126
8253
  }
8254
+ async function createMissionStallRequest(mission, liveUnits, reason, fingerprint, deps) {
8255
+ const representative = liveUnits[0];
8256
+ const decisionKey = `mission:${mission.id}:stall:${fingerprint}`;
8257
+ const existing = await deps.findByKey(decisionKey);
8258
+ let record = existing?.status === "pending" ? existing : void 0;
8259
+ let packetHtmlPath;
8260
+ if (record === void 0) {
8261
+ const observed = {
8262
+ provider: representative.provider,
8263
+ prs: []
8264
+ };
8265
+ const packet = deps.buildDecisionPacket(packetInput(representative, mission, observed, reason, "human_decision"));
8266
+ packetHtmlPath = await deps.writeDecisionPacketHtml(packet.packetId, packet.html);
8267
+ record = {
8268
+ decisionId: packet.decisionId,
8269
+ decisionKey,
8270
+ type: "human_decision",
8271
+ status: "pending",
8272
+ packetId: packet.packetId,
8273
+ inputFingerprint: fingerprint,
8274
+ options: decisionOptions("human_decision").map((option) => ({ id: option.id })),
8275
+ createdMs: Date.now()
8276
+ };
8277
+ await deps.upsertDecision(record);
8278
+ }
8279
+ return {
8280
+ requestId: decisionKey,
8281
+ decisionId: record.decisionId,
8282
+ missionId: mission.id,
8283
+ repo: representative.repo,
8284
+ issue: null,
8285
+ pr: null,
8286
+ reason,
8287
+ ...packetHtmlPath !== void 0 ? { packetHtmlPath } : {}
8288
+ };
8289
+ }
8127
8290
  async function createHumanRequest(unit, mission, observed, reason, deps) {
8128
8291
  const { decisionKey, fingerprint, type } = decisionKeyFor(unit, observed, reason);
8129
8292
  const existing = await deps.findByKey(decisionKey);
@@ -8309,9 +8472,10 @@ function shouldEscalateOpenUncorrelated(unit, observed, activeUnits) {
8309
8472
  }
8310
8473
  async function observeBlockedUnit(unit, deps, applied) {
8311
8474
  const decisionId = unit.blockingDecisionId;
8312
- if (!decisionId) return;
8475
+ if (!decisionId) return false;
8313
8476
  const observed = await deps.observeUnit(unit);
8314
8477
  updateUnitFromObservedPrs(unit, observed);
8478
+ const progressed = recordVerifiedProgress(unit, observed);
8315
8479
  unit.lastCheckedMs = Date.now();
8316
8480
  const mutation = observed.externalMutation;
8317
8481
  let decision;
@@ -8358,6 +8522,7 @@ async function observeBlockedUnit(unit, deps, applied) {
8358
8522
  applied.push(`reconciled external close for ${repoLabel$1(unit.repo)}#${unit.pr ?? "?"}`);
8359
8523
  } else if (mutation === "merged_uncorrelated" || mutation === "open_uncorrelated") consola.warn(`first-mate: an UNCORRELATED ${mutation === "merged_uncorrelated" ? "merged" : "open"} PR was observed for blocked ${unit.missionId}:${unitHandle(unit)} — leaving blocked for human reconciliation`);
8360
8524
  await deps.upsertUnit(unit.repo, unit);
8525
+ return progressed;
8361
8526
  }
8362
8527
  /**
8363
8528
  * Lightweight, idempotent, best-effort reconciliation at the start of a drive.
@@ -8418,9 +8583,30 @@ async function assertFenceHeld(effect) {
8418
8583
  const token = currentFenceToken();
8419
8584
  if (token !== void 0 && !await isCurrentFencingToken(token)) throw new Error(`first-mate: drive lease lost before ${effect} (token ${token}) — skipping side effect`);
8420
8585
  }
8421
- async function executeAction(action, unit, mission, observed, evidence, policy, deps, needsModel, needsHuman, applied, order) {
8586
+ async function executeAction(action, unit, mission, observed, evidence, policy, deps, needsModel, needsHuman, applied, order, renewLease) {
8422
8587
  switch (action.kind) {
8423
8588
  case "dispatch": return;
8589
+ case "retry_plan": {
8590
+ const repo = agentRepo(unit.repo);
8591
+ const model = resolveCloudAgentModel(unit.model ?? mission.defaultModel);
8592
+ const dateStr = unit.artifactDateStr ?? artifactDate(Date.now());
8593
+ unit.artifactDateStr = dateStr;
8594
+ unit.planRetries = (unit.planRetries ?? 0) + 1;
8595
+ if (await dispatchWithOutbox(unit, deps, ({ idempotencyKey, promptTag }) => deps.startTask(repo, {
8596
+ prompt: planPrompt(unit, mission, dateStr) + promptTag,
8597
+ model,
8598
+ createPullRequest: false,
8599
+ idempotencyKey
8600
+ }), renewLease, (started) => {
8601
+ unit.provider = providerState(started.state, "queued");
8602
+ unit.phase = "plan";
8603
+ unit.dispatchMode = "plan";
8604
+ unit.blockingDecisionId = null;
8605
+ unit.implementerLab = unit.agent;
8606
+ unit.lastSteer = { atMs: Date.now() };
8607
+ })) applied.push(`retried plan task for ${unit.missionId}:${unitHandle(unit)}`);
8608
+ return;
8609
+ }
8424
8610
  case "steer":
8425
8611
  consola.debug("first-mate controller received direct steer action; v1 skips it");
8426
8612
  return;
@@ -8485,9 +8671,12 @@ async function upsertMissionDurable(mission, deps) {
8485
8671
  function isUndispatched(unit) {
8486
8672
  return unit.provider === "none" && unit.taskId === null && unit.dispatch === void 0;
8487
8673
  }
8488
- /** A dispatch that was interrupted mid-flight (intent persisted, no taskId yet). */
8674
+ /**
8675
+ * A dispatch interrupted mid-flight. Successor dispatches retain the prior taskId
8676
+ * until the new task is confirmed, so the durable intent itself is the signal.
8677
+ */
8489
8678
  function isDispatchInterrupted(unit) {
8490
- return unit.dispatch !== void 0 && unit.taskId === null;
8679
+ return unit.dispatch !== void 0;
8491
8680
  }
8492
8681
  function isActiveMissionUnit(unit, missions) {
8493
8682
  return missions.get(unit.missionId)?.status === "active";
@@ -8509,8 +8698,59 @@ function activeCountsByAgent(units) {
8509
8698
  }
8510
8699
  return counts;
8511
8700
  }
8512
- function hasActiveBuildUnit(missionId, units) {
8513
- return units.some((unit) => unit.missionId === missionId && unit.dispatchMode === "build" && unit.terminal !== true && (unit.provider !== "none" || unit.taskId !== null || unit.dispatch !== void 0));
8701
+ function activeBuildUnits(missionId, units) {
8702
+ return units.filter((unit) => unit.missionId === missionId && unit.dispatchMode === "build" && unit.terminal !== true && (unit.provider !== "none" || unit.taskId !== null || unit.dispatch !== void 0));
8703
+ }
8704
+ /**
8705
+ * Normalize a declared file-scope entry to a comparable path prefix: collapse
8706
+ * backslashes, strip a leading `./`, and — because ANY glob metacharacter (`*`,
8707
+ * `?`, `[`, `{`) makes the matched set unbounded — collapse the scope to the
8708
+ * PARENT DIRECTORY of its first glob segment (`src/*.ts` → `src`, `src/f?.ts` →
8709
+ * `src`, `*.ts`/`{a,b}/**` → `` = whole repo). This over-approximates (it can
8710
+ * over-serialize) but NEVER declares two globby scopes falsely disjoint. A
8711
+ * whole-repo scope (`""`, `.`, `/`, or a root-level glob) overlaps everything.
8712
+ * Case is preserved — paths are case-sensitive on the primary Linux CI.
8713
+ */
8714
+ function normalizeScope(raw) {
8715
+ let s = raw.trim().replace(/\\/g, "/").replace(/^\.\//, "");
8716
+ const glob = s.search(/[*?{[]/);
8717
+ if (glob !== -1) {
8718
+ const lastSlash = s.lastIndexOf("/", glob);
8719
+ s = lastSlash === -1 ? "" : s.slice(0, lastSlash);
8720
+ }
8721
+ s = s.replace(/^\.$/, "").replace(/\/+$/, "");
8722
+ return s;
8723
+ }
8724
+ /** Two single scopes overlap when equal, or one is a directory-prefix of the other. */
8725
+ function scopesOverlap(a, b) {
8726
+ if (a === "" || b === "") return true;
8727
+ return a === b || a.startsWith(`${b}/`) || b.startsWith(`${a}/`);
8728
+ }
8729
+ /**
8730
+ * Two file-scope LISTS are PROVABLY disjoint only when BOTH are non-empty and no
8731
+ * pair of entries overlaps. Absent/empty → NOT disjoint (unknown scope → caller
8732
+ * must serialize; the safe, pre-existing behavior).
8733
+ */
8734
+ function fileScopesDisjoint(a, b) {
8735
+ if (a === void 0 || b === void 0 || a.length === 0 || b.length === 0) return false;
8736
+ const na = a.map(normalizeScope);
8737
+ const nb = b.map(normalizeScope);
8738
+ return na.every((x) => nb.every((y) => !scopesOverlap(x, y)));
8739
+ }
8740
+ /**
8741
+ * Concurrent-build gate. A mission serializes builds by default (one active build
8742
+ * per mission — avoids racing PRs on overlapping files). A candidate build may run
8743
+ * CONCURRENTLY with the mission's other active builds only when independence is
8744
+ * PROVEN: under the per-mission `maxConcurrentBuilds` cap AND its declared
8745
+ * `fileScopes` are non-empty and disjoint from EVERY other active build's scopes.
8746
+ * Unknown or overlapping scope → serialize. Dependency ordering is gated
8747
+ * separately by `depsSatisfied`, so this only governs concurrency among ready units.
8748
+ */
8749
+ function canDispatchBuild(candidate, mission, units) {
8750
+ const active = activeBuildUnits(mission.id, units).filter((u) => u !== candidate);
8751
+ if (active.length === 0) return true;
8752
+ if (active.length >= maxConcurrentBuildsOf(mission)) return false;
8753
+ return active.every((other) => fileScopesDisjoint(candidate.fileScopes, other.fileScopes));
8514
8754
  }
8515
8755
  /** Default plan-review gate for a mission (absent → the hard, current flow). */
8516
8756
  function planGateOf(mission) {
@@ -8523,6 +8763,13 @@ function planGateOf(mission) {
8523
8763
  */
8524
8764
  const DEFAULT_MAX_FIX_CYCLES = 12;
8525
8765
  const DEFAULT_MAX_COPILOT_COMMENTS = 8;
8766
+ /**
8767
+ * Default per-mission concurrent-build cap. Builds still serialize unless the
8768
+ * units prove disjoint `fileScopes`, so this only bounds parallelism among
8769
+ * provably-independent units; a mission without declared scopes behaves exactly
8770
+ * as before (one active build at a time).
8771
+ */
8772
+ const DEFAULT_MAX_CONCURRENT_BUILDS = 4;
8526
8773
  function maxFixCyclesOf(mission) {
8527
8774
  const value = mission.maxFixCycles;
8528
8775
  return typeof value === "number" && Number.isInteger(value) && value >= 1 ? value : DEFAULT_MAX_FIX_CYCLES;
@@ -8531,6 +8778,10 @@ function maxCopilotCommentsOf(mission) {
8531
8778
  const value = mission.maxCopilotComments;
8532
8779
  return typeof value === "number" && Number.isInteger(value) && value >= 1 ? value : DEFAULT_MAX_COPILOT_COMMENTS;
8533
8780
  }
8781
+ function maxConcurrentBuildsOf(mission) {
8782
+ const value = mission.maxConcurrentBuilds;
8783
+ return typeof value === "number" && Number.isInteger(value) && value >= 1 ? value : DEFAULT_MAX_CONCURRENT_BUILDS;
8784
+ }
8534
8785
  /** Format an epoch-ms timestamp as a UTC `YYYY-MM-DD` date for artifact paths. */
8535
8786
  function artifactDate(ms) {
8536
8787
  return new Date(ms).toISOString().slice(0, 10);
@@ -8547,6 +8798,13 @@ function artifactSlug(unit) {
8547
8798
  function unitIdInstruction(unit) {
8548
8799
  return `Controller correlation marker: ${`unit-id: ${unit.id ?? unitHandle(unit)}`}. The Agent-Tasks API has no branch/label field, so this is cooperative: include this exact marker in the branch name if possible, put it on its own line in the PR body, and include it in the first commit message trailer.`;
8549
8800
  }
8801
+ /**
8802
+ * Prevention layer for plan-mode stalls: the documented lever to keep a Copilot
8803
+ * cloud agent from parking in `waiting_for_user` is to instruct it to proceed on
8804
+ * best judgment and record assumptions instead of pausing to ask. Included in
8805
+ * every dispatch prompt; mirrored in scaffolded repo guidance.
8806
+ */
8807
+ const AUTONOMY_DIRECTIVE = "Work autonomously: do not stop to ask clarifying questions. If a requirement is ambiguous, choose the most reasonable interpretation, state the assumption explicitly (in the plan and the pull request description), and continue. Surface open questions as notes rather than blocking on them.";
8550
8808
  function planPrompt(unit, mission, dateStr) {
8551
8809
  const slug = artifactSlug(unit);
8552
8810
  const parts = [
@@ -8558,6 +8816,7 @@ function planPrompt(unit, mission, dateStr) {
8558
8816
  `Persist your work as durable artifacts committed on the branch: write your research and findings to \`docs/research/${dateStr}-${slug}.md\` and your step-by-step implementation plan to \`docs/plans/${dateStr}-${slug}.md\`. Create the \`docs/research\` and \`docs/plans\` directories if they do not exist, and commit both files on the branch so the implementation task can read them.`
8559
8817
  ];
8560
8818
  if (mission.houseRules !== void 0) parts.splice(2, 0, `House rules:\n${mission.houseRules}`);
8819
+ parts.push(AUTONOMY_DIRECTIVE);
8561
8820
  parts.push(renderDod([mission.acceptanceCriteria]));
8562
8821
  return parts.join("\n\n");
8563
8822
  }
@@ -8574,6 +8833,7 @@ function buildPrompt(unit, mission, dateStr) {
8574
8833
  if (hasPlan) parts.push(`Approved plan (authoritative — implement this):\n${unit.planExcerpt.trim()}`);
8575
8834
  parts.push("Implement this work unit end-to-end on a new branch and open a pull request for review. Follow the approved plan above. Keep the change focused on this unit and do not modify unrelated files. If anything about the acceptance criteria is ambiguous, make a reasonable choice and note it in the PR description.");
8576
8835
  parts.push(hasPlan ? `If a committed implementation plan for this unit is present (\`docs/plans/${dateStr}-${slug}.md\`, or a file under \`docs/plans/\` whose name ends with \`-${slug}.md\`) and the research at \`docs/research/${dateStr}-${slug}.md\`, read them for extra detail — but the approved plan above is authoritative and does not depend on those files existing. Keep any such artifacts up to date with deviations you make, and if a \`LEARNINGS.md\` exists at the repository root, append a dated entry summarizing what you learned.` : `Read the committed implementation plan at \`docs/plans/${dateStr}-${slug}.md\` and the research at \`docs/research/${dateStr}-${slug}.md\` (if the exact dated filename is absent, locate the plan committed for this unit under \`docs/plans/\` whose name ends with \`-${slug}.md\`) and implement it. Keep those artifacts up to date with any deviations you make, and if a \`LEARNINGS.md\` exists at the repository root, append a dated entry summarizing what you learned.`);
8836
+ parts.push(AUTONOMY_DIRECTIVE);
8577
8837
  parts.push(renderDod([mission.acceptanceCriteria]));
8578
8838
  return parts.join("\n\n");
8579
8839
  }
@@ -8599,14 +8859,15 @@ function dispatchIdempotencyKey(unit, attempt) {
8599
8859
  const unitId = unit.id ?? (unit.issue !== null ? `issue-${unit.issue}` : "unit");
8600
8860
  return `dispatch:${unit.repo.owner}/${unit.repo.name}#${unitId}@${attempt}`;
8601
8861
  }
8602
- async function dispatchWithOutbox(unit, deps, start, renewLease) {
8862
+ async function dispatchWithOutbox(unit, deps, start, renewLease, applySuccess) {
8603
8863
  if (unit.dispatch !== void 0) {
8604
8864
  consola.debug(`first-mate: skipping dispatch for ${unit.missionId}:${unitHandle(unit)} — a dispatch intent (${unit.dispatch.id}) is already pending; recovery resolves it`);
8605
8865
  return null;
8606
8866
  }
8607
8867
  if (renewLease !== void 0 && !await renewLease()) throw new Error(`first-mate: drive lease renewal failed before dispatch side effect for ${unit.missionId}:${unitHandle(unit)} — aborting`);
8608
- const attempt = 1;
8868
+ const attempt = (unit.dispatchAttempts ?? (unit.taskId ? 1 : 0)) + 1;
8609
8869
  const key = dispatchIdempotencyKey(unit, attempt);
8870
+ unit.dispatchAttempts = attempt;
8610
8871
  unit.dispatch = {
8611
8872
  id: key,
8612
8873
  requestedMs: Date.now(),
@@ -8626,6 +8887,7 @@ async function dispatchWithOutbox(unit, deps, start, renewLease) {
8626
8887
  if (task.taskId.length === 0) return null;
8627
8888
  unit.taskId = task.taskId;
8628
8889
  unit.dispatch = void 0;
8890
+ applySuccess?.(task);
8629
8891
  await deps.upsertUnit(unit.repo, unit);
8630
8892
  await deps.dispatchOutbox?.markDone(key);
8631
8893
  return task;
@@ -8664,7 +8926,7 @@ async function dispatchWave(units, missions, maxInFlightPerProvider, deps, appli
8664
8926
  if (current >= maxInFlightPerProvider) continue;
8665
8927
  const mission = missions.get(unit.missionId);
8666
8928
  if (mission === void 0) continue;
8667
- if (unit.dispatchMode === "build" && hasActiveBuildUnit(unit.missionId, allCountUnits)) continue;
8929
+ if (unit.dispatchMode === "build" && !canDispatchBuild(unit, mission, allCountUnits)) continue;
8668
8930
  if (renewLease !== void 0 && !await renewLease()) {
8669
8931
  applied.push("dispatch wave stopped: drive lease lost mid-sweep");
8670
8932
  break;
@@ -8830,6 +9092,8 @@ async function advance(input = {}, deps = defaultDeps) {
8830
9092
  const scopedMissions = input.missionId ? missions.filter((m) => m.id === input.missionId) : missions;
8831
9093
  const missionsById = missionMap(scopedMissions);
8832
9094
  let order = needsHuman.length;
9095
+ const progressedMissionIds = /* @__PURE__ */ new Set();
9096
+ const observedUnits = /* @__PURE__ */ new Set();
8833
9097
  try {
8834
9098
  await reconcile(scopedUnits, deps, applied);
8835
9099
  } catch (err) {
@@ -8840,7 +9104,8 @@ async function advance(input = {}, deps = defaultDeps) {
8840
9104
  order += 1;
8841
9105
  if (unit.blockingDecisionId) {
8842
9106
  try {
8843
- await observeBlockedUnit(unit, deps, applied);
9107
+ observedUnits.add(unit);
9108
+ if (await observeBlockedUnit(unit, deps, applied)) progressedMissionIds.add(unit.missionId);
8844
9109
  } catch (err) {
8845
9110
  consola.warn(`first-mate: blocked unit ${unit.missionId}:${unitHandle(unit)} observe failed:`, err);
8846
9111
  applied.push(`error observing blocked ${unit.missionId}:${unitHandle(unit)}: ${errText(err)}`);
@@ -8864,7 +9129,20 @@ async function advance(input = {}, deps = defaultDeps) {
8864
9129
  continue;
8865
9130
  }
8866
9131
  const observed = await deps.observeUnit(unit);
8867
- if (await reconcileObservedPrState(unit, observed, deps, applied)) continue;
9132
+ observedUnits.add(unit);
9133
+ const prevEmptyState = {
9134
+ headSha: unit.headSha ?? null,
9135
+ baseRef: unit.baseRef ?? null,
9136
+ isDraft: unit.prIsDraft
9137
+ };
9138
+ updateUnitFromObservedPrs(unit, observed);
9139
+ if (recordVerifiedProgress(unit, observed)) progressedMissionIds.add(unit.missionId);
9140
+ unit.lastCheckedMs = Date.now();
9141
+ if (await reconcileObservedPrState(unit, observed, deps, applied)) {
9142
+ if (recordVerifiedProgress(unit, observed)) progressedMissionIds.add(unit.missionId);
9143
+ await deps.upsertUnit(unit.repo, unit);
9144
+ continue;
9145
+ }
8868
9146
  if (shouldEscalateOpenUncorrelated(unit, observed, scopedUnits)) {
8869
9147
  needsHuman.push({
8870
9148
  request: await createHumanRequest(unit, mission, observed, `uncorrelated open same-bot PR #${observed.externalPr ?? "unknown"} appears to be orphaned — human reconciliation required before first mate can continue`, deps),
@@ -8875,13 +9153,6 @@ async function advance(input = {}, deps = defaultDeps) {
8875
9153
  continue;
8876
9154
  }
8877
9155
  const evidence = await fillFuzzyFields(unit, mission, observed, deps);
8878
- const prevEmptyState = {
8879
- headSha: unit.headSha ?? null,
8880
- baseRef: unit.baseRef ?? null,
8881
- isDraft: unit.prIsDraft
8882
- };
8883
- updateUnitFromObservedPrs(unit, observed);
8884
- unit.lastCheckedMs = Date.now();
8885
9156
  const failSig = failureSignature(observed);
8886
9157
  if (unit.lastFailSig !== void 0 && unit.lastFailSig !== null && failSig !== unit.lastFailSig) unit.retries = 0;
8887
9158
  unit.lastFailSig = failSig;
@@ -8890,7 +9161,11 @@ async function advance(input = {}, deps = defaultDeps) {
8890
9161
  } catch (err) {
8891
9162
  consola.debug(`first-mate: dismissStaleOwnReviews skipped for ${unit.missionId}:${unitHandle(unit)}:`, err);
8892
9163
  }
8893
- if (await maybeMergeWithApproval(unit, observed, evidence, deps, applied)) continue;
9164
+ if (await maybeMergeWithApproval(unit, observed, evidence, deps, applied)) {
9165
+ if (recordVerifiedProgress(unit, observed)) progressedMissionIds.add(unit.missionId);
9166
+ await deps.upsertUnit(unit.repo, unit);
9167
+ continue;
9168
+ }
8894
9169
  const classified = classify(observed, unit);
8895
9170
  unit.provider = classified.provider;
8896
9171
  unit.phase = classified.phase;
@@ -8912,19 +9187,56 @@ async function advance(input = {}, deps = defaultDeps) {
8912
9187
  reason: "the agent's pull request still has no changes and its head has not advanced across repeated observations"
8913
9188
  };
8914
9189
  }
8915
- await executeAction(action, unit, mission, observed, evidence, policy, deps, needsModel, needsHuman, applied, requestOrder);
9190
+ await executeAction(action, unit, mission, observed, evidence, policy, deps, needsModel, needsHuman, applied, requestOrder, input.renewLease);
8916
9191
  await deps.upsertUnit(unit.repo, unit);
8917
9192
  } catch (err) {
8918
9193
  consola.warn(`first-mate: unit ${unit.missionId}:${unitHandle(unit)} step failed:`, err);
8919
9194
  applied.push(`error advancing ${unit.missionId}:${unitHandle(unit)}: ${errText(err)}`);
8920
9195
  }
8921
9196
  }
9197
+ for (const mission of scopedMissions) {
9198
+ if (mission.status !== "active") continue;
9199
+ const liveUnits = scopedUnits.filter((unit) => unit.missionId === mission.id && unit.terminal !== true);
9200
+ if (liveUnits.length === 0 || liveUnits.some((unit) => !observedUnits.has(unit))) continue;
9201
+ const now = Date.now();
9202
+ if (progressedMissionIds.has(mission.id)) {
9203
+ mission.noProgressWakes = 0;
9204
+ delete mission.stallSinceMs;
9205
+ delete mission.stallEscalatedFingerprint;
9206
+ mission.updatedMs = now;
9207
+ await upsertMissionDurable(mission, deps);
9208
+ continue;
9209
+ }
9210
+ mission.noProgressWakes = (mission.noProgressWakes ?? 0) + 1;
9211
+ mission.stallSinceMs ??= now;
9212
+ mission.updatedMs = now;
9213
+ const stallFingerprint = digest(liveUnits.map((unit) => `${unitHandle(unit)}:${unit.provider}:${unit.lastNoProgressReason ?? "no verified progress"}`).sort().join("|"));
9214
+ const oldEnough = now - mission.stallSinceMs >= STALL_MS;
9215
+ const impossible = mission.noProgressWakes >= STALL_WAKES && liveUnits.every((unit) => unit.provider === "in_progress" && unit.pr === null);
9216
+ if (mission.noProgressWakes >= STALL_WAKES && (oldEnough || impossible) && mission.stallEscalatedFingerprint !== stallFingerprint) {
9217
+ const ageMinutes = Math.max(0, Math.floor((now - mission.stallSinceMs) / 6e4));
9218
+ const diagnosis = liveUnits.map((unit) => `${unitHandle(unit)} [provider=${unit.provider}]: ${unit.lastNoProgressReason ?? "no verified progress"}`).join("; ");
9219
+ const reason = `mission is not progressing toward merge after ${mission.noProgressWakes} wakes (${ageMinutes}m): ${diagnosis}`;
9220
+ needsHuman.push({
9221
+ request: await createMissionStallRequest(mission, liveUnits, reason, stallFingerprint, deps),
9222
+ sortKey: Math.min(...liveUnits.map(sortKey)),
9223
+ order: order++
9224
+ });
9225
+ mission.stallEscalatedFingerprint = stallFingerprint;
9226
+ }
9227
+ await upsertMissionDurable(mission, deps);
9228
+ }
8922
9229
  for (const mission of scopedMissions) {
8923
9230
  if (mission.status !== "active") continue;
8924
9231
  if (mission.everDecomposed !== true) continue;
8925
9232
  if (scopedUnits.some((unit) => unit.missionId === mission.id && isActiveUnit(unit, missionsById))) continue;
8926
9233
  if (scopedUnits.some((unit) => unit.missionId === mission.id && Boolean(unit.blockingDecisionId))) continue;
8927
9234
  if (hasQueuedRequestForMission(mission.id, needsModel, needsHuman)) continue;
9235
+ const unmergedTerminal = scopedUnits.filter((unit) => unit.missionId === mission.id && unit.terminal === true && unit.artifact !== "pr_merged");
9236
+ if (unmergedTerminal.length > 0) {
9237
+ applied.push(`mission ${mission.id} NOT completed: ${unmergedTerminal.length} unit(s) ended without a merge — done requires every unit merged`);
9238
+ continue;
9239
+ }
8928
9240
  mission.status = "done";
8929
9241
  mission.updatedMs = Date.now();
8930
9242
  await upsertMissionDurable(mission, deps);
@@ -8972,9 +9284,99 @@ async function advance(input = {}, deps = defaultDeps) {
8972
9284
  return fenceToken !== void 0 ? runFenced(fenceToken, runDrive) : runDrive();
8973
9285
  }
8974
9286
 
9287
+ //#endregion
9288
+ //#region src/lib/first-mate/operating-protocol.ts
9289
+ /**
9290
+ * Single source of truth for the condensed CEO/CTO/CPO operating sequence.
9291
+ *
9292
+ * Shared by BOTH the scaffolded product playbook (`buildPlaybook` in
9293
+ * `scaffold-spec.ts`, committed into a repo and read by the GitHub cloud agents)
9294
+ * AND the operator-facing `gh-first-mate-operate` skill (read by the local
9295
+ * first-mate lead that shapes missions). Keeping ONE copy is what prevents the
9296
+ * cloud-agent surface and the operator surface from drifting apart.
9297
+ */
9298
+ const CONDENSED_OPERATING_SEQUENCE = `1. **DISCOVER:** find a struggling moment; require three corroborated sources; log hire/fire criteria.
9299
+ 2. **NICHE:** choose one reachable beachhead with a credible 1,000-fan path; pass a distribution test and go/no-go table.
9300
+ 3. **POSITION:** map do-nothing, workarounds, and competitors; prove differentiated value; test one sentence with real prospects.
9301
+ 4. **SCOPE:** run the riskiest-assumption test against a pre-set threshold; freeze v0.1 with must-be, performance, and at least one delight.
9302
+ 5. **BUILD:** ADRs, trunk/flags, test pyramid, CI/CD and DORA; verify HTTP 200, green CI, five-minute quickstart, WCAG, and Web Vitals.
9303
+ 6. **LAUNCH:** README and docs sell the job; launch sequentially where the beachhead lives; instrument the aha moment.
9304
+ 7. **MEASURE:** AARRR, time-to-first-value, Sean Ellis ≥40% very disappointed, and a flattening retention curve.
9305
+ 8. **ITERATE:** weekly opportunity-solution tree from real evidence; RICE validated options only; thresholded experiments and changelog.
9306
+ 9. **GROW:** scale retained-user channels and shareable-artifact loops within explicit economics.
9307
+ 10. **GOVERN:** OODA daily inside Build-Measure-Learn; log hypothesis → experiment → metric → threshold → outcome; advance only on externally verifiable checkpoints.`;
9308
+ /**
9309
+ * Single source of truth for the DEFINITION OF REPO GREATNESS — the shipping-
9310
+ * infrastructure bar a repo must clear to be "great", stated as checkpoints a
9311
+ * third party can verify with NO special access (a green check, a `gh api`, a
9312
+ * `curl`, a `cosign verify`). Shared by the scaffolded playbook (`buildPlaybook`),
9313
+ * the operator/CEO skills, and the eval framework, so the bar never drifts across
9314
+ * surfaces. Every item is "done" only with a real evidence handle, never a
9315
+ * self-reported claim.
9316
+ *
9317
+ * Currency note (2026): INP replaced FID; npm/crates.io OIDC Trusted Publishing is
9318
+ * GA; GitHub build-provenance attestations meet SLSA Build L3; VS Code Marketplace
9319
+ * still needs a long-lived PAT (the one OIDC gap). SEO indexing/ranking and
9320
+ * E-E-A-T are NOT deterministically gradeable by anonymous curl — treat as soft
9321
+ * signals (proxy via byline/date/architecture + Search Console).
9322
+ */
9323
+ const DEFINITION_OF_GREATNESS = `A repo is GREAT on the shipping axis only when every claim is externally verifiable by a stranger. The organizing invariant: **version file == git tag == GitHub Release == registry version == CHANGELOG heading == live-site build stamp** — most checkpoints are just cross-checks of this identity.
9324
+
9325
+ **Leading vs lagging — the anti-hallucination rule for greatness.** Every checkpoint is either **[LEADING]** (a file/config an agent can add in ONE commit — necessary hygiene, NEVER proof of greatness) or **[LAGGING]** (requires real humans acting over time — response times, repeat contributors, dependents, retained downloads) or **[SOFT]** (real but not deterministically gradeable). An autonomous agent can fabricate every leading artifact while producing zero community. So a repo is GREAT only when at least one LAGGING signal has actually MOVED — not when the leading boxes are merely ticked. Never treat file-presence as evidence of community, adoption, or trust.
9326
+
9327
+ ## Universal core (every repo)
9328
+ - **Branch protection via a ruleset** [LEADING] on the default branch: required checks (lint + typecheck + test + build) green, strict (up-to-date), ≥1 review, linear history, conversation resolution. Verify: \`gh api repos/$R/rules/branches/main\`.
9329
+ - **Matrix CI** [LEADING] (OS × version) green on the default branch; gate on a synthetic all-green job that \`needs:\` every matrix cell. Verify: \`gh api repos/$R/commits/main/check-runs --jq '[.check_runs[].conclusion]|unique'\` == \`["success"]\`.
9330
+ - **Supply-chain security** [LEADING]: CodeQL + Dependabot (deps + security + \`github-actions\`) + secret scanning + push protection on, low/zero open alerts; OpenSSF Scorecard published. Verify: \`gh api repos/$R/code-scanning/alerts?state=open\`, \`.../dependabot/alerts?state=open\`, \`api.scorecard.dev/projects/github.com/$R\`.
9331
+ - **Security DISCLOSURE posture** [LEADING] (distinct from build hardening): \`SECURITY.md\` with a disclosure channel + response SLA; GitHub Private Vulnerability Reporting enabled; \`/.well-known/security.txt\` (RFC 9116) served on the site; an **OpenSSF Best Practices badge** (passing → silver → gold; state is API-readable at bestpractices.openssf.org). [LAGGING] a real advisory/CVE track record + time-to-patch.
9332
+ - **Workflow hygiene** [LEADING]: \`permissions:\` least-privilege (default \`contents: read\`, escalate per-job); every \`uses:\` pinned to a 40-char SHA; reproducible installs (lockfile + \`npm ci\`/\`--frozen\`/\`--require-hashes\`). Flaky tests QUARANTINED (\`continue-on-error\`, non-required, tracked as issues) — never retried-until-green.
9333
+ - **Safe to depend on** [LEADING/verifiable]: a written deprecation/stability/support-window policy + an **API-breaking-change CI gate** (\`cargo-semver-checks\` / \`@microsoft/api-extractor\` / \`japicmp\`) that fails a PR on an undeclared breaking change; migration guides for majors. [LAGGING] unplanned breaking releases trend to ~0.
9334
+ - **Docs that TEACH, not just rank** [LEADING/verifiable]: Diataxis structure (tutorial / how-to / reference / explanation) + a generated **API reference** + **examples EXECUTED in CI** (doctest / tested code fences — the one docs check that resists rot); a \`<5-min\` time-to-first-success smoke test from a clean container. [SOFT] whether the quickstart is genuinely clear.
9335
+ - **Legal & provenance** [LEADING/verifiable]: REUSE/SPDX headers (\`reuse lint\`), DCO or CLA enforced on PRs, a dependency-license scan in CI, correct \`NOTICE\`/attribution; \`actions/attest-build-provenance\` on release artifacts (SLSA Build L3) — verify \`gh attestation verify <artifact> --repo $R\`.
9336
+ - **Quality beyond green CI** [LEADING, gate ONLY when the surface applies]: OSS-Fuzz (parsers / security-sensitive), a perf-regression gate (hot paths), accessibility CI (axe-core / Lighthouse a11y ≥ target) for any UI, i18n wired. Track mutation-testing score (Stryker / \`cargo-mutants\`) — do NOT hard-gate a raw coverage %.
9337
+ - **Releases** [LEADING]: SemVer single-sourced; **Keep a Changelog** \`CHANGELOG.md\` (dated, \`[Unreleased]\`); tagged **GitHub Releases** with generated notes; release automation (release-please / semantic-release). Verify: \`gh release view vX.Y.Z --json tagName,isLatest,body,assets\`.
9338
+ - **README media** [LEADING]: theme-adaptive hero (\`<picture>\`/\`#gh-dark-mode-only\`) + a demo (GIF / asciinema / linked video) + alt text on every image; repo social-preview set. Verify: \`grep\` the README + \`curl -I\` the assets return image 200.
9339
+
9340
+ ## Community, sustainability & trust (Pillar C — the contributor funnel + who keeps it alive)
9341
+ Great OSS is a project other humans JOIN, TRUST, and SUSTAIN — not just a well-shipped artifact. Most of this is LAGGING: measure the trajectory, never the file. A great score here REQUIRES a lagging signal to have moved.
9342
+ - **Contributor funnel** [LAGGING]: time-to-first-response on new issues/PRs (CHAOSS; healthy ≈ ≤2 business days, a HUMAN not a bot), median PR-merge time, and **repeat-contributor rate** (contributors with ≥2 contributions across periods) — the real "is the onramp working" signal. [LEADING] \`CONTRIBUTING.md\` / \`CODE_OF_CONDUCT.md\` / \`SUPPORT.md\` / \`CODEOWNERS\` / issue + PR templates present (\`gh api repos/$R/community/profile\`) + good-first-issue / help-wanted labels — a HYGIENE FLOOR only, never proof.
9343
+ - **Sustainability & governance** [LEADING] \`.github/FUNDING.yml\`, \`GOVERNANCE.md\`/\`MAINTAINERS.md\`, \`ADOPTERS.md\`; [LAGGING] **Contributor Absence Factor** (bus-factor: min contributors making 50% of contributions) ≥ 2–3, **committers from ≥2 organizations** (CNCF graduated bar), ≥3 real adopters, and **liveness** — recent last-commit / last-release on a predictable cadence (abandonment is the most common death).
9344
+ - **Adoption verdict** [LAGGING]: **dependents / "Used by" count** (dependency-graph API — the un-fakeable adoption signal funders use), retained-download trend (not a launch-day spike), third-party integrations/tutorials, issue-to-star ratio (stars alone are vanity).
9345
+
9346
+ ## By final destination (detect from the repo root)
9347
+ - **Web app / docs → GitHub Pages** (\`build_type == "workflow"\`, auto-deploy on merge, protected \`github-pages\` env):
9348
+ - **Live-content proof:** stamp \`_site/BUILD_SHA.txt\`; the live edge must serve it — \`curl -fsSL "$SITE/BUILD_SHA.txt?cb=$(date +%s)"\` == \`git rev-parse HEAD\`.
9349
+ - **Google-discoverable SEO:** \`sitemap.xml\` + \`robots.txt\` (with \`Sitemap:\`) + self-referential \`rel=canonical\` + no accidental noindex (custom \`404.html\` gets \`meta robots noindex\`); full **OG + Twitter cards** + **JSON-LD \`SoftwareApplication\`** (+ \`WebSite\`/\`BreadcrumbList\`); **Lighthouse SEO ≥ 0.90**; **Core Web Vitals** LCP ≤ 2.5s / INP ≤ 200ms / CLS ≤ 0.1; HTTPS enforced + correct custom-domain DNS; **Search Console** verified + sitemap submitted + IndexNow ping from the deploy. og-image 1200×630 returns 200. Verify via \`curl\`/\`lighthouse\`/PageSpeed API.
9350
+ - Content targeting real developer queries (error strings, "X vs Y", tutorials), dated changelog.
9351
+ - **JS library / CLI → npm:** OIDC Trusted Publishing (\`id-token: write\`, \`--provenance\`, NO \`NPM_TOKEN\`). Verify: \`npm view <pkg> version\`, \`npm install <pkg>\`, \`npm audit signatures\`.
9352
+ - **Python → PyPI:** Trusted Publisher (\`pypa/gh-action-pypi-publish\`, \`environment: pypi\`, no token) + PEP 740 attestation. Verify: \`pip install <pkg>==X.Y.Z\`.
9353
+ - **Rust → crates.io:** Trusted Publishing (\`rust-lang/crates-io-auth-action\`; first publish manual). Verify: \`cargo add <crate>@X.Y.Z\`.
9354
+ - **Service / container → GHCR:** multi-arch + \`provenance: true\` + \`sbom: true\` + keyless cosign. Verify: \`cosign verify\` + \`verify-attestation\` + \`docker pull\`.
9355
+ - **GitHub Action → Marketplace:** complete \`action.yml\` + a floating major tag (\`v1\`). Verify: \`uses: owner/action@v1\` resolves; listed publicly.
9356
+ - **Go module → proxy (tag-driven):** tag \`vX.Y.Z\` (\`/vN\` in the module path for major ≥ 2). Verify: \`go install ...@vX.Y.Z\`; \`proxy.golang.org\`/\`sum.golang.org\` have it.
9357
+
9358
+ ## Publish pipeline (final destination)
9359
+ End to end: reusable build+test (\`workflow_call\`) → release automation → tag + GitHub Release → \`on: release: published\` publish jobs, EACH behind its own protected environment + OIDC (no long-lived tokens) → provenance attestation on every artifact. Gate same-workflow steps with \`needs:\`; use protected environments + required reviewers so "push to main" becomes "a human approves" while auth stays tokenless. NEVER \`pull_request_target\` to check out untrusted fork code.
9360
+
9361
+ ## UI/UX excellence, VERIFIED BY BROWSING (Pillar D — every user-viewable surface, proven by VIEWING the rendered result, never guessed from code)
9362
+ Governing rule: NEVER infer UI/UX quality from source, markdown, or a returned 200 — DRIVE the running artifact in a real browser (\`mcp__browser__*\` / Playwright), capture the actual pixels at real viewports (375/768/1280+), light+dark + reduced-motion, walk the real flows, and judge THOSE pixels. Applies to EVERY surface a human sees: the product UI, the README as GitHub RENDERS it, the live Pages site, the docs, the rendered Release page, the social/og share-card as it appears when shared, and the 404. This UPGRADES the curl/grep "README media" + a11y checks above from "the asset exists / a score passed" to "the rendered result was viewed." A captured screenshot proves a state was rendered and viewed, NOT that it is good. No LAGGING item here — the lagging proof of frictionless UX is the activation / time-to-first-value / retention curve in the outcome bar; Pillar D is the LEADING craft that de-risks it.
9363
+ - **State matrix captured** [LEADING]: every key screen driven through ALL states — empty, loading/skeleton, error, success, first-run/onboarding, partial, edge/overflow (long text, huge/tiny data) — at mobile 375 / tablet 768 / desktop 1280+ × light+dark. Verify: a Playwright spec drives + screenshots each cell, run green.
9364
+ - **Visual-regression baselines** [LEADING]: \`expect(page).toHaveScreenshot()\` goldens committed + green in CI (any unintended pixel change fails), render env pinned. Green proves NO CHANGE from a BLESSED baseline, not beauty.
9365
+ - **Accessibility gate** [LEADING]: \`@axe-core/playwright\` ZERO serious/critical on each key screen AND state; keyboard reaches every control with a visible focus indicator; pointer targets ≥ 24×24 CSS px (WCAG 2.2 SC 2.5.8 / 2.4.11 / 2.4.13). axe catches ~57% of issues by volume — a floor, not "accessible."
9366
+ - **Contrast + dark-mode + reduced-motion** [LEADING]: text contrast ≥ 4.5:1 (≥ 3:1 large text / UI) measured from the rendered result; a REAL \`prefers-color-scheme\` dark theme (not inverted) and \`prefers-reduced-motion\` (animation actually suppressed), captured via \`page.emulateMedia\`. WCAG 2.2 SC 1.4.3.
9367
+ - **CWV + Lighthouse budgets** [LEADING, web surfaces]: LCP ≤ 2.5s / INP ≤ 200ms / CLS ≤ 0.1 (75th pct); Lighthouse Perf/A11y/Best-Practices/SEO ≥ 0.90 asserted in CI (Lighthouse CI). Perf ≠ flow friction — a floor.
9368
+ - **Rendered README as GitHub shows it** [LEADING]: \`mcp__browser__navigate\` the repo page + screenshot the RENDERED README in light AND dark — every image/badge loads (no broken-image glyph), theme-adaptive \`<picture>\` swaps, relative links resolve, no raw-markdown artifacts. A 200 proves reachable, not that the hero reads well.
9369
+ - **Rendered Pages / docs / Release / og share-card** [LEADING]: each published surface DRIVEN + screenshotted at its real URL — live Pages + docs (nav works, no overflow, consistent chrome), the latest Release page (notes render, links 200), the share-card (og:image exactly 1200×630 or social-preview 1280×640, returns 200, text legible when actually rendered).
9370
+ - **Is it beautiful, elegant, professional, frictionless?** [SOFT/advisory]: an LLM vision rubric scores the REAL screenshots (never the code) on visual hierarchy, spacing rhythm, type/readability, restrained palette, alignment/grid, consistency, motion-with-purpose, and whether each state genuinely helps (empty guides the next action; error is plain-language + recovery; onboarding lands the aha < 5 min). Cite the exact screenshot per finding. Record as ADVISORY evidence with the images — NEVER a verified checkmark or greatness box-tick.
9371
+ Anti-cargo-cult (do NOT fake a check): a screenshot file existing, a green \`toHaveScreenshot\` over an unreviewed baseline, an axe pass on only the happy path, a \`<meta viewport>\` tag, or a passing CWV score are NECESSARY floors, NOT proof of a beautiful, frictionless UI. The only honest proofs are the deterministic gates (each for its property) and the advisory rubric over real pixels, labeled advisory.
9372
+
9373
+ ## Soft signals & anti-cargo-cult (do NOT fake a checkmark, do NOT hard-gate)
9374
+ SEO indexing/ranking is never guaranteed by Google; E-E-A-T/content quality has no exposed score. Proxy via byline/last-updated presence, content architecture, and (with auth) Search Console Performance — record as evidence, not a binary pass. Do NOT hard-gate raw test-coverage %, raw star count, "has a Discord", Diataxis "compliance" as a box-tick, or any \`community/profile\` file-presence number treated as evidence of community — taken as proof of greatness these ARE the hallucinated-progress trap. The intangibles that separate LOVED from merely-functional — a clear vision/opinion, DX that delights, great error messages, brand/name/story, a maintainer who cares — are real but SOFT: estimate them with an advisory rubric and label the score advisory, never verified.`;
9375
+
8975
9376
  //#endregion
8976
9377
  //#region src/lib/first-mate/scaffold-spec.ts
8977
9378
  const COPILOT_SETUP_JOB_NAME = "copilot-setup-steps";
9379
+ const COPILOT_SETUP_PATH = ".github/workflows/copilot-setup-steps.yml";
8978
9380
  const COPILOT_SETUP_TIMEOUT_MAX = 59;
8979
9381
  const GUIDANCE_PATHS = [
8980
9382
  "AGENTS.md",
@@ -8986,7 +9388,14 @@ const ENHANCEABLE_PATHS = new Set([
8986
9388
  ...GUIDANCE_PATHS,
8987
9389
  "docs/adr/0001-record-architecture-decisions.md",
8988
9390
  "LEARNINGS.md",
8989
- "CHANGELOG.md"
9391
+ "CHANGELOG.md",
9392
+ "docs/playbook/README.md",
9393
+ "SECURITY.md",
9394
+ ".github/CONTRIBUTING.md",
9395
+ "CODE_OF_CONDUCT.md",
9396
+ "SUPPORT.md",
9397
+ "GOVERNANCE.md",
9398
+ "ADOPTERS.md"
8990
9399
  ]);
8991
9400
  const COPILOT_SETUP_TIMEOUT_MINUTES = 15;
8992
9401
  const ROLE_AGENT_NAMES = [
@@ -8994,7 +9403,10 @@ const ROLE_AGENT_NAMES = [
8994
9403
  "implementer",
8995
9404
  "reviewer",
8996
9405
  "researcher",
8997
- "tester"
9406
+ "tester",
9407
+ "ceo",
9408
+ "cto",
9409
+ "cpo"
8998
9410
  ];
8999
9411
  const DEFAULT_CI_MATRIX = ["ubuntu-latest", "windows-latest"];
9000
9412
  function buildScaffoldFiles(opts) {
@@ -9021,17 +9433,103 @@ function buildScaffoldFiles(opts) {
9021
9433
  content: buildTestInstructions(normalized)
9022
9434
  },
9023
9435
  {
9024
- path: ".github/workflows/copilot-setup-steps.yml",
9436
+ path: COPILOT_SETUP_PATH,
9025
9437
  content: buildCopilotSetupWorkflow(normalized)
9026
9438
  },
9027
9439
  {
9028
9440
  path: ".github/workflows/ci.yml",
9029
9441
  content: buildCiWorkflow(normalized)
9030
9442
  },
9443
+ ...normalized.hasSite ? [
9444
+ {
9445
+ path: ".github/workflows/pages.yml",
9446
+ content: buildPagesWorkflow(normalized)
9447
+ },
9448
+ {
9449
+ path: "public/robots.txt",
9450
+ content: buildRobotsTxt()
9451
+ },
9452
+ {
9453
+ path: "public/sitemap.xml",
9454
+ content: buildSitemap()
9455
+ },
9456
+ {
9457
+ path: "public/seo-head.html",
9458
+ content: buildSeoHead()
9459
+ },
9460
+ {
9461
+ path: "public/404.html",
9462
+ content: buildNotFoundPage()
9463
+ },
9464
+ {
9465
+ path: "public/.well-known/security.txt",
9466
+ content: buildSecurityTxt()
9467
+ }
9468
+ ] : [],
9469
+ {
9470
+ path: ".github/workflows/codeql.yml",
9471
+ content: buildCodeqlWorkflow(normalized)
9472
+ },
9473
+ {
9474
+ path: ".github/dependabot.yml",
9475
+ content: buildDependabot(normalized)
9476
+ },
9477
+ {
9478
+ path: ".github/workflows/release.yml",
9479
+ content: buildReleaseWorkflow(normalized)
9480
+ },
9481
+ {
9482
+ path: ".github/workflows/publish.yml",
9483
+ content: buildPublishWorkflow(normalized)
9484
+ },
9485
+ {
9486
+ path: ".github/workflows/media.yml",
9487
+ content: buildMediaWorkflow(normalized)
9488
+ },
9489
+ {
9490
+ path: ".github/workflows/maintainability.yml",
9491
+ content: buildMaintainabilityWorkflow(normalized)
9492
+ },
9031
9493
  {
9032
9494
  path: ".github/pull_request_template.md",
9033
9495
  content: buildPullRequestTemplate(normalized)
9034
9496
  },
9497
+ {
9498
+ path: ".github/ISSUE_TEMPLATE/config.yml",
9499
+ content: buildIssueTemplateConfig()
9500
+ },
9501
+ {
9502
+ path: "SECURITY.md",
9503
+ content: buildSecurityPolicy(normalized)
9504
+ },
9505
+ {
9506
+ path: ".github/CONTRIBUTING.md",
9507
+ content: buildContributing(normalized)
9508
+ },
9509
+ {
9510
+ path: "CODE_OF_CONDUCT.md",
9511
+ content: buildCodeOfConduct()
9512
+ },
9513
+ {
9514
+ path: "SUPPORT.md",
9515
+ content: buildSupport()
9516
+ },
9517
+ {
9518
+ path: ".github/CODEOWNERS",
9519
+ content: buildCodeowners()
9520
+ },
9521
+ {
9522
+ path: "GOVERNANCE.md",
9523
+ content: buildGovernance()
9524
+ },
9525
+ {
9526
+ path: ".github/FUNDING.yml",
9527
+ content: buildFunding()
9528
+ },
9529
+ {
9530
+ path: "ADOPTERS.md",
9531
+ content: buildAdopters()
9532
+ },
9035
9533
  {
9036
9534
  path: "docs/adrs/0000-template.md",
9037
9535
  content: buildAdrTemplate()
@@ -9052,6 +9550,10 @@ function buildScaffoldFiles(opts) {
9052
9550
  path: "docs/research/README.md",
9053
9551
  content: buildDatedEntryReadme("Research")
9054
9552
  },
9553
+ {
9554
+ path: "docs/playbook/README.md",
9555
+ content: buildPlaybook()
9556
+ },
9055
9557
  {
9056
9558
  path: "LEARNINGS.md",
9057
9559
  content: buildLearnings(normalized)
@@ -9075,6 +9577,14 @@ function planScaffoldFiles(opts) {
9075
9577
  });
9076
9578
  continue;
9077
9579
  }
9580
+ if (file.path === COPILOT_SETUP_PATH && copilotSetupIsInert(existingByPath.get(file.path) ?? "")) {
9581
+ filesToCommit.push(file);
9582
+ reports.push({
9583
+ path: file.path,
9584
+ status: "overwritten"
9585
+ });
9586
+ continue;
9587
+ }
9078
9588
  if (opts.mode === "overwrite-approved") {
9079
9589
  filesToCommit.push(file);
9080
9590
  reports.push({
@@ -9119,6 +9629,29 @@ function planScaffoldFiles(opts) {
9119
9629
  reports
9120
9630
  };
9121
9631
  }
9632
+ /**
9633
+ * True when an existing `copilot-setup-steps.yml` is one of OUR inert stubs — a
9634
+ * bare "echo" environment step with NO dependency install. Such a file leaves the
9635
+ * Copilot cloud agent's container without dependencies, so it cannot build / lint /
9636
+ * test and returns an empty draft PR (the #1 real-world cause of empty coding-agent
9637
+ * PRs). Narrow by design: a real custom setup (any recognized install command) is
9638
+ * NOT matched, so a user's hand-tuned environment is never clobbered.
9639
+ */
9640
+ function copilotSetupIsInert(content) {
9641
+ if (content.trim().length === 0) return false;
9642
+ if ([
9643
+ "npm ci",
9644
+ "npm install",
9645
+ "pnpm install",
9646
+ "yarn install",
9647
+ "bun install",
9648
+ "go mod download",
9649
+ "cargo fetch",
9650
+ "pip install",
9651
+ "Detect toolchain and install"
9652
+ ].some((token) => content.includes(token))) return false;
9653
+ return /run:\s*echo\b/.test(content) || content.includes("Set up environment");
9654
+ }
9122
9655
  function appendMissingSections(current, desired) {
9123
9656
  const currentHeadings = new Set(sectionHeadings(current));
9124
9657
  const missing = splitTopLevelSections(desired).filter((section) => !currentHeadings.has(section.heading));
@@ -9161,6 +9694,8 @@ function normalizeScaffoldOpts(opts) {
9161
9694
  defaultBranch: opts.defaultBranch?.trim() || "<!-- TODO: confirm the default branch. -->",
9162
9695
  techStack: opts.techStack?.trim() || "<!-- TODO: fill in languages, frameworks, package managers, services, and runtime versions. -->",
9163
9696
  packageManager: opts.packageManager?.trim() || "<!-- TODO: confirm the package manager or build tool. -->",
9697
+ finalDestination: opts.finalDestination ?? "unknown",
9698
+ hasSite: opts.hasSite ?? false,
9164
9699
  commands: opts.commands ?? {},
9165
9700
  tests: opts.tests ?? {},
9166
9701
  ci: opts.ci ?? {
@@ -9176,7 +9711,7 @@ function buildGuidance(opts) {
9176
9711
  const commandBlock = commandLines(opts.commands);
9177
9712
  const primaryOs = opts.ci.primaryOs || "<!-- TODO: choose the primary supported OS. -->";
9178
9713
  const ciMatrix = opts.ci.matrix.length > 0 ? opts.ci.matrix.join(", ") : "<!-- TODO: define CI OS matrix. -->";
9179
- const uiEvidence = opts.uiEvidenceRequired ? "- UI-impacting changes include before/after screenshots or recorded browser evidence in the PR." : "- If a change affects user-visible UI or CLI output, include before/after evidence in the PR.";
9714
+ const uiEvidence = opts.uiEvidenceRequired ? "- UI-impacting changes include before/after screenshots of the RENDERED result (product UI, and the README / Pages / docs as they render) at real viewports (mobile + desktop), light + dark — driven in a browser, never guessed from code." : "- If a change affects user-visible UI or CLI output, include before/after evidence in the PR — for UI, a screenshot of the rendered result at real viewports, not a code diff.";
9180
9715
  const notes = opts.detectedNotes.length > 0 ? opts.detectedNotes.map((note) => `- ${note}`).join("\n") : "- <!-- TODO: add repo-specific hazards, flaky areas, rate limits, and platform traps as they are discovered. -->";
9181
9716
  return `# Repository guidance for ${opts.repoName}
9182
9717
 
@@ -9242,6 +9777,13 @@ ${opts.projectStructure.map((entry) => `- ${entry}`).join("\n")}
9242
9777
  - Solved problems, incidents, and debugging notes live in \`docs/history/YYYY-MM-DD-slug.md\`.
9243
9778
  - Durable project learnings live in \`LEARNINGS.md\`; update it when a future contributor would otherwise rediscover the same fact.
9244
9779
 
9780
+ ## Operating autonomously
9781
+
9782
+ - Proceed on best judgment for reversible choices within the mission scope; do not pause for clarification.
9783
+ - State assumptions explicitly in the plan and PR body, surface unresolved questions there, choose the safest reasonable path, and continue.
9784
+ - Use \`docs/playbook/README.md\` as the product operating protocol. Wear the \`ceo\`, \`cto\`, or \`cpo\` operator hat when strategy, engineering direction, or product judgment is needed, then delegate execution to the planner, implementer, reviewer, researcher, and tester roles.
9785
+ - Stop only when required input is unavailable, an action is destructive or outside scope, or spend, pricing, legal, privacy, or security authority requires a human decision.
9786
+
9245
9787
  ## Handoff
9246
9788
 
9247
9789
  Every handoff should include:
@@ -9438,28 +9980,113 @@ function buildRoleAgent(role) {
9438
9980
  "Failures that require implementation work"
9439
9981
  ],
9440
9982
  model: "gpt-5.6-sol"
9441
- }
9442
- }[role];
9443
- const modelLine = spec.model === void 0 ? "" : `model: ${spec.model}\n`;
9444
- return `---
9445
- name: ${role}
9446
- description: ${spec.description}
9447
- ${modelLine}---
9448
-
9449
- # ${capitalize(role)}
9450
-
9451
- ## Purpose
9452
-
9453
- ${spec.purpose}
9454
-
9455
- ## When to use
9456
-
9457
- ${spec.when.map((line) => `- ${line}`).join("\n")}
9458
-
9459
- ## Inputs (cold-start contract)
9460
-
9461
- A delegated task starts from a blank context. The caller must include:
9462
-
9983
+ },
9984
+ ceo: {
9985
+ description: "Operate the product strategy, growth, and launch loop; turn evidence into auditable bets and delegate execution.",
9986
+ purpose: "Own direction and momentum across discovery, niche, MVP, launch, measurement, iteration, and growth. Govern with OODA inside Build-Measure-Learn, then delegate bounded work to the product, engineering, research, planning, implementation, review, and test roles.",
9987
+ when: [
9988
+ "The product needs a strategic sequence, go/no-go decision, launch plan, or growth loop.",
9989
+ "Evidence conflicts or a bet needs an explicit hypothesis, metric, and threshold.",
9990
+ "Work spans product and engineering and needs one accountable operator."
9991
+ ],
9992
+ method: [
9993
+ "Follow `docs/playbook/README.md` in order: discovery → niche → MVP → launch → measure → iterate → grow; do not skip discovery or distribution.",
9994
+ "Run a daily OODA loop inside each Build-Measure-Learn phase: observe external evidence, orient against the current hypothesis, decide with a pre-set threshold, act through delegated execution roles, then measure.",
9995
+ "Choose launch channels where the beachhead persona already gathers; sequence Show HN, Product Hunt, dev.to, and build-in-public rather than broadcasting everywhere at once.",
9996
+ "Treat the README as the landing page, GitHub Pages and Diataxis docs as marketing, and shareable artifacts as growth loops. Use AARRR with activation defined as experienced value; stars are awareness, not activation.",
9997
+ "Log every material decision as hypothesis → experiment → metric → threshold → outcome, link its evidence, and update the next bet."
9998
+ ],
9999
+ quality: [
10000
+ "Every claimed checkpoint is externally verifiable: a real HTTP 200, green CI run, observed analytics event, or real survey sample size, never a self-reported 'done'.",
10001
+ "Do not over-build without first running a distribution test; viral attention is not product-market fit.",
10002
+ "Never manipulate users or fabricate demand, evidence, testimonials, or progress. Do not authorize spend, paid acquisition, discounts, pricing, contracts, or other economic commitments beyond explicit hard limits set by a human."
10003
+ ],
10004
+ output: [
10005
+ "Current phase and externally verified checkpoint",
10006
+ "Decision log: hypothesis, experiment, metric, threshold, outcome",
10007
+ "Next bounded bets with owners and kill criteria",
10008
+ "Delegations to cpo/cto and execution roles"
10009
+ ],
10010
+ model: "claude-opus-4.8"
10011
+ },
10012
+ cto: {
10013
+ description: "Set engineering direction and quality bars for a simple, reliable, accessible product that ships continuously.",
10014
+ purpose: "Make engineering accelerate learning without mortgaging reliability. Define the architecture and delivery system, record non-trivial decisions, and delegate implementation, testing, and review to execution roles.",
10015
+ when: [
10016
+ "A product bet needs an architecture, delivery plan, API or developer-experience contract, or technical risk decision.",
10017
+ "Quality, reliability, accessibility, performance, security, or operability needs an explicit bar.",
10018
+ "The team must trade scope against learning speed without creating avoidable complexity."
10019
+ ],
10020
+ method: [
10021
+ "Write an ADR for every non-trivial technical or process decision; prefer radical simplicity and delete accidental complexity.",
10022
+ "Design Stripe-grade APIs and developer experience: coherent naming, actionable errors, stable contracts, safe defaults, copy-paste quickstarts, and Diataxis tutorials/how-to/reference/explanation docs.",
10023
+ "Use trunk-based development with short-lived branches and feature flags, a test pyramid, continuous delivery, and CI/CD. Track all DORA four keys; speed and stability reinforce each other rather than trade off.",
10024
+ "Define and enforce product budgets: a timed cold-start quickstart under five minutes, WCAG 2.2 AA, Core Web Vitals thresholds in CI, and defaults that just work.",
10025
+ "Delegate scoped delivery to planner/implementer/tester and require adversarial reviewer evidence before calling the system ready."
10026
+ ],
10027
+ quality: [
10028
+ "Every claimed checkpoint is externally verifiable: a real HTTP 200, a reproducible cold-start timer, green CI, measured Web Vitals, or an accessibility audit, never a self-reported 'done'.",
10029
+ "Do not over-build infrastructure without a distribution or learning test; viral attention is not product-market fit.",
10030
+ "Keep external side effects and economic authority bounded. Never incur spend, change pricing, purchase services, or expand privileges beyond explicit human-set hard limits."
10031
+ ],
10032
+ output: [
10033
+ "Architecture and ADR decisions",
10034
+ "Delivery slices, feature-flag and rollback plan",
10035
+ "Quality budgets and measured evidence",
10036
+ "Delegations and residual technical risks"
10037
+ ],
10038
+ model: "claude-opus-4.8"
10039
+ },
10040
+ cpo: {
10041
+ description: "Discover a real underserved job, select a beachhead, position it, and scope a lovable evidence-seeking product.",
10042
+ purpose: "Own product truth from struggling moment through activation and product-market fit. Turn customer evidence into a narrow proposition and frozen release scope, then delegate research and delivery.",
10043
+ when: [
10044
+ "The customer, problem, niche, positioning, MVP, activation moment, or roadmap is uncertain.",
10045
+ "A feature request needs validation against observed jobs and opportunity evidence.",
10046
+ "The product needs a go/no-go niche decision or product-market-fit assessment."
10047
+ ],
10048
+ method: [
10049
+ "Run Jobs-To-Be-Done interviews around the struggling moment, prior solution, forces of progress, and hire/fire criteria. Require at least three independent corroborated sources before treating a pain as real.",
10050
+ "Segment with Disciplined Entrepreneurship criteria, choose one beachhead with a credible 1,000 true-fans floor, and publish a go/no-go table covering urgency, reachability, willingness to switch/pay, competition, and founder advantage.",
10051
+ "Apply Obviously Awesome positioning: competitive alternatives including do-nothing and workaround status quo, unique attributes, value, proof, best-fit customer, category, relevant trends, and a one-sentence pitch.",
10052
+ "Scope with a Riskiest-Assumption Test whose kill/pivot threshold is set before the experiment. Classify Kano must-be/performance/delight, ship at least one delight, and freeze a v0.1 cut list.",
10053
+ "Maintain a weekly opportunity-solution tree from interviews and issues. Use RICE only to sequence already-validated options. Define the aha moment and instrument time-to-first-value under five minutes.",
10054
+ "Declare product-market fit only when both the Sean Ellis survey reaches at least 40% 'very disappointed' and a relevant cohort retention curve flattens."
10055
+ ],
10056
+ quality: [
10057
+ "Every claimed checkpoint is externally verifiable: interview notes from at least three corroborating sources, a real survey sample size, observed activation events, or a measured retention cohort, never a self-reported 'done'.",
10058
+ "Do not over-build before a distribution test; viral attention is not product-market fit.",
10059
+ "Never manipulate participants or fabricate demand. Research incentives, spend, discounts, and pricing authority stay within explicit human-set hard limits."
10060
+ ],
10061
+ output: [
10062
+ "Evidence ledger and current product phase",
10063
+ "Beachhead go/no-go table and positioning pitch",
10064
+ "Riskiest-assumption experiment, threshold, and frozen v0.1 cut list",
10065
+ "Activation/PMF measures and delegations to execution roles"
10066
+ ],
10067
+ model: "claude-opus-4.8"
10068
+ }
10069
+ }[role];
10070
+ const modelLine = spec.model === void 0 ? "" : `model: ${spec.model}\n`;
10071
+ return `---
10072
+ name: ${role}
10073
+ description: ${spec.description}
10074
+ ${modelLine}---
10075
+
10076
+ # ${capitalize(role)}
10077
+
10078
+ ## Purpose
10079
+
10080
+ ${spec.purpose}
10081
+
10082
+ ## When to use
10083
+
10084
+ ${spec.when.map((line) => `- ${line}`).join("\n")}
10085
+
10086
+ ## Inputs (cold-start contract)
10087
+
10088
+ A delegated task starts from a blank context. The caller must include:
10089
+
9463
10090
  - The goal or artifact to work on, pasted or linked precisely.
9464
10091
  - Acceptance criteria and constraints.
9465
10092
  - Relevant files, PRs, issues, ADRs, plans, or prior decisions.
@@ -9531,7 +10158,10 @@ function buildCiWorkflow(opts) {
9531
10158
  on:
9532
10159
  pull_request:
9533
10160
  push:
9534
- branches: [${opts.defaultBranch.startsWith("<!--") ? "main" : opts.defaultBranch}]
10161
+ branches: [${opts.defaultBranch.startsWith("<!--") ? "main # TODO: confirm the default branch" : opts.defaultBranch}]
10162
+
10163
+ permissions:
10164
+ contents: read
9535
10165
 
9536
10166
  jobs:
9537
10167
  quality-gate:
@@ -9542,7 +10172,7 @@ jobs:
9542
10172
  matrix:
9543
10173
  os: [${osList.join(", ")}]
9544
10174
  steps:
9545
- - uses: actions/checkout@v4
10175
+ - uses: actions/checkout@${CHECKOUT_SHA} # v4
9546
10176
  ${setupStepsFor(opts)}
9547
10177
  - name: Run repository quality gate
9548
10178
  run: |
@@ -9551,44 +10181,463 @@ ${runLines}
9551
10181
  }
9552
10182
  function setupStepsFor(opts) {
9553
10183
  const pm = opts.packageManager;
9554
- if (pm === "bun") return ` - uses: oven-sh/setup-bun@v2
10184
+ if (pm === "bun") return ` - uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2
9555
10185
  - name: Install dependencies
9556
10186
  run: bun install --frozen-lockfile`;
9557
- if (pm === "pnpm") return ` - uses: actions/setup-node@v4
10187
+ if (pm === "pnpm") return ` - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
9558
10188
  with:
9559
10189
  node-version: 22
9560
10190
  cache: pnpm
9561
- - uses: pnpm/action-setup@v4
10191
+ - uses: pnpm/action-setup@7d2c2a2c7a1fb2f07e4646f7b4602195e3d0c57e # v4
9562
10192
  with:
9563
10193
  run_install: false
9564
10194
  - name: Install dependencies
9565
10195
  run: pnpm install --frozen-lockfile`;
9566
- if (pm === "yarn") return ` - uses: actions/setup-node@v4
10196
+ if (pm === "yarn") return ` - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
9567
10197
  with:
9568
10198
  node-version: 22
9569
10199
  cache: yarn
9570
10200
  - name: Install dependencies
9571
10201
  run: yarn install --immutable`;
9572
- if (pm === "npm") return ` - uses: actions/setup-node@v4
10202
+ if (pm === "npm") return ` - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
9573
10203
  with:
9574
10204
  node-version: 22
9575
10205
  cache: npm
9576
10206
  - name: Install dependencies
9577
10207
  run: npm ci`;
9578
- if (opts.techStack.toLowerCase().includes("go")) return ` - uses: actions/setup-go@v5
10208
+ if (opts.techStack.toLowerCase().includes("go")) return ` - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5
9579
10209
  with:
9580
- go-version-file: go.mod`;
10210
+ go-version-file: go.mod
10211
+ - name: Download modules
10212
+ run: go mod download`;
9581
10213
  if (opts.techStack.toLowerCase().includes("rust")) return ` - name: Set up Rust
9582
- run: rustup show`;
9583
- if (opts.techStack.toLowerCase().includes("python")) return ` - uses: actions/setup-python@v5
10214
+ run: rustup show
10215
+ - name: Fetch dependencies
10216
+ run: cargo fetch`;
10217
+ if (opts.techStack.toLowerCase().includes("python")) return ` - uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5
9584
10218
  with:
9585
10219
  python-version: "3.x"
9586
10220
  - name: Install dependencies
9587
10221
  run: |
9588
10222
  python -m pip install --upgrade pip
9589
- if [ -f requirements.txt ]; then pip install -r requirements.txt; fi`;
9590
- return ` - name: Set up environment
9591
- run: echo "TODO: add language/runtime setup"`;
10223
+ if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
10224
+ if [ -f pyproject.toml ]; then pip install -e . || pip install .; fi`;
10225
+ return ` - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
10226
+ with:
10227
+ node-version: 22
10228
+ - name: Detect toolchain and install dependencies
10229
+ shell: bash
10230
+ run: |
10231
+ set -euo pipefail
10232
+ if [ -f bun.lockb ] || [ -f bun.lock ]; then
10233
+ npm install -g bun && bun install --frozen-lockfile
10234
+ elif [ -f pnpm-lock.yaml ]; then
10235
+ corepack enable && pnpm install --frozen-lockfile
10236
+ elif [ -f yarn.lock ]; then
10237
+ corepack enable && yarn install --immutable
10238
+ elif [ -f package-lock.json ]; then
10239
+ npm ci
10240
+ elif [ -f package.json ]; then
10241
+ npm install
10242
+ elif [ -f go.mod ]; then
10243
+ go mod download
10244
+ elif [ -f Cargo.toml ]; then
10245
+ cargo fetch
10246
+ elif [ -f requirements.txt ]; then
10247
+ python -m pip install --upgrade pip && pip install -r requirements.txt
10248
+ elif [ -f pyproject.toml ]; then
10249
+ python -m pip install --upgrade pip && { pip install -e . || pip install .; }
10250
+ else
10251
+ echo "::warning::copilot-setup-steps found no recognized dependency manifest; the agent environment may be missing dependencies. Add a real install step for this repo's stack."
10252
+ fi`;
10253
+ }
10254
+ const CHECKOUT_SHA = "11bd71901bbe5b1630ceea73d27597364c9af683";
10255
+ const CONFIGURE_PAGES_SHA = "983d7736d9b0ae728b81ab479565c72886d7745b";
10256
+ const UPLOAD_PAGES_SHA = "56afc609e74202658d3ffba0e8f6dda462b719fa";
10257
+ const DEPLOY_PAGES_SHA = "decdde0ac072f6f71b3a7fa0b3c73a7a62cc8a28";
10258
+ const UPLOAD_ARTIFACT_SHA = "65462800fd760344b1a7b4382951275a0abb4808";
10259
+ function buildPagesWorkflow(opts) {
10260
+ return `name: Pages
10261
+
10262
+ on:
10263
+ push:
10264
+ branches: [${opts.defaultBranch.startsWith("<!--") ? "main # TODO: confirm the default branch" : opts.defaultBranch}]
10265
+ workflow_dispatch:
10266
+
10267
+ permissions:
10268
+ contents: read
10269
+
10270
+ concurrency:
10271
+ group: pages
10272
+ cancel-in-progress: false
10273
+
10274
+ jobs:
10275
+ build:
10276
+ runs-on: ubuntu-latest
10277
+ steps:
10278
+ - uses: actions/checkout@${CHECKOUT_SHA} # v4
10279
+ - uses: actions/configure-pages@${CONFIGURE_PAGES_SHA} # v5
10280
+ - name: Build site and stamp deployed revision
10281
+ run: |
10282
+ ${opts.commands.build ?? "echo \"TODO: build the site into _site\""}
10283
+ mkdir -p _site
10284
+ printf '%s\\n' "\${{ github.sha }}" > _site/BUILD_SHA.txt
10285
+ - uses: actions/upload-pages-artifact@${UPLOAD_PAGES_SHA} # v3
10286
+ with:
10287
+ path: _site
10288
+ deploy:
10289
+ needs: build
10290
+ runs-on: ubuntu-latest
10291
+ environment:
10292
+ name: github-pages
10293
+ url: \${{ steps.deployment.outputs.page_url }}
10294
+ permissions:
10295
+ pages: write
10296
+ id-token: write
10297
+ steps:
10298
+ - name: Deploy Pages
10299
+ id: deployment
10300
+ uses: actions/deploy-pages@${DEPLOY_PAGES_SHA} # v4
10301
+ `;
10302
+ }
10303
+ function codeqlLanguages(opts) {
10304
+ const stack = opts.techStack.toLowerCase();
10305
+ const languages = [];
10306
+ if (/javascript|typescript|node|react|vue|svelte/.test(stack)) languages.push("javascript-typescript");
10307
+ if (stack.includes("python")) languages.push("python");
10308
+ if (stack.includes("go")) languages.push("go");
10309
+ if (/rust|cargo/.test(stack)) languages.push("rust");
10310
+ return languages.length > 0 ? languages.join(", ") : "javascript-typescript # TODO: confirm CodeQL language";
10311
+ }
10312
+ function buildCodeqlWorkflow(opts) {
10313
+ return `name: CodeQL
10314
+
10315
+ on:
10316
+ push:
10317
+ branches: [${opts.defaultBranch.startsWith("<!--") ? "main # TODO: confirm the default branch" : opts.defaultBranch}]
10318
+ pull_request:
10319
+ schedule:
10320
+ - cron: "17 3 * * 1"
10321
+
10322
+ permissions:
10323
+ contents: read
10324
+
10325
+ jobs:
10326
+ analyze:
10327
+ runs-on: ubuntu-latest
10328
+ permissions:
10329
+ contents: read
10330
+ security-events: write
10331
+ strategy:
10332
+ matrix:
10333
+ language: [${codeqlLanguages(opts)}]
10334
+ steps:
10335
+ - uses: actions/checkout@${CHECKOUT_SHA} # v4
10336
+ - uses: github/codeql-action/init@b374143c1149a9115d881581d29b8390bbcbb59c # v3
10337
+ with:
10338
+ languages: \${{ matrix.language }}
10339
+ - uses: github/codeql-action/autobuild@b374143c1149a9115d881581d29b8390bbcbb59c # v3
10340
+ - uses: github/codeql-action/analyze@b374143c1149a9115d881581d29b8390bbcbb59c # v3
10341
+ `;
10342
+ }
10343
+ function dependabotEcosystem(opts) {
10344
+ if ([
10345
+ "npm",
10346
+ "bun",
10347
+ "pnpm",
10348
+ "yarn"
10349
+ ].includes(opts.packageManager)) return "npm";
10350
+ if (opts.finalDestination === "pypi") return "pip";
10351
+ if (opts.finalDestination === "crates") return "cargo";
10352
+ if (opts.finalDestination === "go-proxy") return "gomod";
10353
+ if (opts.finalDestination === "ghcr") return "docker";
10354
+ return "<!-- TODO: choose a supported package ecosystem -->";
10355
+ }
10356
+ function buildDependabot(opts) {
10357
+ return `version: 2
10358
+ updates:
10359
+ - package-ecosystem: "${dependabotEcosystem(opts)}"
10360
+ directory: "/"
10361
+ schedule:
10362
+ interval: weekly
10363
+ - package-ecosystem: github-actions
10364
+ directory: "/"
10365
+ schedule:
10366
+ interval: weekly
10367
+ `;
10368
+ }
10369
+ function releaseType(opts) {
10370
+ if (opts.finalDestination === "pypi") return "python";
10371
+ if (opts.finalDestination === "crates") return "rust";
10372
+ if (opts.finalDestination === "go-proxy") return "go";
10373
+ if (opts.finalDestination === "npm" || opts.finalDestination === "vscode-marketplace" || opts.finalDestination === "actions-marketplace") return "node";
10374
+ return "simple";
10375
+ }
10376
+ function buildReleaseWorkflow(opts) {
10377
+ return `name: Release
10378
+
10379
+ on:
10380
+ push:
10381
+ branches: [${opts.defaultBranch.startsWith("<!--") ? "main # TODO: confirm the default branch" : opts.defaultBranch}]
10382
+
10383
+ permissions:
10384
+ contents: read
10385
+
10386
+ jobs:
10387
+ release-please:
10388
+ runs-on: ubuntu-latest
10389
+ permissions:
10390
+ contents: write
10391
+ pull-requests: write
10392
+ steps:
10393
+ - uses: googleapis/release-please-action@a02a34c4d625f9be7cb89156071d8567266a2445 # v4
10394
+ with:
10395
+ release-type: ${releaseType(opts)}
10396
+ # This workflow creates the tag, changelog, and GitHub Release. Publishing is
10397
+ # deliberately separate in publish.yml and starts only on release: published.
10398
+ `;
10399
+ }
10400
+ function apiCompatibilitySteps(opts) {
10401
+ if (opts.finalDestination === "crates") return ` - name: Check Rust API compatibility
10402
+ run: |
10403
+ cargo install cargo-semver-checks --locked
10404
+ cargo semver-checks check-release`;
10405
+ if (opts.finalDestination === "npm" || opts.finalDestination === "vscode-marketplace" || opts.finalDestination === "actions-marketplace") return ` - name: Check TypeScript API compatibility
10406
+ if: \${{ hashFiles('api-extractor.json') != '' }}
10407
+ run: npx --no-install api-extractor run --local
10408
+ - name: API compatibility wiring reminder
10409
+ if: \${{ hashFiles('api-extractor.json') == '' }}
10410
+ run: echo "TODO: configure @microsoft/api-extractor and commit its API report"`;
10411
+ return ` - name: Declare API compatibility policy
10412
+ run: echo "TODO: wire the ecosystem's breaking-change checker when this repository exposes a stable public API"`;
10413
+ }
10414
+ function buildMaintainabilityWorkflow(opts) {
10415
+ const exampleCommand = opts.commands.test ?? "echo \"TODO: execute every documented example or doctest in CI\"";
10416
+ return `name: Maintainability
10417
+
10418
+ on:
10419
+ pull_request:
10420
+ push:
10421
+ branches: [${opts.defaultBranch.startsWith("<!--") ? "main # TODO: confirm the default branch" : opts.defaultBranch}]
10422
+
10423
+ permissions:
10424
+ contents: read
10425
+
10426
+ jobs:
10427
+ policy-gates:
10428
+ runs-on: ubuntu-latest
10429
+ steps:
10430
+ - uses: actions/checkout@${CHECKOUT_SHA} # v4
10431
+ - name: REUSE and SPDX compliance
10432
+ run: |
10433
+ python -m pip install reuse
10434
+ reuse lint
10435
+ - name: Dependency license policy
10436
+ run: echo "TODO: wire the ecosystem-specific dependency-license scanner and approved-license policy"
10437
+ ${apiCompatibilitySteps(opts)}
10438
+ - name: Execute documented examples
10439
+ run: ${exampleCommand}
10440
+ `;
10441
+ }
10442
+ function buildPublishWorkflow(opts) {
10443
+ const common = `name: Publish\n\non:\n release:\n types: [published]\n\npermissions:\n contents: read\n\n`;
10444
+ if (opts.finalDestination === "npm") return `${common}jobs:\n npm:\n runs-on: ubuntu-latest\n environment: npm\n permissions:\n contents: read\n id-token: write\n attestations: write\n steps:\n - uses: actions/checkout@${CHECKOUT_SHA} # v4\n - name: Set up Node for OIDC trusted publishing\n uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4\n with:\n node-version: 22\n registry-url: https://registry.npmjs.org\n - run: npm ci\n - run: npm pack\n - uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2\n with:\n subject-path: '*.tgz'\n - run: npm publish --provenance --access public *.tgz\n`;
10445
+ if (opts.finalDestination === "pypi") return `${common}jobs:\n pypi:\n runs-on: ubuntu-latest\n environment: pypi\n permissions:\n contents: read\n id-token: write\n attestations: write\n steps:\n - uses: actions/checkout@${CHECKOUT_SHA} # v4\n - run: python -m pip install --upgrade build\n - run: python -m build\n - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0\n - uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2\n with:\n subject-path: dist/*\n`;
10446
+ if (opts.finalDestination === "crates") return `${common}jobs:\n crates:\n runs-on: ubuntu-latest\n environment: crates-io\n permissions:\n contents: read\n id-token: write\n attestations: write\n steps:\n - uses: actions/checkout@${CHECKOUT_SHA} # v4\n - id: auth\n uses: rust-lang/crates-io-auth-action@e919bc7605cde86df457cf5b93c5e103838bd879 # v1\n - run: cargo publish\n env:\n CARGO_REGISTRY_TOKEN: \${{ steps.auth.outputs.token }}\n - uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2\n with:\n subject-path: target/package/*.crate\n`;
10447
+ if (opts.finalDestination === "ghcr") return `${common}jobs:\n ghcr:\n runs-on: ubuntu-latest\n environment: ghcr\n permissions:\n contents: read\n packages: write\n id-token: write\n attestations: write\n steps:\n - uses: actions/checkout@${CHECKOUT_SHA} # v4\n - uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3\n with:\n registry: ghcr.io\n username: \${{ github.actor }}\n password: \${{ github.token }}\n - name: Build and push image\n id: build\n uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5\n with:\n push: true\n tags: ghcr.io/\${{ github.repository }}:\${{ github.event.release.tag_name }}\n provenance: true\n sbom: true\n - uses: sigstore/cosign-installer@4959ce089c160fddf62f7b42464195ba1a56d382 # v3\n - name: Keyless-sign image digest\n run: cosign sign --yes "ghcr.io/\${{ github.repository }}@\${{ steps.build.outputs.digest }}"\n - uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2\n with:\n subject-name: ghcr.io/\${{ github.repository }}\n subject-digest: \${{ steps.build.outputs.digest }}\n push-to-registry: true\n`;
10448
+ return `${common}jobs:\n destination-note:\n runs-on: ubuntu-latest\n steps:\n - run: echo "TODO: ${opts.finalDestination === "go-proxy" ? "Go modules publish through signed SemVer tags and proxy.golang.org; no registry upload is required" : opts.finalDestination === "github-pages" ? "Pages deployment is handled by pages.yml; no package registry applies" : opts.finalDestination === "actions-marketplace" ? "complete Marketplace listing and maintain the floating major tag; no OIDC registry exists" : opts.finalDestination === "vscode-marketplace" ? "VS Code Marketplace does not support OIDC; require explicit human approval before configuring its PAT-based publish exception" : "resolve the final destination before enabling publication"}."\n`;
10449
+ }
10450
+ function buildMediaWorkflow(opts) {
10451
+ const install = opts.commands.install ?? "echo \"TODO: record the install command\"";
10452
+ const HAS_PW = "${{ hashFiles('**/playwright.config.*') != '' }}";
10453
+ return `name: Media evidence & UI verification
10454
+
10455
+ on:
10456
+ workflow_dispatch:
10457
+ # Enable on PRs once your UI/site paths are known, so visual-regression + a11y actually PROTECT
10458
+ # every user-viewable surface on each change (until then the steps below no-op green):
10459
+ # pull_request:
10460
+ # paths: ["<!-- TODO: your UI/site source globs -->"]
10461
+
10462
+ permissions:
10463
+ contents: read
10464
+
10465
+ # Pillar D — UI/UX and every user-viewable surface are VERIFIED BY BROWSING: drive the running
10466
+ # artifact in a real browser and judge the RENDERED pixels. Never infer UI quality from source,
10467
+ # a passing build, or an HTTP 200. See docs/playbook/README.md Phase 4 (iterate-to-polish loop).
10468
+ jobs:
10469
+ verify-ui:
10470
+ runs-on: ubuntu-latest
10471
+ steps:
10472
+ - uses: actions/checkout@${CHECKOUT_SHA} # v4
10473
+ - name: State-matrix screenshots + visual regression (Playwright)
10474
+ if: ${HAS_PW}
10475
+ run: |
10476
+ ${install}
10477
+ npx --no-install playwright install --with-deps chromium
10478
+ # Your spec MUST drive each key screen through ALL states — empty / loading / error / success /
10479
+ # first-run / edge — at 375 / 768 / 1280 x light+dark (page.emulateMedia), and assert
10480
+ # expect(page).toHaveScreenshot() against committed baselines so any unintended pixel change fails.
10481
+ npx --no-install playwright test
10482
+ - name: Accessibility gate (axe-core, zero serious/critical)
10483
+ if: ${HAS_PW}
10484
+ run: echo "TODO: assert @axe-core/playwright inside the spec above; fail on ANY serious/critical violation on each key screen AND state; keyboard-reach + visible focus + >=24px targets (WCAG 2.2)."
10485
+ - name: Core Web Vitals + Lighthouse budgets (web surfaces)
10486
+ if: \${{ hashFiles('**/lighthouserc.*', '**/.lighthouserc.*') != '' }}
10487
+ run: npx --no-install @lhci/cli autorun
10488
+ - name: Capture launch media (screenshots, 1200x630 og-image, demo)
10489
+ run: |
10490
+ echo "TODO: capture desktop + mobile launch screenshots and a 1200x630 og-image.png from the RENDERED site."
10491
+ echo "Optional: install VHS from a checksum-pinned release and record artifacts/demo.gif."
10492
+ ${opts.commands.build ?? "true"}
10493
+ - name: Wire-up reminder (no browser verification configured yet)
10494
+ if: \${{ hashFiles('**/playwright.config.*') == '' }}
10495
+ run: echo "TODO: add a Playwright config + a state-matrix spec (toHaveScreenshot + @axe-core/playwright) so UI is VERIFIED BY BROWSING, not guessed. See docs/playbook/README.md Phase 4."
10496
+ - uses: actions/upload-artifact@${UPLOAD_ARTIFACT_SHA} # v4
10497
+ with:
10498
+ name: launch-media
10499
+ path: |
10500
+ artifacts/screenshots/**
10501
+ artifacts/og-image.png
10502
+ artifacts/demo.gif
10503
+ playwright-report/**
10504
+ test-results/**
10505
+ if-no-files-found: warn
10506
+ `;
10507
+ }
10508
+ function buildRobotsTxt() {
10509
+ return `User-agent: *\nAllow: /\nSitemap: <!-- TODO: insert the canonical absolute sitemap URL -->\n`;
10510
+ }
10511
+ function buildSitemap() {
10512
+ return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n <!-- TODO: generate canonical absolute <url><loc> entries during the site build. -->\n</urlset>\n`;
10513
+ }
10514
+ function buildSeoHead() {
10515
+ return `<!-- Merge these tags into the site's real <head>; replace every TODO with canonical absolute URLs. -->\n<link rel="canonical" href="<!-- TODO: canonical page URL -->">\n<meta property="og:type" content="website">\n<meta property="og:title" content="<!-- TODO: product name -->">\n<meta property="og:description" content="<!-- TODO: concise value proposition -->">\n<meta property="og:image" content="<!-- TODO: absolute 1200x630 og-image URL -->">\n<meta name="twitter:card" content="summary_large_image">\n<script type="application/ld+json">{"@context":"https://schema.org","@type":"SoftwareApplication","name":"<!-- TODO: product name -->","url":"<!-- TODO: canonical site URL -->"}<\/script>\n`;
10516
+ }
10517
+ function buildNotFoundPage() {
10518
+ return `<!doctype html>\n<html lang="en"><head><meta charset="utf-8"><meta name="robots" content="noindex"><title>Not found</title></head><body><main><h1>Page not found</h1><p><a href="/">Return home</a></p></main></body></html>\n`;
10519
+ }
10520
+ function buildSecurityTxt() {
10521
+ return `Contact: <!-- TODO: security disclosure email or HTTPS form -->\nExpires: <!-- TODO: RFC 3339 date less than one year from publication -->\nCanonical: <!-- TODO: absolute /.well-known/security.txt URL -->\nPolicy: <!-- TODO: absolute SECURITY.md or security-policy URL -->\nPreferred-Languages: en\n`;
10522
+ }
10523
+ function buildSecurityPolicy(opts) {
10524
+ return `# Security policy
10525
+
10526
+ ## Supported versions
10527
+
10528
+ <!-- TODO: list supported release lines and their security-support windows. -->
10529
+
10530
+ ## Reporting a vulnerability
10531
+
10532
+ Please do not open a public issue. Use GitHub Private Vulnerability Reporting for ${opts.repoName}, or contact <!-- TODO: monitored security disclosure address -->.
10533
+
10534
+ ## Response targets
10535
+
10536
+ - Acknowledge a report within **2 business days**.
10537
+ - Provide an initial assessment or request for more information within **7 calendar days**.
10538
+ - Share remediation status at least every **14 calendar days** until closure.
10539
+
10540
+ These are response targets, not a promise that every report can be fixed within a fixed period. Coordinated disclosure timing will be agreed with the reporter.
10541
+ `;
10542
+ }
10543
+ function buildContributing(opts) {
10544
+ return `# Contributing
10545
+
10546
+ ## Set up
10547
+
10548
+ 1. Read the repository guidance and relevant ADRs.
10549
+ 2. Install dependencies: ${commandOrTodo(opts.commands.install, "confirm install command")}
10550
+ 3. Run tests: ${commandOrTodo(opts.commands.test, "record test command")}
10551
+
10552
+ ## Find a first contribution
10553
+
10554
+ Look for issues labeled 'good first issue' or 'help wanted'. Before starting larger work, comment with the behavior you intend to change and the verification you will add.
10555
+
10556
+ ## Pull requests
10557
+
10558
+ Keep one concern per PR. Include acceptance criteria, tests, failure modes considered, documentation updates, and exact verification output. Follow the repository's code of conduct and do not weaken CI to land a change.
10559
+ `;
10560
+ }
10561
+ function buildCodeOfConduct() {
10562
+ return `# Contributor Covenant Code of Conduct
10563
+
10564
+ ## Our pledge
10565
+
10566
+ We pledge to make participation in this community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, experience, education, socioeconomic status, nationality, appearance, race, caste, color, religion, or sexual identity and orientation.
10567
+
10568
+ ## Our standards
10569
+
10570
+ Use welcoming and inclusive language, respect differing viewpoints, accept constructive feedback, focus on what is best for the community, and show empathy. Harassment, insults, public or private intimidation, and publishing others' private information are unacceptable.
10571
+
10572
+ ## Enforcement
10573
+
10574
+ Report unacceptable behavior to <!-- TODO: private conduct-reporting channel -->. Maintainers will investigate promptly, protect reporter privacy where possible, and apply proportionate corrective action.
10575
+
10576
+ This policy adopts the Contributor Covenant, version 2.1. See https://www.contributor-covenant.org/version/2/1/code_of_conduct.html for the full enforcement guidelines and attribution required by that license.
10577
+ `;
10578
+ }
10579
+ function buildSupport() {
10580
+ return `# Support
10581
+
10582
+ ## Questions and help
10583
+
10584
+ <!-- TODO: name the supported discussion forum, issue category, or community channel. -->
10585
+
10586
+ ## Bugs
10587
+
10588
+ Search existing issues, then open a bug report with reproduction steps, expected and actual behavior, environment details, and relevant logs with secrets removed.
10589
+
10590
+ ## Security
10591
+
10592
+ Do not report vulnerabilities publicly. Follow [SECURITY.md](SECURITY.md).
10593
+
10594
+ ## Scope and response
10595
+
10596
+ Support is provided on a best-effort basis. <!-- TODO: state maintained versions, normal response expectations, and commercial support if any. -->
10597
+ `;
10598
+ }
10599
+ function buildCodeowners() {
10600
+ return `# TODO: replace placeholder owners with real maintainers or teams.\n* @OWNER/MAINTAINERS\n.github/ @OWNER/MAINTAINERS\nSECURITY.md @OWNER/SECURITY\n`;
10601
+ }
10602
+ function buildGovernance() {
10603
+ return `# Governance
10604
+
10605
+ ## Roles
10606
+
10607
+ - Contributors propose changes and participate in review.
10608
+ - Maintainers review, release, triage, and steward project health.
10609
+ - Security responders handle private vulnerability reports.
10610
+
10611
+ <!-- TODO: list current maintainers, affiliations, and contact paths. -->
10612
+
10613
+ ## Decisions
10614
+
10615
+ Routine reversible decisions use lazy consensus in issues or PRs. Architecture and durable process changes require an ADR. Conflicts of interest must be disclosed. Security-sensitive and irreversible decisions require explicit maintainer approval.
10616
+
10617
+ ## Becoming or leaving a maintainer
10618
+
10619
+ Maintainer nominations should be based on sustained, constructive contributions and community trust, not employer or funding status. Record nominations and decisions publicly. Departing maintainers should transfer ownership and access promptly.
10620
+
10621
+ ## Sustainability
10622
+
10623
+ Review contributor response time, repeat-contributor rate, contributor absence factor, organizational diversity, release cadence, and adopter evidence at least quarterly. These lagging signals, not this file's presence, show whether governance works.
10624
+ `;
10625
+ }
10626
+ function buildFunding() {
10627
+ return `# TODO: uncomment and fill only funding platforms the project actually owns.\n# github: [maintainer]\n# open_collective: project\n# custom: [https://example.invalid/sponsor]\n`;
10628
+ }
10629
+ function buildAdopters() {
10630
+ return `# Adopters
10631
+
10632
+ Real users may add themselves through a pull request. Do not add organizations without their consent.
10633
+
10634
+ | Organization / project | Public evidence | How it is used | Contact or PR |
10635
+ | --- | --- | --- | --- |
10636
+ | <!-- TODO: verified adopter --> | <!-- public URL --> | <!-- production, evaluation, integration --> | <!-- consent evidence --> |
10637
+ `;
10638
+ }
10639
+ function buildIssueTemplateConfig() {
10640
+ return `blank_issues_enabled: false\ncontact_links:\n - name: Security vulnerability\n url: <!-- TODO: GitHub private vulnerability reporting URL -->\n about: Report security issues privately; do not open a public issue.\n - name: Support\n url: <!-- TODO: discussions or support URL -->\n about: Ask usage questions and get help.\n`;
9592
10641
  }
9593
10642
  function preferredRunner(opts) {
9594
10643
  const primary = opts.ci.primaryOs?.toLowerCase() ?? "";
@@ -9636,7 +10685,7 @@ List the realistic failure modes you considered, how you tested each one, and an
9636
10685
 
9637
10686
  ## UI evidence
9638
10687
 
9639
- ${opts.uiEvidenceRequired ? "Attach before/after screenshots or recordings for every user-visible surface changed." : "If user-visible behavior changed, attach before/after screenshots, recordings, or CLI output."}
10688
+ ${opts.uiEvidenceRequired ? "Attach screenshots of the RENDERED result for every user-viewable surface changed — the product UI, and any changed README / Pages / docs as they render — at mobile + desktop, light + dark, driven in a real browser. Never infer UI quality from source or a passing build." : "If user-visible behavior changed, attach before/after evidence — for UI, a screenshot of the rendered result at real viewports; for a CLI, the real output."}
9640
10689
 
9641
10690
  ## Notes / follow-ups
9642
10691
 
@@ -9764,6 +10813,175 @@ Each ${title.toLowerCase()} entry should include:
9764
10813
  - links to related issues, PRs, ADRs, and follow-ups
9765
10814
  `;
9766
10815
  }
10816
+ function buildPlaybook() {
10817
+ return `# Autonomous product operating playbook
10818
+
10819
+ ## Definition of greatness (verifiable)
10820
+
10821
+ ${DEFINITION_OF_GREATNESS}
10822
+
10823
+ Run the phases in order. A phase exits only on its externally verifiable checkpoint. Store source links, raw counts, command output, analytics queries, and decisions in issues, plans, research, ADRs, or PRs so another operator can audit the claim.
10824
+
10825
+ ## Phase 0 — Discover
10826
+
10827
+ - Interview people at a concrete struggling moment. Capture the trigger, prior behavior, forces pushing and pulling change, current workaround, and the criteria that would make them hire or fire a solution.
10828
+ - Mine support threads, issues, communities, searches, and observed workflows for the same job. Separate a repeated behavior from a stated preference.
10829
+ - Treat pain as real only after at least three independent sources corroborate the same struggling moment and consequence.
10830
+ - Write the first falsifiable hypothesis and the cheapest test that could disprove it.
10831
+
10832
+ Decision criteria: the job is specific, consequential, recurrent, and currently served by a workaround people can describe. Otherwise continue discovery or stop.
10833
+
10834
+ Exit checkpoint: an evidence ledger links at least three independent corroborating sources, verbatim hire/fire criteria, and a named owner who can reproduce the evidence.
10835
+
10836
+ ## Phase 1 — Niche
10837
+
10838
+ - Segment candidate customers with Disciplined Entrepreneurship criteria: common job, purchasing process, reachability, urgency, switching friction, competition, and ability to become a reference.
10839
+ - Estimate a credible path to a 1,000 true-fans floor. Do not use total-addressable-market theater as a substitute for reachable people.
10840
+ - Run a distribution test in the channels where each segment already gathers before building for it.
10841
+ - Publish the go/no-go table and select one beachhead.
10842
+
10843
+ | Candidate | Urgent job | Reachable now | Will switch/pay | Weak alternatives | Founder advantage | 1,000-fan path | Go/no-go |
10844
+ | --- | --- | --- | --- | --- | --- | --- | --- |
10845
+ | <!-- segment --> | <!-- evidence --> | <!-- channel/test --> | <!-- evidence --> | <!-- alternatives --> | <!-- evidence --> | <!-- estimate --> | <!-- decision --> |
10846
+
10847
+ Decision criteria: go only when the niche is reachable, has the repeated job from Phase 0, and passes a real distribution test. Kill or narrow otherwise.
10848
+
10849
+ Exit checkpoint: the completed table links observed channel responses and names one beachhead; a reviewer can recount real prospects reached and responses received.
10850
+
10851
+ ## Phase 2 — Position
10852
+
10853
+ Apply the Obviously Awesome sequence:
10854
+
10855
+ 1. List competitive alternatives.
10856
+ 2. Include doing nothing and the status-quo workaround.
10857
+ 3. Identify unique attributes.
10858
+ 4. Translate attributes into customer value.
10859
+ 5. Prove the value with evidence.
10860
+ 6. Identify customers who care most.
10861
+ 7. Choose the market category that makes the value obvious.
10862
+ 8. Add only trends that strengthen relevance.
10863
+ 9. Align product, sales, and marketing language.
10864
+ 10. Test the positioning with beachhead prospects and revise.
10865
+
10866
+ | Competitive alternative | Why it is hired now | Where it fails the job | Our differentiated value | Proof |
10867
+ | --- | --- | --- | --- | --- |
10868
+ | Do nothing / tolerate it | <!-- reason --> | <!-- cost --> | <!-- value --> | <!-- evidence --> |
10869
+ | Manual workaround | <!-- reason --> | <!-- cost --> | <!-- value --> | <!-- evidence --> |
10870
+
10871
+ One-sentence pitch: For [beachhead] who struggle with [job], [product] is a [category] that [primary value], unlike [main alternative], because [proof-backed differentiator].
10872
+
10873
+ Decision criteria: a prospect in the beachhead can accurately repeat who it is for, why it matters, and why the status quo is worse.
10874
+
10875
+ Exit checkpoint: recorded or written tests with real beachhead prospects show the pitch was understood without explanation; store the sample size and exact responses.
10876
+
10877
+ ## Phase 3 — Scope
10878
+
10879
+ - Rank assumptions by impact and uncertainty. Design a Riskiest-Assumption Test before implementation.
10880
+ - Set the numeric kill, pivot, and continue thresholds before collecting results; never move the threshold after seeing data.
10881
+ - Classify scope with Kano: must-be, performance, and delight. Include every true must-be, the minimum performance needed for the job, and at least one memorable delight.
10882
+ - Define the aha moment and instrument a path to time-to-first-value under five minutes.
10883
+ - Freeze a v0.1 cut list. New requests replace an item or wait; they do not silently expand scope.
10884
+
10885
+ Decision criteria: the cheapest test clears its pre-set threshold and the cut list can deliver the job end-to-end without speculative platform work.
10886
+
10887
+ Exit checkpoint: the repository contains the dated experiment, raw result, threshold decision, instrumented aha event, and frozen v0.1 cut list approved in a plan or issue.
10888
+
10889
+ ## Phase 4 — Build
10890
+
10891
+ - Record every non-trivial architecture or long-lived process decision in an ADR. Prefer radical simplicity.
10892
+ - Use trunk-based development, short-lived branches, and feature flags for incomplete or reversible exposure.
10893
+ - Apply the test pyramid: many fast unit checks, focused integration checks, and few critical end-to-end journeys.
10894
+ - Ship through CI/CD and track all DORA four keys. Speed and stability improve together; do not trade one away by weakening checks.
10895
+ - Make APIs and developer experience Stripe-grade: coherent names, actionable errors, stable contracts, safe defaults, copy-paste examples.
10896
+ - Write Diataxis tutorials, how-to guides, reference, and explanation. Time the quickstart from a cold machine and keep it under five minutes.
10897
+ - Enforce WCAG 2.2 AA and Core Web Vitals budgets in CI. Defaults must just work.
10898
+ - Verify every user-viewable surface BY BROWSING, never by guessing from code. Drive the running artifact through each state (empty / loading / error / success / first-run / edge) at 375 / 768 / 1280 x light+dark, screenshot the actual pixels, critique them against a professional bar (visual hierarchy, spacing rhythm, type/readability, restrained palette, alignment, motion-with-purpose, and whether each state genuinely helps), fix the top defects at the design-system level, then re-drive and re-capture until the vision rubric is clean and the deterministic gates are green (visual-regression \`toHaveScreenshot\`, \`@axe-core/playwright\` zero serious/critical, contrast, CWV). Commit the baselines so polish cannot silently regress.
10899
+
10900
+ Decision criteria: the frozen cut list works as one coherent journey and every quality budget has executable or observed evidence.
10901
+
10902
+ Exit checkpoint: required CI is green; a cold-start recording/log reaches first value in under five minutes; a real deployment returns HTTP 200; accessibility and Web Vitals reports meet their budgets; the state-matrix Playwright screenshots + visual-regression baselines are committed and green, axe reports zero serious/critical, and the UI was judged from the rendered pixels, not the source.
10903
+
10904
+ ## Phase 5 — Launch
10905
+
10906
+ - Turn the README into the landing page: the beachhead job, proof, five-minute quickstart, examples, limits, and next action above internal architecture detail.
10907
+ - Publish the GitHub Pages site and complete the Diataxis documentation. Treat docs as marketing because they let prospects experience competence before adoption.
10908
+ - Launch first where the beachhead persona already lives. Sequence, do not spray: Show HN for Hacker News builders, dev.to for developer education, Product Hunt for its discovery audience, and build-in-public for communities already following the problem.
10909
+ - Give each channel a channel-native artifact and measurable activation link. Respond to questions and capture objections as discovery input.
10910
+ - Verify every launch surface as it actually RENDERS, never from markdown or a 200: browse the repo page and screenshot the rendered README in light AND dark (every image/badge loads, the theme-adaptive hero swaps, relative links resolve, no raw-markdown artifacts), the live Pages site and docs (nav works, no overflow), the latest Release page (notes render, links resolve), and the og / social share-card (1200x630, legible when shared). Fix whatever looks off, re-capture, and only then call it launched.
10911
+
10912
+ Decision criteria: launch traffic reaches the instrumented aha path and produces conversations or usage from the selected beachhead, not merely impressions.
10913
+
10914
+ Exit checkpoint: public URLs return HTTP 200; channel posts are live; analytics record real visitors reaching or failing the aha event; support responses and objections are linked; the rendered README, Pages, docs, Release page, and og-card were driven + screenshotted and read well, not merely reachable.
10915
+
10916
+ ## Phase 6 — Measure
10917
+
10918
+ - Define AARRR with explicit events. Stars and impressions are acquisition signals, never activation.
10919
+ - Measure the aha moment and median/p90 time-to-first-value; target under five minutes.
10920
+ - Run the Sean Ellis test with a reported sample size and segment: at least 40% must answer "very disappointed".
10921
+ - Plot cohort retention over a relevant product interval and require the curve to flatten. The Sean Ellis result and retention shape are both required for a product-market-fit claim.
10922
+ - Compare outcomes to the pre-registered hypothesis and threshold, including negative results.
10923
+
10924
+ Decision criteria: continue only from observed behavior. Strong survey sentiment without retention, or retention without strong dependence, is promising evidence but not product-market fit.
10925
+
10926
+ Exit checkpoint: an auditable dashboard/export contains activation, time-to-first-value, AARRR, Sean Ellis responses with real N, and cohort retention; the PMF conclusion cites both required tests.
10927
+
10928
+ ## Phase 7 — Iterate
10929
+
10930
+ - Build a weekly opportunity-solution tree from interviews, support, issues, lost users, and behavioral data: outcome → opportunities → solutions → experiments.
10931
+ - Validate opportunities before solutions. Use RICE only to sequence options that already cleared validation; a high score cannot make an unvalidated idea true.
10932
+ - Select the next riskiest assumption, pre-register its threshold, run the smallest experiment, and update the tree.
10933
+ - Publish a changelog entry for every release and close the loop with affected users.
10934
+
10935
+ Decision criteria: each iteration traces from external evidence to opportunity to experiment to measured outcome; vanity requests do not bypass the tree.
10936
+
10937
+ Exit checkpoint: the dated tree links source evidence, the selected experiment, threshold, measured result, release, and user follow-up.
10938
+
10939
+ ## Phase 8 — Grow
10940
+
10941
+ - Scale only channels that have produced retained and activated users in the beachhead.
10942
+ - Create shareable-artifact loops where normal product use produces something useful others can see, reuse, or discuss; measure invites and downstream activation, not raw shares.
10943
+ - Invest in community infrastructure, examples, integrations, and contribution paths that compound trust.
10944
+ - Use docs as search and education: answer the real job, alternatives, migration questions, and failure modes with proof.
10945
+ - Expand to adjacent segments one at a time and rerun niche, positioning, activation, and retention checks.
10946
+
10947
+ Decision criteria: growth preserves or improves activation and cohort retention and has bounded, approved economics.
10948
+
10949
+ Exit checkpoint: cohort and channel reports show incremental retained users, a measured share/referral loop, and acquisition economics within explicit human-approved limits.
10950
+
10951
+ ## Governance
10952
+
10953
+ Daily OODA inside each phase:
10954
+
10955
+ 1. **Observe:** collect fresh customer, product, delivery, and distribution evidence.
10956
+ 2. **Orient:** compare it with the current job, segment, assumptions, decision log, and constraints.
10957
+ 3. **Decide:** choose one reversible next action against a pre-set threshold; escalate only destructive, regulated, or economically unauthorized actions.
10958
+ 4. **Act:** delegate bounded work, execute, and capture the result.
10959
+
10960
+ At phase scale, run Build-Measure-Learn: build the smallest testable artifact, measure externally observable behavior, learn against the threshold, then persist or pivot. Do not enter the next phase until its checkpoint is independently reproducible.
10961
+
10962
+ Decision log format:
10963
+
10964
+ | Date | Hypothesis | Experiment | Metric | Pre-set threshold | Outcome/evidence | Decision | Owner/next check |
10965
+ | --- | --- | --- | --- | --- | --- | --- | --- |
10966
+ | YYYY-MM-DD | <!-- falsifiable claim --> | <!-- smallest test --> | <!-- measure --> | <!-- kill/pivot/continue --> | <!-- URL, query, output, real N --> | <!-- result --> | <!-- owner/date --> |
10967
+
10968
+ Hard authority limits: no operator may fabricate evidence, manipulate users, incur spend, start paid acquisition, set or change pricing, issue discounts, enter contracts, expand privileges, or make regulated/legal/privacy commitments unless a human has provided explicit boundaries. Within scope, proceed on best judgment and record assumptions rather than pausing for routine clarification.
10969
+
10970
+ ## Anti-patterns
10971
+
10972
+ - **Over-building without distribution:** run a reachability or channel test before extending product scope.
10973
+ - **Hallucinated progress:** Project Vend and TheAgentCompany-style evaluations show a 70%+ autonomous-task failure base rate; never convert activity or a narrative into completion. Require real HTTP 200 responses, green CI, observed analytics, deployment state, or a real survey N.
10974
+ - **No distribution plan:** "build it and they will come" is not a plan. Name the beachhead, channel, artifact, owner, and activation event before launch.
10975
+ - **Manipulation or unbounded economic judgment:** no dark patterns, fabricated scarcity/social proof, undisclosed persuasion, speculative purchases, autonomous pricing, or spend outside human-set limits.
10976
+ - **Demo equals reality:** a local screenshot or scripted happy path is not a deployed, accessible, observable product. Verify the production journey cold.
10977
+ - **Viral equals product-market fit:** attention, stars, posts, and shares do not replace the Sean Ellis threshold plus a flattening cohort retention curve.
10978
+ - **Metrics after the fact:** choosing thresholds after seeing results destroys the test. Pre-register kill, pivot, and continue criteria.
10979
+
10980
+ ## One-page operating sequence
10981
+
10982
+ ${CONDENSED_OPERATING_SEQUENCE}
10983
+ `;
10984
+ }
9767
10985
  function buildLearnings(opts) {
9768
10986
  return `# Learnings
9769
10987
 
@@ -10294,7 +11512,7 @@ function tier1LiveEnabled() {
10294
11512
  return !envOptOut(process.env.GH_ROUTER_FM_TIER1_LIVE);
10295
11513
  }
10296
11514
  const DETERMINISTIC_VERIFIERS = { decompose: isValidDecomposeVerdict };
10297
- function isNonNegativeInteger(value) {
11515
+ function isNonNegativeInteger$1(value) {
10298
11516
  return typeof value === "number" && Number.isInteger(value) && value >= 0;
10299
11517
  }
10300
11518
  function dependsOnIndices(indices, rawIndex, total) {
@@ -10302,7 +11520,7 @@ function dependsOnIndices(indices, rawIndex, total) {
10302
11520
  if (!Array.isArray(indices)) return null;
10303
11521
  const result = [];
10304
11522
  for (const idx of indices) {
10305
- if (!isNonNegativeInteger(idx) || idx >= total || idx === rawIndex) return null;
11523
+ if (!isNonNegativeInteger$1(idx) || idx >= total || idx === rawIndex) return null;
10306
11524
  result.push(idx);
10307
11525
  }
10308
11526
  return result;
@@ -10416,6 +11634,142 @@ function decideRoute(kind, verdict) {
10416
11634
  };
10417
11635
  }
10418
11636
 
11637
+ //#endregion
11638
+ //#region src/lib/first-mate/strategy-store.ts
11639
+ const STRATEGIES_VERSION = 1;
11640
+ function strategiesPath() {
11641
+ return nodePath.join(PATHS.FIRST_MATE_DIR, "strategy.json");
11642
+ }
11643
+ function asRecord$1(value) {
11644
+ return typeof value === "object" && value !== null ? value : void 0;
11645
+ }
11646
+ function isFiniteNumber(value) {
11647
+ return typeof value === "number" && Number.isFinite(value);
11648
+ }
11649
+ function isNonNegativeInteger(value) {
11650
+ return typeof value === "number" && Number.isInteger(value) && value >= 0;
11651
+ }
11652
+ function isOptionalString(value) {
11653
+ return value === void 0 || typeof value === "string";
11654
+ }
11655
+ function isOptionalStringArray(value) {
11656
+ return value === void 0 || Array.isArray(value) && value.every((entry) => typeof entry === "string");
11657
+ }
11658
+ function isStrategyBet(value) {
11659
+ const bet = asRecord$1(value);
11660
+ return bet !== void 0 && typeof bet.hypothesis === "string" && typeof bet.metric === "string" && typeof bet.threshold === "string" && (bet.decisionRule === "kill" || bet.decisionRule === "pivot" || bet.decisionRule === "continue");
11661
+ }
11662
+ function isOptionalStrategyBet(value) {
11663
+ return value === void 0 || isStrategyBet(value);
11664
+ }
11665
+ function isStrategyGreatnessItem(value) {
11666
+ const entry = asRecord$1(value);
11667
+ return entry !== void 0 && typeof entry.item === "string" && (entry.status === "done" || entry.status === "pending") && isOptionalString(entry.evidence);
11668
+ }
11669
+ function isOptionalGreatnessChecklist(value) {
11670
+ return value === void 0 || Array.isArray(value) && value.every(isStrategyGreatnessItem);
11671
+ }
11672
+ function isStrategyDecisionEntry(value) {
11673
+ const entry = asRecord$1(value);
11674
+ return entry !== void 0 && isFiniteNumber(entry.atMs) && typeof entry.decision === "string" && typeof entry.rationale === "string" && isOptionalString(entry.evidenceRef);
11675
+ }
11676
+ function isOptionalDecisionLog(value) {
11677
+ return value === void 0 || Array.isArray(value) && value.every(isStrategyDecisionEntry);
11678
+ }
11679
+ function isNextStrategicAction(value) {
11680
+ if (value === void 0) return true;
11681
+ const next = asRecord$1(value);
11682
+ return next !== void 0 && typeof next.action === "string" && isOptionalString(next.trigger);
11683
+ }
11684
+ function isStrategyRecord(value) {
11685
+ const record = asRecord$1(value);
11686
+ return record !== void 0 && typeof record.missionId === "string" && record.missionId.length > 0 && isOptionalStringArray(record.repos) && isOptionalString(record.currentPhase) && isOptionalStrategyBet(record.activeBet) && isOptionalGreatnessChecklist(record.greatnessChecklist) && isOptionalDecisionLog(record.decisionLog) && isOptionalStringArray(record.openAssumptions) && isNextStrategicAction(record.nextStrategicAction) && isFiniteNumber(record.updatedMs) && isOptionalString(record.updatedByWake);
11687
+ }
11688
+ function parseStrategies(raw) {
11689
+ if (raw === void 0) return {
11690
+ rev: 0,
11691
+ strategies: []
11692
+ };
11693
+ try {
11694
+ const parsed = asRecord$1(JSON.parse(raw));
11695
+ if (!parsed || parsed.version !== STRATEGIES_VERSION || !Array.isArray(parsed.strategies)) return {
11696
+ rev: 0,
11697
+ strategies: []
11698
+ };
11699
+ const rev = isNonNegativeInteger(parsed.rev) ? parsed.rev : 0;
11700
+ const cleaned = parsed.strategies.filter(isStrategyRecord);
11701
+ if (cleaned.length !== parsed.strategies.length) consola.debug(`first-mate strategies dropped ${parsed.strategies.length - cleaned.length} corrupt strategy record(s)`);
11702
+ return {
11703
+ rev,
11704
+ strategies: cleaned
11705
+ };
11706
+ } catch (err) {
11707
+ consola.debug("first-mate strategies corrupt, starting empty:", err);
11708
+ return {
11709
+ rev: 0,
11710
+ strategies: []
11711
+ };
11712
+ }
11713
+ }
11714
+ async function readStrategiesFile() {
11715
+ let raw;
11716
+ try {
11717
+ raw = await fs.readFile(strategiesPath(), "utf8");
11718
+ } catch (err) {
11719
+ if (err.code !== "ENOENT") consola.debug("first-mate strategies read skipped:", err);
11720
+ raw = void 0;
11721
+ }
11722
+ const parsed = parseStrategies(raw);
11723
+ return {
11724
+ version: STRATEGIES_VERSION,
11725
+ rev: parsed.rev,
11726
+ strategies: parsed.strategies
11727
+ };
11728
+ }
11729
+ async function withStrategiesMutation(work) {
11730
+ const { result } = await commitJsonCas({
11731
+ path: strategiesPath(),
11732
+ parse: (raw) => {
11733
+ const parsed = parseStrategies(raw);
11734
+ return {
11735
+ rev: parsed.rev,
11736
+ value: parsed.strategies
11737
+ };
11738
+ },
11739
+ mutate: async (strategies) => {
11740
+ return {
11741
+ value: strategies,
11742
+ result: await work(strategies)
11743
+ };
11744
+ },
11745
+ build: (strategies, rev) => ({
11746
+ version: STRATEGIES_VERSION,
11747
+ rev,
11748
+ strategies
11749
+ })
11750
+ });
11751
+ return result;
11752
+ }
11753
+ async function readStrategies() {
11754
+ return (await readStrategiesFile()).strategies;
11755
+ }
11756
+ async function readStrategy(missionId) {
11757
+ return (await readStrategies()).find((record) => record.missionId === missionId);
11758
+ }
11759
+ async function upsertStrategy(rec) {
11760
+ await withStrategiesMutation((strategies) => {
11761
+ const existing = strategies.find((entry) => entry.missionId === rec.missionId);
11762
+ const next = {
11763
+ ...existing,
11764
+ ...rec,
11765
+ decisionLog: [...existing?.decisionLog ?? [], ...rec.decisionLog ?? []],
11766
+ updatedMs: Date.now()
11767
+ };
11768
+ const kept = strategies.filter((entry) => entry.missionId !== rec.missionId);
11769
+ strategies.splice(0, strategies.length, ...kept, next);
11770
+ });
11771
+ }
11772
+
10419
11773
  //#endregion
10420
11774
  //#region src/lib/first-mate/tools.ts
10421
11775
  const FIRST_MATE_GROUP = "first-mate";
@@ -10501,6 +11855,41 @@ const ScaffoldRepoArgsSchema = z.object({
10501
11855
  base_ref: z.string().trim().min(1).optional(),
10502
11856
  detection_overrides: ScaffoldDetectionOverridesSchema.optional()
10503
11857
  }).strict();
11858
+ const StrategyBetSchema = z.object({
11859
+ hypothesis: z.string(),
11860
+ metric: z.string(),
11861
+ threshold: z.string(),
11862
+ decisionRule: z.enum([
11863
+ "kill",
11864
+ "pivot",
11865
+ "continue"
11866
+ ])
11867
+ }).strict();
11868
+ const StrategyGreatnessItemSchema = z.object({
11869
+ item: z.string(),
11870
+ status: z.enum(["done", "pending"]),
11871
+ evidence: z.string().optional()
11872
+ }).strict();
11873
+ const StrategyDecisionEntrySchema = z.object({
11874
+ atMs: z.number().finite(),
11875
+ decision: z.string(),
11876
+ rationale: z.string(),
11877
+ evidenceRef: z.string().optional()
11878
+ }).strict();
11879
+ const NextStrategicActionSchema = z.object({
11880
+ action: z.string(),
11881
+ trigger: z.string().optional()
11882
+ }).strict();
11883
+ const WriteStrategyArgsSchema = z.object({
11884
+ mission_id: z.string().trim().min(1),
11885
+ repos: z.array(z.string()).optional(),
11886
+ currentPhase: z.string().optional(),
11887
+ activeBet: StrategyBetSchema.optional(),
11888
+ greatnessChecklist: z.array(StrategyGreatnessItemSchema).optional(),
11889
+ decisionLog: z.array(StrategyDecisionEntrySchema).optional(),
11890
+ openAssumptions: z.array(z.string()).optional(),
11891
+ nextStrategicAction: NextStrategicActionSchema.optional()
11892
+ }).strip();
10504
11893
  function createFirstMateTools(depsOverride = {}) {
10505
11894
  const deps = {
10506
11895
  ...defaultMergeCloseDeps(),
@@ -10686,6 +12075,80 @@ function createFirstMateTools(depsOverride = {}) {
10686
12075
  inactiveSummary: summarizeInactiveMissions(missions)
10687
12076
  });
10688
12077
  }),
12078
+ tool$1("read_strategy", "Reads the durable strategic record for one first-mate mission so a fresh CEO can rehydrate its current phase, active bet, greatness evidence, decisions, assumptions, and next action. Returns a minimal empty record when no strategy has been written yet. Use at the start of each strategic wake before making portfolio decisions.", objectSchema({ mission_id: stringProp("Mission id whose strategy should be read.") }, ["mission_id"]), async (args) => {
12079
+ const missionId = requiredString(args, "mission_id");
12080
+ return ok(await readStrategy(missionId) ?? {
12081
+ missionId,
12082
+ currentPhase: null,
12083
+ activeBet: null,
12084
+ greatnessChecklist: [],
12085
+ decisionLog: [],
12086
+ openAssumptions: [],
12087
+ nextStrategicAction: null,
12088
+ repos: [],
12089
+ updatedMs: 0
12090
+ });
12091
+ }),
12092
+ tool$1("write_strategy", "Persists the strategic continuity record for one first-mate mission after a CEO wake. Inputs contain only the state a fresh CEO needs to rehydrate: phase, bet, greatness evidence, append-only decisions, assumptions, next action, and repo handles. Decision-log entries append to existing history while the other supplied fields replace their prior values. Returns the durable update timestamp.", {
12093
+ ...objectSchema({
12094
+ mission_id: stringProp("Mission id whose strategy should be updated."),
12095
+ repos: stringArrayProp("Repository handles relevant to the mission."),
12096
+ currentPhase: stringProp("Current condensed operating-sequence phase."),
12097
+ activeBet: objectProp("Current falsifiable strategic bet.", {
12098
+ hypothesis: stringProp("Hypothesis being tested."),
12099
+ metric: stringProp("Metric used to evaluate the bet."),
12100
+ threshold: stringProp("Threshold that triggers the decision rule."),
12101
+ decisionRule: enumProp([
12102
+ "kill",
12103
+ "pivot",
12104
+ "continue"
12105
+ ], "Action when the threshold is evaluated.")
12106
+ }, [
12107
+ "hypothesis",
12108
+ "metric",
12109
+ "threshold",
12110
+ "decisionRule"
12111
+ ]),
12112
+ greatnessChecklist: arrayOfObjectsProp("Definition-of-greatness items and evidence handles.", {
12113
+ item: stringProp("Greatness criterion."),
12114
+ status: enumProp(["done", "pending"], "Current criterion status."),
12115
+ evidence: stringProp("Optional evidence handle.")
12116
+ }, ["item", "status"]),
12117
+ decisionLog: arrayOfObjectsProp("Strategic decision entries to append.", {
12118
+ atMs: numberProp("Decision timestamp in milliseconds."),
12119
+ decision: stringProp("Decision made."),
12120
+ rationale: stringProp("Why the decision was made."),
12121
+ evidenceRef: stringProp("Optional evidence handle.")
12122
+ }, [
12123
+ "atMs",
12124
+ "decision",
12125
+ "rationale"
12126
+ ]),
12127
+ openAssumptions: stringArrayProp("Strategic assumptions still requiring evidence."),
12128
+ nextStrategicAction: objectProp("Next strategic action and optional trigger.", {
12129
+ action: stringProp("Action to take next."),
12130
+ trigger: stringProp("Optional condition that triggers the action.")
12131
+ }, ["action"])
12132
+ }, ["mission_id"]),
12133
+ additionalProperties: true
12134
+ }, async (args) => {
12135
+ const input = parseWriteStrategyArgs(args);
12136
+ await upsertStrategy({
12137
+ missionId: input.mission_id,
12138
+ ...input.repos !== void 0 ? { repos: input.repos } : {},
12139
+ ...input.currentPhase !== void 0 ? { currentPhase: input.currentPhase } : {},
12140
+ ...input.activeBet !== void 0 ? { activeBet: input.activeBet } : {},
12141
+ ...input.greatnessChecklist !== void 0 ? { greatnessChecklist: input.greatnessChecklist } : {},
12142
+ ...input.decisionLog !== void 0 ? { decisionLog: input.decisionLog } : {},
12143
+ ...input.openAssumptions !== void 0 ? { openAssumptions: input.openAssumptions } : {},
12144
+ ...input.nextStrategicAction !== void 0 ? { nextStrategicAction: input.nextStrategicAction } : {},
12145
+ updatedMs: 0
12146
+ });
12147
+ return ok({
12148
+ ok: true,
12149
+ updatedMs: (await readStrategy(input.mission_id))?.updatedMs ?? 0
12150
+ });
12151
+ }),
10689
12152
  tool$1("merge_pr", "Merges a live GitHub pull request immediately, which is irreversible through this tool once GitHub accepts the merge. Inputs identify the repo and PR, bind the action to the exact reviewed head SHA, optionally bind the reviewed base branch, select the merge method, and can explicitly override ownership with allow_unowned. Returns GitHub's merge result and merge SHA after the live PR passes head-SHA and optional base concurrency checks, ownership checks, OPEN/not-draft/mergeable checks, and available CI or workflow checks. Use only after out-of-band human authorization for this exact head; this tool does not consult the first-mate approval ledger. It is not for PRs that are not agent-authored or correlated to a first-mate unit (unit.pr === pr) unless allow_unowned is intentionally set; a repo with no configured CI can merge on operator review plus the non-CI guards.", objectSchema({
10690
12153
  repo: stringProp("Repository as an owner/name string."),
10691
12154
  pr: numberProp("Pull request number."),
@@ -11153,6 +12616,16 @@ async function detectScaffoldOptions(input) {
11153
12616
  const workflowSignal = [...workflowNames, ...workflowTexts];
11154
12617
  const primaryOs = input.overrides?.primary_os ?? detectPrimaryOs(readme, workflowSignal);
11155
12618
  const matrix = detectCiMatrix(primaryOs, workflowSignal);
12619
+ const hasSite = detectHasSite(rootNames, frameworkNames);
12620
+ const finalDestination = detectFinalDestination({
12621
+ packageJsonRaw: packageJsonText,
12622
+ hasPackageJson: packageJson !== void 0,
12623
+ goMod,
12624
+ pyproject,
12625
+ cargoToml,
12626
+ rootNames,
12627
+ hasSite
12628
+ });
11156
12629
  return {
11157
12630
  repoName: input.repoSlug,
11158
12631
  repoDescription: input.repoDescription ?? summarizeReadme(readme),
@@ -11174,9 +12647,60 @@ async function detectScaffoldOptions(input) {
11174
12647
  cargoToml,
11175
12648
  workflows: workflowNames,
11176
12649
  primaryOs
11177
- })
12650
+ }),
12651
+ finalDestination,
12652
+ hasSite
11178
12653
  };
11179
12654
  }
12655
+ /**
12656
+ * True when the repo builds a web site worth an SEO/Pages scaffold: a root
12657
+ * `index.html`, or a static-site / web framework in its dependencies. Kept
12658
+ * conservative (a UI *library* is not a site) — over-seeding SEO templates with
12659
+ * TODOs is low-harm, but a false site would seed an irrelevant Pages workflow.
12660
+ */
12661
+ function detectHasSite(rootNames, frameworkNames) {
12662
+ if (new Set(rootNames.map((n) => n.toLowerCase())).has("index.html")) return true;
12663
+ const siteFrameworks = new Set([
12664
+ "vite",
12665
+ "next",
12666
+ "nuxt",
12667
+ "astro",
12668
+ "@docusaurus/core",
12669
+ "gatsby",
12670
+ "vitepress",
12671
+ "@11ty/eleventy",
12672
+ "@sveltejs/kit",
12673
+ "remix",
12674
+ "solid-start"
12675
+ ]);
12676
+ return frameworkNames.some((n) => siteFrameworks.has(n.toLowerCase()));
12677
+ }
12678
+ /**
12679
+ * Detect the repo's PUBLIC final destination from root signals. Only assigns a
12680
+ * specific destination on a CLEAR signal; genuinely ambiguous → "unknown" (the
12681
+ * scaffold then seeds CI + Pages-if-site but NO publish workflow — a safe
12682
+ * false-negative, never a wrong publish pipeline).
12683
+ */
12684
+ function detectFinalDestination(args) {
12685
+ const root = new Set(args.rootNames.map((n) => n.toLowerCase()));
12686
+ const has = (n) => root.has(n.toLowerCase());
12687
+ const raw = args.packageJsonRaw ?? "";
12688
+ if (has("action.yml") || has("action.yaml")) return "actions-marketplace";
12689
+ if (/"engines"\s*:\s*\{[^}]*"vscode"/.test(raw)) return "vscode-marketplace";
12690
+ if (args.cargoToml !== void 0) return "crates";
12691
+ if (args.pyproject !== void 0 || has("setup.py")) return "pypi";
12692
+ if (args.goMod !== void 0) return "go-proxy";
12693
+ if (args.hasPackageJson) {
12694
+ const isPublishableLib = !/"private"\s*:\s*true/.test(raw) && /"(bin|exports|main|module)"\s*:/.test(raw);
12695
+ if (args.hasSite) return "github-pages";
12696
+ if (isPublishableLib) return "npm";
12697
+ if (has("dockerfile")) return "ghcr";
12698
+ return "unknown";
12699
+ }
12700
+ if (has("dockerfile")) return "ghcr";
12701
+ if (args.hasSite) return "github-pages";
12702
+ return "unknown";
12703
+ }
11180
12704
  async function readFirstAvailableText(repo, paths, ref, signal) {
11181
12705
  for (const path$1 of paths) {
11182
12706
  const content = await readRepoTextFile(repo, path$1, ref, signal);
@@ -11403,9 +12927,17 @@ function stringRecord(value) {
11403
12927
  function parseScaffoldRepoArgs(args) {
11404
12928
  const parsed = ScaffoldRepoArgsSchema.safeParse(args);
11405
12929
  if (parsed.success) return parsed.data;
11406
- throw new FirstMateToolInputError("INVALID_ARGUMENT", parsed.error.issues.map((issue) => {
12930
+ throwSchemaError(parsed.error, "scaffold_repo");
12931
+ }
12932
+ function parseWriteStrategyArgs(args) {
12933
+ const parsed = WriteStrategyArgsSchema.safeParse(args);
12934
+ if (parsed.success) return parsed.data;
12935
+ throwSchemaError(parsed.error, "write_strategy");
12936
+ }
12937
+ function throwSchemaError(error, toolName) {
12938
+ throw new FirstMateToolInputError("INVALID_ARGUMENT", error.issues.map((issue) => {
11407
12939
  return `${issue.path.length === 0 ? "arguments" : `arguments.${issue.path.join(".")}`}: ${issue.message}`;
11408
- }).join("; ") || "arguments must match the scaffold_repo schema");
12940
+ }).join("; ") || `arguments must match the ${toolName} schema`);
11409
12941
  }
11410
12942
  function ok(value) {
11411
12943
  return jsonResult(value, false);
@@ -11516,6 +13048,12 @@ function arrayOfObjectsProp(description, properties, required) {
11516
13048
  items: objectSchema(properties, required)
11517
13049
  };
11518
13050
  }
13051
+ function objectProp(description, properties, required) {
13052
+ return {
13053
+ ...objectSchema(properties, required),
13054
+ description
13055
+ };
13056
+ }
11519
13057
  function anyProp(description) {
11520
13058
  return { description };
11521
13059
  }
@@ -30891,7 +32429,7 @@ function buildPeerAwarenessSummary(opts) {
30891
32429
  if (opts.standInAvailable) lines.push(`\`mcp__${key("decide")}__stand_in\` returns a three-lab consensus for a decision when the user is unavailable.`);
30892
32430
  if (opts.browseAvailable) lines.push(`\`mcp__${key("browser")}__*\` drives a real Chrome or Edge browser.`);
30893
32431
  if (opts.fleetAvailable) lines.push(`\`mcp__${key("fleet")}__*\` drives remote ai-or-die coding sessions.`);
30894
- if (opts.agentToolsAvailable === true) lines.push("The `/gh-first-mate` skill drives a durable GitHub cloud-agent loop.");
32432
+ if (opts.agentToolsAvailable === true) lines.push("In `--agents` mode you are the CEO of the product: `/gh-first-mate-conduct` is the fleet conductor — one durable loop that drives one or many repos, each by its own per-repo CEO subagent — and `/gh-first-mate-operate` is your operating protocol (niche → MVP → launch → iterate) toward a verifiable greatness bar; get verified work out of the team, never a self-reported \"done\".");
30895
32433
  lines.push("");
30896
32434
  lines.push(`Each tool's own description carries when to use it and when not. The full per-tool inventory (models, gating, workers, skills) is in the "Peer review and advisor" section of your CLAUDE.md project instructions.`);
30897
32435
  return lines.join("\n");
@@ -32030,5 +33568,5 @@ function enumerateInjectedMcpToolNames(groupKeys, opts = {}) {
32030
33568
  }
32031
33569
 
32032
33570
  //#endregion
32033
- export { buildAdvisorStream as $, setupCopilotToken as $t, trustRepo as A, readResponseBodyCapped as At, runWorkerAgent as B, buildWorkspaceHeaderJson as Bt, fileLastPromptStore as C, getTokenCount as Ct, repoRoot as D, createResponses as Dt, repoFingerprint as E, pickEndpoint as Et, EXPLORE_DEFAULT_MODEL as F, extractTarGzMember as Ft, toolbeltEnabled as G, DEFAULT_CODEX_MODEL_FALLBACKS as Gt, buildEnv as H, toolbeltPathOverride as Ht, IMPLEMENT_DEFAULT_MODEL as I, extractZipMember as It, TOOLBELT_TOOLS$1 as J, UPSTREAM_INACTIVITY_TIMEOUT_MS as Jt, toolbeltSkipSet as K, DEFAULT_PORT as Kt, PLAN_DEFAULT_MODEL as L, shouldUseInsecureTls as Lt, resolveSealedGate as M, provisionBrowserAssets as Mt, BROWSE_DEFAULT_MODEL as N, hasSupportedBrowserInstalled as Nt, stopGateEnabledForRepo as O, createChatCompletions as Ot, DEFAULT_MODEL as P, provisionAndIndexColbert as Pt, ADVISOR_TOOL_INSTRUCTIONS as Q, withInstallLock as Qt, REVIEW_DEFAULT_MODEL as R, ArtifactClient as Rt, fileFindingsStore as S, createMessages as St, isSubagentContext as T, resolveMcpToolTimeoutMs as Tt, availableToolCommands as U, DEFAULT_CLAUDE_MODEL_FALLBACKS as Ut, withNoOutputRetry as V, collapsePathKeys as Vt, buildToolbeltAwareness as W, DEFAULT_CODEX_MODEL as Wt, searchWeb as X, pickClaudeDefault as Xt, assetFor as Y, generateRandomPort as Yt, ADVISOR_INTERNAL_TOOL_NAME as Z, getPackageVersion as Zt, stopGateDisabled as _, copilotBaseUrl as _n, nativeSubagentModel as _t, buildPeerAwarenessSnippet as a, cacheVSCodeVersion as an, logStreamError as at, stopReviewEnabled as b, state as bn, shimDefaultsToXhigh as bt, personasFor as c, resolveCodexModel as cn, handleMcpDelete as ct, buildStopHookCommand as d, getModels as dn, artifactToolsEnabled as dt, setupGitHubAgentToken as en, injectAdvisorTool as et, captureLaunchBaseline as f, getGitHubUser as fn, browseAgentEnabled as ft, launchBaselineKey as g, GITHUB_API_BASE_URL as gn, geminiAvailable as gt, injectStopHookIntoSettingsFile as h, forwardError as hn, fleetToolsEnabled as ht, buildAgentPrompt as i, cacheModels as in, isControllerClosedError as it, liveExec as j, parseJsonOrDiagnose as jt, stopReviewStateDir as k, MAX_RESPONSE_BODY_BYTES as kt, buildArtifactOpenHookCommand as l, resolveModel as ln, handleMcpPost as lt, fileBlockBudget as m, HTTPError as mn, browserToolsEnabled as mt, MCP_GROUPS as n, tryRefreshAndRetry as nn, buildAnthropicErrorEvent as nt, buildPeerAwarenessSummary as o, filterBetaHeader as on, readIteratorWithTimeout as ot, decideStopHook as p, fetchWithTransientRetry as pn, browserCompoundToolsEnabled as pt, vscodeRipgrepPath as q, UPSTREAM_FETCH_TIMEOUT_MS as qt, assertMcpToolSurfaceConsistent as r, cacheCopilotVersion as rn, buildOpenAIErrorEvent as rt, enumerateInjectedMcpToolNames as s, isNullish as sn, relayAnthropicStream as st, GROUP_META as t, setupGitHubToken as tn, isAdvisorRequested as tt, buildSessionBindHookCommand as u, sleep as un, agentToolsEnabled as ut, stopGateId as v, copilotHeaders as vn, standInToolEnabled as vt, fileReviewDebounce as w, assembleResponsesPayload as wt, fileBaselineStore as x, countTokens as xt, stopGatePlanMode as y, githubHeaders as yn, workerToolsEnabled as yt, appendPlanReminder as z, buildWorkspaceHeaderHelperCommand as zt };
32034
- //# sourceMappingURL=peer-mcp-personas-C3ii7ZqP.js.map
33571
+ export { buildAdvisorStream as $, getPackageVersion as $t, trustRepo as A, readResponseBodyCapped as At, runWorkerAgent as B, ArtifactClient as Bt, fileLastPromptStore as C, getTokenCount as Ct, repoRoot as D, createResponses as Dt, repoFingerprint as E, pickEndpoint as Et, EXPLORE_DEFAULT_MODEL as F, extractTarGzMember as Ft, toolbeltEnabled as G, DEFAULT_CLAUDE_MODEL_FALLBACKS as Gt, buildEnv as H, buildWorkspaceHeaderJson as Ht, IMPLEMENT_DEFAULT_MODEL as I, extractZipMember as It, TOOLBELT_TOOLS$1 as J, DEFAULT_PORT as Jt, toolbeltSkipSet as K, DEFAULT_CODEX_MODEL as Kt, PLAN_DEFAULT_MODEL as L, CONDENSED_OPERATING_SEQUENCE as Lt, resolveSealedGate as M, provisionBrowserAssets as Mt, BROWSE_DEFAULT_MODEL as N, hasSupportedBrowserInstalled as Nt, stopGateEnabledForRepo as O, createChatCompletions as Ot, DEFAULT_MODEL as P, provisionAndIndexColbert as Pt, ADVISOR_TOOL_INSTRUCTIONS as Q, pickClaudeDefault as Qt, REVIEW_DEFAULT_MODEL as R, DEFINITION_OF_GREATNESS as Rt, fileFindingsStore as S, state as Sn, createMessages as St, isSubagentContext as T, resolveMcpToolTimeoutMs as Tt, availableToolCommands as U, collapsePathKeys as Ut, withNoOutputRetry as V, buildWorkspaceHeaderHelperCommand as Vt, buildToolbeltAwareness as W, toolbeltPathOverride as Wt, searchWeb as X, UPSTREAM_INACTIVITY_TIMEOUT_MS as Xt, assetFor as Y, UPSTREAM_FETCH_TIMEOUT_MS as Yt, ADVISOR_INTERNAL_TOOL_NAME as Z, generateRandomPort as Zt, stopGateDisabled as _, forwardError as _n, nativeSubagentModel as _t, buildPeerAwarenessSnippet as a, cacheCopilotVersion as an, logStreamError as at, stopReviewEnabled as b, copilotHeaders as bn, shimDefaultsToXhigh as bt, personasFor as c, filterBetaHeader as cn, handleMcpDelete as ct, buildStopHookCommand as d, resolveModel as dn, artifactToolsEnabled as dt, withInstallLock as en, injectAdvisorTool as et, captureLaunchBaseline as f, sleep as fn, browseAgentEnabled as ft, launchBaselineKey as g, HTTPError as gn, geminiAvailable as gt, injectStopHookIntoSettingsFile as h, fetchWithTransientRetry as hn, fleetToolsEnabled as ht, buildAgentPrompt as i, tryRefreshAndRetry as in, isControllerClosedError as it, liveExec as j, parseJsonOrDiagnose as jt, stopReviewStateDir as k, MAX_RESPONSE_BODY_BYTES as kt, buildArtifactOpenHookCommand as l, isNullish as ln, handleMcpPost as lt, fileBlockBudget as m, getGitHubUser as mn, browserToolsEnabled as mt, MCP_GROUPS as n, setupGitHubAgentToken as nn, buildAnthropicErrorEvent as nt, buildPeerAwarenessSummary as o, cacheModels as on, readIteratorWithTimeout as ot, decideStopHook as p, getModels as pn, browserCompoundToolsEnabled as pt, vscodeRipgrepPath as q, DEFAULT_CODEX_MODEL_FALLBACKS as qt, assertMcpToolSurfaceConsistent as r, setupGitHubToken as rn, buildOpenAIErrorEvent as rt, enumerateInjectedMcpToolNames as s, cacheVSCodeVersion as sn, relayAnthropicStream as st, GROUP_META as t, setupCopilotToken as tn, isAdvisorRequested as tt, buildSessionBindHookCommand as u, resolveCodexModel as un, agentToolsEnabled as ut, stopGateId as v, GITHUB_API_BASE_URL as vn, standInToolEnabled as vt, fileReviewDebounce as w, assembleResponsesPayload as wt, fileBaselineStore as x, githubHeaders as xn, countTokens as xt, stopGatePlanMode as y, copilotBaseUrl as yn, workerToolsEnabled as yt, appendPlanReminder as z, shouldUseInsecureTls as zt };
33572
+ //# sourceMappingURL=peer-mcp-personas-CxpFD-rW.js.map