empirical-sdd 0.26.1 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -8,6 +8,21 @@ under the alpha rules in [docs/versioning.md](docs/versioning.md).
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.27.0] - 2026-08-21
12
+
13
+ ### Changed
14
+
15
+ - Made Tracker Policy v2 lifecycle comments human-first across GitHub, Linear,
16
+ and Jira, with plain-language status, focused action requests, friendly
17
+ evidence links, native Jira formatting, and provider-safe exact recovery
18
+ markers that remain compatible with existing comments.
19
+
20
+ ### Migration
21
+
22
+ - Existing Schema 5 and Tracker Policy v1/v2 repositories require no state
23
+ migration. Visibility cadence, ticket bindings, issue descriptions, and
24
+ historical comments remain compatible.
25
+
11
26
  ## [0.26.1] - 2026-08-21
12
27
 
13
28
  ### Fixed
@@ -193,7 +208,8 @@ Published through GitHub Actions trusted publishing with npm provenance.
193
208
 
194
209
  - Prepared and released package version `0.20.2`.
195
210
 
196
- [Unreleased]: https://github.com/goempirical/empirical-sdd/compare/v0.26.1...HEAD
211
+ [Unreleased]: https://github.com/goempirical/empirical-sdd/compare/v0.27.0...HEAD
212
+ [0.27.0]: https://github.com/goempirical/empirical-sdd/compare/v0.26.1...v0.27.0
197
213
  [0.26.1]: https://github.com/goempirical/empirical-sdd/compare/v0.26.0...v0.26.1
198
214
  [0.26.0]: https://github.com/goempirical/empirical-sdd/compare/v0.25.0...v0.26.0
199
215
  [0.25.0]: https://github.com/goempirical/empirical-sdd/compare/v0.24.1...v0.25.0
package/README.md CHANGED
@@ -4,7 +4,7 @@ Agent-neutral, resumable spec-driven development for coding agents. Empirical
4
4
  turns an ordinary change request into a deterministic workflow with durable
5
5
  state, reviewable evidence, and safe Git integration.
6
6
 
7
- > Empirical 0.26 is alpha software. It requires Node.js 22 or newer.
7
+ > Empirical 0.27 is alpha software. It requires Node.js 22 or newer.
8
8
 
9
9
  ## Install
10
10
 
package/dist/cli.js CHANGED
@@ -14862,7 +14862,7 @@ function verifyReceiptDigest(receipt) {
14862
14862
  throw new Error(`Evidence receipt ${parsed.id} has an asserted result inconsistent with its provenance.`);
14863
14863
  }
14864
14864
  }
14865
- var SCHEMA_VERSION = 5, POLICY_SCHEMA_VERSION = 2, MANIFEST_SCHEMA_VERSION = 2, RECEIPT_SCHEMA_VERSION = 1, PRODUCT_VERSION = "0.26.1", workflowSchema, executionModeSchema, riskFloorSchema, completionLevelSchema, phaseSchema, workflowStatusSchema, criterionSchema, evidenceKindSchema, commandPolicySchema, evidencePolicySchema, projectPolicySchema, impactManifestSchema, authorizationSchema, COMPLETION_ORDER, receiptProvenanceSchema, receiptBaseSchema, executedReceiptSchema, artifactRecordSchema, collectedReceiptSchema, evidenceReceiptSchema;
14865
+ var SCHEMA_VERSION = 5, POLICY_SCHEMA_VERSION = 2, MANIFEST_SCHEMA_VERSION = 2, RECEIPT_SCHEMA_VERSION = 1, PRODUCT_VERSION = "0.27.0", workflowSchema, executionModeSchema, riskFloorSchema, completionLevelSchema, phaseSchema, workflowStatusSchema, criterionSchema, evidenceKindSchema, commandPolicySchema, evidencePolicySchema, projectPolicySchema, impactManifestSchema, authorizationSchema, COMPLETION_ORDER, receiptProvenanceSchema, receiptBaseSchema, executedReceiptSchema, artifactRecordSchema, collectedReceiptSchema, evidenceReceiptSchema;
14866
14866
  var init_protocol = __esm(() => {
14867
14867
  init_zod();
14868
14868
  workflowSchema = exports_external.enum(["fast", "complex"]);
@@ -24050,7 +24050,7 @@ class ProjectStore {
24050
24050
  return { ...await migrateSchema4To5(this.root) };
24051
24051
  }
24052
24052
  if (rawVersion !== SCHEMA_VERSION) {
24053
- throw new EmpiricalError("MIGRATION_REQUIRED", `Schema ${rawVersion} is unsupported; Empirical 0.26 migrates only Schema 4 to Schema 5`);
24053
+ throw new EmpiricalError("MIGRATION_REQUIRED", `Schema ${rawVersion} is unsupported; Empirical 0.27 migrates only Schema 4 to Schema 5`);
24054
24054
  }
24055
24055
  await project.ensureProjectMetadata();
24056
24056
  return project.withResourceLock("specs", async () => {
@@ -37258,6 +37258,341 @@ function isMissingFile(error51) {
37258
37258
  return isRecord4(error51) && error51.code === "ENOENT";
37259
37259
  }
37260
37260
 
37261
+ // src/tracker-comments.ts
37262
+ var TRACKER_MARKER_ORIGIN = "https://github.com/goempirical/empirical-sdd";
37263
+ var EFFECT_KEY = /^sha256:[a-f0-9]{64}$/;
37264
+ var HUMAN_TEXT_LIMIT = 320;
37265
+ var WORK_LABEL_LIMIT = 96;
37266
+ var EVIDENCE_LIMIT = 10;
37267
+ var JIRA_SCAN_LIMIT = 1e4;
37268
+ var COMMENT_PROPERTY = "empirical-sdd-effect";
37269
+ function renderTrackerMilestone(provider, projection, effectKey) {
37270
+ const identity = trackerMilestoneIdentity(effectKey);
37271
+ const view = createTrackerMilestoneView(projection);
37272
+ if (provider === "github") {
37273
+ return {
37274
+ provider,
37275
+ body: [...renderMarkdownSections(view), "", identity.github].join(`
37276
+ `)
37277
+ };
37278
+ }
37279
+ if (provider === "linear") {
37280
+ return {
37281
+ provider,
37282
+ body: [...renderMarkdownSections(view), "", identity.linear].join(`
37283
+ `)
37284
+ };
37285
+ }
37286
+ return {
37287
+ provider,
37288
+ body: renderJiraMilestone(view, identity),
37289
+ property: { key: COMMENT_PROPERTY, value: identity.effectKey }
37290
+ };
37291
+ }
37292
+ function createTrackerMilestoneView(projection) {
37293
+ const summary = humanText(projection.summary, HUMAN_TEXT_LIMIT);
37294
+ const stopped = projection.status === "blocked" || projection.status === "awaiting_human";
37295
+ const actionText = stopped ? humanText(projection.blocker ?? projection.summary, HUMAN_TEXT_LIMIT) ?? (projection.status === "awaiting_human" ? "A decision or input is required before work can continue." : "Work cannot continue until this blocker is resolved.") : null;
37296
+ return {
37297
+ headline: milestoneHeadline(projection),
37298
+ work: humanizeFeature(projection.feature),
37299
+ summary: summary === actionText ? null : summary,
37300
+ action: actionText ? {
37301
+ label: projection.status === "awaiting_human" ? "Action needed" : "Blocker",
37302
+ text: actionText
37303
+ } : null,
37304
+ evidence: milestoneEvidence(projection.artifacts ?? [])
37305
+ };
37306
+ }
37307
+ function inspectTrackerMilestoneMarker(provider, comment, effectKey) {
37308
+ const identity = trackerMilestoneIdentity(effectKey);
37309
+ if (provider === "github" || provider === "linear") {
37310
+ if (typeof comment.body !== "string")
37311
+ return "absent";
37312
+ return inspectMarkdownMarker(comment.body, provider === "github" ? identity.github : identity.linear, identity);
37313
+ }
37314
+ const body = inspectJiraBodyMarker(comment.body, identity);
37315
+ const property = inspectJiraPropertyMarker(comment.properties, identity);
37316
+ if (body === "malformed" || property === "malformed")
37317
+ return "malformed";
37318
+ return body === "match" || property === "match" ? "match" : "absent";
37319
+ }
37320
+ function trackerMilestoneIdentity(effectKey) {
37321
+ if (!EFFECT_KEY.test(effectKey)) {
37322
+ throw new EmpiricalError("TRACKER_MARKER_INVALID", "Tracker milestone identity requires one exact effect digest");
37323
+ }
37324
+ const url2 = `${TRACKER_MARKER_ORIGIN}#empirical-milestone:${effectKey}`;
37325
+ return {
37326
+ effectKey,
37327
+ url: url2,
37328
+ legacyMarkdown: `[Empirical milestone](<${url2}>)`,
37329
+ github: `<!-- empirical-sdd-effect:${effectKey} -->`,
37330
+ linear: `_[Managed by Empirical](<${url2}>)_`
37331
+ };
37332
+ }
37333
+ function milestoneHeadline(projection) {
37334
+ if (projection.status === "awaiting_human")
37335
+ return "Input needed";
37336
+ if (projection.status === "blocked")
37337
+ return "Work is blocked";
37338
+ if (projection.status === "done" || projection.phase === "done") {
37339
+ switch (projection.completionLevel) {
37340
+ case "implemented":
37341
+ return "Implementation complete";
37342
+ case "verified":
37343
+ return "Verification complete";
37344
+ case "integrated":
37345
+ return "Integration complete";
37346
+ case "delivered":
37347
+ return "Delivery complete";
37348
+ case "published":
37349
+ return "Publication complete";
37350
+ default:
37351
+ return "Workflow complete";
37352
+ }
37353
+ }
37354
+ switch (projection.phase) {
37355
+ case "idle":
37356
+ case "shape":
37357
+ case "specify":
37358
+ case "design":
37359
+ return "Defining the work";
37360
+ case "plan":
37361
+ return "Plan ready";
37362
+ case "implement":
37363
+ case "context":
37364
+ return "Implementation in progress";
37365
+ case "verify":
37366
+ return "Verification in progress";
37367
+ case "review":
37368
+ return "Ready for review";
37369
+ case "integrate":
37370
+ return "Integration in progress";
37371
+ case "deliver":
37372
+ return "Delivery in progress";
37373
+ case "publish":
37374
+ return "Publication in progress";
37375
+ case "archive":
37376
+ return "Wrapping up";
37377
+ default:
37378
+ return "Work in progress";
37379
+ }
37380
+ }
37381
+ function humanizeFeature(feature) {
37382
+ const value = feature.split("-").filter(Boolean).join(" ");
37383
+ const sentence = `${value[0]?.toUpperCase() ?? ""}${value.slice(1)}`;
37384
+ return boundText(sentence, WORK_LABEL_LIMIT) ?? "Empirical work";
37385
+ }
37386
+ function humanText(value, limit) {
37387
+ if (!value?.trim())
37388
+ return null;
37389
+ const normalized = value.normalize("NFKC").replace(/\[([^\]\r\n]{0,128})\]\([^\)\r\n]{1,2048}\)/g, "$1").replace(/\bhttps?:\/\/\S+/gi, "[link omitted]").replace(/\b(?:bearer|basic)\s+[A-Za-z0-9._~+/=-]+/gi, "[REDACTED]").replace(/\b(?:gh[pousr]_|github_pat_|lin_api_)[A-Za-z0-9_]{16,}\b/gi, "[REDACTED]").replace(/((?:authorization|password|secret|api[_ -]?key|token)\s*[:=]\s*)\S+/gi, "$1[REDACTED]").replace(/\b(?:executed|collected)-[a-z0-9-]+\b/gi, "[REDACTED]").replace(/\b(?:sha256:)?[a-f0-9]{40,64}\b/gi, "[REDACTED]").replace(/empirical-(?:milestone|sdd-effect):/gi, "Empirical update ").replace(/<!--|-->/g, " ").replace(/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g, " ").replace(/@/g, "@").replace(/\s+/g, " ").trim();
37390
+ return boundText(normalized, limit);
37391
+ }
37392
+ function boundText(value, limit) {
37393
+ const points = [...value.trim()];
37394
+ if (points.length === 0)
37395
+ return null;
37396
+ if (points.length <= limit)
37397
+ return points.join("");
37398
+ return `${points.slice(0, Math.max(1, limit - 1)).join("").trimEnd()}…`;
37399
+ }
37400
+ function milestoneEvidence(artifacts) {
37401
+ const candidates = artifacts.flatMap((artifact) => {
37402
+ const url2 = safeEvidenceUrl(artifact.url);
37403
+ return url2 ? [{ base: evidenceLabel(artifact.mediaType), url: url2 }] : [];
37404
+ }).slice(0, EVIDENCE_LIMIT);
37405
+ const totals = new Map;
37406
+ for (const candidate of candidates) {
37407
+ totals.set(candidate.base, (totals.get(candidate.base) ?? 0) + 1);
37408
+ }
37409
+ const seen = new Map;
37410
+ return candidates.map((candidate) => {
37411
+ const index = (seen.get(candidate.base) ?? 0) + 1;
37412
+ seen.set(candidate.base, index);
37413
+ return {
37414
+ label: (totals.get(candidate.base) ?? 0) > 1 ? `${candidate.base} ${index}` : candidate.base,
37415
+ url: candidate.url
37416
+ };
37417
+ });
37418
+ }
37419
+ function evidenceLabel(mediaType) {
37420
+ if (mediaType.startsWith("image/"))
37421
+ return "Screenshot";
37422
+ if (mediaType === "application/pdf")
37423
+ return "Document";
37424
+ if (mediaType === "application/json")
37425
+ return "Report";
37426
+ if (mediaType === "text/plain" || mediaType === "text/markdown")
37427
+ return "Notes";
37428
+ return "Evidence";
37429
+ }
37430
+ function safeEvidenceUrl(value) {
37431
+ if (!value || value !== value.trim() || value.length > 2048 || /[\u0000-\u001f\u007f<>]/.test(value)) {
37432
+ return null;
37433
+ }
37434
+ if (hasUnsafeLinkMaterial(value))
37435
+ return null;
37436
+ try {
37437
+ const url2 = new URL(value);
37438
+ if (url2.protocol !== "https:" || url2.username || url2.password)
37439
+ return null;
37440
+ let decoded;
37441
+ try {
37442
+ decoded = decodeURIComponent(`${url2.pathname}${url2.search}${url2.hash}`);
37443
+ } catch {
37444
+ return null;
37445
+ }
37446
+ if (hasUnsafeLinkMaterial(decoded))
37447
+ return null;
37448
+ return url2.toString();
37449
+ } catch {
37450
+ return null;
37451
+ }
37452
+ }
37453
+ function hasUnsafeLinkMaterial(value) {
37454
+ return /\b(?:bearer|basic)\s+/i.test(value) || /\b(?:gh[pousr]_|github_pat_|lin_api_)[A-Za-z0-9_]{16,}\b/i.test(value) || /(?:authorization|password|secret|api[_ -]?key|token)\s*[:=]/i.test(value) || /(?:empirical-(?:milestone|sdd-effect):|sha256:)/i.test(value);
37455
+ }
37456
+ function renderMarkdownSections(view) {
37457
+ const lines = [
37458
+ `## ${escapeMarkdown(view.headline)}`,
37459
+ `**${escapeMarkdown(view.work)}**`
37460
+ ];
37461
+ if (view.summary)
37462
+ lines.push("", escapeMarkdown(view.summary));
37463
+ if (view.action) {
37464
+ lines.push("", `### ${view.action.label}`, escapeMarkdown(view.action.text));
37465
+ }
37466
+ if (view.evidence.length > 0) {
37467
+ lines.push("", "### Evidence", ...view.evidence.map((entry) => `- [${entry.label}](<${entry.url}>)`));
37468
+ }
37469
+ return lines;
37470
+ }
37471
+ function escapeMarkdown(value) {
37472
+ return value.replace(/([!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~])/g, "\\$1");
37473
+ }
37474
+ function renderJiraMilestone(view, identity) {
37475
+ const content = [
37476
+ jiraHeading(view.headline, 2),
37477
+ jiraParagraph(view.work, [{ type: "strong" }])
37478
+ ];
37479
+ if (view.summary)
37480
+ content.push(jiraParagraph(view.summary));
37481
+ if (view.action) {
37482
+ content.push(jiraHeading(view.action.label, 3), jiraParagraph(view.action.text));
37483
+ }
37484
+ if (view.evidence.length > 0) {
37485
+ content.push(jiraHeading("Evidence", 3), {
37486
+ type: "bulletList",
37487
+ content: view.evidence.map((entry) => ({
37488
+ type: "listItem",
37489
+ content: [jiraParagraph(entry.label, [{ type: "link", attrs: { href: entry.url } }])]
37490
+ }))
37491
+ });
37492
+ }
37493
+ content.push(jiraParagraph("Managed by Empirical", [
37494
+ { type: "em" },
37495
+ { type: "link", attrs: { href: identity.url } }
37496
+ ]));
37497
+ return { version: 1, type: "doc", content };
37498
+ }
37499
+ function jiraHeading(text, level) {
37500
+ return {
37501
+ type: "heading",
37502
+ attrs: { level },
37503
+ content: [{ type: "text", text }]
37504
+ };
37505
+ }
37506
+ function jiraParagraph(text, marks = []) {
37507
+ return {
37508
+ type: "paragraph",
37509
+ content: [{ type: "text", text, ...marks.length > 0 ? { marks } : {} }]
37510
+ };
37511
+ }
37512
+ function inspectMarkdownMarker(body, current, identity) {
37513
+ const related = countOccurrences(body, identity.effectKey);
37514
+ if (related === 0)
37515
+ return "absent";
37516
+ const lines = body.split(/\r?\n/);
37517
+ const recognized = lines.filter((line) => line === current || line === identity.legacyMarkdown).length;
37518
+ return related === 1 && recognized === 1 ? "match" : "malformed";
37519
+ }
37520
+ function inspectJiraBodyMarker(body, identity) {
37521
+ const scan = { related: 0, recognized: 0 };
37522
+ let visited = 0;
37523
+ const walk = (value) => {
37524
+ visited += 1;
37525
+ if (visited > JIRA_SCAN_LIMIT) {
37526
+ scan.related += 1;
37527
+ return;
37528
+ }
37529
+ if (typeof value === "string") {
37530
+ scan.related += countOccurrences(value, identity.effectKey);
37531
+ if (value === identity.legacyMarkdown)
37532
+ scan.recognized += 1;
37533
+ return;
37534
+ }
37535
+ if (Array.isArray(value)) {
37536
+ for (const entry of value)
37537
+ walk(entry);
37538
+ return;
37539
+ }
37540
+ if (!isRecord5(value))
37541
+ return;
37542
+ if (value.type === "text" && value.text === "Managed by Empirical" && Array.isArray(value.marks)) {
37543
+ for (const rawMark of value.marks) {
37544
+ if (!isRecord5(rawMark) || rawMark.type !== "link" || !isRecord5(rawMark.attrs))
37545
+ continue;
37546
+ if (rawMark.attrs.href === identity.url)
37547
+ scan.recognized += 1;
37548
+ }
37549
+ }
37550
+ for (const entry of Object.values(value))
37551
+ walk(entry);
37552
+ };
37553
+ walk(body);
37554
+ if (scan.related === 0)
37555
+ return "absent";
37556
+ return scan.related === 1 && scan.recognized === 1 ? "match" : "malformed";
37557
+ }
37558
+ function inspectJiraPropertyMarker(properties, identity) {
37559
+ const values = [];
37560
+ if (Array.isArray(properties)) {
37561
+ for (const rawProperty of properties) {
37562
+ if (isRecord5(rawProperty) && rawProperty.key === COMMENT_PROPERTY)
37563
+ values.push(rawProperty.value);
37564
+ }
37565
+ } else if (isRecord5(properties) && Object.hasOwn(properties, COMMENT_PROPERTY)) {
37566
+ values.push(properties[COMMENT_PROPERTY]);
37567
+ }
37568
+ if (values.length === 0)
37569
+ return "absent";
37570
+ let related = 0;
37571
+ let recognized = 0;
37572
+ for (const value of values) {
37573
+ if (typeof value !== "string")
37574
+ continue;
37575
+ related += countOccurrences(value, identity.effectKey);
37576
+ if (value === identity.effectKey || value === identity.legacyMarkdown)
37577
+ recognized += 1;
37578
+ }
37579
+ if (related === 0)
37580
+ return "absent";
37581
+ return related === 1 && recognized === 1 ? "match" : "malformed";
37582
+ }
37583
+ function countOccurrences(value, token) {
37584
+ let count = 0;
37585
+ let offset = 0;
37586
+ while ((offset = value.indexOf(token, offset)) !== -1) {
37587
+ count += 1;
37588
+ offset += token.length;
37589
+ }
37590
+ return count;
37591
+ }
37592
+ function isRecord5(value) {
37593
+ return typeof value === "object" && value !== null && !Array.isArray(value);
37594
+ }
37595
+
37261
37596
  // src/tracking.ts
37262
37597
  init_protocol();
37263
37598
  var TRACKER_SCHEMA_VERSION = 2;
@@ -38535,8 +38870,7 @@ async function projectRemoteTicketV2(root, policy, binding, pending, credentials
38535
38870
  if (shouldPublishMilestone(policy, binding, pending.projection)) {
38536
38871
  const commentKey = trackerEffectKey(policy, pending.projection, "comment");
38537
38872
  if (!hasTrackerEffect(activePending, commentKey)) {
38538
- const marker2 = milestoneMarker(commentKey);
38539
- const commentId = await publishRemoteMilestone(policy, binding, renderMilestone(pending.projection, marker2), marker2, credentials, dependencies);
38873
+ const commentId = await publishRemoteMilestone(policy, binding, pending.projection, commentKey, credentials, dependencies);
38540
38874
  activePending = await acknowledgeTrackerEffect(root, activePending, { key: commentKey, kind: "comment", remoteId: commentId, at: now(dependencies) });
38541
38875
  }
38542
38876
  for (const artifact of pending.projection.artifacts ?? []) {
@@ -38626,8 +38960,12 @@ async function transitionRemoteTicketV2(policy, binding, projection, credentials
38626
38960
  }
38627
38961
  return { ...binding, url: `${jiraOrigin(policy)}/browse/${encodeURIComponent(binding.remoteKey)}` };
38628
38962
  }
38629
- async function publishRemoteMilestone(policy, binding, body, marker2, credentials, dependencies) {
38963
+ async function publishRemoteMilestone(policy, binding, projection, effectKey, credentials, dependencies) {
38630
38964
  if (policy.provider === "github") {
38965
+ const milestone2 = renderTrackerMilestone("github", projection, effectKey);
38966
+ if (milestone2.provider !== "github") {
38967
+ throw new EmpiricalError("TRACKER_RENDERER_MISMATCH", "GitHub milestone renderer returned another provider payload");
38968
+ }
38631
38969
  const token = accessTokenFor(credentials, "github");
38632
38970
  const found2 = [];
38633
38971
  let complete2 = false;
@@ -38639,8 +38977,16 @@ async function publishRemoteMilestone(policy, binding, body, marker2, credential
38639
38977
  }, [200], [token]);
38640
38978
  if (!Array.isArray(comments))
38641
38979
  throw new EmpiricalError("TRACKER_MALFORMED_RESPONSE", "GitHub milestone comments are malformed");
38642
- const matches = comments.map((entry) => record4(entry, "GitHub milestone comment")).filter((comment) => typeof comment.body === "string" && comment.body.includes(marker2));
38643
- found2.push(...matches.map((comment) => String(requiredNumber(comment, "id", "GitHub milestone comment id"))));
38980
+ for (const entry of comments) {
38981
+ const comment = record4(entry, "GitHub milestone comment");
38982
+ const inspection = inspectTrackerMilestoneMarker("github", comment, effectKey);
38983
+ if (inspection === "malformed") {
38984
+ throw new EmpiricalError("TRACKER_MARKER_MALFORMED", "A GitHub comment contains malformed milestone ownership evidence");
38985
+ }
38986
+ if (inspection === "match") {
38987
+ found2.push(String(requiredNumber(comment, "id", "GitHub milestone comment id")));
38988
+ }
38989
+ }
38644
38990
  if (comments.length < 100) {
38645
38991
  complete2 = true;
38646
38992
  break;
@@ -38656,11 +39002,15 @@ async function publishRemoteMilestone(policy, binding, body, marker2, credential
38656
39002
  method: "POST",
38657
39003
  url: `https://api.github.com/repos/${encodeURIComponent(policy.target.owner)}/${encodeURIComponent(policy.target.repository)}/issues/${encodeURIComponent(binding.remoteKey)}/comments`,
38658
39004
  headers: githubHeaders(token),
38659
- body: JSON.stringify({ body })
39005
+ body: JSON.stringify({ body: milestone2.body })
38660
39006
  }, [201], [token]);
38661
39007
  return String(requiredNumber(created2, "id", "GitHub milestone comment id"));
38662
39008
  }
38663
39009
  if (policy.provider === "linear") {
39010
+ const milestone2 = renderTrackerMilestone("linear", projection, effectKey);
39011
+ if (milestone2.provider !== "linear") {
39012
+ throw new EmpiricalError("TRACKER_RENDERER_MISMATCH", "Linear milestone renderer returned another provider payload");
39013
+ }
38664
39014
  const accessToken = linearAuthorizationFor(credentials);
38665
39015
  let after = null;
38666
39016
  const cursors = new Set;
@@ -38672,8 +39022,16 @@ async function publishRemoteMilestone(policy, binding, body, marker2, credential
38672
39022
  }`, { id: binding.remoteId, after }, accessToken, dependencies);
38673
39023
  const issue2 = record4(data2.issue, "Linear milestone issue");
38674
39024
  const comments = connection(issue2.comments, "Linear milestone comments");
38675
- const matches = comments.nodes.map((entry) => record4(entry, "Linear milestone comment")).filter((comment) => typeof comment.body === "string" && comment.body.includes(marker2));
38676
- found2.push(...matches.map((comment) => requiredString(comment, "id", "Linear milestone comment id")));
39025
+ for (const entry of comments.nodes) {
39026
+ const comment = record4(entry, "Linear milestone comment");
39027
+ const inspection = inspectTrackerMilestoneMarker("linear", comment, effectKey);
39028
+ if (inspection === "malformed") {
39029
+ throw new EmpiricalError("TRACKER_MARKER_MALFORMED", "A Linear comment contains malformed milestone ownership evidence");
39030
+ }
39031
+ if (inspection === "match") {
39032
+ found2.push(requiredString(comment, "id", "Linear milestone comment id"));
39033
+ }
39034
+ }
38677
39035
  if (!comments.hasNextPage) {
38678
39036
  complete2 = true;
38679
39037
  break;
@@ -38688,12 +39046,16 @@ async function publishRemoteMilestone(policy, binding, body, marker2, credential
38688
39046
  return found2[0];
38689
39047
  const data = await linearGraphql(`mutation Milestone($input: CommentCreateInput!) {
38690
39048
  commentCreate(input: $input) { success comment { id body } }
38691
- }`, { input: { issueId: binding.remoteId, body } }, accessToken, dependencies);
39049
+ }`, { input: { issueId: binding.remoteId, body: milestone2.body } }, accessToken, dependencies);
38692
39050
  const create = record4(data.commentCreate, "Linear commentCreate");
38693
39051
  if (create.success !== true)
38694
39052
  throw new EmpiricalError("TRACKER_MALFORMED_RESPONSE", "Linear did not confirm milestone comment creation");
38695
39053
  return requiredString(record4(create.comment, "Linear milestone comment"), "id", "Linear milestone comment id");
38696
39054
  }
39055
+ const milestone = renderTrackerMilestone("jira", projection, effectKey);
39056
+ if (milestone.provider !== "jira") {
39057
+ throw new EmpiricalError("TRACKER_RENDERER_MISMATCH", "Jira milestone renderer returned another provider payload");
39058
+ }
38697
39059
  const jira = jiraRequestContext(policy.target.siteUrl, credentials);
38698
39060
  let startAt = 0;
38699
39061
  const found = [];
@@ -38701,13 +39063,21 @@ async function publishRemoteMilestone(policy, binding, body, marker2, credential
38701
39063
  for (let page = 0;page < TRACKER_DISCOVERY_LIMIT; page += 1) {
38702
39064
  const response = record4(await requestJson(dependencies, {
38703
39065
  method: "GET",
38704
- url: `${jira.apiOrigin}/rest/api/3/issue/${encodeURIComponent(binding.remoteKey)}/comment?startAt=${startAt}&maxResults=100`,
39066
+ url: `${jira.apiOrigin}/rest/api/3/issue/${encodeURIComponent(binding.remoteKey)}/comment?startAt=${startAt}&maxResults=100&expand=properties`,
38705
39067
  headers: jira.headers
38706
39068
  }, [200], jira.secrets), "Jira milestone comments");
38707
39069
  if (!Array.isArray(response.comments))
38708
39070
  throw new EmpiricalError("TRACKER_MALFORMED_RESPONSE", "Jira milestone comments are malformed");
38709
- const matches = response.comments.map((entry) => record4(entry, "Jira milestone comment")).filter((comment) => JSON.stringify(comment.body ?? {}).includes(marker2));
38710
- found.push(...matches.map((comment) => requiredString(comment, "id", "Jira milestone comment id")));
39071
+ for (const entry of response.comments) {
39072
+ const comment = record4(entry, "Jira milestone comment");
39073
+ const inspection = inspectTrackerMilestoneMarker("jira", comment, effectKey);
39074
+ if (inspection === "malformed") {
39075
+ throw new EmpiricalError("TRACKER_MARKER_MALFORMED", "A Jira comment contains malformed milestone ownership evidence");
39076
+ }
39077
+ if (inspection === "match") {
39078
+ found.push(requiredString(comment, "id", "Jira milestone comment id"));
39079
+ }
39080
+ }
38711
39081
  const total = optionalFiniteNumber(response.total) ?? response.comments.length;
38712
39082
  if (startAt + response.comments.length >= total) {
38713
39083
  complete = true;
@@ -38727,7 +39097,7 @@ async function publishRemoteMilestone(policy, binding, body, marker2, credential
38727
39097
  method: "POST",
38728
39098
  url: `${jira.apiOrigin}/rest/api/3/issue/${encodeURIComponent(binding.remoteKey)}/comment`,
38729
39099
  headers: jira.headers,
38730
- body: JSON.stringify({ body: jiraAdf(body), properties: [{ key: "empirical-sdd-effect", value: marker2 }] })
39100
+ body: JSON.stringify({ body: milestone.body, properties: [milestone.property] })
38731
39101
  }, [201], jira.secrets), "Jira milestone comment");
38732
39102
  return requiredString(created, "id", "Jira milestone comment id");
38733
39103
  }
@@ -38849,27 +39219,6 @@ function trackerEffectKey(policy, projection, kind, artifactDigest = null) {
38849
39219
  artifactDigest
38850
39220
  });
38851
39221
  }
38852
- function milestoneMarker(key) {
38853
- return `[Empirical milestone](<${LINEAR_MARKER_ORIGIN}#empirical-milestone:${key}>)`;
38854
- }
38855
- function renderMilestone(projection, marker2) {
38856
- const clean = (value) => value?.replace(/<!--|-->/g, "").replace(/\s+/g, " ").trim();
38857
- const summary = clean(projection.summary);
38858
- const blocker = clean(projection.blocker);
38859
- const artifacts = projection.artifacts ?? [];
38860
- return [
38861
- `## Empirical milestone · ${readableTrackerToken(projection.phase)}`,
38862
- marker2,
38863
- `- Feature: ${projection.feature}`,
38864
- `- Revision: ${projection.revision}`,
38865
- `- Progress: ${readableTrackerToken(projection.progress)}`,
38866
- `- Completion: ${readableCompletionLevel(projection.completionLevel)}`,
38867
- ...summary ? [`- Summary: ${summary}`] : [],
38868
- ...blocker ? [`- Blocker: ${blocker}`] : [],
38869
- ...artifacts.length ? ["- Reviewable artifacts:", ...artifacts.map((artifact) => artifact.url ? ` - ${artifact.path} · ${artifact.receiptId} · ${artifact.url}` : ` - ${artifact.path} · ${artifact.receiptId} · provider upload/link pending or unsupported`)] : []
38870
- ].join(`
38871
- `);
38872
- }
38873
39222
  function hasTrackerEffect(pending, key) {
38874
39223
  return (pending.effects ?? []).some((effect) => effect.key === key);
38875
39224
  }
@@ -40301,14 +40650,13 @@ Gate: ${summary}` : "";
40301
40650
  ].join(`
40302
40651
  `);
40303
40652
  }
40304
- var LINEAR_MARKER_ORIGIN = "https://github.com/goempirical/empirical-sdd";
40305
40653
  function linearProjectionBoundary(feature, boundary) {
40306
40654
  const label = boundary === "start" ? "Delivery status" : "Managed by Empirical SDD · local workflow is authoritative";
40307
40655
  const heading = boundary === "start" ? "## " : "";
40308
- return `${heading}[${label}](<${LINEAR_MARKER_ORIGIN}#empirical-sdd:${feature}:${boundary}>)`;
40656
+ return `${heading}[${label}](<${TRACKER_MARKER_ORIGIN}#empirical-sdd:${feature}:${boundary}>)`;
40309
40657
  }
40310
40658
  function linearCreateMarkerLine(intent, idempotencyKey) {
40311
- return `[Crash-safe synchronization enabled](<${LINEAR_MARKER_ORIGIN}#${intent.marker}:${idempotencyKey}>)`;
40659
+ return `[Crash-safe synchronization enabled](<${TRACKER_MARKER_ORIGIN}#${intent.marker}:${idempotencyKey}>)`;
40312
40660
  }
40313
40661
  function readableTrackerToken(value) {
40314
40662
  return value.split(/[-_]/).filter(Boolean).map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`).join(" ");
@@ -40320,7 +40668,7 @@ function renderLinearProjection(projection) {
40320
40668
  const summary = projection.summary?.replace(/<!--|-->/g, "").replace(/\s+/g, " ").trim();
40321
40669
  return [
40322
40670
  linearProjectionBoundary(projection.feature, "start"),
40323
- `[${readableTrackerToken(projection.progress)}](<${LINEAR_MARKER_ORIGIN}#${projection.marker}>)`,
40671
+ `[${readableTrackerToken(projection.progress)}](<${TRACKER_MARKER_ORIGIN}#${projection.marker}>)`,
40324
40672
  `- Phase: ${readableTrackerToken(projection.phase)}`,
40325
40673
  `- Workflow: ${readableTrackerToken(projection.status)}`,
40326
40674
  `- Revision: ${projection.revision}`,
@@ -40337,11 +40685,11 @@ function upsertLinearMarkerBlock(existing, projection) {
40337
40685
  const legacyEnd = `<!-- empirical-sdd:${projection.feature}:end -->`;
40338
40686
  const readableStart = linearProjectionBoundary(projection.feature, "start");
40339
40687
  const readableEnd = linearProjectionBoundary(projection.feature, "end");
40340
- const priorReadableStart = `[**Empirical SDD**](${LINEAR_MARKER_ORIGIN}#empirical-sdd:${projection.feature}:start)`;
40341
- const priorCanonicalStart = `**[Empirical SDD](<${LINEAR_MARKER_ORIGIN}#empirical-sdd:${projection.feature}:start>)`;
40342
- const priorReadableEnd = `[Managed projection](${LINEAR_MARKER_ORIGIN}#empirical-sdd:${projection.feature}:end)`;
40343
- const priorCanonicalEnd = `[Managed projection](<${LINEAR_MARKER_ORIGIN}#empirical-sdd:${projection.feature}:end>)`;
40344
- const priorHeadingStart = `## [Empirical SDD](<${LINEAR_MARKER_ORIGIN}#empirical-sdd:${projection.feature}:start>)`;
40688
+ const priorReadableStart = `[**Empirical SDD**](${TRACKER_MARKER_ORIGIN}#empirical-sdd:${projection.feature}:start)`;
40689
+ const priorCanonicalStart = `**[Empirical SDD](<${TRACKER_MARKER_ORIGIN}#empirical-sdd:${projection.feature}:start>)`;
40690
+ const priorReadableEnd = `[Managed projection](${TRACKER_MARKER_ORIGIN}#empirical-sdd:${projection.feature}:end)`;
40691
+ const priorCanonicalEnd = `[Managed projection](<${TRACKER_MARKER_ORIGIN}#empirical-sdd:${projection.feature}:end>)`;
40692
+ const priorHeadingStart = `## [Empirical SDD](<${TRACKER_MARKER_ORIGIN}#empirical-sdd:${projection.feature}:start>)`;
40345
40693
  const priorIndentedReadableEnd = ` ${readableEnd}`;
40346
40694
  const markerFragments = [
40347
40695
  `empirical-sdd:${projection.feature}:start`,
@@ -40382,12 +40730,12 @@ function linearCreateMarkerMatches(value, marker2) {
40382
40730
  const markerPrefix2 = `${marker2}:`;
40383
40731
  const legacyStartPrefix = `<!-- ${markerPrefix2}`;
40384
40732
  const readablePrefixes = [
40385
- `[Recovery reference](${LINEAR_MARKER_ORIGIN}#${markerPrefix2}`,
40386
- `[Crash-safe synchronization enabled](${LINEAR_MARKER_ORIGIN}#${markerPrefix2}`
40733
+ `[Recovery reference](${TRACKER_MARKER_ORIGIN}#${markerPrefix2}`,
40734
+ `[Crash-safe synchronization enabled](${TRACKER_MARKER_ORIGIN}#${markerPrefix2}`
40387
40735
  ];
40388
40736
  const canonicalReadablePrefixes = [
40389
- `[Recovery reference](<${LINEAR_MARKER_ORIGIN}#${markerPrefix2}`,
40390
- `[Crash-safe synchronization enabled](<${LINEAR_MARKER_ORIGIN}#${markerPrefix2}`
40737
+ `[Recovery reference](<${TRACKER_MARKER_ORIGIN}#${markerPrefix2}`,
40738
+ `[Crash-safe synchronization enabled](<${TRACKER_MARKER_ORIGIN}#${markerPrefix2}`
40391
40739
  ];
40392
40740
  const matches = [];
40393
40741
  const consumed = new Set;
@@ -40445,7 +40793,7 @@ function migrateLinearCreateMarker(existing, feature) {
40445
40793
  const lines = existing.replace(/\r\n/g, `
40446
40794
  `).split(`
40447
40795
  `);
40448
- const replacement = `[Crash-safe synchronization enabled](<${LINEAR_MARKER_ORIGIN}#${marker2}:${match.idempotencyKey}>)`;
40796
+ const replacement = `[Crash-safe synchronization enabled](<${TRACKER_MARKER_ORIGIN}#${marker2}:${match.idempotencyKey}>)`;
40449
40797
  return [
40450
40798
  ...lines.slice(0, match.start),
40451
40799
  replacement,
@@ -44091,7 +44439,7 @@ function mcpDefaultRoot() {
44091
44439
  }
44092
44440
  async function prepareTrackerOAuthHandoff(server, subject, dependencies) {
44093
44441
  const elicitation = server.server.getClientCapabilities()?.elicitation;
44094
- if (!isRecord5(elicitation) || !isRecord5(elicitation.url))
44442
+ if (!isRecord6(elicitation) || !isRecord6(elicitation.url))
44095
44443
  return;
44096
44444
  const authorization = await trackerOAuthAuthorization(subject, dependencies);
44097
44445
  if (!authorization)
@@ -44113,13 +44461,13 @@ async function toolResult(operation2) {
44113
44461
  const value = await operation2();
44114
44462
  return {
44115
44463
  content: [{ type: "text", text: JSON.stringify(value, null, 2) }],
44116
- structuredContent: isRecord5(value) ? value : { value }
44464
+ structuredContent: isRecord6(value) ? value : { value }
44117
44465
  };
44118
44466
  } catch (error51) {
44119
44467
  return { isError: true, content: [{ type: "text", text: asErrorMessage(error51) }] };
44120
44468
  }
44121
44469
  }
44122
- function isRecord5(value) {
44470
+ function isRecord6(value) {
44123
44471
  return typeof value === "object" && value !== null && !Array.isArray(value);
44124
44472
  }
44125
44473