@mrrlin-dev/mcp 0.3.64 → 0.3.68
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/bin.cjs +1051 -605
- package/package.json +5 -5
package/dist/bin.cjs
CHANGED
|
@@ -20658,14 +20658,17 @@ var toml = __toESM(require_toml(), 1);
|
|
|
20658
20658
|
|
|
20659
20659
|
// src/api-base-url.ts
|
|
20660
20660
|
var DEFAULT_API_BASE_URL = "http://127.0.0.1:8787";
|
|
20661
|
-
var STAGING_API_BASE_URL = "https://
|
|
20661
|
+
var STAGING_API_BASE_URL = "https://api-staging.mrrlin.com";
|
|
20662
20662
|
function normalizeUrl(value) {
|
|
20663
20663
|
return value.replace(/\/$/, "");
|
|
20664
20664
|
}
|
|
20665
20665
|
var RETIRED_API_BASE_URLS = {
|
|
20666
20666
|
// Retired 2026-07-28 when staging moved to the mrrlin2 Vercel team; serves a decommissioned
|
|
20667
20667
|
// Supabase project and now answers `GET /health` with 402 DEPLOYMENT_DISABLED. Added in 0.3.50.
|
|
20668
|
-
"mrrlin-api-fnnzzz-fnnzzzs-projects.vercel.app": STAGING_API_BASE_URL
|
|
20668
|
+
"mrrlin-api-fnnzzz-fnnzzzs-projects.vercel.app": STAGING_API_BASE_URL,
|
|
20669
|
+
// Retired 2026-08-14 when staging decoupled onto the stable custom domain api-staging.mrrlin.com.
|
|
20670
|
+
// A persisted operator configuration may still pin the mrrlin3 Vercel alias; retarget it in one hop.
|
|
20671
|
+
"mrrlin-api-mrrlin3.vercel.app": STAGING_API_BASE_URL
|
|
20669
20672
|
};
|
|
20670
20673
|
function isSameApiHost(a, b) {
|
|
20671
20674
|
const origin = (value) => {
|
|
@@ -36387,6 +36390,9 @@ var openApiOperationIds = {
|
|
|
36387
36390
|
getExecutionRun: "getExecutionRun",
|
|
36388
36391
|
getProjectSchedulerPolicy: "getProjectSchedulerPolicy",
|
|
36389
36392
|
getProjectDeployWorkflow: "getProjectDeployWorkflow",
|
|
36393
|
+
getDeployTopology: "getDeployTopology",
|
|
36394
|
+
writeDeployTopology: "writeDeployTopology",
|
|
36395
|
+
getTaskDeployTarget: "getTaskDeployTarget",
|
|
36390
36396
|
getSchedulerPendingTasks: "getSchedulerPendingTasks",
|
|
36391
36397
|
wakeDirector: "wakeDirector",
|
|
36392
36398
|
getGithubInstallationRepositories: "getGithubInstallationRepositories",
|
|
@@ -36742,6 +36748,16 @@ var mrrlinTaskEventAutonomyLevelSchema = external_exports.enum([
|
|
|
36742
36748
|
]);
|
|
36743
36749
|
var mrrlinAutoDeploySchema = external_exports.enum(["off", "dev", "prod"]);
|
|
36744
36750
|
var mrrlinTaskDomainSchema = external_exports.enum(["code", "seo", "marketing", "content"]);
|
|
36751
|
+
function normalizeTaskDomain(value) {
|
|
36752
|
+
if (typeof value !== "string")
|
|
36753
|
+
return "code";
|
|
36754
|
+
const v = value.trim().toLowerCase();
|
|
36755
|
+
return mrrlinTaskDomainSchema.options.includes(v) ? v : "code";
|
|
36756
|
+
}
|
|
36757
|
+
function domainScopedReviewHash(rawHash, domain2) {
|
|
36758
|
+
const d = normalizeTaskDomain(domain2);
|
|
36759
|
+
return d === "code" ? rawHash : `${rawHash}@${d}`;
|
|
36760
|
+
}
|
|
36745
36761
|
var mrrlinSpecApprovedSchema = external_exports.enum(["pending", "approved", "skipped", "skipped_legacy"]);
|
|
36746
36762
|
var mrrlinTaskSourceSchema = external_exports.enum(["fixture-import", "api"]);
|
|
36747
36763
|
var mrrlinPlanStatusSchema = external_exports.enum(["active", "paused", "done", "archived"]);
|
|
@@ -37123,6 +37139,47 @@ function refineTaskCreateDomain(data, ctx) {
|
|
|
37123
37139
|
}
|
|
37124
37140
|
var mrrlinTaskCreateSchema = mrrlinTaskCreateShapeSchema.superRefine(refineTaskCreateDomain);
|
|
37125
37141
|
var mrrlinTaskCreateWithoutIdSchema = mrrlinTaskCreateShapeSchema.omit({ id: true }).superRefine(refineTaskCreateDomain);
|
|
37142
|
+
var mrrlinDeployWorkflowEnvSchema = external_exports.enum(["dev", "prod"]);
|
|
37143
|
+
var mrrlinTaskDeployTargetInputSchema = external_exports.object({
|
|
37144
|
+
env: mrrlinDeployWorkflowEnvSchema,
|
|
37145
|
+
targetLabel: external_exports.string().min(1),
|
|
37146
|
+
workflowId: external_exports.number().int().positive(),
|
|
37147
|
+
workflowPath: external_exports.string().min(1),
|
|
37148
|
+
workflowSha: external_exports.string().min(1),
|
|
37149
|
+
topologyFingerprint: external_exports.string().min(1)
|
|
37150
|
+
});
|
|
37151
|
+
var deployTopologyResponseSchema = external_exports.object({
|
|
37152
|
+
data: external_exports.object({
|
|
37153
|
+
fingerprint: external_exports.string().nullable(),
|
|
37154
|
+
snapshot: external_exports.array(external_exports.object({ id: external_exports.number(), path: external_exports.string(), sha: external_exports.string(), state: external_exports.string() })).nullable(),
|
|
37155
|
+
entries: external_exports.array(external_exports.object({
|
|
37156
|
+
label: external_exports.string(),
|
|
37157
|
+
env: mrrlinDeployWorkflowEnvSchema,
|
|
37158
|
+
workflowId: external_exports.number(),
|
|
37159
|
+
workflowPath: external_exports.string(),
|
|
37160
|
+
workflowSha: external_exports.string()
|
|
37161
|
+
}))
|
|
37162
|
+
})
|
|
37163
|
+
});
|
|
37164
|
+
var mrrlinDeployTopologyWriteSchema = external_exports.object({
|
|
37165
|
+
fingerprint: external_exports.string().min(1),
|
|
37166
|
+
entries: external_exports.array(external_exports.object({
|
|
37167
|
+
label: external_exports.string().min(1),
|
|
37168
|
+
env: mrrlinDeployWorkflowEnvSchema,
|
|
37169
|
+
workflowId: external_exports.number().int().positive(),
|
|
37170
|
+
workflowPath: external_exports.string().min(1),
|
|
37171
|
+
workflowSha: external_exports.string().min(1)
|
|
37172
|
+
}))
|
|
37173
|
+
});
|
|
37174
|
+
var taskDeployTargetResponseSchema = external_exports.object({
|
|
37175
|
+
data: external_exports.object({
|
|
37176
|
+
targetLabel: external_exports.string(),
|
|
37177
|
+
workflowId: external_exports.number(),
|
|
37178
|
+
workflowPath: external_exports.string(),
|
|
37179
|
+
workflowSha: external_exports.string(),
|
|
37180
|
+
topologyFingerprint: external_exports.string()
|
|
37181
|
+
})
|
|
37182
|
+
});
|
|
37126
37183
|
var mrrlinTaskUpdateSchema = external_exports.object({
|
|
37127
37184
|
actor: external_exports.string().min(1).optional(),
|
|
37128
37185
|
assignee: mrrlinTaskAssigneeSchema.optional(),
|
|
@@ -37145,8 +37202,13 @@ var mrrlinTaskUpdateSchema = external_exports.object({
|
|
|
37145
37202
|
title: external_exports.string().min(1).optional(),
|
|
37146
37203
|
type: mrrlinTaskTypeSchema.optional(),
|
|
37147
37204
|
followUpAt: external_exports.string().datetime().nullable().optional(),
|
|
37148
|
-
verificationDueAt: external_exports.string().datetime().nullable().optional()
|
|
37149
|
-
|
|
37205
|
+
verificationDueAt: external_exports.string().datetime().nullable().optional(),
|
|
37206
|
+
deployTargets: external_exports.array(mrrlinTaskDeployTargetInputSchema).optional(),
|
|
37207
|
+
// design.md D5 step 6 hint (task 3.16): the deploy gate's own bookkeeping write when the
|
|
37208
|
+
// fingerprint moved but the topology still maps the label to the same workflow. Never sent
|
|
37209
|
+
// by any other caller.
|
|
37210
|
+
reverifyRequested: external_exports.boolean().optional()
|
|
37211
|
+
}).refine((value) => value.assignee !== void 0 || value.autoDeploy !== void 0 || value.domain !== void 0 || value.autonomyLevel !== void 0 || value.autonomyReason !== void 0 || value.category !== void 0 || value.directorThreadId !== void 0 || value.dueDate !== void 0 || value.followUpAt !== void 0 || value.impact !== void 0 || value.notes !== void 0 || value.planId !== void 0 || value.specApproved !== void 0 || value.specWikiPageId !== void 0 || value.status !== void 0 || value.subcategory !== void 0 || value.tags !== void 0 || value.title !== void 0 || value.type !== void 0 || value.verificationDueAt !== void 0 || value.deployTargets !== void 0 || value.reverifyRequested !== void 0, { message: "TaskUpdate requires at least one field" }).superRefine((value, context) => {
|
|
37150
37212
|
if (value.autonomyLevel !== void 0 && value.autonomyReason === void 0) {
|
|
37151
37213
|
context.addIssue({
|
|
37152
37214
|
code: external_exports.ZodIssueCode.custom,
|
|
@@ -37230,6 +37292,7 @@ var mrrlinWikiPageUpdateSchema = external_exports.object({
|
|
|
37230
37292
|
}).refine((value) => Object.keys(value).length > 0, { message: "WikiPageUpdate requires at least one field" });
|
|
37231
37293
|
var mrrlinWikiPageListFiltersSchema = external_exports.object({
|
|
37232
37294
|
folder: external_exports.string().min(1).optional(),
|
|
37295
|
+
id: mrrlinWikiPageIdSchema.optional(),
|
|
37233
37296
|
q: external_exports.string().min(1).optional(),
|
|
37234
37297
|
taskId: mrrlinTaskIdSchema.optional()
|
|
37235
37298
|
});
|
|
@@ -37806,7 +37869,6 @@ var mrrlinProjectSchedulerPolicyUpsertSchema = external_exports.object({
|
|
|
37806
37869
|
var projectSchedulerPolicyResponseSchema = external_exports.object({
|
|
37807
37870
|
data: mrrlinProjectSchedulerPolicySchema
|
|
37808
37871
|
});
|
|
37809
|
-
var mrrlinDeployWorkflowEnvSchema = external_exports.enum(["dev", "prod"]);
|
|
37810
37872
|
var mrrlinDeployWorkflowSourceSchema = external_exports.enum(["agent", "operator"]);
|
|
37811
37873
|
var mrrlinDeployWorkflowStateSchema = external_exports.enum(["resolved", "ambiguous"]);
|
|
37812
37874
|
var mrrlinProjectDeployWorkflowSchema = external_exports.object({
|
|
@@ -39291,6 +39353,29 @@ function createMrrlinClient(config2) {
|
|
|
39291
39353
|
throw error51;
|
|
39292
39354
|
}
|
|
39293
39355
|
},
|
|
39356
|
+
async getDeployTopology(projectSlug) {
|
|
39357
|
+
const body = await request(`${projectPath(projectSlug)}/deploy-topology`);
|
|
39358
|
+
return deployTopologyResponseSchema.parse(body).data;
|
|
39359
|
+
},
|
|
39360
|
+
async writeDeployTopology(projectSlug, input) {
|
|
39361
|
+
const body = await request(`${projectPath(projectSlug)}/deploy-topology`, {
|
|
39362
|
+
body: JSON.stringify(mrrlinDeployTopologyWriteSchema.parse(input)),
|
|
39363
|
+
headers: { "content-type": "application/json" },
|
|
39364
|
+
method: "PUT"
|
|
39365
|
+
});
|
|
39366
|
+
return deployTopologyResponseSchema.parse(body).data;
|
|
39367
|
+
},
|
|
39368
|
+
async getTaskDeployTarget(projectSlug, taskId, env) {
|
|
39369
|
+
try {
|
|
39370
|
+
const body = await request(`${projectPath(projectSlug)}/tasks/${taskId}/deploy-target/${env}`);
|
|
39371
|
+
return taskDeployTargetResponseSchema.parse(body).data;
|
|
39372
|
+
} catch (error51) {
|
|
39373
|
+
if (error51 instanceof MrrlinApiError && error51.status === 404 && error51.code === "DEPLOY_TARGET_UNSET") {
|
|
39374
|
+
return null;
|
|
39375
|
+
}
|
|
39376
|
+
throw error51;
|
|
39377
|
+
}
|
|
39378
|
+
},
|
|
39294
39379
|
async upsertProjectDeployWorkflow(projectSlug, env, input) {
|
|
39295
39380
|
const body = await request(`${projectPath(projectSlug)}/deploy-workflows/${env}`, {
|
|
39296
39381
|
body: JSON.stringify(input),
|
|
@@ -39965,7 +40050,7 @@ function startCodexProbe(deps = {}) {
|
|
|
39965
40050
|
}
|
|
39966
40051
|
|
|
39967
40052
|
// src/_generated/version.ts
|
|
39968
|
-
var PKG_VERSION = "0.3.
|
|
40053
|
+
var PKG_VERSION = "0.3.68";
|
|
39969
40054
|
|
|
39970
40055
|
// src/api-base-url-migrate.ts
|
|
39971
40056
|
var import_node_fs2 = __toESM(require("node:fs"), 1);
|
|
@@ -40936,41 +41021,303 @@ function errMessage(err) {
|
|
|
40936
41021
|
return err instanceof Error ? err.message : String(err);
|
|
40937
41022
|
}
|
|
40938
41023
|
|
|
40939
|
-
// src/consensus/
|
|
40940
|
-
|
|
40941
|
-
|
|
40942
|
-
|
|
40943
|
-
|
|
40944
|
-
|
|
40945
|
-
|
|
40946
|
-
|
|
40947
|
-
|
|
41024
|
+
// src/consensus/verdict.ts
|
|
41025
|
+
var CATEGORIES = [
|
|
41026
|
+
"correctness",
|
|
41027
|
+
"security",
|
|
41028
|
+
"scope",
|
|
41029
|
+
"ambiguity",
|
|
41030
|
+
"performance",
|
|
41031
|
+
"ops"
|
|
41032
|
+
];
|
|
41033
|
+
var CAT_SET = new Set(CATEGORIES);
|
|
41034
|
+
function parseVerdict(raw) {
|
|
41035
|
+
const verdictMatch = raw.match(/\*\*Verdict\*\*:\s*(APPROVE|REQUEST_CHANGES|REJECT)\b/i);
|
|
41036
|
+
const parsedVerdict = verdictMatch?.[1]?.toUpperCase();
|
|
41037
|
+
const verdict = parsedVerdict === "APPROVE" || parsedVerdict === "REQUEST_CHANGES" || parsedVerdict === "REJECT" ? parsedVerdict : "REQUEST_CHANGES";
|
|
41038
|
+
const afterCritical = raw.split(/\*\*Critical issues\*\*/i)[1];
|
|
41039
|
+
const block = afterCritical?.split(/\n\*\*/)[0] ?? "";
|
|
41040
|
+
const issues = [];
|
|
41041
|
+
const parseFallbacks = [];
|
|
41042
|
+
for (const line of block.split(/\r?\n/)) {
|
|
41043
|
+
const bulletMatch = line.match(/^\s*-\s+(.*\S)\s*$/);
|
|
41044
|
+
if (!bulletMatch) continue;
|
|
41045
|
+
const body = bulletMatch[1] ?? "";
|
|
41046
|
+
if (/^\(?(empty list|none)\b/i.test(body)) continue;
|
|
41047
|
+
if (!body) continue;
|
|
41048
|
+
const catMatch = body.match(/`?\[(\w+)\]`?/);
|
|
41049
|
+
const catToken = catMatch?.[1]?.toLowerCase() ?? "";
|
|
41050
|
+
const isKnownCat = catToken !== "" && CAT_SET.has(catToken);
|
|
41051
|
+
const category = isKnownCat ? catToken : "ambiguity";
|
|
41052
|
+
if (!catMatch || !isKnownCat) {
|
|
41053
|
+
parseFallbacks.push(body);
|
|
40948
41054
|
}
|
|
40949
|
-
|
|
41055
|
+
const text = body.replace(/`?\[\w+\]`?\s*/, "").trim();
|
|
41056
|
+
issues.push({ category, text });
|
|
40950
41057
|
}
|
|
40951
|
-
return
|
|
41058
|
+
return { verdict, issues, parseFallbacks };
|
|
41059
|
+
}
|
|
41060
|
+
function parseArbiterDecisions(raw) {
|
|
41061
|
+
const decisions = [];
|
|
41062
|
+
const afterDecisions = raw.split(/\*\*Decisions\*\*/i)[1];
|
|
41063
|
+
const block = afterDecisions?.split(/\n\*\*Adjudicated/i)[0] ?? "";
|
|
41064
|
+
for (const line of block.split(/\r?\n/)) {
|
|
41065
|
+
const m = line.match(
|
|
41066
|
+
/^\s*-\s+`?\[([^\]]+)\]`?\s+(accept|dismiss|defer)\b(?:\s*[—-]\s*(.*\S))?\s*$/i
|
|
41067
|
+
);
|
|
41068
|
+
if (!m) continue;
|
|
41069
|
+
const id = m[1] ?? "";
|
|
41070
|
+
const decision = (m[2] ?? "accept").toLowerCase();
|
|
41071
|
+
const reason = (m[3] ?? "").trim();
|
|
41072
|
+
decisions.push({ id, decision, reason });
|
|
41073
|
+
}
|
|
41074
|
+
return { decisions };
|
|
40952
41075
|
}
|
|
40953
41076
|
|
|
40954
|
-
// src/consensus/
|
|
40955
|
-
function
|
|
40956
|
-
const
|
|
40957
|
-
return
|
|
41077
|
+
// src/consensus/arbiter.ts
|
|
41078
|
+
function buildArbiterPrompt(issues) {
|
|
41079
|
+
const list = issues.map((i) => `- [${i.id}] (${i.category}) ${i.text}`).join("\n");
|
|
41080
|
+
return `Adjudicate each issue below.
|
|
41081
|
+
|
|
41082
|
+
ISSUES:
|
|
41083
|
+
${list}`;
|
|
40958
41084
|
}
|
|
40959
|
-
async function
|
|
40960
|
-
|
|
40961
|
-
|
|
40962
|
-
|
|
40963
|
-
|
|
40964
|
-
|
|
41085
|
+
async function adjudicate(issues, deps) {
|
|
41086
|
+
if (issues.length === 0) return { adjudicatedVerdict: "APPROVE", accepted: [], dismissed: [] };
|
|
41087
|
+
const byId = new Map(issues.map((i) => [i.id, i]));
|
|
41088
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
41089
|
+
const res = await deps.runArbiter(buildArbiterPrompt(issues));
|
|
41090
|
+
if (!res.ok) continue;
|
|
41091
|
+
const { decisions } = parseArbiterDecisions(res.text);
|
|
41092
|
+
const seen = new Set(decisions.map((d) => d.id));
|
|
41093
|
+
const everyDecided = issues.every((i) => seen.has(i.id));
|
|
41094
|
+
const missingReason = decisions.some((d) => d.decision !== "accept" && !d.reason);
|
|
41095
|
+
if (!everyDecided || missingReason && attempt === 0) continue;
|
|
41096
|
+
const accepted = [];
|
|
41097
|
+
const dismissed = [];
|
|
41098
|
+
for (const i of issues) {
|
|
41099
|
+
const d = decisions.find((x) => x.id === i.id);
|
|
41100
|
+
if (!d || d.decision !== "accept" && !d.reason) {
|
|
41101
|
+
accepted.push(i);
|
|
41102
|
+
continue;
|
|
41103
|
+
}
|
|
41104
|
+
if (d.decision === "accept") accepted.push(i);
|
|
41105
|
+
else dismissed.push({ id: i.id, decision: d.decision, reason: d.reason });
|
|
40965
41106
|
}
|
|
40966
|
-
|
|
40967
|
-
const plan = plans.find((p) => p.specWikiPageId === page.id);
|
|
40968
|
-
if (plan?.domain != null) return normalizeDomain(plan.domain);
|
|
40969
|
-
const project = await client.getProject(projectSlug);
|
|
40970
|
-
return normalizeDomain(project?.defaultDomain);
|
|
40971
|
-
} catch {
|
|
40972
|
-
return "code";
|
|
41107
|
+
return { adjudicatedVerdict: accepted.length === 0 ? "APPROVE" : "REQUEST_CHANGES", accepted, dismissed };
|
|
40973
41108
|
}
|
|
41109
|
+
return { adjudicatedVerdict: "REQUEST_CHANGES", accepted: [...byId.values()], dismissed: [] };
|
|
41110
|
+
}
|
|
41111
|
+
|
|
41112
|
+
// src/consensus/semaphore.ts
|
|
41113
|
+
function createBoundedSemaphore(slots) {
|
|
41114
|
+
let available = slots;
|
|
41115
|
+
const queue = [];
|
|
41116
|
+
return {
|
|
41117
|
+
acquire: () => available > 0 ? (available -= 1, Promise.resolve()) : new Promise((resolve) => queue.push(resolve)),
|
|
41118
|
+
release: () => {
|
|
41119
|
+
const next = queue.shift();
|
|
41120
|
+
if (next) next();
|
|
41121
|
+
else available += 1;
|
|
41122
|
+
}
|
|
41123
|
+
};
|
|
41124
|
+
}
|
|
41125
|
+
|
|
41126
|
+
// src/consensus/escalation.ts
|
|
41127
|
+
function diagnoseStall(history, configuredRounds) {
|
|
41128
|
+
const n = history.length;
|
|
41129
|
+
const nonRespPerRound = history.map((h) => h.headOutcomes.filter((o) => !o.responded).length);
|
|
41130
|
+
const infraRoundCount = nonRespPerRound.filter((c) => c >= 2).length;
|
|
41131
|
+
const infraQualifies = infraRoundCount > n / 2;
|
|
41132
|
+
if (infraQualifies) {
|
|
41133
|
+
const roundIndices = [];
|
|
41134
|
+
nonRespPerRound.forEach((c, i) => {
|
|
41135
|
+
if (c >= 2) roundIndices.push(i + 1);
|
|
41136
|
+
});
|
|
41137
|
+
const evidence2 = `INFRA basis: \u22652 non\u2011responding heads in ${infraRoundCount} of ${n} rounds (rounds ${roundIndices.join(",")})`;
|
|
41138
|
+
let grant = 3;
|
|
41139
|
+
const headroom = 2 * configuredRounds - n;
|
|
41140
|
+
grant = Math.max(1, Math.min(grant, headroom));
|
|
41141
|
+
return { diagnosis: "INFRA", grantedRounds: grant, evidence: evidence2 };
|
|
41142
|
+
}
|
|
41143
|
+
if (n >= 2) {
|
|
41144
|
+
const counts = history.map((h) => h.accepted.length);
|
|
41145
|
+
const last = counts[n - 1];
|
|
41146
|
+
const secondLast = counts[n - 2];
|
|
41147
|
+
const lastFall = last < secondLast;
|
|
41148
|
+
if (lastFall) {
|
|
41149
|
+
let falls = 1;
|
|
41150
|
+
if (n >= 3 && secondLast < counts[n - 3]) {
|
|
41151
|
+
falls++;
|
|
41152
|
+
}
|
|
41153
|
+
let grant = 2 + falls;
|
|
41154
|
+
if (grant > 5) grant = 5;
|
|
41155
|
+
const headroom = 2 * configuredRounds - n;
|
|
41156
|
+
grant = Math.max(1, Math.min(grant, headroom));
|
|
41157
|
+
const seqDesc = counts.map((c, i) => `round${i + 1}:${c}`).join(" \u2192 ");
|
|
41158
|
+
const evidence2 = `SUBSTANCE basis: accepted count fell (${seqDesc})`;
|
|
41159
|
+
return { diagnosis: "SUBSTANCE", grantedRounds: grant, evidence: evidence2 };
|
|
41160
|
+
}
|
|
41161
|
+
}
|
|
41162
|
+
const evidence = n === 0 ? "empty history" : `no infra or substance basis found in ${n} rounds`;
|
|
41163
|
+
return { diagnosis: "AMBIGUITY", grantedRounds: 0, evidence };
|
|
41164
|
+
}
|
|
41165
|
+
|
|
41166
|
+
// src/consensus/loop.ts
|
|
41167
|
+
async function runConsensus(input) {
|
|
41168
|
+
const sem = createBoundedSemaphore(input.concurrency ?? input.heads.length);
|
|
41169
|
+
const history = [];
|
|
41170
|
+
let artifact = input.artifact;
|
|
41171
|
+
let calls = 0;
|
|
41172
|
+
const dismissedFwd = [];
|
|
41173
|
+
let effectiveRounds = input.rounds;
|
|
41174
|
+
let escalated = false;
|
|
41175
|
+
let decision;
|
|
41176
|
+
for (let round = 1; round <= effectiveRounds; round++) {
|
|
41177
|
+
if (calls + input.heads.length > input.maxHeadCalls) {
|
|
41178
|
+
return { outcome: "CONSENSUS_BLOCKED", rounds: round - 1, finalArtifact: artifact, history, ...decision ? { escalation: decision } : {} };
|
|
41179
|
+
}
|
|
41180
|
+
const dismissedBlock = dismissedFwd.length ? `PREVIOUSLY DISMISSED (do not re-raise unless new information):
|
|
41181
|
+
${dismissedFwd.slice(-20).join("\n")}` : "";
|
|
41182
|
+
const results = await Promise.all(
|
|
41183
|
+
input.heads.map(async (persona, idx) => {
|
|
41184
|
+
await sem.acquire();
|
|
41185
|
+
try {
|
|
41186
|
+
calls++;
|
|
41187
|
+
const res = await input.runHead(persona, artifact, dismissedBlock);
|
|
41188
|
+
return { idx, res };
|
|
41189
|
+
} catch (e) {
|
|
41190
|
+
return { idx, res: { ok: false, error: String(e) } };
|
|
41191
|
+
} finally {
|
|
41192
|
+
sem.release();
|
|
41193
|
+
}
|
|
41194
|
+
})
|
|
41195
|
+
);
|
|
41196
|
+
const responding = results.filter(
|
|
41197
|
+
(r) => r.res.ok
|
|
41198
|
+
);
|
|
41199
|
+
if (responding.length === 0) {
|
|
41200
|
+
return { outcome: "CONSENSUS_UNAVAILABLE", rounds: round, finalArtifact: artifact, history };
|
|
41201
|
+
}
|
|
41202
|
+
const headOutcomes = input.heads.map((persona) => ({
|
|
41203
|
+
persona,
|
|
41204
|
+
responded: false
|
|
41205
|
+
}));
|
|
41206
|
+
const issues = [];
|
|
41207
|
+
for (const { idx, res } of responding) {
|
|
41208
|
+
const parsed = parseVerdict(res.text);
|
|
41209
|
+
parsed.issues.forEach((iss, n) => issues.push({ ...iss, id: `r${round}-h${idx}-${n}` }));
|
|
41210
|
+
const outcome = headOutcomes[idx];
|
|
41211
|
+
if (outcome) {
|
|
41212
|
+
outcome.responded = true;
|
|
41213
|
+
outcome.verdict = parsed.verdict;
|
|
41214
|
+
}
|
|
41215
|
+
if (parsed.verdict !== "APPROVE" && parsed.issues.length === 0 && parsed.parseFallbacks.length === 0) {
|
|
41216
|
+
issues.push({
|
|
41217
|
+
category: "ambiguity",
|
|
41218
|
+
text: `head ${input.heads[idx] ?? idx}/${idx} returned ${parsed.verdict} but no parseable critical issues (possible malformed/blocked review)`,
|
|
41219
|
+
id: `r${round}-h${idx}-0`
|
|
41220
|
+
});
|
|
41221
|
+
}
|
|
41222
|
+
}
|
|
41223
|
+
let adj;
|
|
41224
|
+
if (issues.length > 0) {
|
|
41225
|
+
if (calls + 1 > input.maxHeadCalls) {
|
|
41226
|
+
return { outcome: "CONSENSUS_BLOCKED", rounds: round, finalArtifact: artifact, history, ...decision ? { escalation: decision } : {} };
|
|
41227
|
+
}
|
|
41228
|
+
calls++;
|
|
41229
|
+
try {
|
|
41230
|
+
adj = await adjudicate(issues, { runArbiter: input.runArbiter });
|
|
41231
|
+
} catch {
|
|
41232
|
+
adj = { adjudicatedVerdict: "REQUEST_CHANGES", accepted: issues, dismissed: [] };
|
|
41233
|
+
}
|
|
41234
|
+
} else {
|
|
41235
|
+
adj = { adjudicatedVerdict: "APPROVE", accepted: [], dismissed: [] };
|
|
41236
|
+
}
|
|
41237
|
+
history.push({ round, accepted: adj.accepted, dismissed: adj.dismissed, headOutcomes });
|
|
41238
|
+
adj.dismissed.forEach((d) => dismissedFwd.push(`- ${d.id}: ${d.reason}`));
|
|
41239
|
+
if (adj.accepted.length === 0) {
|
|
41240
|
+
return { outcome: "CONVERGED", rounds: round, finalArtifact: artifact, history, ...decision ? { escalation: decision } : {} };
|
|
41241
|
+
}
|
|
41242
|
+
if (round === effectiveRounds) {
|
|
41243
|
+
if (!decision) {
|
|
41244
|
+
decision = diagnoseStall(history, input.rounds);
|
|
41245
|
+
}
|
|
41246
|
+
if (decision.grantedRounds > 0 && !escalated) {
|
|
41247
|
+
escalated = true;
|
|
41248
|
+
effectiveRounds = Math.min(effectiveRounds + decision.grantedRounds, input.rounds * 2);
|
|
41249
|
+
} else {
|
|
41250
|
+
return { outcome: "CONSENSUS_BLOCKED", rounds: round, finalArtifact: artifact, history, ...decision ? { escalation: decision } : {} };
|
|
41251
|
+
}
|
|
41252
|
+
}
|
|
41253
|
+
artifact = await input.revise(artifact, adj.accepted);
|
|
41254
|
+
}
|
|
41255
|
+
return { outcome: "CONSENSUS_BLOCKED", rounds: effectiveRounds, finalArtifact: artifact, history };
|
|
41256
|
+
}
|
|
41257
|
+
|
|
41258
|
+
// src/consensus/spec-identity.ts
|
|
41259
|
+
function validateSpecIdentity(markdown, taskId) {
|
|
41260
|
+
const target = taskId.toUpperCase();
|
|
41261
|
+
const fieldToken = frontmatterTaskToken(markdown);
|
|
41262
|
+
if (fieldToken !== null && fieldToken.toUpperCase() !== target) return false;
|
|
41263
|
+
const headingToken = firstHeadingToken(markdown);
|
|
41264
|
+
if (headingToken !== null && headingToken.toUpperCase() !== target) return false;
|
|
41265
|
+
return true;
|
|
41266
|
+
}
|
|
41267
|
+
var TASK_ID_TOKEN = /\b[A-Za-z]{1,12}-\d{1,10}\b/;
|
|
41268
|
+
var ATX_HEADING = /^ {0,3}#{1,6}\s+(.*)$/;
|
|
41269
|
+
var FRONTMATTER_TASK = /^task(?:_id)?:\s*(.+)$/i;
|
|
41270
|
+
function frontmatterTaskToken(markdown) {
|
|
41271
|
+
const lines2 = markdown.split("\n");
|
|
41272
|
+
const firstContent = lines2.findIndex((l) => l.trim() !== "");
|
|
41273
|
+
if (firstContent === -1 || lines2[firstContent]?.trim() !== "---") return null;
|
|
41274
|
+
for (let i = firstContent + 1; i < lines2.length; i++) {
|
|
41275
|
+
const trimmed = (lines2[i] ?? "").trim();
|
|
41276
|
+
if (trimmed === "---") break;
|
|
41277
|
+
const m = FRONTMATTER_TASK.exec(trimmed);
|
|
41278
|
+
if (m) return firstTaskIdToken(m[1] ?? "");
|
|
41279
|
+
}
|
|
41280
|
+
return null;
|
|
41281
|
+
}
|
|
41282
|
+
function firstHeadingToken(markdown) {
|
|
41283
|
+
const heading = firstHeading(markdown);
|
|
41284
|
+
return heading === null ? null : firstTaskIdToken(heading);
|
|
41285
|
+
}
|
|
41286
|
+
function firstHeading(markdown) {
|
|
41287
|
+
const lines2 = markdown.split("\n");
|
|
41288
|
+
let i = 0;
|
|
41289
|
+
const firstContent = lines2.findIndex((l) => l.trim() !== "");
|
|
41290
|
+
if (firstContent !== -1 && lines2[firstContent]?.trim() === "---") {
|
|
41291
|
+
i = firstContent + 1;
|
|
41292
|
+
while (i < lines2.length && lines2[i]?.trim() !== "---") i++;
|
|
41293
|
+
i++;
|
|
41294
|
+
}
|
|
41295
|
+
let inFence = false;
|
|
41296
|
+
let inComment = false;
|
|
41297
|
+
for (; i < lines2.length; i++) {
|
|
41298
|
+
const line = lines2[i] ?? "";
|
|
41299
|
+
const trimmed = line.trim();
|
|
41300
|
+
if (inComment) {
|
|
41301
|
+
if (trimmed.includes("-->")) inComment = false;
|
|
41302
|
+
continue;
|
|
41303
|
+
}
|
|
41304
|
+
if (trimmed.startsWith("```") || trimmed.startsWith("~~~")) {
|
|
41305
|
+
inFence = !inFence;
|
|
41306
|
+
continue;
|
|
41307
|
+
}
|
|
41308
|
+
if (inFence) continue;
|
|
41309
|
+
if (trimmed.startsWith("<!--")) {
|
|
41310
|
+
if (!trimmed.includes("-->")) inComment = true;
|
|
41311
|
+
continue;
|
|
41312
|
+
}
|
|
41313
|
+
const m = ATX_HEADING.exec(line);
|
|
41314
|
+
if (m) return m[1] ?? "";
|
|
41315
|
+
}
|
|
41316
|
+
return null;
|
|
41317
|
+
}
|
|
41318
|
+
function firstTaskIdToken(text) {
|
|
41319
|
+
const m = TASK_ID_TOKEN.exec(text);
|
|
41320
|
+
return m ? m[0] : null;
|
|
40974
41321
|
}
|
|
40975
41322
|
|
|
40976
41323
|
// src/consensus/acceptance-criteria.ts
|
|
@@ -41003,6 +41350,249 @@ function parseAcceptanceCriteria(markdown) {
|
|
|
41003
41350
|
return items.length > 0 ? { items } : null;
|
|
41004
41351
|
}
|
|
41005
41352
|
|
|
41353
|
+
// src/consensus/spec-gate.ts
|
|
41354
|
+
var SEED_ABSENCE_QUESTION = "What are the requirements for this task? There is no operator request on record, so the spec cannot be validated against your intent \u2014 please state what this task must do.";
|
|
41355
|
+
var SEED_ABSENCE_MARKER = "There is no operator request on record";
|
|
41356
|
+
function mapAcceptedIssueToClarification(issue2) {
|
|
41357
|
+
const text = issue2.text.toLowerCase();
|
|
41358
|
+
let kind;
|
|
41359
|
+
if (issue2.category === "scope") {
|
|
41360
|
+
const directionCues = ["only", "or also", "narrower", "broader", "include", "exclude", "versus", "vs"];
|
|
41361
|
+
kind = directionCues.some((cue) => text.includes(cue)) ? "scope_direction" : "missing_requirement";
|
|
41362
|
+
} else {
|
|
41363
|
+
const acceptanceCues = ["acceptance", "done", "success", "criteria", "satisfy"];
|
|
41364
|
+
kind = acceptanceCues.some((cue) => text.includes(cue)) ? "acceptance_criteria_detail" : "default_choice";
|
|
41365
|
+
}
|
|
41366
|
+
return {
|
|
41367
|
+
kind,
|
|
41368
|
+
diagnostic: false,
|
|
41369
|
+
rawQuestion: issue2.text,
|
|
41370
|
+
whyBlocking: `reviewer raised a ${issue2.category} issue`,
|
|
41371
|
+
category: issue2.category
|
|
41372
|
+
};
|
|
41373
|
+
}
|
|
41374
|
+
var CLARIFICATION_CAP_BY_IMPACT = { low: 2, medium: 3, high: 5 };
|
|
41375
|
+
function resolveClarificationCap(impact) {
|
|
41376
|
+
const tier = impact?.trim().toLowerCase();
|
|
41377
|
+
if (tier === "medium") return CLARIFICATION_CAP_BY_IMPACT.medium;
|
|
41378
|
+
if (tier === "high") return CLARIFICATION_CAP_BY_IMPACT.high;
|
|
41379
|
+
return CLARIFICATION_CAP_BY_IMPACT.low;
|
|
41380
|
+
}
|
|
41381
|
+
function headIndexOf(id) {
|
|
41382
|
+
const match = /^r\d+-h(\d+)-\d+$/.exec(id);
|
|
41383
|
+
return match?.[1] !== void 0 ? Number(match[1]) : null;
|
|
41384
|
+
}
|
|
41385
|
+
function orderClarificationIssues(issues) {
|
|
41386
|
+
const distinctHeadsByKey = /* @__PURE__ */ new Map();
|
|
41387
|
+
for (const issue2 of issues) {
|
|
41388
|
+
const key = `${issue2.category} ${issue2.text}`;
|
|
41389
|
+
const heads = distinctHeadsByKey.get(key) ?? /* @__PURE__ */ new Set();
|
|
41390
|
+
const headIdx = headIndexOf(issue2.id);
|
|
41391
|
+
if (headIdx !== null) heads.add(headIdx);
|
|
41392
|
+
distinctHeadsByKey.set(key, heads);
|
|
41393
|
+
}
|
|
41394
|
+
const categoryRank = (category) => category === "scope" ? 0 : category === "ambiguity" ? 1 : 2;
|
|
41395
|
+
return issues.map((issue2, index) => ({ issue: issue2, index })).sort((a, b) => {
|
|
41396
|
+
const rankDiff = categoryRank(a.issue.category) - categoryRank(b.issue.category);
|
|
41397
|
+
if (rankDiff !== 0) return rankDiff;
|
|
41398
|
+
const headCountA = distinctHeadsByKey.get(`${a.issue.category} ${a.issue.text}`)?.size ?? 0;
|
|
41399
|
+
const headCountB = distinctHeadsByKey.get(`${b.issue.category} ${b.issue.text}`)?.size ?? 0;
|
|
41400
|
+
if (headCountA !== headCountB) return headCountB - headCountA;
|
|
41401
|
+
return a.index - b.index;
|
|
41402
|
+
}).map(({ issue: issue2 }) => issue2);
|
|
41403
|
+
}
|
|
41404
|
+
function applyClarificationCap(issues, impact) {
|
|
41405
|
+
const ordered = orderClarificationIssues(issues);
|
|
41406
|
+
const cap = resolveClarificationCap(impact);
|
|
41407
|
+
return {
|
|
41408
|
+
kept: ordered.slice(0, cap).map(mapAcceptedIssueToClarification),
|
|
41409
|
+
dropped: ordered.slice(cap).map(mapAcceptedIssueToClarification)
|
|
41410
|
+
};
|
|
41411
|
+
}
|
|
41412
|
+
async function runSpecGate(_target, deps, opts) {
|
|
41413
|
+
if (!await deps.consensusEnabled()) return { outcome: "SKIPPED" };
|
|
41414
|
+
const intent = await deps.readIntent();
|
|
41415
|
+
if (intent.taskId !== null && (intent.seed === null || intent.seed.trim() === "")) {
|
|
41416
|
+
return {
|
|
41417
|
+
outcome: "CONSENSUS_BLOCKED",
|
|
41418
|
+
operatorClarifications: [
|
|
41419
|
+
{
|
|
41420
|
+
kind: "missing_requirement",
|
|
41421
|
+
diagnostic: false,
|
|
41422
|
+
// Filed byte-for-byte from the shared constant so `loadIntent` can later recognise its
|
|
41423
|
+
// answered form and lift the answer into the seed (GT-1005). Extracting the constant must
|
|
41424
|
+
// not change the filed text; the wiring.test.ts marker-substring assertion locks that.
|
|
41425
|
+
rawQuestion: SEED_ABSENCE_QUESTION,
|
|
41426
|
+
whyBlocking: "operator_seed_request is empty",
|
|
41427
|
+
category: "scope"
|
|
41428
|
+
}
|
|
41429
|
+
]
|
|
41430
|
+
};
|
|
41431
|
+
}
|
|
41432
|
+
const baseHeads = resolveHeads(intent.domain, intent.impact);
|
|
41433
|
+
const extraHeads = opts?.extraHeads ?? [];
|
|
41434
|
+
const heads = [...baseHeads, ...extraHeads.filter((h) => !baseHeads.includes(h))];
|
|
41435
|
+
const spec = await deps.readSpec();
|
|
41436
|
+
let currentHash = spec.contentHash;
|
|
41437
|
+
let currentMarkdown = spec.markdownBody;
|
|
41438
|
+
let conflicted = false;
|
|
41439
|
+
if (parseAcceptanceCriteria(currentMarkdown) === null) {
|
|
41440
|
+
const bootstrapIssue = {
|
|
41441
|
+
id: "acceptance-criteria-bootstrap",
|
|
41442
|
+
category: "scope",
|
|
41443
|
+
text: "This spec has no `## Acceptance criteria` section. Add one, written as a markdown checklist (`- [ ] \u2026` items), derived from this spec's OWN requirements and the operator's stated intent. Each item must be concretely verifiable \u2014 a check the finished deliverable either passes or fails. Change NOTHING ELSE about the spec."
|
|
41444
|
+
};
|
|
41445
|
+
const revised = await deps.runReviser(currentMarkdown, [bootstrapIssue]);
|
|
41446
|
+
const bootstrapFailed = (why) => {
|
|
41447
|
+
process.stderr.write(`[spec-gate] acceptance-criteria bootstrap failed for ${spec.pageId}: ${why}
|
|
41448
|
+
`);
|
|
41449
|
+
return {
|
|
41450
|
+
outcome: "CONSENSUS_BLOCKED",
|
|
41451
|
+
diagnostics: [
|
|
41452
|
+
{
|
|
41453
|
+
kind: "acceptance_criteria_unauthorable",
|
|
41454
|
+
diagnostic: true,
|
|
41455
|
+
rawQuestion: "This spec has no machine-usable acceptance criteria and the reviser could not add them automatically. A human needs to write an `## Acceptance criteria` section as a markdown checklist (`- [ ] \u2026` items) describing exactly what the finished deliverable must satisfy \u2014 the review of the produced work is scored against it.",
|
|
41456
|
+
whyBlocking: "spec is missing a parseable `## Acceptance criteria` checklist and self-authoring failed",
|
|
41457
|
+
category: "scope"
|
|
41458
|
+
}
|
|
41459
|
+
]
|
|
41460
|
+
};
|
|
41461
|
+
};
|
|
41462
|
+
if (!revised.ok) return bootstrapFailed("reviser unavailable");
|
|
41463
|
+
if (intent.taskId !== null && !validateSpecIdentity(revised.text, intent.taskId)) {
|
|
41464
|
+
return bootstrapFailed(`revision heading does not identify ${intent.taskId}`);
|
|
41465
|
+
}
|
|
41466
|
+
if (parseAcceptanceCriteria(revised.text) === null) {
|
|
41467
|
+
return bootstrapFailed("revised spec still has no parseable `## Acceptance criteria` checklist");
|
|
41468
|
+
}
|
|
41469
|
+
const cas = await deps.casUpdateSpec(revised.text, currentHash);
|
|
41470
|
+
if (!cas.ok) {
|
|
41471
|
+
await deps.writeEvidence({ specPageId: spec.pageId, outcome: "SPEC_CHANGED_DURING_REVIEW" });
|
|
41472
|
+
return { outcome: "SPEC_CHANGED_DURING_REVIEW" };
|
|
41473
|
+
}
|
|
41474
|
+
currentHash = cas.contentHash;
|
|
41475
|
+
currentMarkdown = revised.text;
|
|
41476
|
+
}
|
|
41477
|
+
const result = await runConsensus({
|
|
41478
|
+
artifact: currentMarkdown,
|
|
41479
|
+
heads,
|
|
41480
|
+
rounds: deps.rounds,
|
|
41481
|
+
maxHeadCalls: deps.maxHeadCalls,
|
|
41482
|
+
runHead: deps.runHead,
|
|
41483
|
+
runArbiter: deps.runArbiter,
|
|
41484
|
+
revise: async (artifact, accepted) => {
|
|
41485
|
+
const revised = await deps.runReviser(artifact, accepted);
|
|
41486
|
+
if (!revised.ok) return artifact;
|
|
41487
|
+
if (intent.taskId !== null && !validateSpecIdentity(revised.text, intent.taskId)) {
|
|
41488
|
+
process.stderr.write(
|
|
41489
|
+
`[spec-gate] discarding drifted revision for ${spec.pageId}: heading does not identify ${intent.taskId}
|
|
41490
|
+
`
|
|
41491
|
+
);
|
|
41492
|
+
return artifact;
|
|
41493
|
+
}
|
|
41494
|
+
const cas = await deps.casUpdateSpec(revised.text, currentHash);
|
|
41495
|
+
if (!cas.ok) {
|
|
41496
|
+
conflicted = true;
|
|
41497
|
+
throw new Error("SPEC_CHANGED_DURING_REVIEW");
|
|
41498
|
+
}
|
|
41499
|
+
currentHash = cas.contentHash;
|
|
41500
|
+
currentMarkdown = revised.text;
|
|
41501
|
+
return revised.text;
|
|
41502
|
+
}
|
|
41503
|
+
// Only OUR conflict throw is swallowed (→ null). Anything else is a real
|
|
41504
|
+
// error and must propagate so it is not silently masked as a conflict.
|
|
41505
|
+
}).catch((e) => conflicted ? null : Promise.reject(e));
|
|
41506
|
+
if (conflicted || result === null) {
|
|
41507
|
+
await deps.writeEvidence({ specPageId: spec.pageId, outcome: "SPEC_CHANGED_DURING_REVIEW" });
|
|
41508
|
+
return { outcome: "SPEC_CHANGED_DURING_REVIEW" };
|
|
41509
|
+
}
|
|
41510
|
+
if (result.outcome === "CONVERGED" && intent.taskId !== null && !validateSpecIdentity(currentMarkdown, intent.taskId)) {
|
|
41511
|
+
process.stderr.write(
|
|
41512
|
+
`[spec-gate] blocking promotion for ${spec.pageId}: converged spec heading does not identify ${intent.taskId}
|
|
41513
|
+
`
|
|
41514
|
+
);
|
|
41515
|
+
return {
|
|
41516
|
+
outcome: "CONSENSUS_BLOCKED",
|
|
41517
|
+
diagnostics: [
|
|
41518
|
+
{
|
|
41519
|
+
kind: "spec_identity_mismatch",
|
|
41520
|
+
diagnostic: true,
|
|
41521
|
+
rawQuestion: `SPEC_IDENTITY_MISMATCH: this spec page gates ${intent.taskId}, but its heading identifies a different task \u2014 rewrite the spec heading and content for ${intent.taskId}, or re-link the task.`,
|
|
41522
|
+
whyBlocking: "converged spec content belongs to a different task than the one it gates",
|
|
41523
|
+
category: "scope"
|
|
41524
|
+
}
|
|
41525
|
+
]
|
|
41526
|
+
};
|
|
41527
|
+
}
|
|
41528
|
+
let keptClarifications = [];
|
|
41529
|
+
let droppedByCap = [];
|
|
41530
|
+
if (result.outcome === "CONSENSUS_BLOCKED") {
|
|
41531
|
+
const accepted = result.history.at(-1)?.accepted ?? [];
|
|
41532
|
+
const materialIssues = accepted.filter((issue2) => issue2.category === "ambiguity" || issue2.category === "scope");
|
|
41533
|
+
const capped = applyClarificationCap(materialIssues, intent.impact);
|
|
41534
|
+
keptClarifications = capped.kept;
|
|
41535
|
+
droppedByCap = capped.dropped;
|
|
41536
|
+
}
|
|
41537
|
+
await deps.writeEvidence({
|
|
41538
|
+
specPageId: spec.pageId,
|
|
41539
|
+
outcome: result.outcome,
|
|
41540
|
+
rounds: result.rounds,
|
|
41541
|
+
history: result.history,
|
|
41542
|
+
contentHash: currentHash,
|
|
41543
|
+
...droppedByCap.length > 0 ? { droppedByCap } : {}
|
|
41544
|
+
});
|
|
41545
|
+
if (result.outcome === "CONVERGED") {
|
|
41546
|
+
await deps.promoteSpecReady(currentHash);
|
|
41547
|
+
return { outcome: "CONVERGED" };
|
|
41548
|
+
}
|
|
41549
|
+
if (result.outcome === "CONSENSUS_BLOCKED") {
|
|
41550
|
+
return {
|
|
41551
|
+
outcome: "CONSENSUS_BLOCKED",
|
|
41552
|
+
...keptClarifications.length > 0 ? { operatorClarifications: keptClarifications } : {},
|
|
41553
|
+
...droppedByCap.length > 0 ? { droppedByCap } : {},
|
|
41554
|
+
...result.escalation ? { escalation: result.escalation } : {}
|
|
41555
|
+
};
|
|
41556
|
+
}
|
|
41557
|
+
return { outcome: result.outcome };
|
|
41558
|
+
}
|
|
41559
|
+
|
|
41560
|
+
// src/consensus/resolve-gated-task.ts
|
|
41561
|
+
async function resolveGatedTaskId(client, projectSlug, page) {
|
|
41562
|
+
const tasks = await client.listTasks(projectSlug);
|
|
41563
|
+
const linking = tasks.find((t) => t.specWikiPageId === page.id);
|
|
41564
|
+
if (linking) {
|
|
41565
|
+
if (page.taskId !== linking.id) {
|
|
41566
|
+
throw new Error(
|
|
41567
|
+
`spec page ${page.id} is linked by task ${linking.id} (tasks.spec_wiki_page_id) but its wiki_pages.task_id back-pointer is ${page.taskId === null ? "null" : page.taskId} \u2014 refusing to gate with a drifted identity`
|
|
41568
|
+
);
|
|
41569
|
+
}
|
|
41570
|
+
return linking.id;
|
|
41571
|
+
}
|
|
41572
|
+
return page.taskId;
|
|
41573
|
+
}
|
|
41574
|
+
|
|
41575
|
+
// src/consensus/review-identity.ts
|
|
41576
|
+
function domainScopedReviewHash2(rawHash, domain2) {
|
|
41577
|
+
return domainScopedReviewHash(rawHash, domain2);
|
|
41578
|
+
}
|
|
41579
|
+
async function resolveSpecDomain(client, projectSlug, page) {
|
|
41580
|
+
try {
|
|
41581
|
+
const taskId = await resolveGatedTaskId(client, projectSlug, page);
|
|
41582
|
+
if (taskId !== null) {
|
|
41583
|
+
const task = await client.getTask(projectSlug, taskId);
|
|
41584
|
+
return normalizeDomain(task?.domain);
|
|
41585
|
+
}
|
|
41586
|
+
const plans = await client.listPlans(projectSlug);
|
|
41587
|
+
const plan = plans.find((p) => p.specWikiPageId === page.id);
|
|
41588
|
+
if (plan?.domain != null) return normalizeDomain(plan.domain);
|
|
41589
|
+
const project = await client.getProject(projectSlug);
|
|
41590
|
+
return normalizeDomain(project?.defaultDomain);
|
|
41591
|
+
} catch {
|
|
41592
|
+
return "code";
|
|
41593
|
+
}
|
|
41594
|
+
}
|
|
41595
|
+
|
|
41006
41596
|
// src/consensus/code-gate-git.ts
|
|
41007
41597
|
var import_node_child_process7 = require("node:child_process");
|
|
41008
41598
|
var DIFF_MAX_BYTES = 2e5;
|
|
@@ -41295,16 +41885,25 @@ function buildSpecGateDeps(args) {
|
|
|
41295
41885
|
throw new Error(`gated task ${projectSlug}/${taskId} not found for spec ${specPageId} \u2014 cannot gate`);
|
|
41296
41886
|
}
|
|
41297
41887
|
const notes = task.notes ?? null;
|
|
41888
|
+
const taskInboxItems = await client.listInboxItems(projectSlug, { taskId });
|
|
41298
41889
|
const rawSeed = task.operatorSeedRequest ?? null;
|
|
41299
41890
|
let resolvedSeed = rawSeed;
|
|
41300
41891
|
if (rawSeed === null || rawSeed.trim() === "") {
|
|
41301
|
-
const
|
|
41302
|
-
|
|
41303
|
-
|
|
41892
|
+
const answered = taskInboxItems.find(
|
|
41893
|
+
(item) => item.origin === "gate" && item.status === "acknowledged" && item.answer !== null && item.answer.trim() !== "" && item.body.includes(SEED_ABSENCE_MARKER)
|
|
41894
|
+
);
|
|
41895
|
+
if (answered && answered.answer) {
|
|
41896
|
+
resolvedSeed = "(No operator statement was recorded when this task was created. Derived from the operator's answer to the gate's requirements question:)\n\n" + answered.answer;
|
|
41897
|
+
} else {
|
|
41898
|
+
const parsed = notes ? parseAcceptanceCriteria(notes) : null;
|
|
41899
|
+
if (parsed !== null && parsed.items.length > 0) {
|
|
41900
|
+
resolvedSeed = "(No operator statement on record for this task. Derived from the task's own notes \u2014 the following acceptance criteria were extracted verbatim:)\n\n" + parsed.items.map((c) => `- ${c}`).join("\n");
|
|
41901
|
+
}
|
|
41304
41902
|
}
|
|
41305
41903
|
}
|
|
41306
|
-
const
|
|
41307
|
-
|
|
41904
|
+
const adjudicated = taskInboxItems.filter(
|
|
41905
|
+
(item) => item.origin === "gate" && (item.status === "acknowledged" || item.status === "rejected") && item.decidedBy !== "gate"
|
|
41906
|
+
).map((item) => ({
|
|
41308
41907
|
body: item.body,
|
|
41309
41908
|
answer: item.answer,
|
|
41310
41909
|
status: item.status
|
|
@@ -41436,7 +42035,7 @@ ${artifact}`,
|
|
|
41436
42035
|
const { domain: domain2 } = await loadIntent();
|
|
41437
42036
|
try {
|
|
41438
42037
|
await client.recordSpecConsensusReview(projectSlug, specPageId, {
|
|
41439
|
-
contentHash:
|
|
42038
|
+
contentHash: domainScopedReviewHash2(contentHash, domain2),
|
|
41440
42039
|
outcome: String(outcome),
|
|
41441
42040
|
rounds: typeof rounds2 === "number" ? rounds2 : 0
|
|
41442
42041
|
});
|
|
@@ -41648,7 +42247,7 @@ You revise a deliverable to address the accepted issues so it satisfies the acce
|
|
|
41648
42247
|
const rounds2 = typeof r["rounds"] === "number" ? r["rounds"] : 0;
|
|
41649
42248
|
try {
|
|
41650
42249
|
await client.recordSpecConsensusReview(projectSlug, deliverablePageId, {
|
|
41651
|
-
contentHash:
|
|
42250
|
+
contentHash: domainScopedReviewHash2(contentHash, domain2),
|
|
41652
42251
|
outcome,
|
|
41653
42252
|
rounds: rounds2
|
|
41654
42253
|
});
|
|
@@ -41831,6 +42430,111 @@ ${body}`;
|
|
|
41831
42430
|
}
|
|
41832
42431
|
return { markdown: body, tldrChars };
|
|
41833
42432
|
}
|
|
42433
|
+
function extractDeployTarget(markdown) {
|
|
42434
|
+
const section = /(## Deploy Target\s*\n)([\s\S]*?)(?=\n## |$)/i.exec(markdown);
|
|
42435
|
+
if (!section || section.index === void 0) {
|
|
42436
|
+
return { markdown, deployTarget: null };
|
|
42437
|
+
}
|
|
42438
|
+
const remaining = markdown.slice(0, section.index) + markdown.slice(section.index + section[0].length);
|
|
42439
|
+
const fenced = /```(?:json)?\s*\n([\s\S]*?)```/m.exec(section[2] ?? "");
|
|
42440
|
+
let deployTarget = null;
|
|
42441
|
+
if (fenced) {
|
|
42442
|
+
try {
|
|
42443
|
+
const raw = JSON.parse(fenced[1] ?? "");
|
|
42444
|
+
if (raw !== null && typeof raw === "object" && typeof raw.label === "string") {
|
|
42445
|
+
const parsed = { label: raw.label };
|
|
42446
|
+
if (Array.isArray(raw.derive)) {
|
|
42447
|
+
const derive = raw.derive.filter(isDeployTopologyEntry);
|
|
42448
|
+
if (derive.length === raw.derive.length) parsed.derive = derive;
|
|
42449
|
+
}
|
|
42450
|
+
deployTarget = parsed;
|
|
42451
|
+
}
|
|
42452
|
+
} catch {
|
|
42453
|
+
deployTarget = null;
|
|
42454
|
+
}
|
|
42455
|
+
}
|
|
42456
|
+
return { markdown: remaining, deployTarget };
|
|
42457
|
+
}
|
|
42458
|
+
function isDeployTopologyEntry(value) {
|
|
42459
|
+
if (value === null || typeof value !== "object") return false;
|
|
42460
|
+
const e = value;
|
|
42461
|
+
return typeof e.label === "string" && e.label.length > 0 && (e.env === "dev" || e.env === "prod") && typeof e.workflowId === "number" && Number.isInteger(e.workflowId) && e.workflowId > 0 && typeof e.workflowPath === "string" && e.workflowPath.length > 0 && typeof e.workflowSha === "string" && e.workflowSha.length > 0;
|
|
42462
|
+
}
|
|
42463
|
+
function buildDeployTargetPromptSection(ctx) {
|
|
42464
|
+
if (ctx.kind === "closed_menu") {
|
|
42465
|
+
const labels = [...new Set(ctx.entries.map((entry) => entry.label))];
|
|
42466
|
+
return [
|
|
42467
|
+
``,
|
|
42468
|
+
`--- DEPLOY TARGET (required final section) ---`,
|
|
42469
|
+
`This project's deploy topology is already resolved for its current workflow inventory. Pick the ONE label this task deploys under from this CLOSED menu \u2014 never invent a new label:`,
|
|
42470
|
+
...labels.map((label) => `- ${label}`),
|
|
42471
|
+
``,
|
|
42472
|
+
`Append a final section \`## Deploy Target\` whose ONLY content is a single fenced json block of exactly this shape:`,
|
|
42473
|
+
"```json",
|
|
42474
|
+
`{"label": "<one of the menu labels above>"}`,
|
|
42475
|
+
"```"
|
|
42476
|
+
].join("\n");
|
|
42477
|
+
}
|
|
42478
|
+
return [
|
|
42479
|
+
``,
|
|
42480
|
+
`--- DEPLOY TARGET (required final section) ---`,
|
|
42481
|
+
`This project's deploy topology is NOT yet resolved for its current workflow inventory. Below is the raw GitHub workflow list (workflowId / path / blob sha / state); every value used below must be copied VERBATIM from it \u2014 never invented:`,
|
|
42482
|
+
``,
|
|
42483
|
+
JSON.stringify(ctx.snapshot, null, 2),
|
|
42484
|
+
``,
|
|
42485
|
+
`First derive the FULL label map: identify every deploy target in the snapshot, give each a short kebab-case \`label\`, and classify each workflow into an \`env\` of exactly "dev" or "prod".`,
|
|
42486
|
+
`Heuristic for dev vs prod (there is no fixed naming rule): prefer an explicit \`dev-\`/\`prod-\` filename prefix, then a \`/dev/\` or \`/prod/\` directory segment; if neither applies, group the workflows serving the same target and classify from the pair's context.`,
|
|
42487
|
+
`Then pick the ONE \`label\` this task deploys under, from among the labels you just derived.`,
|
|
42488
|
+
``,
|
|
42489
|
+
`Append a final section \`## Deploy Target\` whose ONLY content is a single fenced json block of exactly this shape:`,
|
|
42490
|
+
"```json",
|
|
42491
|
+
`{"label": "<label this task deploys under>", "derive": [{"label":"<target label>","env":"dev"|"prod","workflowId":N,"workflowPath":"<path>","workflowSha":"<blob sha>"}, ...]}`,
|
|
42492
|
+
"```",
|
|
42493
|
+
``,
|
|
42494
|
+
`\`derive\` is the FULL map of every deploy target you can identify in the snapshot (not just this task's pick), so later tasks reuse this derivation instead of re-deriving. \`env\` must be exactly "dev" or "prod". \`workflowId\`/\`workflowPath\`/\`workflowSha\` must be copied verbatim from the snapshot above.`
|
|
42495
|
+
].join("\n");
|
|
42496
|
+
}
|
|
42497
|
+
function resolveDeployTargetRows(canonical, deployTarget, autoDeploy, fingerprint) {
|
|
42498
|
+
if (autoDeploy === "off") return {};
|
|
42499
|
+
const filtered = canonical.filter((entry) => entry.label === deployTarget.label);
|
|
42500
|
+
const requiredEnvs = autoDeploy === "prod" ? ["dev", "prod"] : ["dev"];
|
|
42501
|
+
const missingEnvs = [];
|
|
42502
|
+
const deployTargets = [];
|
|
42503
|
+
for (const env of requiredEnvs) {
|
|
42504
|
+
const match = filtered.find((entry) => entry.env === env);
|
|
42505
|
+
if (!match) {
|
|
42506
|
+
missingEnvs.push(env);
|
|
42507
|
+
continue;
|
|
42508
|
+
}
|
|
42509
|
+
deployTargets.push({
|
|
42510
|
+
env,
|
|
42511
|
+
targetLabel: deployTarget.label,
|
|
42512
|
+
workflowId: match.workflowId,
|
|
42513
|
+
workflowPath: match.workflowPath,
|
|
42514
|
+
workflowSha: match.workflowSha,
|
|
42515
|
+
topologyFingerprint: fingerprint
|
|
42516
|
+
});
|
|
42517
|
+
}
|
|
42518
|
+
if (missingEnvs.length > 0) return { skippedReason: "incomplete_label", missingEnvs };
|
|
42519
|
+
return { deployTargets };
|
|
42520
|
+
}
|
|
42521
|
+
function findDuplicateLabelEnvPairs(entries) {
|
|
42522
|
+
const seen = /* @__PURE__ */ new Set();
|
|
42523
|
+
const duplicateKeys = /* @__PURE__ */ new Set();
|
|
42524
|
+
for (const entry of entries) {
|
|
42525
|
+
const key = `${entry.label}\0${entry.env}`;
|
|
42526
|
+
if (seen.has(key)) duplicateKeys.add(key);
|
|
42527
|
+
seen.add(key);
|
|
42528
|
+
}
|
|
42529
|
+
const pairs = [];
|
|
42530
|
+
for (const key of duplicateKeys) {
|
|
42531
|
+
const parts = key.split("\0");
|
|
42532
|
+
const label = parts[0] ?? "";
|
|
42533
|
+
const env = parts[1] === "prod" ? "prod" : "dev";
|
|
42534
|
+
pairs.push({ label, env });
|
|
42535
|
+
}
|
|
42536
|
+
return pairs;
|
|
42537
|
+
}
|
|
41834
42538
|
function nextWikiPageId(pages) {
|
|
41835
42539
|
let max = 0;
|
|
41836
42540
|
for (const page of pages) {
|
|
@@ -41841,7 +42545,7 @@ function nextWikiPageId(pages) {
|
|
|
41841
42545
|
}
|
|
41842
42546
|
return `WK-${max + 1}`;
|
|
41843
42547
|
}
|
|
41844
|
-
function buildDraftSpecPrompt(task, constitution) {
|
|
42548
|
+
function buildDraftSpecPrompt(task, constitution, deployTarget = null) {
|
|
41845
42549
|
const budget = resolveSpecBudget(task.impact);
|
|
41846
42550
|
const lines2 = [
|
|
41847
42551
|
`Write a complete implementation spec for the task below.`,
|
|
@@ -41878,6 +42582,7 @@ function buildDraftSpecPrompt(task, constitution) {
|
|
|
41878
42582
|
`The \`## TL;DR\` section MUST be at most ${DRAFT_SPEC_TLDR_MAX_CHARS} characters.`,
|
|
41879
42583
|
`The spec MUST contain at most ${budget.maxRequirements} acceptance-criteria items and no more than ${budget.maxChars} characters total.`
|
|
41880
42584
|
);
|
|
42585
|
+
if (deployTarget) lines2.push(buildDeployTargetPromptSection(deployTarget));
|
|
41881
42586
|
return lines2.join("\n");
|
|
41882
42587
|
}
|
|
41883
42588
|
function skipped(reason, extra = {}) {
|
|
@@ -41926,6 +42631,22 @@ async function runDraftSpec(ctx, input, deps) {
|
|
|
41926
42631
|
}
|
|
41927
42632
|
if (getActiveBlockedByIds(task.blockedBy, blockers).length > 0) return skipped("blocked_by_active");
|
|
41928
42633
|
}
|
|
42634
|
+
const autoDeploy = task.autoDeploy ?? "off";
|
|
42635
|
+
let topology = null;
|
|
42636
|
+
let deploySkippedReason = null;
|
|
42637
|
+
let deploySkippedDetail = null;
|
|
42638
|
+
if (autoDeploy !== "off") {
|
|
42639
|
+
try {
|
|
42640
|
+
topology = await client.getDeployTopology(projectSlug);
|
|
42641
|
+
} catch {
|
|
42642
|
+
deploySkippedReason = "topology_unavailable";
|
|
42643
|
+
topology = null;
|
|
42644
|
+
}
|
|
42645
|
+
}
|
|
42646
|
+
if (topology && (topology.fingerprint === null || topology.entries.length === 0 && topology.snapshot === null)) {
|
|
42647
|
+
deploySkippedReason = topology.fingerprint === null ? "no_bound_deploy_target" : "topology_snapshot_unavailable";
|
|
42648
|
+
topology = null;
|
|
42649
|
+
}
|
|
41929
42650
|
let persona;
|
|
41930
42651
|
try {
|
|
41931
42652
|
persona = loadPersonaFn(DRAFT_SPEC_PERSONA);
|
|
@@ -41954,7 +42675,8 @@ async function runDraftSpec(ctx, input, deps) {
|
|
|
41954
42675
|
} catch {
|
|
41955
42676
|
constitution = null;
|
|
41956
42677
|
}
|
|
41957
|
-
const
|
|
42678
|
+
const deployPromptContext = autoDeploy !== "off" && topology ? topology.entries.length > 0 ? { kind: "closed_menu", entries: topology.entries } : { kind: "derive", snapshot: topology.snapshot ?? [] } : null;
|
|
42679
|
+
const prompt = buildDraftSpecPrompt(task, constitution, deployPromptContext);
|
|
41958
42680
|
const exec = await runExec({
|
|
41959
42681
|
prompt,
|
|
41960
42682
|
developerInstructions: persona,
|
|
@@ -41969,7 +42691,8 @@ async function runDraftSpec(ctx, input, deps) {
|
|
|
41969
42691
|
if (exec.text.trim().length === 0) {
|
|
41970
42692
|
return { kind: "retry", failureReason: "zero_output", error: "spec authoring produced empty output" };
|
|
41971
42693
|
}
|
|
41972
|
-
const { markdown,
|
|
42694
|
+
const { markdown: specMarkdown, deployTarget: parsedDeployTarget } = extractDeployTarget(exec.text);
|
|
42695
|
+
const { markdown, tldrChars } = enforceSpecOutputContract(specMarkdown, taskId, task.title);
|
|
41973
42696
|
let pageId;
|
|
41974
42697
|
if (adoptedId) {
|
|
41975
42698
|
try {
|
|
@@ -42029,6 +42752,45 @@ async function runDraftSpec(ctx, input, deps) {
|
|
|
42029
42752
|
}
|
|
42030
42753
|
pageId = created;
|
|
42031
42754
|
}
|
|
42755
|
+
let deployTargets;
|
|
42756
|
+
if (topology && parsedDeployTarget) {
|
|
42757
|
+
const fingerprint = topology.fingerprint;
|
|
42758
|
+
if (fingerprint === null) {
|
|
42759
|
+
deploySkippedReason = deploySkippedReason ?? "no_bound_deploy_target";
|
|
42760
|
+
} else if (topology.entries.length > 0) {
|
|
42761
|
+
const resolved = resolveDeployTargetRows(topology.entries, parsedDeployTarget, autoDeploy, fingerprint);
|
|
42762
|
+
deployTargets = resolved.deployTargets;
|
|
42763
|
+
if (resolved.skippedReason) {
|
|
42764
|
+
deploySkippedReason = resolved.skippedReason;
|
|
42765
|
+
if (resolved.skippedReason === "incomplete_label") {
|
|
42766
|
+
deploySkippedDetail = `label "${parsedDeployTarget.label}" has no entry for: ${(resolved.missingEnvs ?? []).join(", ")}`;
|
|
42767
|
+
}
|
|
42768
|
+
}
|
|
42769
|
+
} else {
|
|
42770
|
+
const derive = parsedDeployTarget.derive ?? [];
|
|
42771
|
+
const duplicateLabelEnvPairs = findDuplicateLabelEnvPairs(derive);
|
|
42772
|
+
if (derive.length === 0) {
|
|
42773
|
+
deploySkippedReason = "no_derivation";
|
|
42774
|
+
} else if (duplicateLabelEnvPairs.length > 0) {
|
|
42775
|
+
deploySkippedReason = "ambiguous_derivation";
|
|
42776
|
+
deploySkippedDetail = `duplicate label/env pair(s): ${duplicateLabelEnvPairs.map((pair) => `${pair.label}/${pair.env}`).join(", ")}`;
|
|
42777
|
+
} else {
|
|
42778
|
+
try {
|
|
42779
|
+
const written = await client.writeDeployTopology(projectSlug, { fingerprint, entries: derive });
|
|
42780
|
+
const resolved = resolveDeployTargetRows(written.entries, parsedDeployTarget, autoDeploy, fingerprint);
|
|
42781
|
+
deployTargets = resolved.deployTargets;
|
|
42782
|
+
if (resolved.skippedReason) {
|
|
42783
|
+
deploySkippedReason = resolved.skippedReason;
|
|
42784
|
+
if (resolved.skippedReason === "incomplete_label") {
|
|
42785
|
+
deploySkippedDetail = `label "${parsedDeployTarget.label}" has no entry for: ${(resolved.missingEnvs ?? []).join(", ")}`;
|
|
42786
|
+
}
|
|
42787
|
+
}
|
|
42788
|
+
} catch {
|
|
42789
|
+
deploySkippedReason = "topology_write_failed";
|
|
42790
|
+
}
|
|
42791
|
+
}
|
|
42792
|
+
}
|
|
42793
|
+
}
|
|
42032
42794
|
let fresh;
|
|
42033
42795
|
try {
|
|
42034
42796
|
fresh = await client.getTask(projectSlug, taskId);
|
|
@@ -42045,8 +42807,12 @@ async function runDraftSpec(ctx, input, deps) {
|
|
|
42045
42807
|
if (fresh.specWikiPageId) {
|
|
42046
42808
|
return skipped("lost_link_race", { pageId, linkedPageId: fresh.specWikiPageId });
|
|
42047
42809
|
}
|
|
42810
|
+
const linkPatch = { specWikiPageId: pageId };
|
|
42811
|
+
if (deployTargets !== void 0 && deployTargets.length > 0) {
|
|
42812
|
+
linkPatch.deployTargets = deployTargets;
|
|
42813
|
+
}
|
|
42048
42814
|
try {
|
|
42049
|
-
await client.updateTask(projectSlug, taskId,
|
|
42815
|
+
await client.updateTask(projectSlug, taskId, linkPatch);
|
|
42050
42816
|
} catch (error51) {
|
|
42051
42817
|
return {
|
|
42052
42818
|
kind: "retry",
|
|
@@ -42054,16 +42820,20 @@ async function runDraftSpec(ctx, input, deps) {
|
|
|
42054
42820
|
error: `failed to link spec page ${pageId} to task ${taskId}: ${errMessage2(error51)}`
|
|
42055
42821
|
};
|
|
42056
42822
|
}
|
|
42823
|
+
const drafted = {
|
|
42824
|
+
outcome: "DRAFTED",
|
|
42825
|
+
pageId,
|
|
42826
|
+
adopted: adoptedId !== null,
|
|
42827
|
+
constitution: constitution !== null ? "included" : "missing",
|
|
42828
|
+
tldrChars,
|
|
42829
|
+
relatedContext: "skipped_no_client_method"
|
|
42830
|
+
};
|
|
42831
|
+
if (deploySkippedReason !== null) drafted.deployTargetSkippedReason = deploySkippedReason;
|
|
42832
|
+
if (deploySkippedDetail !== null) drafted.deployTargetSkippedDetail = deploySkippedDetail;
|
|
42833
|
+
drafted.taskTitle = task.title;
|
|
42057
42834
|
return {
|
|
42058
42835
|
kind: "done",
|
|
42059
|
-
resultJson: JSON.stringify(
|
|
42060
|
-
outcome: "DRAFTED",
|
|
42061
|
-
pageId,
|
|
42062
|
-
adopted: adoptedId !== null,
|
|
42063
|
-
constitution: constitution !== null ? "included" : "missing",
|
|
42064
|
-
tldrChars,
|
|
42065
|
-
relatedContext: "skipped_no_client_method"
|
|
42066
|
-
}),
|
|
42836
|
+
resultJson: JSON.stringify(drafted),
|
|
42067
42837
|
finalReviewedHash: null
|
|
42068
42838
|
};
|
|
42069
42839
|
}
|
|
@@ -42077,12 +42847,23 @@ function onComplete(outcome, input) {
|
|
|
42077
42847
|
if (parsed.outcome === "SKIPPED") {
|
|
42078
42848
|
return {
|
|
42079
42849
|
kind: "info",
|
|
42850
|
+
taskId: input.taskId,
|
|
42080
42851
|
body: `Spec draft for ${input.taskId} skipped (${parsed.reason ?? "unknown"}); nothing changed.`
|
|
42081
42852
|
};
|
|
42082
42853
|
}
|
|
42854
|
+
if (parsed.deployTargetSkippedReason === "ambiguous_derivation" || parsed.deployTargetSkippedReason === "incomplete_label") {
|
|
42855
|
+
return {
|
|
42856
|
+
kind: "handoff",
|
|
42857
|
+
taskId: input.taskId,
|
|
42858
|
+
id: `II-deploy-target-${parsed.deployTargetSkippedReason}-${input.taskId}`,
|
|
42859
|
+
body: `Spec for ${input.taskId} was drafted (page ${parsed.pageId ?? "?"} in Specs/), but its deploy target needs a human: ${parsed.deployTargetSkippedDetail ?? parsed.deployTargetSkippedReason}. The spec itself proceeds to the normal consensus review; the deploy target stays unresolved until this is fixed.`
|
|
42860
|
+
};
|
|
42861
|
+
}
|
|
42862
|
+
const taskRef = parsed.taskTitle ? `${input.taskId} \u2014 ${parsed.taskTitle}` : input.taskId;
|
|
42083
42863
|
return {
|
|
42084
42864
|
kind: "info",
|
|
42085
|
-
|
|
42865
|
+
taskId: input.taskId,
|
|
42866
|
+
body: `Spec drafted automatically for ${taskRef} \u2014 page ${parsed.pageId ?? "?"} in Specs/. The next scheduler tick will run the consensus review.`
|
|
42086
42867
|
};
|
|
42087
42868
|
}
|
|
42088
42869
|
const summary = outcome.error;
|
|
@@ -42451,34 +43232,42 @@ async function tryProdAutoRevert(deps, projectSlug, run3, task) {
|
|
|
42451
43232
|
return null;
|
|
42452
43233
|
}
|
|
42453
43234
|
}
|
|
42454
|
-
function
|
|
42455
|
-
return workflows.map((workflow) => `${workflow.id}:${workflow.path}`).sort().join(",");
|
|
42456
|
-
}
|
|
42457
|
-
async function resolveWorkflowId(deps, projectSlug, run3, env) {
|
|
43235
|
+
async function resolveWorkflowId(deps, projectSlug, run3, task, env) {
|
|
42458
43236
|
const pinned = env === "dev" ? run3.devWorkflowId : run3.prodWorkflowId;
|
|
42459
|
-
if (pinned)
|
|
42460
|
-
|
|
42461
|
-
|
|
42462
|
-
|
|
42463
|
-
|
|
42464
|
-
|
|
42465
|
-
|
|
42466
|
-
|
|
42467
|
-
|
|
42468
|
-
|
|
42469
|
-
|
|
42470
|
-
|
|
42471
|
-
|
|
42472
|
-
|
|
42473
|
-
|
|
42474
|
-
|
|
42475
|
-
|
|
42476
|
-
|
|
42477
|
-
|
|
42478
|
-
|
|
42479
|
-
|
|
43237
|
+
if (pinned) {
|
|
43238
|
+
return { workflowId: pinned, workflowPath: null, blockReason: null, pinned: true, reverifyRequested: false };
|
|
43239
|
+
}
|
|
43240
|
+
const target = await deps.getTaskDeployTarget(projectSlug, task.id, env);
|
|
43241
|
+
if (!target) {
|
|
43242
|
+
return { workflowId: null, workflowPath: null, blockReason: "deploy_target_unset", pinned: false, reverifyRequested: false };
|
|
43243
|
+
}
|
|
43244
|
+
const snapshot = await deps.getRepoSnapshot(projectSlug);
|
|
43245
|
+
const activeWorkflows = (snapshot?.lastSnapshot ?? []).filter((w) => w.state === "active");
|
|
43246
|
+
const liveMatch = activeWorkflows.find((w) => w.id === target.workflowId);
|
|
43247
|
+
if (!liveMatch || liveMatch.path !== target.workflowPath) {
|
|
43248
|
+
return { workflowId: null, workflowPath: null, blockReason: "deploy_target_inactive", pinned: false, reverifyRequested: false };
|
|
43249
|
+
}
|
|
43250
|
+
if (liveMatch.sha !== target.workflowSha) {
|
|
43251
|
+
return { workflowId: null, workflowPath: null, blockReason: "deploy_target_modified", pinned: false, reverifyRequested: false };
|
|
43252
|
+
}
|
|
43253
|
+
const currentFingerprint = snapshot?.lastFingerprint ?? null;
|
|
43254
|
+
if (currentFingerprint !== null && currentFingerprint === target.topologyFingerprint) {
|
|
43255
|
+
return { workflowId: target.workflowId, workflowPath: target.workflowPath, blockReason: null, pinned: false, reverifyRequested: false };
|
|
43256
|
+
}
|
|
43257
|
+
if (currentFingerprint === null) {
|
|
43258
|
+
deps.recordDeployTargetTelemetry("deploy_target_cache_missing");
|
|
43259
|
+
return { workflowId: null, workflowPath: null, blockReason: "deploy_target_cache_missing", pinned: false, reverifyRequested: false };
|
|
43260
|
+
}
|
|
43261
|
+
const entry = await deps.lookupTopologyEntry(projectSlug, currentFingerprint, target.targetLabel, env);
|
|
43262
|
+
if (!entry) {
|
|
43263
|
+
deps.recordDeployTargetTelemetry("deploy_target_cache_missing");
|
|
43264
|
+
return { workflowId: null, workflowPath: null, blockReason: "deploy_target_cache_missing", pinned: false, reverifyRequested: false };
|
|
42480
43265
|
}
|
|
42481
|
-
|
|
43266
|
+
if (entry.workflowId !== target.workflowId) {
|
|
43267
|
+
deps.recordDeployTargetTelemetry("deploy_target_reassigned");
|
|
43268
|
+
return { workflowId: null, workflowPath: null, blockReason: "deploy_target_reassigned", pinned: false, reverifyRequested: false };
|
|
43269
|
+
}
|
|
43270
|
+
return { workflowId: target.workflowId, workflowPath: target.workflowPath, blockReason: null, pinned: false, reverifyRequested: true };
|
|
42482
43271
|
}
|
|
42483
43272
|
async function runDeployGateTick(deps, projectSlug, run3, task) {
|
|
42484
43273
|
const artifacts = await deps.client.listExecutionArtifacts(projectSlug, run3.id);
|
|
@@ -42500,7 +43289,10 @@ async function runDeployGateTick(deps, projectSlug, run3, task) {
|
|
|
42500
43289
|
return { kind: "done" };
|
|
42501
43290
|
}
|
|
42502
43291
|
const env = activeEnv;
|
|
42503
|
-
const resolution = await resolveWorkflowId(deps, projectSlug, run3, env);
|
|
43292
|
+
const resolution = await resolveWorkflowId(deps, projectSlug, run3, task, env);
|
|
43293
|
+
if (resolution.reverifyRequested) {
|
|
43294
|
+
await deps.setTaskReverifyRequested(projectSlug, task.id);
|
|
43295
|
+
}
|
|
42504
43296
|
const workflowId = resolution.workflowId;
|
|
42505
43297
|
const attempt = latestByEnv(artifacts, "deploy_attempt", env, { realAttemptsOnly: true });
|
|
42506
43298
|
if (!attempt) {
|
|
@@ -42527,9 +43319,13 @@ async function runDeployGateTick(deps, projectSlug, run3, task) {
|
|
|
42527
43319
|
await deps.handoff(projectSlug, task, run3, "verification_steps_missing");
|
|
42528
43320
|
return { kind: "handoff", reason: "verification_steps_missing" };
|
|
42529
43321
|
}
|
|
43322
|
+
if (resolution.blockReason) {
|
|
43323
|
+
await deps.handoff(projectSlug, task, run3, `${resolution.blockReason}:${env}`);
|
|
43324
|
+
return { kind: "handoff", reason: `${resolution.blockReason}:${env}` };
|
|
43325
|
+
}
|
|
42530
43326
|
if (!workflowId) {
|
|
42531
|
-
await deps.handoff(projectSlug, task, run3, `
|
|
42532
|
-
return { kind: "handoff", reason: `
|
|
43327
|
+
await deps.handoff(projectSlug, task, run3, `deploy_target_unset:${env}`);
|
|
43328
|
+
return { kind: "handoff", reason: `deploy_target_unset:${env}` };
|
|
42533
43329
|
}
|
|
42534
43330
|
if (!run3.branch) {
|
|
42535
43331
|
await deps.handoff(projectSlug, task, run3, "run_branch_missing");
|
|
@@ -42547,19 +43343,13 @@ async function runDeployGateTick(deps, projectSlug, run3, task) {
|
|
|
42547
43343
|
try {
|
|
42548
43344
|
await deps.client.dispatchTaskDeploy(projectSlug, task.id, { env, ref: run3.branch, workflowId: String(workflowId) });
|
|
42549
43345
|
} catch (error51) {
|
|
42550
|
-
if (resolution.
|
|
42551
|
-
|
|
42552
|
-
projectSlug,
|
|
42553
|
-
task,
|
|
42554
|
-
run3,
|
|
42555
|
-
`deploy_workflow_operator_choice_stale:${env}:${String(error51).slice(0, 120)}`
|
|
42556
|
-
);
|
|
42557
|
-
return { kind: "handoff", reason: `deploy_workflow_operator_choice_stale:${env}` };
|
|
43346
|
+
if (!resolution.pinned && !resolution.reverifyRequested) {
|
|
43347
|
+
deps.recordDeployTargetTelemetry("deploy_dispatch_failed_fingerprint_unchanged");
|
|
42558
43348
|
}
|
|
42559
43349
|
await deps.handoff(projectSlug, task, run3, `workflow_dispatch_failed:${env}:${String(error51).slice(0, 120)}`);
|
|
42560
43350
|
return { kind: "handoff", reason: `workflow_dispatch_failed:${env}` };
|
|
42561
43351
|
}
|
|
42562
|
-
if (resolution.
|
|
43352
|
+
if (!resolution.pinned) {
|
|
42563
43353
|
await deps.client.updateExecutionRun(projectSlug, run3.id, {
|
|
42564
43354
|
[env === "dev" ? "devWorkflowId" : "prodWorkflowId"]: workflowId
|
|
42565
43355
|
});
|
|
@@ -44651,7 +45441,35 @@ async function runBackstopSweep(deps, drive = (slug, id) => driveRun(deps, slug,
|
|
|
44651
45441
|
await deployGateHandoff(deps, projectSlug, t, r.id, reason, r.leaseId);
|
|
44652
45442
|
},
|
|
44653
45443
|
budgetSeconds: deps.deployGate.budgetSeconds ?? deployPollBudgetSeconds(),
|
|
44654
|
-
now: () => /* @__PURE__ */ new Date()
|
|
45444
|
+
now: () => /* @__PURE__ */ new Date(),
|
|
45445
|
+
getTaskDeployTarget: async (projectSlug, taskId, env) => deps.client.getTaskDeployTarget(projectSlug, taskId, env),
|
|
45446
|
+
getRepoSnapshot: async (projectSlug) => {
|
|
45447
|
+
try {
|
|
45448
|
+
const topology = await deps.client.getDeployTopology(projectSlug);
|
|
45449
|
+
return { lastFingerprint: topology.fingerprint, lastSnapshot: topology.snapshot };
|
|
45450
|
+
} catch {
|
|
45451
|
+
return null;
|
|
45452
|
+
}
|
|
45453
|
+
},
|
|
45454
|
+
lookupTopologyEntry: async (projectSlug, fingerprint, label, env) => {
|
|
45455
|
+
try {
|
|
45456
|
+
const topology = await deps.client.getDeployTopology(projectSlug);
|
|
45457
|
+
if (topology.fingerprint !== fingerprint) return null;
|
|
45458
|
+
const entry = topology.entries.find((e) => e.label === label && e.env === env);
|
|
45459
|
+
return entry ? { workflowId: entry.workflowId, workflowPath: entry.workflowPath, workflowSha: entry.workflowSha } : null;
|
|
45460
|
+
} catch {
|
|
45461
|
+
return null;
|
|
45462
|
+
}
|
|
45463
|
+
},
|
|
45464
|
+
setTaskReverifyRequested: async (projectSlug, taskId) => {
|
|
45465
|
+
await deps.client.updateTask(projectSlug, taskId, { reverifyRequested: true }).catch(() => {
|
|
45466
|
+
});
|
|
45467
|
+
},
|
|
45468
|
+
recordDeployTargetTelemetry: (counter) => {
|
|
45469
|
+
console.log(
|
|
45470
|
+
JSON.stringify({ event: "deploy_target_telemetry", counter, projectSlug: slug, taskId: task.id })
|
|
45471
|
+
);
|
|
45472
|
+
}
|
|
44655
45473
|
};
|
|
44656
45474
|
await runDeployGateTick(gateDeps, slug, run3, task);
|
|
44657
45475
|
} catch {
|
|
@@ -44831,240 +45649,6 @@ async function runAsyncBackstopSweep(deps, drive = (slug, jobId) => driveAsyncJo
|
|
|
44831
45649
|
}
|
|
44832
45650
|
}
|
|
44833
45651
|
|
|
44834
|
-
// src/consensus/verdict.ts
|
|
44835
|
-
var CATEGORIES = [
|
|
44836
|
-
"correctness",
|
|
44837
|
-
"security",
|
|
44838
|
-
"scope",
|
|
44839
|
-
"ambiguity",
|
|
44840
|
-
"performance",
|
|
44841
|
-
"ops"
|
|
44842
|
-
];
|
|
44843
|
-
var CAT_SET = new Set(CATEGORIES);
|
|
44844
|
-
function parseVerdict(raw) {
|
|
44845
|
-
const verdictMatch = raw.match(/\*\*Verdict\*\*:\s*(APPROVE|REQUEST_CHANGES|REJECT)\b/i);
|
|
44846
|
-
const parsedVerdict = verdictMatch?.[1]?.toUpperCase();
|
|
44847
|
-
const verdict = parsedVerdict === "APPROVE" || parsedVerdict === "REQUEST_CHANGES" || parsedVerdict === "REJECT" ? parsedVerdict : "REQUEST_CHANGES";
|
|
44848
|
-
const afterCritical = raw.split(/\*\*Critical issues\*\*/i)[1];
|
|
44849
|
-
const block = afterCritical?.split(/\n\*\*/)[0] ?? "";
|
|
44850
|
-
const issues = [];
|
|
44851
|
-
const parseFallbacks = [];
|
|
44852
|
-
for (const line of block.split(/\r?\n/)) {
|
|
44853
|
-
const bulletMatch = line.match(/^\s*-\s+(.*\S)\s*$/);
|
|
44854
|
-
if (!bulletMatch) continue;
|
|
44855
|
-
const body = bulletMatch[1] ?? "";
|
|
44856
|
-
if (/^\(?(empty list|none)\b/i.test(body)) continue;
|
|
44857
|
-
if (!body) continue;
|
|
44858
|
-
const catMatch = body.match(/`?\[(\w+)\]`?/);
|
|
44859
|
-
const catToken = catMatch?.[1]?.toLowerCase() ?? "";
|
|
44860
|
-
const isKnownCat = catToken !== "" && CAT_SET.has(catToken);
|
|
44861
|
-
const category = isKnownCat ? catToken : "ambiguity";
|
|
44862
|
-
if (!catMatch || !isKnownCat) {
|
|
44863
|
-
parseFallbacks.push(body);
|
|
44864
|
-
}
|
|
44865
|
-
const text = body.replace(/`?\[\w+\]`?\s*/, "").trim();
|
|
44866
|
-
issues.push({ category, text });
|
|
44867
|
-
}
|
|
44868
|
-
return { verdict, issues, parseFallbacks };
|
|
44869
|
-
}
|
|
44870
|
-
function parseArbiterDecisions(raw) {
|
|
44871
|
-
const decisions = [];
|
|
44872
|
-
const afterDecisions = raw.split(/\*\*Decisions\*\*/i)[1];
|
|
44873
|
-
const block = afterDecisions?.split(/\n\*\*Adjudicated/i)[0] ?? "";
|
|
44874
|
-
for (const line of block.split(/\r?\n/)) {
|
|
44875
|
-
const m = line.match(
|
|
44876
|
-
/^\s*-\s+`?\[([^\]]+)\]`?\s+(accept|dismiss|defer)\b(?:\s*[—-]\s*(.*\S))?\s*$/i
|
|
44877
|
-
);
|
|
44878
|
-
if (!m) continue;
|
|
44879
|
-
const id = m[1] ?? "";
|
|
44880
|
-
const decision = (m[2] ?? "accept").toLowerCase();
|
|
44881
|
-
const reason = (m[3] ?? "").trim();
|
|
44882
|
-
decisions.push({ id, decision, reason });
|
|
44883
|
-
}
|
|
44884
|
-
return { decisions };
|
|
44885
|
-
}
|
|
44886
|
-
|
|
44887
|
-
// src/consensus/arbiter.ts
|
|
44888
|
-
function buildArbiterPrompt(issues) {
|
|
44889
|
-
const list = issues.map((i) => `- [${i.id}] (${i.category}) ${i.text}`).join("\n");
|
|
44890
|
-
return `Adjudicate each issue below.
|
|
44891
|
-
|
|
44892
|
-
ISSUES:
|
|
44893
|
-
${list}`;
|
|
44894
|
-
}
|
|
44895
|
-
async function adjudicate(issues, deps) {
|
|
44896
|
-
if (issues.length === 0) return { adjudicatedVerdict: "APPROVE", accepted: [], dismissed: [] };
|
|
44897
|
-
const byId = new Map(issues.map((i) => [i.id, i]));
|
|
44898
|
-
for (let attempt = 0; attempt < 2; attempt++) {
|
|
44899
|
-
const res = await deps.runArbiter(buildArbiterPrompt(issues));
|
|
44900
|
-
if (!res.ok) continue;
|
|
44901
|
-
const { decisions } = parseArbiterDecisions(res.text);
|
|
44902
|
-
const seen = new Set(decisions.map((d) => d.id));
|
|
44903
|
-
const everyDecided = issues.every((i) => seen.has(i.id));
|
|
44904
|
-
const missingReason = decisions.some((d) => d.decision !== "accept" && !d.reason);
|
|
44905
|
-
if (!everyDecided || missingReason && attempt === 0) continue;
|
|
44906
|
-
const accepted = [];
|
|
44907
|
-
const dismissed = [];
|
|
44908
|
-
for (const i of issues) {
|
|
44909
|
-
const d = decisions.find((x) => x.id === i.id);
|
|
44910
|
-
if (!d || d.decision !== "accept" && !d.reason) {
|
|
44911
|
-
accepted.push(i);
|
|
44912
|
-
continue;
|
|
44913
|
-
}
|
|
44914
|
-
if (d.decision === "accept") accepted.push(i);
|
|
44915
|
-
else dismissed.push({ id: i.id, decision: d.decision, reason: d.reason });
|
|
44916
|
-
}
|
|
44917
|
-
return { adjudicatedVerdict: accepted.length === 0 ? "APPROVE" : "REQUEST_CHANGES", accepted, dismissed };
|
|
44918
|
-
}
|
|
44919
|
-
return { adjudicatedVerdict: "REQUEST_CHANGES", accepted: [...byId.values()], dismissed: [] };
|
|
44920
|
-
}
|
|
44921
|
-
|
|
44922
|
-
// src/consensus/semaphore.ts
|
|
44923
|
-
function createBoundedSemaphore(slots) {
|
|
44924
|
-
let available = slots;
|
|
44925
|
-
const queue = [];
|
|
44926
|
-
return {
|
|
44927
|
-
acquire: () => available > 0 ? (available -= 1, Promise.resolve()) : new Promise((resolve) => queue.push(resolve)),
|
|
44928
|
-
release: () => {
|
|
44929
|
-
const next = queue.shift();
|
|
44930
|
-
if (next) next();
|
|
44931
|
-
else available += 1;
|
|
44932
|
-
}
|
|
44933
|
-
};
|
|
44934
|
-
}
|
|
44935
|
-
|
|
44936
|
-
// src/consensus/escalation.ts
|
|
44937
|
-
function diagnoseStall(history, configuredRounds) {
|
|
44938
|
-
const n = history.length;
|
|
44939
|
-
const nonRespPerRound = history.map((h) => h.headOutcomes.filter((o) => !o.responded).length);
|
|
44940
|
-
const infraRoundCount = nonRespPerRound.filter((c) => c >= 2).length;
|
|
44941
|
-
const infraQualifies = infraRoundCount > n / 2;
|
|
44942
|
-
if (infraQualifies) {
|
|
44943
|
-
const roundIndices = [];
|
|
44944
|
-
nonRespPerRound.forEach((c, i) => {
|
|
44945
|
-
if (c >= 2) roundIndices.push(i + 1);
|
|
44946
|
-
});
|
|
44947
|
-
const evidence2 = `INFRA basis: \u22652 non\u2011responding heads in ${infraRoundCount} of ${n} rounds (rounds ${roundIndices.join(",")})`;
|
|
44948
|
-
let grant = 3;
|
|
44949
|
-
const headroom = 2 * configuredRounds - n;
|
|
44950
|
-
grant = Math.max(1, Math.min(grant, headroom));
|
|
44951
|
-
return { diagnosis: "INFRA", grantedRounds: grant, evidence: evidence2 };
|
|
44952
|
-
}
|
|
44953
|
-
if (n >= 2) {
|
|
44954
|
-
const counts = history.map((h) => h.accepted.length);
|
|
44955
|
-
const last = counts[n - 1];
|
|
44956
|
-
const secondLast = counts[n - 2];
|
|
44957
|
-
const lastFall = last < secondLast;
|
|
44958
|
-
if (lastFall) {
|
|
44959
|
-
let falls = 1;
|
|
44960
|
-
if (n >= 3 && secondLast < counts[n - 3]) {
|
|
44961
|
-
falls++;
|
|
44962
|
-
}
|
|
44963
|
-
let grant = 2 + falls;
|
|
44964
|
-
if (grant > 5) grant = 5;
|
|
44965
|
-
const headroom = 2 * configuredRounds - n;
|
|
44966
|
-
grant = Math.max(1, Math.min(grant, headroom));
|
|
44967
|
-
const seqDesc = counts.map((c, i) => `round${i + 1}:${c}`).join(" \u2192 ");
|
|
44968
|
-
const evidence2 = `SUBSTANCE basis: accepted count fell (${seqDesc})`;
|
|
44969
|
-
return { diagnosis: "SUBSTANCE", grantedRounds: grant, evidence: evidence2 };
|
|
44970
|
-
}
|
|
44971
|
-
}
|
|
44972
|
-
const evidence = n === 0 ? "empty history" : `no infra or substance basis found in ${n} rounds`;
|
|
44973
|
-
return { diagnosis: "AMBIGUITY", grantedRounds: 0, evidence };
|
|
44974
|
-
}
|
|
44975
|
-
|
|
44976
|
-
// src/consensus/loop.ts
|
|
44977
|
-
async function runConsensus(input) {
|
|
44978
|
-
const sem = createBoundedSemaphore(input.concurrency ?? input.heads.length);
|
|
44979
|
-
const history = [];
|
|
44980
|
-
let artifact = input.artifact;
|
|
44981
|
-
let calls = 0;
|
|
44982
|
-
const dismissedFwd = [];
|
|
44983
|
-
let effectiveRounds = input.rounds;
|
|
44984
|
-
let escalated = false;
|
|
44985
|
-
let decision;
|
|
44986
|
-
for (let round = 1; round <= effectiveRounds; round++) {
|
|
44987
|
-
if (calls + input.heads.length > input.maxHeadCalls) {
|
|
44988
|
-
return { outcome: "CONSENSUS_BLOCKED", rounds: round - 1, finalArtifact: artifact, history, ...decision ? { escalation: decision } : {} };
|
|
44989
|
-
}
|
|
44990
|
-
const dismissedBlock = dismissedFwd.length ? `PREVIOUSLY DISMISSED (do not re-raise unless new information):
|
|
44991
|
-
${dismissedFwd.slice(-20).join("\n")}` : "";
|
|
44992
|
-
const results = await Promise.all(
|
|
44993
|
-
input.heads.map(async (persona, idx) => {
|
|
44994
|
-
await sem.acquire();
|
|
44995
|
-
try {
|
|
44996
|
-
calls++;
|
|
44997
|
-
const res = await input.runHead(persona, artifact, dismissedBlock);
|
|
44998
|
-
return { idx, res };
|
|
44999
|
-
} catch (e) {
|
|
45000
|
-
return { idx, res: { ok: false, error: String(e) } };
|
|
45001
|
-
} finally {
|
|
45002
|
-
sem.release();
|
|
45003
|
-
}
|
|
45004
|
-
})
|
|
45005
|
-
);
|
|
45006
|
-
const responding = results.filter(
|
|
45007
|
-
(r) => r.res.ok
|
|
45008
|
-
);
|
|
45009
|
-
if (responding.length === 0) {
|
|
45010
|
-
return { outcome: "CONSENSUS_UNAVAILABLE", rounds: round, finalArtifact: artifact, history };
|
|
45011
|
-
}
|
|
45012
|
-
const headOutcomes = input.heads.map((persona) => ({
|
|
45013
|
-
persona,
|
|
45014
|
-
responded: false
|
|
45015
|
-
}));
|
|
45016
|
-
const issues = [];
|
|
45017
|
-
for (const { idx, res } of responding) {
|
|
45018
|
-
const parsed = parseVerdict(res.text);
|
|
45019
|
-
parsed.issues.forEach((iss, n) => issues.push({ ...iss, id: `r${round}-h${idx}-${n}` }));
|
|
45020
|
-
const outcome = headOutcomes[idx];
|
|
45021
|
-
if (outcome) {
|
|
45022
|
-
outcome.responded = true;
|
|
45023
|
-
outcome.verdict = parsed.verdict;
|
|
45024
|
-
}
|
|
45025
|
-
if (parsed.verdict !== "APPROVE" && parsed.issues.length === 0 && parsed.parseFallbacks.length === 0) {
|
|
45026
|
-
issues.push({
|
|
45027
|
-
category: "ambiguity",
|
|
45028
|
-
text: `head ${input.heads[idx] ?? idx}/${idx} returned ${parsed.verdict} but no parseable critical issues (possible malformed/blocked review)`,
|
|
45029
|
-
id: `r${round}-h${idx}-0`
|
|
45030
|
-
});
|
|
45031
|
-
}
|
|
45032
|
-
}
|
|
45033
|
-
let adj;
|
|
45034
|
-
if (issues.length > 0) {
|
|
45035
|
-
if (calls + 1 > input.maxHeadCalls) {
|
|
45036
|
-
return { outcome: "CONSENSUS_BLOCKED", rounds: round, finalArtifact: artifact, history, ...decision ? { escalation: decision } : {} };
|
|
45037
|
-
}
|
|
45038
|
-
calls++;
|
|
45039
|
-
try {
|
|
45040
|
-
adj = await adjudicate(issues, { runArbiter: input.runArbiter });
|
|
45041
|
-
} catch {
|
|
45042
|
-
adj = { adjudicatedVerdict: "REQUEST_CHANGES", accepted: issues, dismissed: [] };
|
|
45043
|
-
}
|
|
45044
|
-
} else {
|
|
45045
|
-
adj = { adjudicatedVerdict: "APPROVE", accepted: [], dismissed: [] };
|
|
45046
|
-
}
|
|
45047
|
-
history.push({ round, accepted: adj.accepted, dismissed: adj.dismissed, headOutcomes });
|
|
45048
|
-
adj.dismissed.forEach((d) => dismissedFwd.push(`- ${d.id}: ${d.reason}`));
|
|
45049
|
-
if (adj.accepted.length === 0) {
|
|
45050
|
-
return { outcome: "CONVERGED", rounds: round, finalArtifact: artifact, history, ...decision ? { escalation: decision } : {} };
|
|
45051
|
-
}
|
|
45052
|
-
if (round === effectiveRounds) {
|
|
45053
|
-
if (!decision) {
|
|
45054
|
-
decision = diagnoseStall(history, input.rounds);
|
|
45055
|
-
}
|
|
45056
|
-
if (decision.grantedRounds > 0 && !escalated) {
|
|
45057
|
-
escalated = true;
|
|
45058
|
-
effectiveRounds = Math.min(effectiveRounds + decision.grantedRounds, input.rounds * 2);
|
|
45059
|
-
} else {
|
|
45060
|
-
return { outcome: "CONSENSUS_BLOCKED", rounds: round, finalArtifact: artifact, history, ...decision ? { escalation: decision } : {} };
|
|
45061
|
-
}
|
|
45062
|
-
}
|
|
45063
|
-
artifact = await input.revise(artifact, adj.accepted);
|
|
45064
|
-
}
|
|
45065
|
-
return { outcome: "CONSENSUS_BLOCKED", rounds: effectiveRounds, finalArtifact: artifact, history };
|
|
45066
|
-
}
|
|
45067
|
-
|
|
45068
45652
|
// src/consensus/deliverable-gate.ts
|
|
45069
45653
|
async function runDeliverableGate(_target, deps) {
|
|
45070
45654
|
if (!await deps.consensusEnabled()) return { outcome: "SKIPPED" };
|
|
@@ -50071,12 +50655,13 @@ var DEFAULT_DIRECTOR_BRIDGE_ALLOWED_ORIGINS = [
|
|
|
50071
50655
|
"http://localhost:*",
|
|
50072
50656
|
"http://127.0.0.1:*",
|
|
50073
50657
|
"http://[::1]:*",
|
|
50074
|
-
// Staging
|
|
50075
|
-
//
|
|
50076
|
-
//
|
|
50077
|
-
//
|
|
50078
|
-
|
|
50079
|
-
"https://
|
|
50658
|
+
// Staging decoupled onto the stable custom domain staging.mrrlin.com on 2026-08-14 (Cloudflare DNS
|
|
50659
|
+
// in front of the mrrlin3 Vercel project). The Vercel-owned web alias (mrrlin-web-mrrlin3.vercel.app)
|
|
50660
|
+
// and the old app.mrrlin.dev origin are dropped from the defaults: the browser now reaches the
|
|
50661
|
+
// bridge from the owned domain, which is stable across future Vercel project/team moves.
|
|
50662
|
+
// See docs/reference/deployments.md.
|
|
50663
|
+
"https://staging.mrrlin.com",
|
|
50664
|
+
// Production web (unchanged).
|
|
50080
50665
|
"https://mrrlin-web.vercel.app"
|
|
50081
50666
|
];
|
|
50082
50667
|
function normalizeReasoningEffort(value) {
|
|
@@ -56231,249 +56816,6 @@ async function searchKnowledge(query, options = {}) {
|
|
|
56231
56816
|
return searchKnowledgeIndex(index, query, options.limit ?? 5);
|
|
56232
56817
|
}
|
|
56233
56818
|
|
|
56234
|
-
// src/consensus/spec-identity.ts
|
|
56235
|
-
function validateSpecIdentity(markdown, taskId) {
|
|
56236
|
-
const target = taskId.toUpperCase();
|
|
56237
|
-
const fieldToken = frontmatterTaskToken(markdown);
|
|
56238
|
-
if (fieldToken !== null && fieldToken.toUpperCase() !== target) return false;
|
|
56239
|
-
const headingToken = firstHeadingToken(markdown);
|
|
56240
|
-
if (headingToken !== null && headingToken.toUpperCase() !== target) return false;
|
|
56241
|
-
return true;
|
|
56242
|
-
}
|
|
56243
|
-
var TASK_ID_TOKEN = /\b[A-Za-z]{1,12}-\d{1,10}\b/;
|
|
56244
|
-
var ATX_HEADING = /^ {0,3}#{1,6}\s+(.*)$/;
|
|
56245
|
-
var FRONTMATTER_TASK = /^task(?:_id)?:\s*(.+)$/i;
|
|
56246
|
-
function frontmatterTaskToken(markdown) {
|
|
56247
|
-
const lines2 = markdown.split("\n");
|
|
56248
|
-
const firstContent = lines2.findIndex((l) => l.trim() !== "");
|
|
56249
|
-
if (firstContent === -1 || lines2[firstContent]?.trim() !== "---") return null;
|
|
56250
|
-
for (let i = firstContent + 1; i < lines2.length; i++) {
|
|
56251
|
-
const trimmed = (lines2[i] ?? "").trim();
|
|
56252
|
-
if (trimmed === "---") break;
|
|
56253
|
-
const m = FRONTMATTER_TASK.exec(trimmed);
|
|
56254
|
-
if (m) return firstTaskIdToken(m[1] ?? "");
|
|
56255
|
-
}
|
|
56256
|
-
return null;
|
|
56257
|
-
}
|
|
56258
|
-
function firstHeadingToken(markdown) {
|
|
56259
|
-
const heading = firstHeading(markdown);
|
|
56260
|
-
return heading === null ? null : firstTaskIdToken(heading);
|
|
56261
|
-
}
|
|
56262
|
-
function firstHeading(markdown) {
|
|
56263
|
-
const lines2 = markdown.split("\n");
|
|
56264
|
-
let i = 0;
|
|
56265
|
-
const firstContent = lines2.findIndex((l) => l.trim() !== "");
|
|
56266
|
-
if (firstContent !== -1 && lines2[firstContent]?.trim() === "---") {
|
|
56267
|
-
i = firstContent + 1;
|
|
56268
|
-
while (i < lines2.length && lines2[i]?.trim() !== "---") i++;
|
|
56269
|
-
i++;
|
|
56270
|
-
}
|
|
56271
|
-
let inFence = false;
|
|
56272
|
-
let inComment = false;
|
|
56273
|
-
for (; i < lines2.length; i++) {
|
|
56274
|
-
const line = lines2[i] ?? "";
|
|
56275
|
-
const trimmed = line.trim();
|
|
56276
|
-
if (inComment) {
|
|
56277
|
-
if (trimmed.includes("-->")) inComment = false;
|
|
56278
|
-
continue;
|
|
56279
|
-
}
|
|
56280
|
-
if (trimmed.startsWith("```") || trimmed.startsWith("~~~")) {
|
|
56281
|
-
inFence = !inFence;
|
|
56282
|
-
continue;
|
|
56283
|
-
}
|
|
56284
|
-
if (inFence) continue;
|
|
56285
|
-
if (trimmed.startsWith("<!--")) {
|
|
56286
|
-
if (!trimmed.includes("-->")) inComment = true;
|
|
56287
|
-
continue;
|
|
56288
|
-
}
|
|
56289
|
-
const m = ATX_HEADING.exec(line);
|
|
56290
|
-
if (m) return m[1] ?? "";
|
|
56291
|
-
}
|
|
56292
|
-
return null;
|
|
56293
|
-
}
|
|
56294
|
-
function firstTaskIdToken(text) {
|
|
56295
|
-
const m = TASK_ID_TOKEN.exec(text);
|
|
56296
|
-
return m ? m[0] : null;
|
|
56297
|
-
}
|
|
56298
|
-
|
|
56299
|
-
// src/consensus/spec-gate.ts
|
|
56300
|
-
function mapAcceptedIssueToClarification(issue2) {
|
|
56301
|
-
const text = issue2.text.toLowerCase();
|
|
56302
|
-
let kind;
|
|
56303
|
-
if (issue2.category === "scope") {
|
|
56304
|
-
const directionCues = ["only", "or also", "narrower", "broader", "include", "exclude", "versus", "vs"];
|
|
56305
|
-
kind = directionCues.some((cue) => text.includes(cue)) ? "scope_direction" : "missing_requirement";
|
|
56306
|
-
} else {
|
|
56307
|
-
const acceptanceCues = ["acceptance", "done", "success", "criteria", "satisfy"];
|
|
56308
|
-
kind = acceptanceCues.some((cue) => text.includes(cue)) ? "acceptance_criteria_detail" : "default_choice";
|
|
56309
|
-
}
|
|
56310
|
-
return {
|
|
56311
|
-
kind,
|
|
56312
|
-
diagnostic: false,
|
|
56313
|
-
rawQuestion: issue2.text,
|
|
56314
|
-
whyBlocking: `reviewer raised a ${issue2.category} issue`,
|
|
56315
|
-
category: issue2.category
|
|
56316
|
-
};
|
|
56317
|
-
}
|
|
56318
|
-
var CLARIFICATION_CAP_BY_IMPACT = { low: 2, medium: 3, high: 5 };
|
|
56319
|
-
function resolveClarificationCap(impact) {
|
|
56320
|
-
const tier = impact?.trim().toLowerCase();
|
|
56321
|
-
if (tier === "medium") return CLARIFICATION_CAP_BY_IMPACT.medium;
|
|
56322
|
-
if (tier === "high") return CLARIFICATION_CAP_BY_IMPACT.high;
|
|
56323
|
-
return CLARIFICATION_CAP_BY_IMPACT.low;
|
|
56324
|
-
}
|
|
56325
|
-
function headIndexOf(id) {
|
|
56326
|
-
const match = /^r\d+-h(\d+)-\d+$/.exec(id);
|
|
56327
|
-
return match?.[1] !== void 0 ? Number(match[1]) : null;
|
|
56328
|
-
}
|
|
56329
|
-
function orderClarificationIssues(issues) {
|
|
56330
|
-
const distinctHeadsByKey = /* @__PURE__ */ new Map();
|
|
56331
|
-
for (const issue2 of issues) {
|
|
56332
|
-
const key = `${issue2.category} ${issue2.text}`;
|
|
56333
|
-
const heads = distinctHeadsByKey.get(key) ?? /* @__PURE__ */ new Set();
|
|
56334
|
-
const headIdx = headIndexOf(issue2.id);
|
|
56335
|
-
if (headIdx !== null) heads.add(headIdx);
|
|
56336
|
-
distinctHeadsByKey.set(key, heads);
|
|
56337
|
-
}
|
|
56338
|
-
const categoryRank = (category) => category === "scope" ? 0 : category === "ambiguity" ? 1 : 2;
|
|
56339
|
-
return issues.map((issue2, index) => ({ issue: issue2, index })).sort((a, b) => {
|
|
56340
|
-
const rankDiff = categoryRank(a.issue.category) - categoryRank(b.issue.category);
|
|
56341
|
-
if (rankDiff !== 0) return rankDiff;
|
|
56342
|
-
const headCountA = distinctHeadsByKey.get(`${a.issue.category} ${a.issue.text}`)?.size ?? 0;
|
|
56343
|
-
const headCountB = distinctHeadsByKey.get(`${b.issue.category} ${b.issue.text}`)?.size ?? 0;
|
|
56344
|
-
if (headCountA !== headCountB) return headCountB - headCountA;
|
|
56345
|
-
return a.index - b.index;
|
|
56346
|
-
}).map(({ issue: issue2 }) => issue2);
|
|
56347
|
-
}
|
|
56348
|
-
function applyClarificationCap(issues, impact) {
|
|
56349
|
-
const ordered = orderClarificationIssues(issues);
|
|
56350
|
-
const cap = resolveClarificationCap(impact);
|
|
56351
|
-
return {
|
|
56352
|
-
kept: ordered.slice(0, cap).map(mapAcceptedIssueToClarification),
|
|
56353
|
-
dropped: ordered.slice(cap).map(mapAcceptedIssueToClarification)
|
|
56354
|
-
};
|
|
56355
|
-
}
|
|
56356
|
-
async function runSpecGate(_target, deps, opts) {
|
|
56357
|
-
if (!await deps.consensusEnabled()) return { outcome: "SKIPPED" };
|
|
56358
|
-
const intent = await deps.readIntent();
|
|
56359
|
-
if (intent.taskId !== null && (intent.seed === null || intent.seed.trim() === "")) {
|
|
56360
|
-
return {
|
|
56361
|
-
outcome: "CONSENSUS_BLOCKED",
|
|
56362
|
-
operatorClarifications: [
|
|
56363
|
-
{
|
|
56364
|
-
kind: "missing_requirement",
|
|
56365
|
-
diagnostic: false,
|
|
56366
|
-
rawQuestion: "What are the requirements for this task? There is no operator request on record, so the spec cannot be validated against your intent \u2014 please state what this task must do.",
|
|
56367
|
-
whyBlocking: "operator_seed_request is empty",
|
|
56368
|
-
category: "scope"
|
|
56369
|
-
}
|
|
56370
|
-
]
|
|
56371
|
-
};
|
|
56372
|
-
}
|
|
56373
|
-
const baseHeads = resolveHeads(intent.domain, intent.impact);
|
|
56374
|
-
const extraHeads = opts?.extraHeads ?? [];
|
|
56375
|
-
const heads = [...baseHeads, ...extraHeads.filter((h) => !baseHeads.includes(h))];
|
|
56376
|
-
const spec = await deps.readSpec();
|
|
56377
|
-
if (parseAcceptanceCriteria(spec.markdownBody) === null) {
|
|
56378
|
-
return {
|
|
56379
|
-
outcome: "CONSENSUS_BLOCKED",
|
|
56380
|
-
operatorClarifications: [
|
|
56381
|
-
{
|
|
56382
|
-
kind: "acceptance_criteria_detail",
|
|
56383
|
-
diagnostic: false,
|
|
56384
|
-
rawQuestion: "This spec has no machine-usable acceptance criteria. Add an `## Acceptance criteria` section written as a markdown checklist (`- [ ] \u2026` items) describing exactly what the finished deliverable must satisfy \u2014 the review of the produced work is scored against it.",
|
|
56385
|
-
whyBlocking: "non-code spec is missing a parseable `## Acceptance criteria` checklist",
|
|
56386
|
-
category: "scope"
|
|
56387
|
-
}
|
|
56388
|
-
]
|
|
56389
|
-
};
|
|
56390
|
-
}
|
|
56391
|
-
let currentHash = spec.contentHash;
|
|
56392
|
-
let currentMarkdown = spec.markdownBody;
|
|
56393
|
-
let conflicted = false;
|
|
56394
|
-
const result = await runConsensus({
|
|
56395
|
-
artifact: spec.markdownBody,
|
|
56396
|
-
heads,
|
|
56397
|
-
rounds: deps.rounds,
|
|
56398
|
-
maxHeadCalls: deps.maxHeadCalls,
|
|
56399
|
-
runHead: deps.runHead,
|
|
56400
|
-
runArbiter: deps.runArbiter,
|
|
56401
|
-
revise: async (artifact, accepted) => {
|
|
56402
|
-
const revised = await deps.runReviser(artifact, accepted);
|
|
56403
|
-
if (!revised.ok) return artifact;
|
|
56404
|
-
if (intent.taskId !== null && !validateSpecIdentity(revised.text, intent.taskId)) {
|
|
56405
|
-
process.stderr.write(
|
|
56406
|
-
`[spec-gate] discarding drifted revision for ${spec.pageId}: heading does not identify ${intent.taskId}
|
|
56407
|
-
`
|
|
56408
|
-
);
|
|
56409
|
-
return artifact;
|
|
56410
|
-
}
|
|
56411
|
-
const cas = await deps.casUpdateSpec(revised.text, currentHash);
|
|
56412
|
-
if (!cas.ok) {
|
|
56413
|
-
conflicted = true;
|
|
56414
|
-
throw new Error("SPEC_CHANGED_DURING_REVIEW");
|
|
56415
|
-
}
|
|
56416
|
-
currentHash = cas.contentHash;
|
|
56417
|
-
currentMarkdown = revised.text;
|
|
56418
|
-
return revised.text;
|
|
56419
|
-
}
|
|
56420
|
-
// Only OUR conflict throw is swallowed (→ null). Anything else is a real
|
|
56421
|
-
// error and must propagate so it is not silently masked as a conflict.
|
|
56422
|
-
}).catch((e) => conflicted ? null : Promise.reject(e));
|
|
56423
|
-
if (conflicted || result === null) {
|
|
56424
|
-
await deps.writeEvidence({ specPageId: spec.pageId, outcome: "SPEC_CHANGED_DURING_REVIEW" });
|
|
56425
|
-
return { outcome: "SPEC_CHANGED_DURING_REVIEW" };
|
|
56426
|
-
}
|
|
56427
|
-
if (result.outcome === "CONVERGED" && intent.taskId !== null && !validateSpecIdentity(currentMarkdown, intent.taskId)) {
|
|
56428
|
-
process.stderr.write(
|
|
56429
|
-
`[spec-gate] blocking promotion for ${spec.pageId}: converged spec heading does not identify ${intent.taskId}
|
|
56430
|
-
`
|
|
56431
|
-
);
|
|
56432
|
-
return {
|
|
56433
|
-
outcome: "CONSENSUS_BLOCKED",
|
|
56434
|
-
diagnostics: [
|
|
56435
|
-
{
|
|
56436
|
-
kind: "spec_identity_mismatch",
|
|
56437
|
-
diagnostic: true,
|
|
56438
|
-
rawQuestion: `SPEC_IDENTITY_MISMATCH: this spec page gates ${intent.taskId}, but its heading identifies a different task \u2014 rewrite the spec heading and content for ${intent.taskId}, or re-link the task.`,
|
|
56439
|
-
whyBlocking: "converged spec content belongs to a different task than the one it gates",
|
|
56440
|
-
category: "scope"
|
|
56441
|
-
}
|
|
56442
|
-
]
|
|
56443
|
-
};
|
|
56444
|
-
}
|
|
56445
|
-
let keptClarifications = [];
|
|
56446
|
-
let droppedByCap = [];
|
|
56447
|
-
if (result.outcome === "CONSENSUS_BLOCKED") {
|
|
56448
|
-
const accepted = result.history.at(-1)?.accepted ?? [];
|
|
56449
|
-
const materialIssues = accepted.filter((issue2) => issue2.category === "ambiguity" || issue2.category === "scope");
|
|
56450
|
-
const capped = applyClarificationCap(materialIssues, intent.impact);
|
|
56451
|
-
keptClarifications = capped.kept;
|
|
56452
|
-
droppedByCap = capped.dropped;
|
|
56453
|
-
}
|
|
56454
|
-
await deps.writeEvidence({
|
|
56455
|
-
specPageId: spec.pageId,
|
|
56456
|
-
outcome: result.outcome,
|
|
56457
|
-
rounds: result.rounds,
|
|
56458
|
-
history: result.history,
|
|
56459
|
-
contentHash: currentHash,
|
|
56460
|
-
...droppedByCap.length > 0 ? { droppedByCap } : {}
|
|
56461
|
-
});
|
|
56462
|
-
if (result.outcome === "CONVERGED") {
|
|
56463
|
-
await deps.promoteSpecReady(currentHash);
|
|
56464
|
-
return { outcome: "CONVERGED" };
|
|
56465
|
-
}
|
|
56466
|
-
if (result.outcome === "CONSENSUS_BLOCKED") {
|
|
56467
|
-
return {
|
|
56468
|
-
outcome: "CONSENSUS_BLOCKED",
|
|
56469
|
-
...keptClarifications.length > 0 ? { operatorClarifications: keptClarifications } : {},
|
|
56470
|
-
...droppedByCap.length > 0 ? { droppedByCap } : {},
|
|
56471
|
-
...result.escalation ? { escalation: result.escalation } : {}
|
|
56472
|
-
};
|
|
56473
|
-
}
|
|
56474
|
-
return { outcome: result.outcome };
|
|
56475
|
-
}
|
|
56476
|
-
|
|
56477
56819
|
// src/consensus/code-gate.ts
|
|
56478
56820
|
var CODE_HEADS = ["code-reviewer"];
|
|
56479
56821
|
var CodeGateUnfixableError = class extends Error {
|
|
@@ -56930,13 +57272,29 @@ var HUMANIZE_TEMPLATE_BY_KIND = {
|
|
|
56930
57272
|
acceptance_criteria_detail: "The direction is clear, but the spec still needs a sharper success check. What should count as done?"
|
|
56931
57273
|
};
|
|
56932
57274
|
var HUMANIZE_CONTEXT_CAP = 200;
|
|
57275
|
+
function truncateToSentenceBoundary(text, cap) {
|
|
57276
|
+
if (text.length <= cap) return text;
|
|
57277
|
+
const sentences = text.match(/.+?[.!?](?=\s|$)/g);
|
|
57278
|
+
if (sentences) {
|
|
57279
|
+
let kept = "";
|
|
57280
|
+
let keptCount = 0;
|
|
57281
|
+
for (const sentence of sentences) {
|
|
57282
|
+
const candidate = kept ? `${kept} ${sentence.trim()}` : sentence.trim();
|
|
57283
|
+
if (candidate.length > cap) break;
|
|
57284
|
+
kept = candidate;
|
|
57285
|
+
keptCount++;
|
|
57286
|
+
}
|
|
57287
|
+
if (kept) return keptCount < sentences.length ? `${kept} \u2026` : kept;
|
|
57288
|
+
}
|
|
57289
|
+
const cutIdx = text.lastIndexOf(" ", cap);
|
|
57290
|
+
return text.slice(0, cutIdx > 0 ? cutIdx : cap).trim() + "\u2026";
|
|
57291
|
+
}
|
|
56933
57292
|
function humanizeClarificationBody(clarification) {
|
|
56934
57293
|
const prefix = HUMANIZE_TEMPLATE_BY_KIND[clarification.kind];
|
|
56935
|
-
|
|
56936
|
-
|
|
56937
|
-
|
|
56938
|
-
|
|
56939
|
-
}
|
|
57294
|
+
const context = truncateToSentenceBoundary(
|
|
57295
|
+
clarification.rawQuestion.replace(/\b(?:parseable|SPEC_IDENTITY_MISMATCH|machine-usable)\b/gi, "").replace(/\s+/g, " ").trim(),
|
|
57296
|
+
HUMANIZE_CONTEXT_CAP
|
|
57297
|
+
);
|
|
56940
57298
|
return context ? `${prefix} ${context}` : prefix;
|
|
56941
57299
|
}
|
|
56942
57300
|
var defaultNativeSpecGateBuilders = { buildSpecGateDeps, runSpecGate };
|
|
@@ -56962,7 +57320,7 @@ async function cachedResult(client, input) {
|
|
|
56962
57320
|
const review = await client.getSpecConsensusReview(
|
|
56963
57321
|
input.projectSlug,
|
|
56964
57322
|
input.pageId,
|
|
56965
|
-
|
|
57323
|
+
domainScopedReviewHash2(page.contentHash, domain2)
|
|
56966
57324
|
);
|
|
56967
57325
|
if (review?.outcome === "CONVERGED") {
|
|
56968
57326
|
const gatedTaskId = await resolveGatedTaskId(client, input.projectSlug, {
|
|
@@ -57075,7 +57433,7 @@ async function handleConsensusBlocked(result, ctx, input, resolvedTaskId) {
|
|
|
57075
57433
|
...suppressedAsAdjudicated.length ? { suppressedAsAdjudicated } : {}
|
|
57076
57434
|
} : void 0;
|
|
57077
57435
|
await ctx.client.recordSpecConsensusReview(input.projectSlug, input.pageId, {
|
|
57078
|
-
contentHash:
|
|
57436
|
+
contentHash: domainScopedReviewHash2(page.contentHash, domain2),
|
|
57079
57437
|
outcome: "CONSENSUS_BLOCKED_CLARIFICATION",
|
|
57080
57438
|
rounds: 0,
|
|
57081
57439
|
...suppressionEvidence ? { suppressionEvidence } : {}
|
|
@@ -57092,7 +57450,12 @@ async function handleConsensusBlocked(result, ctx, input, resolvedTaskId) {
|
|
|
57092
57450
|
if (filedQuestionCount > 0) {
|
|
57093
57451
|
error51 = `Consensus blocked on ${filedQuestionCount} clarification(s); filed as operator question(s).`;
|
|
57094
57452
|
} else if (diagnostics.length > 0 && operatorClarifications.length === 0) {
|
|
57095
|
-
|
|
57453
|
+
const diagnosticKinds = new Set(diagnostics.map((d) => d.kind));
|
|
57454
|
+
if (diagnosticKinds.has("acceptance_criteria_unauthorable")) {
|
|
57455
|
+
error51 = "Consensus blocked: the spec has no machine-usable acceptance criteria and the reviser could not add them automatically. A human needs to write the `## Acceptance criteria` section directly, not an inbox answer.";
|
|
57456
|
+
} else {
|
|
57457
|
+
error51 = "Consensus blocked: the spec content does not match its linked task. This requires fixing the spec/task relationship directly, not an inbox answer.";
|
|
57458
|
+
}
|
|
57096
57459
|
} else if (operatorClarifications.length > 0 && toFile.length === 0 && suppressedAsAdjudicated.length > 0) {
|
|
57097
57460
|
error51 = `Consensus blocked, but all ${suppressedAsAdjudicated.length} clarification(s) were reworded repeats of already-adjudicated questions \u2014 nothing new filed.`;
|
|
57098
57461
|
} else if (operatorClarifications.length > 0) {
|
|
@@ -57111,8 +57474,41 @@ async function handleConsensusBlocked(result, ctx, input, resolvedTaskId) {
|
|
|
57111
57474
|
...resolvedTaskId ? { taskId: resolvedTaskId } : {}
|
|
57112
57475
|
};
|
|
57113
57476
|
}
|
|
57114
|
-
|
|
57115
|
-
|
|
57477
|
+
function shouldRetireGateQuestions(outcome) {
|
|
57478
|
+
return outcome === "CONVERGED";
|
|
57479
|
+
}
|
|
57480
|
+
async function retireGateQuestionsOnConverge(ctx, input, resolvedTaskId) {
|
|
57481
|
+
if (resolvedTaskId === null) return;
|
|
57482
|
+
try {
|
|
57483
|
+
const page = await ctx.client.getWikiPage(input.projectSlug, input.pageId);
|
|
57484
|
+
const contentHash = page?.contentHash ?? null;
|
|
57485
|
+
const inboxItems = await ctx.client.listInboxItems(input.projectSlug, { taskId: resolvedTaskId });
|
|
57486
|
+
for (const item of inboxItems) {
|
|
57487
|
+
if (item.origin !== "gate" || item.status !== "pending" || item.kind !== "question" || item.taskId != null && item.taskId !== resolvedTaskId) {
|
|
57488
|
+
continue;
|
|
57489
|
+
}
|
|
57490
|
+
try {
|
|
57491
|
+
await ctx.client.decideInboxItem(input.projectSlug, item.id, {
|
|
57492
|
+
verdict: "rejected",
|
|
57493
|
+
decidedBy: "gate",
|
|
57494
|
+
reason: `Superseded by converged spec Specs/${input.pageId} (contentHash: ${contentHash ?? "unknown"})`
|
|
57495
|
+
});
|
|
57496
|
+
} catch (itemError) {
|
|
57497
|
+
process.stderr.write(
|
|
57498
|
+
`[mrrlin-mcp consensus] gate-question retirement failed for ${input.projectSlug}/${input.pageId} item ${item.id}: ${itemError instanceof Error ? itemError.message : String(itemError)}
|
|
57499
|
+
`
|
|
57500
|
+
);
|
|
57501
|
+
}
|
|
57502
|
+
}
|
|
57503
|
+
} catch (error51) {
|
|
57504
|
+
process.stderr.write(
|
|
57505
|
+
`[mrrlin-mcp consensus] gate-question retirement failed for ${input.projectSlug}/${input.pageId}: ${error51 instanceof Error ? error51.message : String(error51)}
|
|
57506
|
+
`
|
|
57507
|
+
);
|
|
57508
|
+
}
|
|
57509
|
+
}
|
|
57510
|
+
async function run(ctx, input, builders = defaultNativeSpecGateBuilders) {
|
|
57511
|
+
const result = await runNativeSpecGate(ctx, input, builders);
|
|
57116
57512
|
if (process.env[SHADOW_ENV_FLAG]) {
|
|
57117
57513
|
try {
|
|
57118
57514
|
const specPage = await ctx.client.getWikiPage(input.projectSlug, input.pageId);
|
|
@@ -57133,22 +57529,40 @@ async function run(ctx, input) {
|
|
|
57133
57529
|
}
|
|
57134
57530
|
}
|
|
57135
57531
|
let resolvedTaskId = null;
|
|
57532
|
+
let resolvedTaskTitle = null;
|
|
57136
57533
|
try {
|
|
57137
57534
|
const page = await ctx.client.getWikiPage(input.projectSlug, input.pageId);
|
|
57138
57535
|
resolvedTaskId = page?.taskId ?? null;
|
|
57139
57536
|
} catch {
|
|
57140
57537
|
}
|
|
57538
|
+
const resolveTitle = async () => {
|
|
57539
|
+
if (!resolvedTaskId) return;
|
|
57540
|
+
try {
|
|
57541
|
+
resolvedTaskTitle = (await ctx.client.getTask(input.projectSlug, resolvedTaskId)).title;
|
|
57542
|
+
} catch {
|
|
57543
|
+
}
|
|
57544
|
+
};
|
|
57141
57545
|
if (result.outcome === "CONSENSUS_BLOCKED") {
|
|
57142
57546
|
return handleConsensusBlocked(result, ctx, input, resolvedTaskId);
|
|
57143
57547
|
}
|
|
57548
|
+
if (shouldRetireGateQuestions(result.outcome)) {
|
|
57549
|
+
await retireGateQuestionsOnConverge(ctx, input, resolvedTaskId);
|
|
57550
|
+
}
|
|
57144
57551
|
switch (result.outcome) {
|
|
57145
57552
|
case "CONVERGED":
|
|
57146
57553
|
case "SKIPPED":
|
|
57147
|
-
|
|
57554
|
+
await resolveTitle();
|
|
57555
|
+
return {
|
|
57556
|
+
kind: "done",
|
|
57557
|
+
resultJson: JSON.stringify(result),
|
|
57558
|
+
finalReviewedHash: null,
|
|
57559
|
+
...resolvedTaskId ? { taskId: resolvedTaskId } : {},
|
|
57560
|
+
...resolvedTaskTitle ? { taskTitle: resolvedTaskTitle } : {}
|
|
57561
|
+
};
|
|
57148
57562
|
case "CONSENSUS_UNAVAILABLE":
|
|
57149
|
-
return { kind: "retry", failureReason: "provider_unavailable", error: "Consensus unavailable (provider/quota)." };
|
|
57563
|
+
return { kind: "retry", failureReason: "provider_unavailable", error: "Consensus unavailable (provider/quota).", ...resolvedTaskId ? { taskId: resolvedTaskId } : {} };
|
|
57150
57564
|
case "SPEC_CHANGED_DURING_REVIEW":
|
|
57151
|
-
return { kind: "retry", failureReason: "spec_changed", error: "Spec changed during review." };
|
|
57565
|
+
return { kind: "retry", failureReason: "spec_changed", error: "Spec changed during review.", ...resolvedTaskId ? { taskId: resolvedTaskId } : {} };
|
|
57152
57566
|
default:
|
|
57153
57567
|
return {
|
|
57154
57568
|
kind: "failed",
|
|
@@ -57159,7 +57573,8 @@ async function run(ctx, input) {
|
|
|
57159
57573
|
}
|
|
57160
57574
|
}
|
|
57161
57575
|
function onComplete2(outcome, input) {
|
|
57162
|
-
const taskId =
|
|
57576
|
+
const taskId = "taskId" in outcome ? outcome.taskId : void 0;
|
|
57577
|
+
const taskRef = taskId ? outcome.kind === "done" && outcome.taskTitle ? `${taskId} \u2014 ${outcome.taskTitle}` : taskId : null;
|
|
57163
57578
|
if (outcome.kind === "done") {
|
|
57164
57579
|
let specOutcome = "CONVERGED";
|
|
57165
57580
|
try {
|
|
@@ -57169,12 +57584,14 @@ function onComplete2(outcome, input) {
|
|
|
57169
57584
|
if (specOutcome === "SKIPPED") {
|
|
57170
57585
|
return {
|
|
57171
57586
|
kind: "info",
|
|
57172
|
-
|
|
57587
|
+
...taskId ? { taskId } : {},
|
|
57588
|
+
body: taskRef ? `Consensus is disabled for this project; the spec for ${taskRef} (Specs/${input.pageId}) was not gated and spec_ready is unchanged.` : `Consensus is disabled for this project; Specs/${input.pageId} was not gated and spec_ready is unchanged.`
|
|
57173
57589
|
};
|
|
57174
57590
|
}
|
|
57175
57591
|
return {
|
|
57176
57592
|
kind: "info",
|
|
57177
|
-
|
|
57593
|
+
...taskId ? { taskId } : {},
|
|
57594
|
+
body: taskRef ? `Consensus CONVERGED for ${taskRef} (spec Specs/${input.pageId}); spec_ready set. Ready to create an execution run.` : `Consensus CONVERGED for Specs/${input.pageId}; spec_ready set. Ready to create an execution run.`
|
|
57178
57595
|
};
|
|
57179
57596
|
}
|
|
57180
57597
|
if (outcome.kind === "failed" && (outcome.filedQuestionCount ?? 0) > 0) {
|
|
@@ -57183,14 +57600,14 @@ function onComplete2(outcome, input) {
|
|
|
57183
57600
|
kind: "info",
|
|
57184
57601
|
suppressInbox: true,
|
|
57185
57602
|
...taskId ? { taskId } : {},
|
|
57186
|
-
body: `Consensus blocked on Specs/${input.pageId}; filed ${n} clarifying question${n === 1 ? "" : "s"} \u2014 answer to unblock the run.`
|
|
57603
|
+
body: taskRef ? `Consensus blocked on ${taskRef} (Specs/${input.pageId}); filed ${n} clarifying question${n === 1 ? "" : "s"} \u2014 answer to unblock the run.` : `Consensus blocked on Specs/${input.pageId}; filed ${n} clarifying question${n === 1 ? "" : "s"} \u2014 answer to unblock the run.`
|
|
57187
57604
|
};
|
|
57188
57605
|
}
|
|
57189
|
-
const
|
|
57606
|
+
const handoffTaskRef = taskRef ? ` (${taskRef})` : "";
|
|
57190
57607
|
return {
|
|
57191
57608
|
kind: "handoff",
|
|
57192
57609
|
...taskId ? { taskId } : {},
|
|
57193
|
-
body: `The spec review stopped for Specs/${input.pageId}${
|
|
57610
|
+
body: `The spec review stopped for Specs/${input.pageId}${handoffTaskRef}. ${outcome.error} Review the item, consult the Director if needed, then continue in Codex or handle it manually.`
|
|
57194
57611
|
};
|
|
57195
57612
|
}
|
|
57196
57613
|
var consensusDescriptor = {
|
|
@@ -57214,7 +57631,7 @@ async function cachedResult2(client, input) {
|
|
|
57214
57631
|
const review = await client.getSpecConsensusReview(
|
|
57215
57632
|
input.projectSlug,
|
|
57216
57633
|
input.pageId,
|
|
57217
|
-
|
|
57634
|
+
domainScopedReviewHash2(page.contentHash, domain2)
|
|
57218
57635
|
);
|
|
57219
57636
|
if (review?.outcome === "CONVERGED") {
|
|
57220
57637
|
return { status: "already_converged", payload: { outcome: "CONVERGED", contentHash: page.contentHash } };
|
|
@@ -57224,6 +57641,22 @@ async function cachedResult2(client, input) {
|
|
|
57224
57641
|
}
|
|
57225
57642
|
return null;
|
|
57226
57643
|
}
|
|
57644
|
+
async function resolveTaskScope(ctx, input, { withTitle }) {
|
|
57645
|
+
let taskId = null;
|
|
57646
|
+
let taskTitle = null;
|
|
57647
|
+
try {
|
|
57648
|
+
const page = await ctx.client.getWikiPage(input.projectSlug, input.pageId);
|
|
57649
|
+
taskId = page?.taskId ?? null;
|
|
57650
|
+
} catch {
|
|
57651
|
+
}
|
|
57652
|
+
if (taskId && withTitle) {
|
|
57653
|
+
try {
|
|
57654
|
+
taskTitle = (await ctx.client.getTask(input.projectSlug, taskId)).title;
|
|
57655
|
+
} catch {
|
|
57656
|
+
}
|
|
57657
|
+
}
|
|
57658
|
+
return { taskId, taskTitle };
|
|
57659
|
+
}
|
|
57227
57660
|
async function run2(ctx, input) {
|
|
57228
57661
|
const deps = await buildDeliverableGateDeps({
|
|
57229
57662
|
client: ctx.client,
|
|
@@ -57237,7 +57670,14 @@ async function run2(ctx, input) {
|
|
|
57237
57670
|
deps
|
|
57238
57671
|
);
|
|
57239
57672
|
if (result.outcome === "CONVERGED" || result.outcome === "SKIPPED") {
|
|
57240
|
-
|
|
57673
|
+
const { taskId: taskId2, taskTitle } = await resolveTaskScope(ctx, input, { withTitle: true });
|
|
57674
|
+
return {
|
|
57675
|
+
kind: "done",
|
|
57676
|
+
resultJson: JSON.stringify(result),
|
|
57677
|
+
finalReviewedHash: null,
|
|
57678
|
+
...taskId2 ? { taskId: taskId2 } : {},
|
|
57679
|
+
...taskTitle ? { taskTitle } : {}
|
|
57680
|
+
};
|
|
57241
57681
|
}
|
|
57242
57682
|
const mapped = mapDeliverableGateResult(result);
|
|
57243
57683
|
if (mapped.action === "retry") {
|
|
@@ -57247,10 +57687,9 @@ async function run2(ctx, input) {
|
|
|
57247
57687
|
error: mapped.reason
|
|
57248
57688
|
};
|
|
57249
57689
|
}
|
|
57690
|
+
const { taskId } = await resolveTaskScope(ctx, input, { withTitle: false });
|
|
57250
57691
|
let filedQuestionCount = 0;
|
|
57251
57692
|
if (mapped.action === "questions") {
|
|
57252
|
-
const page = await ctx.client.getWikiPage(input.projectSlug, input.pageId);
|
|
57253
|
-
const taskId = page?.taskId ?? null;
|
|
57254
57693
|
if (taskId) {
|
|
57255
57694
|
filedQuestionCount = await fileDeliverableGateQuestions(ctx.client, input.projectSlug, taskId, mapped.clarifications);
|
|
57256
57695
|
}
|
|
@@ -57260,10 +57699,13 @@ async function run2(ctx, input) {
|
|
|
57260
57699
|
kind: "failed",
|
|
57261
57700
|
failureReason: "consensus_blocked",
|
|
57262
57701
|
error: error51,
|
|
57263
|
-
filedQuestionCount
|
|
57702
|
+
filedQuestionCount,
|
|
57703
|
+
...taskId ? { taskId } : {}
|
|
57264
57704
|
};
|
|
57265
57705
|
}
|
|
57266
57706
|
function onComplete3(outcome, input) {
|
|
57707
|
+
const taskId = outcome.kind === "failed" || outcome.kind === "done" ? outcome.taskId : void 0;
|
|
57708
|
+
const taskRef = taskId ? outcome.kind === "done" && outcome.taskTitle ? `${taskId} \u2014 ${outcome.taskTitle}` : taskId : null;
|
|
57267
57709
|
if (outcome.kind === "done") {
|
|
57268
57710
|
let specOutcome = "CONVERGED";
|
|
57269
57711
|
try {
|
|
@@ -57273,12 +57715,14 @@ function onComplete3(outcome, input) {
|
|
|
57273
57715
|
if (specOutcome === "SKIPPED") {
|
|
57274
57716
|
return {
|
|
57275
57717
|
kind: "info",
|
|
57276
|
-
|
|
57718
|
+
...taskId ? { taskId } : {},
|
|
57719
|
+
body: taskRef ? `Consensus is disabled for this project; the deliverable for ${taskRef} (Deliverables/${input.pageId}) was not gated.` : `Consensus is disabled for this project; Deliverables/${input.pageId} was not gated.`
|
|
57277
57720
|
};
|
|
57278
57721
|
}
|
|
57279
57722
|
return {
|
|
57280
57723
|
kind: "info",
|
|
57281
|
-
|
|
57724
|
+
...taskId ? { taskId } : {},
|
|
57725
|
+
body: taskRef ? `Deliverable consensus CONVERGED for ${taskRef} (Deliverables/${input.pageId}); it meets the acceptance criteria.` : `Deliverable consensus CONVERGED for Deliverables/${input.pageId}; it meets the acceptance criteria.`
|
|
57282
57726
|
};
|
|
57283
57727
|
}
|
|
57284
57728
|
if (outcome.kind === "failed" && (outcome.filedQuestionCount ?? 0) > 0) {
|
|
@@ -57286,12 +57730,14 @@ function onComplete3(outcome, input) {
|
|
|
57286
57730
|
return {
|
|
57287
57731
|
kind: "info",
|
|
57288
57732
|
suppressInbox: true,
|
|
57289
|
-
|
|
57733
|
+
...taskId ? { taskId } : {},
|
|
57734
|
+
body: taskRef ? `Deliverable consensus blocked on ${taskRef} (Deliverables/${input.pageId}); filed ${n} clarifying question${n === 1 ? "" : "s"} \u2014 answer to unblock the run.` : `Deliverable consensus blocked on Deliverables/${input.pageId}; filed ${n} clarifying question${n === 1 ? "" : "s"} \u2014 answer to unblock the run.`
|
|
57290
57735
|
};
|
|
57291
57736
|
}
|
|
57292
57737
|
return {
|
|
57293
57738
|
kind: "handoff",
|
|
57294
|
-
|
|
57739
|
+
...taskId ? { taskId } : {},
|
|
57740
|
+
body: taskRef ? `Deliverable consensus could not complete for ${taskRef} (Deliverables/${input.pageId}): ${outcome.error} Needs operator attention.` : `Deliverable consensus could not complete for Deliverables/${input.pageId}: ${outcome.error} Needs operator attention.`
|
|
57295
57741
|
};
|
|
57296
57742
|
}
|
|
57297
57743
|
var deliverableConsensusDescriptor = {
|
|
@@ -58654,7 +59100,7 @@ function createMrrlinTools(options) {
|
|
|
58654
59100
|
const review = await c.getSpecConsensusReview(
|
|
58655
59101
|
projectSlug,
|
|
58656
59102
|
pageId,
|
|
58657
|
-
|
|
59103
|
+
domainScopedReviewHash2(page.contentHash, reviewDomain)
|
|
58658
59104
|
);
|
|
58659
59105
|
if (review && review.outcome === "CONVERGED") {
|
|
58660
59106
|
try {
|