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.
- package/dist/browser-ext/manifest.json +1 -1
- package/dist/{engine-DZjN7BuD.js → engine-DFuchRoh.js} +1 -1
- package/dist/main.js +176 -8
- package/dist/main.js.map +1 -1
- package/dist/{peer-mcp-personas-C3ii7ZqP.js → peer-mcp-personas-CxpFD-rW.js} +1689 -151
- package/dist/peer-mcp-personas-CxpFD-rW.js.map +1 -0
- package/package.json +1 -1
- package/dist/peer-mcp-personas-C3ii7ZqP.js.map +0 -1
|
@@ -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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
5248
|
+
const choice = asRecord$7(choiceValue);
|
|
5249
5249
|
if (!choice) continue;
|
|
5250
|
-
const delta = asRecord$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
5368
|
-
asRecord$
|
|
5369
|
-
asRecord$
|
|
5370
|
-
asRecord$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
-
|
|
5473
|
-
|
|
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
|
|
5490
|
+
body,
|
|
5491
|
+
retry: false,
|
|
5492
|
+
...input.idempotencyKey ? { headers: { "Idempotency-Key": input.idempotencyKey } } : {}
|
|
5476
5493
|
});
|
|
5477
|
-
return {
|
|
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$
|
|
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$
|
|
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$
|
|
5914
|
+
function isNonNegativeInteger$4(value) {
|
|
5891
5915
|
return typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
5892
5916
|
}
|
|
5893
|
-
function isOptionalString$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
6521
|
+
function isFiniteNumber$2(value) {
|
|
6498
6522
|
return typeof value === "number" && Number.isFinite(value);
|
|
6499
6523
|
}
|
|
6500
|
-
function isNonNegativeInteger$
|
|
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$
|
|
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$
|
|
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$
|
|
6543
|
+
return value === void 0 || isFiniteNumber$2(value);
|
|
6520
6544
|
}
|
|
6521
6545
|
function isRepoRef$1(value) {
|
|
6522
|
-
const repo = asRecord$
|
|
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$
|
|
6531
|
-
return steer !== void 0 && isOptionalString$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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")
|
|
7283
|
-
kind: "
|
|
7284
|
-
|
|
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
|
-
|
|
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$
|
|
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$
|
|
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 (
|
|
7740
|
-
applied.push(`deferred build dispatch for ${unit.missionId}:${unitHandle(unit)}:
|
|
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
|
|
7853
|
-
|
|
7854
|
-
|
|
7855
|
-
|
|
7856
|
-
|
|
7857
|
-
|
|
7858
|
-
|
|
7859
|
-
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
8513
|
-
return units.
|
|
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" &&
|
|
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
|
-
|
|
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
|
-
|
|
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))
|
|
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:
|
|
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
|
|
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
|
-
|
|
9443
|
-
|
|
9444
|
-
|
|
9445
|
-
|
|
9446
|
-
|
|
9447
|
-
|
|
9448
|
-
|
|
9449
|
-
|
|
9450
|
-
|
|
9451
|
-
|
|
9452
|
-
|
|
9453
|
-
|
|
9454
|
-
|
|
9455
|
-
|
|
9456
|
-
|
|
9457
|
-
|
|
9458
|
-
|
|
9459
|
-
|
|
9460
|
-
|
|
9461
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
9591
|
-
|
|
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
|
|
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
|
-
|
|
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("; ") ||
|
|
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("
|
|
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 $,
|
|
32034
|
-
//# sourceMappingURL=peer-mcp-personas-
|
|
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
|