gitifact 0.6.2 → 0.7.1

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.
Files changed (55) hide show
  1. package/README.md +113 -49
  2. package/dist/browser/assets/{Grid-D6Cs5W3k.js → Grid-DlI9bhVm.js} +1 -1
  3. package/dist/browser/assets/{Markdown-B-gnkr0l.js → Markdown-68DgpF6D.js} +5 -5
  4. package/dist/browser/assets/MetadataListItem-Bvdm7SCW.js +1 -0
  5. package/dist/browser/assets/{Table-OoWAxVgg.js → Table-GZptlOQa.js} +2 -2
  6. package/dist/browser/assets/about-LBmmoJtj.js +3 -0
  7. package/dist/browser/assets/activity-DRgs2s8a.jpg +0 -0
  8. package/dist/browser/assets/{activity-gA1jJdpp.js → activity-paVxugse.js} +1 -1
  9. package/dist/browser/assets/changelog-DMndHW2p.js +2 -0
  10. package/dist/browser/assets/{contributors._email-DHQfeuSG.js → contributors._email-Ch0pZAte.js} +1 -1
  11. package/dist/browser/assets/{contributors.index-Bd2NXXUX.js → contributors.index-BwSL0x8o.js} +1 -1
  12. package/dist/browser/assets/document-Ceyg9sKE.js +11 -0
  13. package/dist/browser/assets/feature-requirements-Ci6Hez1P.jpg +0 -0
  14. package/dist/browser/assets/{features._featureId-D69yHwjx.js → features._featureId-DtlIpe3Y.js} +1 -1
  15. package/dist/browser/assets/{features.index-Ct7YPi11.js → features.index-CsxYbWGn.js} +1 -1
  16. package/dist/browser/assets/getting-started-7e77o6gE.css +1 -0
  17. package/dist/browser/assets/getting-started-LVh16CZ8.js +1 -0
  18. package/dist/browser/assets/git-H0K9dpC3.js +1 -0
  19. package/dist/browser/assets/index-DClmARNh.js +48 -0
  20. package/dist/browser/assets/page-header-sbYRXZP4.js +531 -0
  21. package/dist/browser/assets/product-BIGIVBUa.js +10 -0
  22. package/dist/browser/assets/product-DNaVAIwO.css +1 -0
  23. package/dist/browser/assets/{product.index-CMOZsrSy.js → product.index-vzVtG_rC.js} +1 -1
  24. package/dist/browser/assets/project-wiki-BBWDVTfk.jpg +0 -0
  25. package/dist/browser/assets/request-state-J0QLm_8G.js +1 -0
  26. package/dist/browser/assets/{requirements-D5-_AEko.js → requirements-POF-07kZ.js} +1 -1
  27. package/dist/browser/assets/settings-eas7Zq56.js +1 -0
  28. package/dist/browser/assets/{wiki-Dh9TPhWY.js → wiki-fpAHbhnh.js} +1 -1
  29. package/dist/browser/index.html +10 -10
  30. package/dist/i18n/en/block.md +48 -0
  31. package/dist/i18n/en/changelog.md +185 -0
  32. package/dist/i18n/en/docs/commit.md +45 -0
  33. package/dist/i18n/en/docs/design.md +52 -0
  34. package/dist/i18n/en/docs/spec.md +62 -0
  35. package/dist/i18n/en/docs/wiki.default.md +27 -0
  36. package/dist/i18n/en/docs/wiki.md +43 -0
  37. package/dist/i18n/en/docs/workflow.md +72 -0
  38. package/dist/i18n/en/docs/writing.md +54 -0
  39. package/dist/i18n/ko/block.md +5 -5
  40. package/dist/i18n/ko/changelog.md +20 -0
  41. package/dist/i18n/ko/docs/workflow.md +8 -2
  42. package/dist/i18n/ko/docs/writing.md +1 -1
  43. package/dist/main.js +770 -185
  44. package/package.json +4 -4
  45. package/dist/browser/assets/MetadataListItem-CUyEFaPI.js +0 -1
  46. package/dist/browser/assets/about-Jsk-9EI-.js +0 -3
  47. package/dist/browser/assets/changelog-CiWFK5pK.js +0 -2
  48. package/dist/browser/assets/document-C8DLwdPF.js +0 -11
  49. package/dist/browser/assets/git-CStwM7mH.js +0 -1
  50. package/dist/browser/assets/index-jPjBDJh1.js +0 -48
  51. package/dist/browser/assets/page-header-Bn6J9gVJ.js +0 -147
  52. package/dist/browser/assets/product-BSEt07YP.css +0 -1
  53. package/dist/browser/assets/product-Beikpygp.js +0 -10
  54. package/dist/browser/assets/request-state-DkerHkm2.js +0 -1
  55. package/dist/browser/assets/settings-rKkznd6Q.js +0 -1
package/dist/main.js CHANGED
@@ -3514,10 +3514,130 @@ var messages_default = {
3514
3514
  "config.newerSchema": "\uC774 \uD504\uB85C\uC81D\uD2B8\uB294 \uB354 \uC0C8\uB85C\uC6B4 \uC800\uC7A5 \uADDC\uC57D(schemaVersion {version})\uC744 \uC501\uB2C8\uB2E4. npm install -g gitifact@latest\uB85C CLI\uB97C \uC62C\uB9B0 \uB4A4 \uB2E4\uC2DC \uC2E4\uD589\uD558\uC138\uC694."
3515
3515
  };
3516
3516
 
3517
+ // ../../packages/core/dist/shared/i18n/en/messages.json
3518
+ var messages_default2 = {
3519
+ "config.jsonUnreadable": "Could not read configuration JSON. Preserve the original file.",
3520
+ "config.unsupportedFormat": "Unsupported project format. Use a compatible CLI.",
3521
+ "config.invalidFields": "Invalid project configuration fields or values.",
3522
+ "config.jsonUnreadableShort": "Could not read configuration JSON.",
3523
+ "config.schemaFields": "Check the schemaVersion and baseline fields.",
3524
+ "document.pathTooLong": "Wiki page path exceeds the depth or length limit: {path}",
3525
+ "document.fileName": "Wiki filenames must use lowercase letters, digits, hyphens and .md; uppercase root names such as README.md and ARCHITECTURE.md are allowed: {path}",
3526
+ "document.folderName": "Wiki folder names must use lowercase letters, digits and hyphens: {path}",
3527
+ "design.invalidCharacters": "Invalid characters in the design.",
3528
+ "design.idMismatch": "The design ID must match the feature's S-ID.",
3529
+ "design.titleRequired": "A design title is required.",
3530
+ "design.bodyRequired": "Design content is required.",
3531
+ "design.singleTopHeading": "A design must have exactly one top-level heading.",
3532
+ "design.referenceFormat": "Check the design reference comment format.",
3533
+ "design.unclosedFence": "Unclosed code fence in the design.",
3534
+ "document.unsupportedPath": "Unsupported wiki path: {path}",
3535
+ "document.invalidCharacters": "Invalid document characters: {path}",
3536
+ "document.titleRequired": "A document title is required: {path}",
3537
+ "document.bodyRequired": "Document content is required: {path}",
3538
+ "document.singleTopHeading": "A document must have exactly one top-level heading: {path}",
3539
+ "document.noMarkerInBody": "Gitifact comments are not allowed in document content: {path}",
3540
+ "document.unclosedFence": "Unclosed document code fence: {path}",
3541
+ "document.duplicateId": "Duplicate wiki page ID in the repository: {id}",
3542
+ "document.caseOnlyPath": "Document paths differ only in case: {path}",
3543
+ "history.duplicateId": "Duplicate history record ID in the repository: {id}",
3544
+ "spec.orphanFile": "Design or reason file without a requirements document: {path}",
3545
+ "spec.unsupportedPath": "Unsupported specification path: {path}",
3546
+ "spec.invalidCharacters": "Invalid specification characters: {path}",
3547
+ "spec.requirementIdRequired": "A valid ID is required directly below the requirement heading: {path}",
3548
+ "spec.invalidFence": "Invalid code fence.",
3549
+ "spec.titleOnce": "The document title must appear once, at the beginning: {path}",
3550
+ "spec.emptyTitle": "Empty document title.",
3551
+ "spec.titleMissing": "Missing document title: {path}",
3552
+ "spec.emptyRequirementTitle": "Empty requirement title.",
3553
+ "spec.commentPosition": "Unsupported HTML comment position: {path}",
3554
+ "spec.atxOnly": "Only ATX headings starting in column one are supported: {path}",
3555
+ "spec.incomplete": "Incomplete specification: {path}",
3556
+ "spec.emptyOrDuplicateRequirement": "Empty content or duplicate requirement ID: {id}",
3557
+ "history.invalidJsonl": "Could not parse history JSONL.",
3558
+ "history.invalidRecord": "Invalid history record.",
3559
+ "history.invalidFields": "Invalid history record fields, IDs or targets.",
3560
+ "spec.duplicateId": "Duplicate specification ID: {id}",
3561
+ "spec.duplicateRequirementId": "Duplicate requirement ID in the repository: {id}",
3562
+ "history.changed": "An existing history record was modified: {id}",
3563
+ "history.unlinkedRequirement": "A new history record is not linked to this requirement change: {id}",
3564
+ "history.unlinkedDocument": "A new history record is not linked to this document change: {id}",
3565
+ "edit.invalidFeatureName": "Invalid feature folder name.",
3566
+ "edit.operationCount": "operations must contain 1 to 100 entries.",
3567
+ "edit.idGenerated": "Invalid generated ID.",
3568
+ "edit.idUnavailable": "Could not allocate a unique ID.",
3569
+ "edit.invalidOperation": "Invalid edit input.",
3570
+ "edit.unsupportedField": "Unsupported edit field.",
3571
+ "edit.emptyField": "Empty edit field: {field}",
3572
+ "edit.invalidCharacters": "Invalid characters in edit field: {field}",
3573
+ "edit.specNotFound": "Target specification not found: {path}",
3574
+ "edit.samePath": "Already at the same path.",
3575
+ "edit.documentPathExists": "Document path already exists: {path}",
3576
+ "edit.featureExists": "Feature already exists.",
3577
+ "edit.noDesignToDelete": "No design to delete.",
3578
+ "edit.specIdNotFound": "Specification ID not found.",
3579
+ "edit.requirementIdNotFound": "Requirement ID not found.",
3580
+ "edit.sameSpec": "Already belongs to this specification.",
3581
+ "edit.bodyChangesSpecStructure": "The content changes the specification structure.",
3582
+ "edit.bodyChangesDocumentStructure": "The content changes the document structure: {path}",
3583
+ "reason.keepEmptySpec": "Keep an empty specification to preserve its existing reasons: {id}",
3584
+ "reason.committedImmutable": "Committed reasons cannot be edited, deleted or reassigned: {id}",
3585
+ "reason.committedDocumentImmutable": "Committed wiki reasons cannot be edited, deleted or reassigned.",
3586
+ "reason.count": "reasons must be an array with at most 100 entries.",
3587
+ "reason.idGenerated": "Invalid generated reason ID.",
3588
+ "reason.idUnavailable": "Could not allocate a unique reason ID.",
3589
+ "reason.invalidInput": "Invalid reason input.",
3590
+ "reason.targetsRequired": "Check the reason and target requirements.",
3591
+ "reason.documentWithRequirements": "Document reasons cannot also target requirements.",
3592
+ "reason.documentTargetInvalid": "Invalid or duplicate document reason targets.",
3593
+ "reason.documentUnchanged": "Cannot attach a reason to a document with no final change: {id}",
3594
+ "reason.invalidTarget": "Invalid reason target.",
3595
+ "reason.targetKind": "Check the reason target type.",
3596
+ "reason.targetInvalid": "Invalid or duplicate reason targets.",
3597
+ "reason.requirementUnchanged": "Cannot attach a reason to a requirement with no final change: {id}",
3598
+ "reason.keepDeletedSpec": "Keep the original specification to record the deletion reason.",
3599
+ "readError.GIT_NOT_FOUND": "Git executable not found.",
3600
+ "readError.GIT_CONTEXT_OVERRIDE": "Environment variables override the Git context of the current checkout.",
3601
+ "readError.NOT_A_REPOSITORY": "The current directory is not inside a Git working tree.",
3602
+ "readError.UNSUPPORTED_REPOSITORY": "Cannot read a bare repository or Git administrative directory.",
3603
+ "readError.GIT_UNSUPPORTED": "Required Git options or object format are not supported.",
3604
+ "readError.GIT_FAILED": "Git read failed. Check repository access and Git status.",
3605
+ "readError.INVALID_GIT_OUTPUT": "Git status output does not match the supported format.",
3606
+ "readError.UNSUPPORTED_PATH_ENCODING": "A Git path cannot be represented as UTF-8 without loss.",
3607
+ "readError.REPOSITORY_CHANGED": "Repository state changed while reading. Try again shortly.",
3608
+ "readError.READ_LIMIT_EXCEEDED": "Git read exceeded the time or output limit.",
3609
+ "changelog.atLine": "Release notes, line {line}: {reason}",
3610
+ "changelog.emptyVersion": "Version {version} has no entries.",
3611
+ "changelog.invalidDate": "Invalid date: {text}",
3612
+ "changelog.order": "Versions must be ordered newest first: {version}",
3613
+ "changelog.sectionBeforeVersion": "A section heading appears before a version heading.",
3614
+ "changelog.duplicateSection": "Duplicate {section} section in a version.",
3615
+ "changelog.itemOutsideSection": "Entries must appear under ### Added, Changed, Removed or Fixed.",
3616
+ "changelog.unknownLine": 'Unrecognized line. Use "## X.Y.Z - YYYY-MM-DD" for versions, "### Added|Changed|Removed|Fixed" for sections and "- " for entries: {text}',
3617
+ "changelog.noVersions": "No versions found.",
3618
+ "frontmatter.required": "Frontmatter starting with --- is required on the first line.",
3619
+ "frontmatter.unclosed": "Missing closing --- for frontmatter.",
3620
+ "frontmatter.line": "Unsupported frontmatter line: {line}",
3621
+ "frontmatter.scalar": "Frontmatter values must be plain unquoted strings or double-quoted strings: {value}",
3622
+ "frontmatter.id": "The frontmatter id is not a valid {kind} ID.",
3623
+ "frontmatter.unknownKey": "Unsupported frontmatter key: {key}",
3624
+ "source.invalid": "A source needs a title, either path or url, and an optional note.",
3625
+ "source.url": "Source url must use http(s): {url}",
3626
+ "source.path": "Source path must be relative and end in .md: {path}",
3627
+ "edit.documentIdNotFound": "Wiki page ID not found: {id}",
3628
+ "edit.invalidSources": "sources must be an array with at most 50 entries.",
3629
+ "config.unsupportedSchema": "This project uses an older storage convention (schemaVersion {version}) that this CLI does not read. Pre-release conventions are not migrated. If .gitifact contains only config.json, gitifact init can replace it with the current convention.",
3630
+ "config.newerSchema": "This project uses a newer storage convention (schemaVersion {version}). Run npm install -g gitifact@latest and try again."
3631
+ };
3632
+
3517
3633
  // ../../packages/core/dist/shared/i18n/index.js
3518
3634
  var defaultLanguage = "ko";
3519
- var catalogs = { ko: messages_default };
3520
- function t(key, values = {}, lang = defaultLanguage) {
3635
+ var resolveLanguage = () => defaultLanguage;
3636
+ function configureCoreLanguage(resolver) {
3637
+ resolveLanguage = resolver;
3638
+ }
3639
+ var catalogs = { ko: messages_default, en: messages_default2 };
3640
+ function t(key, values = {}, lang = resolveLanguage()) {
3521
3641
  return catalogs[lang][key].replace(/\{(\w+)\}/g, (whole, name) => name in values ? String(values[name]) : whole);
3522
3642
  }
3523
3643
 
@@ -3984,13 +4104,13 @@ function parseSpecPreview(path3, source2, history = "", designSource) {
3984
4104
  const specId = parseFrontmatter(lines, "spec", "S").id;
3985
4105
  let title = "";
3986
4106
  let description = "";
3987
- let current;
4107
+ let current2;
3988
4108
  const requirements = [];
3989
4109
  let fence;
3990
4110
  let waitingId = false;
3991
4111
  const append = (line) => {
3992
- if (current)
3993
- current.body += line + "\n";
4112
+ if (current2)
4113
+ current2.body += line + "\n";
3994
4114
  else
3995
4115
  description += line + "\n";
3996
4116
  };
@@ -3998,9 +4118,9 @@ function parseSpecPreview(path3, source2, history = "", designSource) {
3998
4118
  const line = lines[i];
3999
4119
  if (waitingId) {
4000
4120
  const match = new RegExp(`^<!-- ${marker}-req: (R-${token}) -->$`).exec(line);
4001
- if (!match || !current)
4121
+ if (!match || !current2)
4002
4122
  fail(t("spec.requirementIdRequired", { path: path3 }));
4003
- current.id = match[1];
4123
+ current2.id = match[1];
4004
4124
  waitingId = false;
4005
4125
  continue;
4006
4126
  }
@@ -4019,7 +4139,7 @@ function parseSpecPreview(path3, source2, history = "", designSource) {
4019
4139
  continue;
4020
4140
  }
4021
4141
  if (/^# /.test(line)) {
4022
- if (title || current || description.trim())
4142
+ if (title || current2 || description.trim())
4023
4143
  fail(t("spec.titleOnce", { path: path3 }));
4024
4144
  title = line.slice(2).trim();
4025
4145
  if (!title)
@@ -4029,10 +4149,10 @@ function parseSpecPreview(path3, source2, history = "", designSource) {
4029
4149
  if (/^## /.test(line)) {
4030
4150
  if (!title)
4031
4151
  fail(t("spec.titleMissing", { path: path3 }));
4032
- current = { id: "", title: line.slice(3).trim(), body: "" };
4033
- if (!current.title)
4152
+ current2 = { id: "", title: line.slice(3).trim(), body: "" };
4153
+ if (!current2.title)
4034
4154
  fail(t("spec.emptyRequirementTitle"));
4035
- requirements.push(current);
4155
+ requirements.push(current2);
4036
4156
  waitingId = true;
4037
4157
  continue;
4038
4158
  }
@@ -4436,9 +4556,9 @@ var fail3 = (message) => {
4436
4556
  };
4437
4557
  var equal = (a, b) => JSON.stringify(a) === JSON.stringify(b);
4438
4558
  var historyPath = (s) => s.path.replace(/requirements\.md$/, "history.jsonl");
4439
- function pendingPreviewReasons(before, current) {
4559
+ function pendingPreviewReasons(before, current2) {
4440
4560
  const a = asBundle(before);
4441
- const b = asBundle(current);
4561
+ const b = asBundle(current2);
4442
4562
  return [
4443
4563
  ...b.specs.flatMap((s) => s.history.slice(a.specs.find((p) => p.id === s.id)?.history.length ?? 0).map((h) => ({ specId: s.id, path: historyPath(s), ...h }))),
4444
4564
  ...b.wiki.history.slice(a.wiki.history.length).map((h) => ({ kind: "wiki", path: WIKI_HISTORY_PATH, ...h }))
@@ -4446,21 +4566,21 @@ function pendingPreviewReasons(before, current) {
4446
4566
  }
4447
4567
  function finalSpecPreviewChanges(beforeValue, currentValue) {
4448
4568
  const before = asBundle(beforeValue);
4449
- const current = asBundle(currentValue);
4569
+ const current2 = asBundle(currentValue);
4450
4570
  validateBundle(before);
4451
- validateBundle(current);
4571
+ validateBundle(current2);
4452
4572
  for (const previous of before.specs) {
4453
- const next = current.specs.find((s) => s.id === previous.id);
4573
+ const next = current2.specs.find((s) => s.id === previous.id);
4454
4574
  if (previous.history.length && !next)
4455
4575
  fail3(t("reason.keepEmptySpec", { id: previous.id }));
4456
4576
  if (next && !equal(next.history.slice(0, previous.history.length), previous.history))
4457
4577
  fail3(t("reason.committedImmutable", { id: previous.id }));
4458
4578
  }
4459
- if (!equal(current.wiki.history.slice(0, before.wiki.history.length), before.wiki.history))
4579
+ if (!equal(current2.wiki.history.slice(0, before.wiki.history.length), before.wiki.history))
4460
4580
  fail3(t("reason.committedDocumentImmutable"));
4461
4581
  const clean = {
4462
- specs: current.specs.map((s) => ({ ...s, history: before.specs.find((p) => p.id === s.id)?.history ?? [] })),
4463
- wiki: { documents: current.wiki.documents, history: before.wiki.history }
4582
+ specs: current2.specs.map((s) => ({ ...s, history: before.specs.find((p) => p.id === s.id)?.history ?? [] })),
4583
+ wiki: { documents: current2.wiki.documents, history: before.wiki.history }
4464
4584
  };
4465
4585
  return comparePreviewBundles(before, clean);
4466
4586
  }
@@ -4468,14 +4588,14 @@ function prepareSpecPreview(beforeValue, currentValue, baseFiles, currentFiles,
4468
4588
  const beforeBundle = asBundle(beforeValue);
4469
4589
  const currentBundle = asBundle(currentValue);
4470
4590
  const before = beforeBundle.specs;
4471
- const current = currentBundle.specs;
4591
+ const current2 = currentBundle.specs;
4472
4592
  const delta = finalSpecPreviewChanges(beforeBundle, currentBundle);
4473
4593
  if (!Array.isArray(input2) || input2.length > 100)
4474
4594
  fail3(t("reason.count"));
4475
4595
  const writes = /* @__PURE__ */ new Map();
4476
4596
  const pending = /* @__PURE__ */ new Map();
4477
4597
  const covered = /* @__PURE__ */ new Set();
4478
- const used = new Set([...before.flatMap((s) => s.history), ...current.flatMap((s) => s.history), ...beforeBundle.wiki.history, ...currentBundle.wiki.history].map((h) => h.id));
4598
+ const used = new Set([...before.flatMap((s) => s.history), ...current2.flatMap((s) => s.history), ...beforeBundle.wiki.history, ...currentBundle.wiki.history].map((h) => h.id));
4479
4599
  const wikiPending = [];
4480
4600
  const allocate = () => {
4481
4601
  for (let attempt = 0; attempt < 100; attempt++) {
@@ -4524,7 +4644,7 @@ function prepareSpecPreview(beforeValue, currentValue, baseFiles, currentFiles,
4524
4644
  fail3(t("reason.targetInvalid"));
4525
4645
  const change2 = delta.changes.find((c) => c.id === id) ?? fail3(t("reason.requirementUnchanged", { id }));
4526
4646
  const specId = (change2.after ?? change2.before).specId;
4527
- if (!current.some((s) => s.id === specId))
4647
+ if (!current2.some((s) => s.id === specId))
4528
4648
  fail3(t("reason.keepDeletedSpec"));
4529
4649
  covered.add(id);
4530
4650
  groups.set(specId, [...groups.get(specId) ?? [], id]);
@@ -4532,7 +4652,7 @@ function prepareSpecPreview(beforeValue, currentValue, baseFiles, currentFiles,
4532
4652
  for (const [specId, ids] of groups) {
4533
4653
  const requirements = ids.filter((id) => id.startsWith("R-")).sort();
4534
4654
  const designs = ids.filter((id) => id.startsWith("S-")).sort();
4535
- const spec2 = current.find((s) => s.id === specId);
4655
+ const spec2 = current2.find((s) => s.id === specId);
4536
4656
  const oldCount = before.find((s) => s.id === specId)?.history.length ?? 0;
4537
4657
  const reusable = spec2.history.slice(oldCount).find((h) => h.reason === reason && equal([...h.requirements].sort(), requirements) && equal([...h.designs ?? []].sort(), designs));
4538
4658
  const record3 = { id: reusable?.id ?? allocate(), requirements, ...designs.length ? { designs } : {}, reason };
@@ -4545,7 +4665,7 @@ function prepareSpecPreview(beforeValue, currentValue, baseFiles, currentFiles,
4545
4665
  text2 += (text2 && !text2.endsWith("\n") ? "\n" : "") + records.map((h) => JSON.stringify(h) + "\n").join("");
4546
4666
  return base !== void 0 || records.length ? text2 : null;
4547
4667
  };
4548
- for (const spec2 of current) {
4668
+ for (const spec2 of current2) {
4549
4669
  const old = before.find((s) => s.id === spec2.id);
4550
4670
  const base = old ? baseFiles.get(historyPath(old)) : void 0;
4551
4671
  const records = (pending.get(spec2.id) ?? []).sort((a, b) => (a.requirements[0] ?? a.designs[0]) < (b.requirements[0] ?? b.designs[0]) ? -1 : 1);
@@ -24797,8 +24917,74 @@ var updateV3 = external_exports.discriminatedUnion("ok", [
24797
24917
  external_exports.strictObject({ contract: external_exports.literal("update"), version: external_exports.literal(3), ok: external_exports.literal(false), error: external_exports.strictObject({ code: external_exports.string(), message: external_exports.string() }) })
24798
24918
  ]);
24799
24919
 
24920
+ // ../../packages/contracts/dist/versions/project-init-v6.js
24921
+ var projectInitV6 = external_exports.discriminatedUnion("ok", [
24922
+ external_exports.strictObject({
24923
+ contract: external_exports.literal("project-init"),
24924
+ version: external_exports.literal(6),
24925
+ ok: external_exports.literal(true),
24926
+ outcome: external_exports.enum(["planned", "created", "replaced", "already-initialized"]),
24927
+ rootPath: external_exports.string(),
24928
+ configPath: external_exports.literal(".gitifact/config.json"),
24929
+ schemaVersion: external_exports.literal(2),
24930
+ baseline: external_exports.discriminatedUnion("kind", [
24931
+ external_exports.strictObject({ kind: external_exports.literal("empty") }),
24932
+ external_exports.strictObject({ kind: external_exports.literal("commit"), objectFormat: external_exports.enum(["sha1", "sha256"]), commit: external_exports.string() })
24933
+ ]),
24934
+ agentDocs: external_exports.strictObject({ mode: external_exports.enum(["install", "remove", "skip"]), paths: external_exports.array(external_exports.string()) }),
24935
+ update: updateStateV1,
24936
+ // Present only when a newer release exists. npx refreshes the project blocks without a global install; npmGlobal remains an alternative.
24937
+ install: external_exports.strictObject({ npx: external_exports.string().min(1), npmGlobal: external_exports.string().min(1) }).nullable()
24938
+ }),
24939
+ external_exports.strictObject({ contract: external_exports.literal("project-init"), version: external_exports.literal(6), ok: external_exports.literal(false), error: external_exports.strictObject({ code: external_exports.string(), message: external_exports.string() }) })
24940
+ ]);
24941
+
24942
+ // ../../packages/contracts/dist/versions/update-v4.js
24943
+ var commitReason2 = external_exports.enum(["untracked", "other-changes", "commit-failed"]);
24944
+ var agentDocsCommit2 = external_exports.strictObject({
24945
+ state: external_exports.enum(["not-requested", "nothing", "committed", "skipped"]),
24946
+ commit: external_exports.string().nullable(),
24947
+ paths: external_exports.array(external_exports.string()),
24948
+ message: external_exports.string().nullable(),
24949
+ reason: commitReason2.nullable(),
24950
+ detail: external_exports.string().nullable()
24951
+ }).refine((value) => value.state === "committed" === (value.commit !== null) && value.state === "skipped" === (value.reason !== null), "commit is set exactly when committed, reason exactly when skipped.");
24952
+ var updateV4 = external_exports.discriminatedUnion("ok", [
24953
+ external_exports.strictObject({
24954
+ contract: external_exports.literal("update"),
24955
+ version: external_exports.literal(4),
24956
+ ok: external_exports.literal(true),
24957
+ cliVersion: external_exports.string().min(1),
24958
+ update: updateStateV1,
24959
+ // Present only when a newer release exists. npx refreshes the project blocks without a global install; npmGlobal remains an alternative.
24960
+ install: external_exports.strictObject({ npx: external_exports.string().min(1), npmGlobal: external_exports.string().min(1) }).nullable(),
24961
+ // refreshed: blocks whose version line differed were rewritten. current: every block already matched.
24962
+ // not-initialized: no .gitifact config here, so only the version check ran. no-block: no file carries a block.
24963
+ // missing: files `gitifact init` would add and update does not create, e.g. CLAUDE.md when only AGENTS.md has the block.
24964
+ agentDocs: external_exports.strictObject({ state: external_exports.enum(["refreshed", "current", "not-initialized", "no-block"]), paths: external_exports.array(external_exports.string()), missing: external_exports.array(external_exports.string()) }),
24965
+ commit: agentDocsCommit2
24966
+ }),
24967
+ external_exports.strictObject({ contract: external_exports.literal("update"), version: external_exports.literal(4), ok: external_exports.literal(false), error: external_exports.strictObject({ code: external_exports.string(), message: external_exports.string() }) })
24968
+ ]);
24969
+
24970
+ // ../../packages/contracts/dist/versions/browser-session-v3.js
24971
+ var browserSessionV3 = browserSessionV2.omit({ update: true }).extend({ version: external_exports.literal(3) });
24972
+
24973
+ // ../../packages/contracts/dist/versions/update-check-v1.js
24974
+ var updateCheckV1 = external_exports.strictObject({
24975
+ contract: external_exports.literal("update-check"),
24976
+ version: external_exports.literal(1),
24977
+ ok: external_exports.literal(true),
24978
+ cliVersion: external_exports.string().min(1),
24979
+ update: updateStateV1.refine((value) => value.status !== "checking", "The check must have finished."),
24980
+ command: external_exports.string().min(1).nullable()
24981
+ }).refine((value) => value.update.status === "available" === (value.command !== null), "An update command is present exactly when a newer release is available.");
24982
+
24983
+ // src/shared/i18n/index.ts
24984
+ import { AsyncLocalStorage } from "node:async_hooks";
24985
+
24800
24986
  // src/shared/i18n/ko/messages.json
24801
- var messages_default2 = {
24987
+ var messages_default3 = {
24802
24988
  "help.program": "\uD504\uB85C\uC81D\uD2B8\uC758 \uC694\uAD6C\uC0AC\uD56D\uACFC \uACB0\uC815 \uC774\uB825\uC744 Git\uC5D0 \uB0A8\uAE30\uB294 \uB3C4\uAD6C",
24803
24989
  "help.notYet": "\uC804\uCCB4 \uC774\uB825 \uAC80\uC0AC\uC640 GUI \uC694\uAD6C\uC0AC\uD56D \uD3B8\uC9D1\uC740 \uC544\uC9C1 \uC81C\uACF5\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.",
24804
24990
  "help.status": "\uD604\uC7AC checkout\uC758 Git \uC0C1\uD0DC \uC870\uD68C (gitifact \uAC80\uC0AC \uBBF8\uC2E4\uD589)",
@@ -24972,7 +25158,7 @@ var messages_default2 = {
24972
25158
  "init.text.blockUpdated": "\uBE14\uB85D \uAC31\uC2E0",
24973
25159
  "init.text.storage": "\uC800\uC7A5 \uADDC\uC57D",
24974
25160
  "init.text.agentDocs": "\uC5D0\uC774\uC804\uD2B8 \uC9C0\uCE68",
24975
- "init.text.updateAvailable": "\uC0C8 \uBC84\uC804 {version}\uC774 \uC788\uC2B5\uB2C8\uB2E4. {command}\uB85C \uC124\uCE58\uD55C \uB4A4 gitifact init\uC744 \uB2E4\uC2DC \uC2E4\uD589\uD558\uBA74 \uC9C0\uCE68 \uBE14\uB85D\uC774 \uC0C8 \uBC84\uC804\uC73C\uB85C \uAC31\uC2E0\uB429\uB2C8\uB2E4.",
25161
+ "init.text.updateAvailable": "\uC0C8 \uBC84\uC804 {version}\uC774 \uC788\uC2B5\uB2C8\uB2E4. {command}\uB85C \uC0C8 \uBC84\uC804\uC744 \uC2E4\uD589\uD558\uACE0 \uD504\uB85C\uC81D\uD2B8 \uC9C0\uCE68\uC744 \uAC31\uC2E0\uD558\uC138\uC694.",
24976
25162
  "init.failed": "\uCD08\uAE30\uD654\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4. \uD30C\uC77C \uC811\uADFC \uAD8C\uD55C\uACFC \uC0DD\uC131\uB41C \uC124\uC815\xB7\uC784\uC2DC \uD30C\uC77C\uC744 \uD655\uC778\uD558\uC138\uC694.",
24977
25163
  "config.storeNotDirectory": ".gitifact\uB294 \uB9C1\uD06C\uAC00 \uC544\uB2CC \uC77C\uBC18 \uB514\uB809\uD130\uB9AC\uC5EC\uC57C \uD569\uB2C8\uB2E4.",
24978
25164
  "config.migrateFirst": ".tryce \uC800\uC7A5\uC18C\uC785\uB2C8\uB2E4. gitifact migrate\uB85C .gitifact\uC5D0 \uC804\uD658\uD558\uC138\uC694.",
@@ -25009,7 +25195,6 @@ var messages_default2 = {
25009
25195
  "docs.summary.design": "design.md \uBAA9\uCC28, \uCC38\uC870 \uC8FC\uC11D\uACFC \uCC38\uACE0 \uBB38\uC11C, \uAC1C\uC815 \uBC29\uC2DD",
25010
25196
  "docs.summary.writing": "\uC704\uD0A4\xB7\uC694\uAD6C\uC0AC\uD56D\xB7\uC124\uACC4\uC5D0 \uACF5\uD1B5\uC73C\uB85C \uC801\uC6A9\uD558\uB294 \uBB38\uCCB4, \uC778\uC6A9\uACFC Alert, AI \uC2AC\uB86D \uC81C\uAC70",
25011
25197
  "docs.summary.commit": "spec commit \uC785\uB825, \uC774\uC720 \uAE30\uB85D, \uC2E4\uD328 \uD6C4 \uBCF5\uAD6C",
25012
- "help.browserNoUpdateCheck": "\uC0C8 \uBC84\uC804 \uD655\uC778\uC744 \uC704\uD55C npm \uB808\uC9C0\uC2A4\uD2B8\uB9AC \uC870\uD68C\uB97C \uB054 (GITIFACT_NO_UPDATE_CHECK\uC640 \uAC19\uC74C)",
25013
25198
  "help.updateCommit": "\uAC31\uC2E0\uD55C GITIFACT \uBE14\uB85D\uC774 HEAD\uC640 \uBE14\uB85D \uC548\uC5D0\uC11C\uB9CC \uB2E4\uB974\uBA74 \uADF8 \uD30C\uC77C\uB9CC \uACE0\uC815 \uBA54\uC2DC\uC9C0\uB85C \uCEE4\uBC0B",
25014
25199
  "help.update": "\uC0C8 \uBC84\uC804 \uD655\uC778\uACFC \uC124\uCE58 \uBC29\uBC95 \uC548\uB0B4, \uD504\uB85C\uC81D\uD2B8 \uC9C0\uCE68 \uD30C\uC77C\uC758 GITIFACT \uBE14\uB85D\uC744 \uD604\uC7AC \uBC84\uC804\uC73C\uB85C \uAC31\uC2E0",
25015
25200
  "server.changelogNotFound": "\uD328\uCE58\uB178\uD2B8\uB97C \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.",
@@ -25019,13 +25204,13 @@ var messages_default2 = {
25019
25204
  "update.text.upToDate": "\uCD5C\uC2E0 \uBC84\uC804\uC744 \uC0AC\uC6A9 \uC911\uC785\uB2C8\uB2E4.",
25020
25205
  "update.text.disabled": "\uC0C8 \uBC84\uC804 \uD655\uC778\uC774 \uAEBC\uC838 \uC788\uC2B5\uB2C8\uB2E4.",
25021
25206
  "update.text.unavailable": "\uC0C8 \uBC84\uC804\uC744 \uD655\uC778\uD558\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. \uB124\uD2B8\uC6CC\uD06C \uC5F0\uACB0\uC744 \uD655\uC778\uD558\uC138\uC694.",
25022
- "update.text.install": "npm \uC804\uC5ED \uC124\uCE58\uC6A9: {command}",
25023
- "update.text.afterInstall": "\uC124\uCE58\uD55C \uB4A4 gitifact update --commit\uC744 \uB2E4\uC2DC \uC2E4\uD589\uD558\uBA74 \uC9C0\uCE68 \uBE14\uB85D\uC774 \uC0C8 \uBC84\uC804\uC73C\uB85C \uAC31\uC2E0\uB418\uACE0 \uCEE4\uBC0B\uB429\uB2C8\uB2E4.",
25207
+ "update.text.install": "\uC5C5\uB370\uC774\uD2B8 \uBA85\uB839: {command}",
25208
+ "update.text.afterInstall": "\uC704 \uBA85\uB839\uC740 \uC804\uC5ED \uC124\uCE58 \uC5C6\uC774 \uC0C8 \uBC84\uC804\uC73C\uB85C \uC9C0\uCE68\uC744 \uAC31\uC2E0\uD569\uB2C8\uB2E4. \uD504\uB85C\uC81D\uD2B8 \uC758\uC874\uC131\uC73C\uB85C \uAD00\uB9AC\uD55C\uB2E4\uBA74 \uD574\uB2F9 \uBC84\uC804\uC744 \uBA3C\uC800 \uAC31\uC2E0\uD558\uC138\uC694. \uBE0C\uB77C\uC6B0\uC800\uB3C4 \uC0C8 \uBC84\uC804\uC73C\uB85C \uB2E4\uC2DC \uC2E4\uD589\uD558\uC138\uC694.",
25024
25209
  "update.text.blockRefreshed": "\uC9C0\uCE68 \uBE14\uB85D \uAC31\uC2E0: {paths}",
25025
25210
  "update.text.blockCurrent": "\uC9C0\uCE68 \uBE14\uB85D\uC740 \uC774\uBBF8 \uD604\uC7AC \uBC84\uC804\uC785\uB2C8\uB2E4: {paths}",
25026
- "update.text.noBlock": "GITIFACT \uBE14\uB85D\uC774 \uC788\uB294 \uC9C0\uCE68 \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. gitifact init\uC73C\uB85C \uC124\uCE58\uD558\uC138\uC694.",
25211
+ "update.text.noBlock": "GITIFACT \uBE14\uB85D\uC774 \uC788\uB294 \uC9C0\uCE68 \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. npx --yes gitifact@{version} init\uC73C\uB85C \uC124\uCE58\uD558\uC138\uC694.",
25027
25212
  "update.text.notInitialized": "\uCD08\uAE30\uD654\uB41C \uD504\uB85C\uC81D\uD2B8\uAC00 \uC544\uB2C8\uC5B4\uC11C \uBC84\uC804\uB9CC \uD655\uC778\uD588\uC2B5\uB2C8\uB2E4.",
25028
- "update.text.missing": "{paths} \uD30C\uC77C\uC774 \uC5C6\uC5B4 Claude Code\uAC00 AGENTS.md\uC758 \uC9C0\uCE68\uC744 \uC77D\uC9C0 \uBABB\uD569\uB2C8\uB2E4. gitifact init\uC744 \uB2E4\uC2DC \uC2E4\uD589\uD558\uBA74 AGENTS.md\uB97C \uAC00\uC838\uC624\uB294 \uD30C\uC77C\uC744 \uB9CC\uB4ED\uB2C8\uB2E4.",
25213
+ "update.text.missing": "{paths} \uD30C\uC77C\uC774 \uC5C6\uC5B4 Claude Code\uAC00 AGENTS.md\uC758 \uC9C0\uCE68\uC744 \uC77D\uC9C0 \uBABB\uD569\uB2C8\uB2E4. npx --yes gitifact@{version} init\uC744 \uB2E4\uC2DC \uC2E4\uD589\uD558\uBA74 AGENTS.md\uB97C \uAC00\uC838\uC624\uB294 \uD30C\uC77C\uC744 \uB9CC\uB4ED\uB2C8\uB2E4.",
25029
25214
  "update.text.committed": "\uC9C0\uCE68 \uBE14\uB85D \uCEE4\uBC0B: {commit} ({paths})",
25030
25215
  "update.text.commitNothing": "\uCEE4\uBC0B\uD560 \uC9C0\uCE68 \uBE14\uB85D \uBCC0\uACBD\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
25031
25216
  "update.text.commitSkipped.untracked": "\uCEE4\uBC0B\uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. Git\uC774 \uCD94\uC801\uD558\uC9C0 \uC54A\uB294 \uC9C0\uCE68 \uD30C\uC77C\uC785\uB2C8\uB2E4: {paths}",
@@ -25037,13 +25222,278 @@ var messages_default2 = {
25037
25222
  "server.assetNotFound": "\uC5D0\uC14B\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.",
25038
25223
  "docs.wikiPolicyProject": "\uC6B4\uC601 \uBC29\uCE68 (.gitifact/wiki/README.md)",
25039
25224
  "docs.wikiPolicyDefault": "\uC6B4\uC601 \uBC29\uCE68 (\uAE30\uBCF8\uAC12. .gitifact/wiki/README.md\uAC00 \uC5C6\uC5B4 \uB0B4\uC7A5 \uBC29\uCE68\uC744 \uC2E3\uB294\uB2E4)",
25040
- "init.wikiReadmeTitle": "\uC704\uD0A4 \uC6B4\uC601 \uBC29\uCE68"
25225
+ "init.wikiReadmeTitle": "\uC704\uD0A4 \uC6B4\uC601 \uBC29\uCE68",
25226
+ "help.lang": "\uCD9C\uB825 \uC5B8\uC5B4 (ko \uB610\uB294 en)",
25227
+ "help.updateCheck": "\uD30C\uC77C\uC744 \uBC14\uAFB8\uC9C0 \uC54A\uACE0 \uC0C8 \uBC84\uC804\uB9CC \uD655\uC778"
25228
+ };
25229
+
25230
+ // src/shared/i18n/en/messages.json
25231
+ var messages_default4 = {
25232
+ "help.program": "Track project requirements and decisions in Git",
25233
+ "help.notYet": "Full history validation and GUI requirement editing are not available yet.",
25234
+ "help.status": "Read Git status for the current checkout (Gitifact checks are not run)",
25235
+ "help.format": "Output format",
25236
+ "help.browser": "View the current checkout in a local browser",
25237
+ "help.browserPort": "Listening port (0 selects an available port)",
25238
+ "help.browserDev": "Allow the Vite development server origin",
25239
+ "help.init": "Create project configuration and baseline, and install or refresh GITIFACT instruction blocks",
25240
+ "help.initDryRun": "Preview initialization without creating files",
25241
+ "help.initAgent": "Target agent (default: all existing instruction files, or AGENTS.md if none exist)",
25242
+ "help.initRemoveAgents": "Remove GITIFACT blocks",
25243
+ "help.initSkipAgents": "Leave agent instruction files unchanged",
25244
+ "help.docs": "Print agent guidance (omit the topic to list topics)",
25245
+ "help.docsTopic": "One of workflow, spec, design, wiki, writing, commit",
25246
+ "help.migrate": "Move .tryce to .gitifact, preserving IDs, reasons and history",
25247
+ "help.migrateDryRun": "Preview migration without changing files",
25248
+ "help.spec": "Write, read and record Markdown specifications in Git",
25249
+ "help.specCommit": "Record change reasons and commit related files together",
25250
+ "help.specCommitFile": "JSON file containing reasons, paths, message and authorization (- reads stdin)",
25251
+ "help.specCommitDryRun": "Preview without writing files or creating a commit",
25252
+ "help.specSaveFile": "UTF-8 JSON file containing expected and operations (- reads stdin)",
25253
+ "help.specReadRef": "Commit to read",
25254
+ "help.specDiffFrom": "Earlier commit",
25255
+ "help.specDiffTo": "Later commit",
25256
+ "help.specChanges": "Compare the final working specifications with HEAD and read uncommitted reasons",
25257
+ "help.specWorking": "Read current specifications, wiki, stamp and input file paths",
25258
+ "help.specWorkingStamp": "Print only the stamp and input file paths",
25259
+ "help.specWorkingFeature": "Read one feature folder (excluding the wiki)",
25260
+ "help.specWorkingIds": "Print only IDs, titles and paths, without content",
25261
+ "help.specSave": "Save requirements, designs and wiki pages",
25262
+ "help.specRead": "Read specifications at a commit",
25263
+ "help.specDiff": "Compare specifications between two commits",
25264
+ "git.operationInProgress": "A Git operation is in progress: {marker}",
25265
+ "git.resolveConflicts": "Resolve Git conflicts first.",
25266
+ "migrate.locked": "A lock or recovery data exists: {path}",
25267
+ "migrate.alreadyMigrated": ".gitifact already exists. Compare any remaining .tryce contents before removing them manually.",
25268
+ "migrate.nothingToMigrate": "No .tryce store found.",
25269
+ "migrate.notDirectory": ".tryce must be a regular directory, not a link.",
25270
+ "migrate.noConfig": ".tryce/config.json is missing.",
25271
+ "migrate.legacyJson": "Legacy JSON records are not migrated. Preserve them and check the separate transition procedure.",
25272
+ "migrate.extraEntries": ".tryce contains entries other than config.json and spec. Review them before retrying.",
25273
+ "migrate.notSpecFolder": "Not a specification folder: {feature}",
25274
+ "migrate.unsupportedFile": "Unsupported specification file: .tryce/spec/{feature}/{name}",
25275
+ "migrate.fileTooLarge": "Not a regular file, or larger than 1 MiB: {name}",
25276
+ "migrate.nextCommit": "Review the changes, then use gitifact spec commit to commit the .tryce deletions and .gitifact additions together.",
25277
+ "migrate.nextLegacyJson": "Legacy JSON specifications (tryce.json) are not read. Move their contents to Markdown specifications, then include their deletion in spec commit.",
25278
+ "migrate.nextSkills": "If .agents/skills/tryce-workflow or its .claude copy remains, remove those old integrations and rerun gitifact init to refresh GITIFACT blocks.",
25279
+ "migrate.nextRename": "Update .tryce and tryce-workflow references in .gitignore, AGENTS.md and CLAUDE.md.",
25280
+ "migrate.failed": "Migration failed. Check the state of .tryce and .gitifact.",
25281
+ "preview.notInitialized": "Run gitifact init first.",
25282
+ "preview.legacyProject": "This existing project requires a separate transition.",
25283
+ "preview.inputFile": "Input must be a JSON file no larger than 1 MiB.",
25284
+ "preview.inputSize": "Input exceeds the size limit.",
25285
+ "preview.inputJson": "The input file must be UTF-8 JSON.",
25286
+ "preview.unknownFeature": "No specification in feature folder {feature}.",
25287
+ "preview.failed": "Specification operation failed.",
25288
+ "initRepo.configIgnored": "Configuration is excluded by an ignore rule: {rule}",
25289
+ "initRepo.baselineMismatch": "The baseline does not match the current repository.",
25290
+ "initRepo.baselineUnverified": "Could not verify the baseline commit and its ancestry. Preserve the baseline and inspect the history.",
25291
+ "commit.unknownField": "Unknown commit input field.",
25292
+ "commit.authorizationRequired": "A user request or explicit project policy is required to authorize a commit.",
25293
+ "commit.trailerInMessage": "Specify Gitifact references through requirements, not message trailers.",
25294
+ "commit.invalidReferences": "Invalid requirement references.",
25295
+ "commit.busy": "A previous commit operation or recovery data exists: {path}",
25296
+ "commit.selectPending": "Select all changed specifications and reasons together. Unselected specifications are not added automatically.",
25297
+ "commit.unknownRequirement": "Reference only existing requirement IDs.",
25298
+ "commit.unexpectedFiles": "An empty commit or files outside the selection were detected.",
25299
+ "commit.filterChanged": "A Git filter changed the specification or reason contents.",
25300
+ "commit.specsChangedPreparing": "The specification set changed while preparing the commit.",
25301
+ "commit.integrationStarted": "A Git integration operation has started.",
25302
+ "commit.headChanged": "HEAD changed.",
25303
+ "commit.resultAltered": "The commit ran, but a hook or concurrent operation changed the result.",
25304
+ "commit.indexChangedAfter": "The original index changed after the commit.",
25305
+ "commit.hookChangedStaging": "A hook changed staging after the commit.",
25306
+ "commit.expectedType": "expected must be the string returned by changes.",
25307
+ "commit.existingStaging": "Existing staging has been preserved. Review the selection before retrying.",
25308
+ "commit.staleInput": "Stale input. Run changes again.",
25309
+ "commit.newlinePath": "Specification paths containing newlines are not supported.",
25310
+ "commit.storedShape": "Could not verify the Git representation of the specification files.",
25311
+ "commit.indexChanged": "The index changed during execution.",
25312
+ "commit.fileChangedRunning": "A file or policy changed during execution: {path}",
25313
+ "commit.stagedUnreadable": "Could not read staged specifications or reasons: {path}",
25314
+ "commit.fileChangedPreparing": "A file or policy changed while preparing the commit: {path}",
25315
+ "commit.resultUnknown": "Could not verify the commit result.",
25316
+ "commit.uncertain": "The Git result needs inspection. Do not retry; inspect HEAD and recovery data: {path}",
25317
+ "store.legacyLock": "A previous Tryce lock or recovery data exists: {path}",
25318
+ "store.storeNotDirectory": ".gitifact must be a regular directory.",
25319
+ "store.migrateFirst": "This is a .tryce store. Run gitifact migrate before using it.",
25320
+ "store.legacyProject": "This project format requires a separate transition.",
25321
+ "store.linkOrSpecial": "Links and special files are not supported: {path}",
25322
+ "store.tooManyFiles": "The specification file count limit was exceeded.",
25323
+ "store.tooDeep": "The folder depth limit was exceeded: {path}",
25324
+ "store.legacyJson": "Legacy JSON records are not migrated.",
25325
+ "store.unsupportedPath": "Unsupported specification path: {path}",
25326
+ "store.hardLinkOrSize": "Check for hard links or files larger than 1 MiB.",
25327
+ "store.sizeLimit": "The specification size limit was exceeded.",
25328
+ "store.locked": "A specification save lock or recovery data exists.",
25329
+ "store.changedWhileReading": "Specifications changed while being read. Read them again.",
25330
+ "store.lockedWhileReading": "A save lock appeared while reading.",
25331
+ "store.invalidInput": "Invalid save input.",
25332
+ "store.expectedOperations": "expected and operations are required.",
25333
+ "store.notDirectory": "Not a regular directory: {path}",
25334
+ "store.staleExpected": "Specifications changed since the last read. Run working again.",
25335
+ "store.unsupportedSavePath": "Unsupported save path.",
25336
+ "store.fileSizeLimit": "A specification exceeds the 1 MiB limit.",
25337
+ "store.totalLimit": "The total specification size limit was exceeded.",
25338
+ "store.changedPreparing": "Specifications changed while preparing the save.",
25339
+ "store.changedBeforeWrite": "A file changed immediately before writing.",
25340
+ "store.changedWhileWriting": "Specifications or configuration changed while writing.",
25341
+ "store.recoveryIncomplete": "Automatic recovery could not finish. Other edits were preserved. Recovery data: {path}",
25342
+ "server.closing": "The server is shutting down.",
25343
+ "server.forbiddenHost": "Host is not allowed.",
25344
+ "server.forbiddenOrigin": "Origin is not allowed.",
25345
+ "server.badPath": "Invalid path.",
25346
+ "server.noBody": "Request bodies are not accepted.",
25347
+ "server.crossSite": "Requests from external pages are not allowed.",
25348
+ "server.noQuery": "Query parameters are not accepted.",
25349
+ "server.methodNotAllowed": "Method is not allowed.",
25350
+ "server.apiNotFound": "API endpoint not found.",
25351
+ "server.sessionChanged": "The server session changed. Reconnect to continue.",
25352
+ "server.badRange": "Check the requested range.",
25353
+ "server.changeNotFound": "This commit does not contain that change.",
25354
+ "server.specsUnreadable": "Could not read specifications.",
25355
+ "server.refreshOrigin": "Refresh requires a same-origin Origin header.",
25356
+ "server.fileNotFound": "File not found.",
25357
+ "server.internal": "Could not process the request.",
25358
+ "server.portInUse": "The requested port is in use. Choose another port.",
25359
+ "server.addressUnknown": "Could not determine the server address.",
25360
+ "reader.gitContextOverride": "Reading is disabled when the Git context is overridden.",
25361
+ "reader.notUtf8": "Content is not UTF-8.",
25362
+ "reader.invalidIndexEntry": "Invalid index entry.",
25363
+ "reader.stagingConflictOrLink": "Specification staging contains a conflict or link.",
25364
+ "reader.stagingUnsupported": "Specification staging contains an unsupported file.",
25365
+ "reader.headUnknown": "Could not determine HEAD.",
25366
+ "reader.damagedHead": "A damaged HEAD is not treated as an empty repository.",
25367
+ "reader.objectFormat": "Unsupported Git object format.",
25368
+ "reader.commitUnknown": "Could not resolve the commit.",
25369
+ "reader.invalidTree": "Invalid Git tree.",
25370
+ "reader.linkOrSubmodule": "The specification area contains a link or submodule: {path}",
25371
+ "reader.legacyJson": "Use an older compatible CLI to read legacy JSON records. This command does not migrate them.",
25372
+ "reader.unsupportedEntry": "Unsupported path or file type: {path}",
25373
+ "reader.mixedStores": "A historical commit contains both .gitifact and .tryce specifications. Check the transition commit.",
25374
+ "reader.mixedFormats": "A historical commit mixes old and new specification formats.",
25375
+ "reader.fileLimit": "The specification read file limit was exceeded.",
25376
+ "reader.blobShape": "Check the blob format and the 1 MiB size limit.",
25377
+ "reader.totalLimit": "The total specification read limit of 16 MiB was exceeded.",
25378
+ "reader.blobBoundary": "Unexpected Git blob response boundary.",
25379
+ "prepare.integrationInProgress": "Specifications cannot be committed during a Git integration operation.",
25380
+ "prepare.headChanged": "HEAD or the branch changed. Run changes again.",
25381
+ "commitFiles.relativePath": "Select repository-relative paths to specifications or regular files only.",
25382
+ "commitFiles.linkOrDirectory": "Links, directories and special files cannot be selected.",
25383
+ "commitFiles.nestedRepository": "Nested repositories cannot be selected.",
25384
+ "commitFiles.hardLinkOrSize": "Check for hard links or oversized files.",
25385
+ "commitFiles.invalidInput": "Invalid commit input.",
25386
+ "commitFiles.emptyOrTooLong": "An input string is empty or exceeds the limit.",
25387
+ "commitFiles.invalidPaths": "Invalid file list size or format.",
25388
+ "commitFiles.legacyDeleteOnly": "Legacy records in .gitifact can only be selected for deletion: {path}",
25389
+ "commitFiles.tryceDeleteOnly": "Old .tryce records can only be selected for deletion during migration: {path}",
25390
+ "init.migrationRequired": "This format requires a separate transition. Preserve the configuration and records.",
25391
+ "init.inputChanged": "Input changed while planning initialization.",
25392
+ "init.configDeleted": "A deleted tracked configuration is not treated as a new project.",
25393
+ "init.existingSpecs": "Check the existing specs path.",
25394
+ "init.orphanStore": ".gitifact data without configuration is not adopted automatically.",
25395
+ "init.legacyRecords": "This project contains records written with an older storage convention (schemaVersion {version}), so its configuration was not changed. This CLI does not migrate that convention. Preserve the records elsewhere and leave only config.json in .gitifact before running init again.",
25396
+ "init.headOrIndexChanged": "HEAD or the index changed.",
25397
+ "init.configAppeared": "Another process created the configuration.",
25398
+ "init.changedAfterWrite": "Input changed after creating the configuration. Inspect the current files.",
25399
+ "init.text.skipped": "Skipped",
25400
+ "init.text.blockRemoved": "Block removed",
25401
+ "init.text.blockPlanned": "Block installation planned",
25402
+ "init.text.blockUpdated": "Block refreshed",
25403
+ "init.text.storage": "Storage convention",
25404
+ "init.text.agentDocs": "Agent instructions",
25405
+ "init.text.updateAvailable": "Version {version} is available. Run {command} to use it and refresh the project instructions.",
25406
+ "init.failed": "Initialization failed. Check file permissions and the generated configuration and temporary files.",
25407
+ "config.storeNotDirectory": ".gitifact must be a regular directory, not a link.",
25408
+ "config.migrateFirst": "This is a .tryce store. Run gitifact migrate to move it to .gitifact.",
25409
+ "config.fileShape": "config.json must be a regular file no larger than 64 KiB.",
25410
+ "config.changedWhileReading": "Configuration changed while reading. Retry the command.",
25411
+ "config.notUtf8Json": "Configuration must be UTF-8 JSON.",
25412
+ "config.directoryChanged": "The configuration directory changed. Inspect temporary files.",
25413
+ "config.temporaryChanged": "The temporary configuration changed.",
25414
+ "managedFile.notRegularPath": "Not a regular path: {path}",
25415
+ "managedFile.tooLarge": "Exceeds {kib} KiB: {path}",
25416
+ "managedFile.changedWhileReading": "File changed while reading: {path}",
25417
+ "managedFile.notUtf8": "File must be UTF-8: {path}",
25418
+ "managedFile.targetChanged": "Target path changed: {path}",
25419
+ "managedFile.removeTargetChanged": "Removal target changed: {path}",
25420
+ "managedFile.publishTargetChanged": "Publication target changed: {path}",
25421
+ "agentBlock.endWithoutStart": "An end marker has no matching start marker. Repair the block before retrying.",
25422
+ "agentBlock.startWithoutEnd": "A GITIFACT block has no end marker. Repair the block before retrying.",
25423
+ "agentBlock.duplicateStart": "Multiple GITIFACT start markers found. Remove duplicate blocks before retrying.",
25424
+ "specReader.headUnreadable": "Could not read HEAD.",
25425
+ "specReader.schemaRequired": "A project using the supported schema is required.",
25426
+ "specReader.authorUnreadable": "Could not read Git authors.",
25427
+ "specReader.historyUnreadable": "Could not read Git history.",
25428
+ "specReader.projectChanged": "The project changed while reading. Refresh to continue.",
25429
+ "docs.unknownTopic": "Unknown topic: {topic}. Available topics: {topics}",
25430
+ "status.text.repository": "Repository: {path}",
25431
+ "status.text.changes": "Changed Git paths: {count}; Gitifact checks not run",
25432
+ "status.text.observed": "Observed: {at} (best effort; not a snapshot of file contents)",
25433
+ "browser.invalidPort": "Port must be an integer between 0 and 65535.",
25434
+ "browser.failed": "Could not start the browser server.",
25435
+ "browser.assetSymlink": "Browser assets must not be symlinks.",
25436
+ "browser.assetsUnreadable": "Could not read the browser build. Rebuild with pnpm build.",
25437
+ "docs.summary.workflow": "Startup checks, identifying requirement requests, and completion reporting",
25438
+ "docs.summary.spec": "User stories, acceptance criteria, IDs and spec save input",
25439
+ "docs.summary.design": "Design structure, requirement references, sources and revisions",
25440
+ "docs.summary.writing": "Writing style, quotations, alerts and removing filler from documentation",
25441
+ "docs.summary.commit": "spec commit input, change reasons and recovery after failure",
25442
+ "help.updateCommit": "Commit only instruction files changed solely inside the GITIFACT block, using a fixed message",
25443
+ "help.update": "Check for updates, show installation instructions and refresh existing GITIFACT blocks",
25444
+ "server.changelogNotFound": "Release notes not found.",
25445
+ "server.changelogUnreadable": "Could not read release notes.",
25446
+ "update.text.current": "Current version: {version}",
25447
+ "update.text.available": "New version: {version}",
25448
+ "update.text.upToDate": "You are using the latest version.",
25449
+ "update.text.disabled": "Update checks are disabled.",
25450
+ "update.text.unavailable": "Could not check for updates. Check your network connection.",
25451
+ "update.text.install": "Update command: {command}",
25452
+ "update.text.afterInstall": "This command refreshes the instructions with the new version without a global installation. If you manage Gitifact as a project dependency, update that dependency first. Restart the browser with the new version.",
25453
+ "update.text.blockRefreshed": "Instruction blocks refreshed: {paths}",
25454
+ "update.text.blockCurrent": "Instruction blocks are already current: {paths}",
25455
+ "update.text.noBlock": "No instruction files contain a GITIFACT block. Run npx --yes gitifact@{version} init to install one.",
25456
+ "update.text.notInitialized": "Only the version was checked because this project is not initialized.",
25457
+ "update.text.missing": "{paths} is missing, so Claude Code cannot load the AGENTS.md instructions. Rerun npx --yes gitifact@{version} init to create the import file.",
25458
+ "update.text.committed": "Instruction blocks committed: {commit} ({paths})",
25459
+ "update.text.commitNothing": "No instruction block changes to commit.",
25460
+ "update.text.commitSkipped.untracked": "Not committed. Instruction files are untracked: {paths}",
25461
+ "update.text.commitSkipped.other-changes": "Not committed. There are uncommitted edits outside the blocks: {paths}",
25462
+ "update.text.commitSkipped.commit-failed": "Not committed. git commit was rejected; refreshed files remain in place: {paths}",
25463
+ "update.commitResultUnknown": "Could not verify the commit result. Inspect the parents and changed files of HEAD {commit}.",
25464
+ "update.failed": "Update check failed.",
25465
+ "docs.summary.wiki": "Wiki format, save commands, assets and this project's wiki policy (wiki/README.md)",
25466
+ "server.assetNotFound": "Asset not found.",
25467
+ "docs.wikiPolicyProject": "Wiki policy (.gitifact/wiki/README.md)",
25468
+ "docs.wikiPolicyDefault": "Default wiki policy (.gitifact/wiki/README.md is missing)",
25469
+ "init.wikiReadmeTitle": "Wiki policy",
25470
+ "help.lang": "Output language (ko or en)",
25471
+ "help.updateCheck": "Check for a newer version without changing files"
25041
25472
  };
25042
25473
 
25043
25474
  // src/shared/i18n/index.ts
25044
- var defaultLanguage2 = "ko";
25045
- var catalogs2 = { ko: messages_default2 };
25046
- function t2(key, values = {}, lang = defaultLanguage2) {
25475
+ var defaultLanguage2 = "en";
25476
+ var catalogs2 = { ko: messages_default3, en: messages_default4 };
25477
+ var requestLanguage = new AsyncLocalStorage();
25478
+ var current = environmentLanguage();
25479
+ var explicit;
25480
+ function resolveLanguage2(value) {
25481
+ return /^ko(?:[-_.@]|$)/i.test(value ?? "") ? "ko" : "en";
25482
+ }
25483
+ function environmentLanguage(env = process.env) {
25484
+ return resolveLanguage2(env.GITIFACT_LANG || env.LC_ALL || env.LC_MESSAGES || env.LANG || Intl.DateTimeFormat().resolvedOptions().locale);
25485
+ }
25486
+ function configureLanguage(language, selected) {
25487
+ current = language;
25488
+ explicit = selected;
25489
+ }
25490
+ var getLanguage = () => requestLanguage.getStore() ?? current;
25491
+ var explicitLanguage = () => explicit;
25492
+ function withLanguage(language, action) {
25493
+ return requestLanguage.run(language, action);
25494
+ }
25495
+ configureCoreLanguage(getLanguage);
25496
+ function t2(key, values = {}, lang = getLanguage()) {
25047
25497
  return catalogs2[lang][key].replace(/\{(\w+)\}/g, (whole, name) => name in values ? String(values[name]) : whole);
25048
25498
  }
25049
25499
 
@@ -25117,63 +25567,13 @@ import { readFile as readFile5 } from "node:fs/promises";
25117
25567
 
25118
25568
  // src/server/status-session.ts
25119
25569
  import { randomUUID as randomUUID2 } from "node:crypto";
25120
-
25121
- // src/shared/update-check.ts
25122
- var updateCheckTimeoutMs = 3e3;
25123
- var release2 = /^(\d+)\.(\d+)\.(\d+)$/;
25124
- var running = /^(\d+)\.(\d+)\.(\d+)(-[0-9A-Za-z.-]+)?$/;
25125
- function isNewerRelease(latest, current) {
25126
- const next = release2.exec(latest);
25127
- const now = running.exec(current);
25128
- if (!next || !now) return false;
25129
- for (let index = 1; index <= 3; index++) {
25130
- const difference = Number(next[index]) - Number(now[index]);
25131
- if (difference !== 0) return difference > 0;
25132
- }
25133
- return now[4] !== void 0;
25134
- }
25135
- var updateCheckDisabled = (env, flagged = false) => flagged || env.GITIFACT_NO_UPDATE_CHECK !== void 0 && env.GITIFACT_NO_UPDATE_CHECK !== "" && env.GITIFACT_NO_UPDATE_CHECK !== "0";
25136
- var checkingUpdate = { status: "checking", latestVersion: null };
25137
- var disabledUpdate = { status: "disabled", latestVersion: null };
25138
- async function resolveUpdate(current, fetchLatest, signal, timeoutMs = updateCheckTimeoutMs) {
25139
- const controller = new AbortController();
25140
- const abort = () => controller.abort();
25141
- const timer = setTimeout(abort, timeoutMs);
25142
- signal?.addEventListener("abort", abort, { once: true });
25143
- if (signal?.aborted) controller.abort();
25144
- try {
25145
- const latest = await Promise.race([
25146
- fetchLatest(controller.signal),
25147
- new Promise((_resolve, reject) => {
25148
- if (controller.signal.aborted) reject(new Error("aborted"));
25149
- else controller.signal.addEventListener("abort", () => reject(new Error("aborted")), { once: true });
25150
- })
25151
- ]);
25152
- if (typeof latest !== "string" || !release2.test(latest)) return { status: "unavailable", latestVersion: null };
25153
- return { status: isNewerRelease(latest, current) ? "available" : "up-to-date", latestVersion: latest };
25154
- } catch {
25155
- return { status: "unavailable", latestVersion: null };
25156
- } finally {
25157
- clearTimeout(timer);
25158
- signal?.removeEventListener("abort", abort);
25159
- }
25160
- }
25161
- var npmGlobalInstall = (version2) => "npm install -g gitifact@" + version2;
25162
-
25163
- // src/server/status-session.ts
25164
25570
  function createStatusSession(initial, read, cliVersion = "0.0.0") {
25165
25571
  const identity2 = { sessionId: randomUUID2(), repository: { key: initial.repository.key, worktreeKey: initial.repository.worktreeKey } };
25166
- let update = disabledUpdate;
25572
+ const session = browserSessionV3.parse({ contract: "browser-session", version: 3, ...identity2, cliVersion });
25167
25573
  let latest = initial;
25168
25574
  let pending;
25169
25575
  return {
25170
- // The identity never changes; only the update state moves (checking → a final state), so the session is built per read.
25171
- get session() {
25172
- return browserSessionV2.parse({ contract: "browser-session", version: 2, ...identity2, cliVersion, update });
25173
- },
25174
- setUpdate(next) {
25175
- update = next;
25176
- },
25576
+ session,
25177
25577
  get latest() {
25178
25578
  return latest;
25179
25579
  },
@@ -25292,7 +25692,7 @@ async function dispatch(routes, sessionId, url2, path3, request, response, allow
25292
25692
  if (reply && !response.destroyed) json2(response, reply.status, reply.body);
25293
25693
  } catch (error62) {
25294
25694
  if (error62 instanceof HttpError) return fail4(response, error62.status, error62.code, error62.message);
25295
- fail4(response, 503, "INTERNAL_ERROR", error62 instanceof Error && error62.message ? error62.message : found.unreadable ?? t2("server.internal"));
25695
+ fail4(response, 503, "INTERNAL_ERROR", error62 instanceof Error && error62.message ? error62.message : found.unreadable?.() ?? t2("server.internal"));
25296
25696
  }
25297
25697
  }
25298
25698
 
@@ -25301,12 +25701,12 @@ import { readdir, readFile } from "node:fs/promises";
25301
25701
  import { join } from "node:path";
25302
25702
  async function loadBrowserAssets(directory) {
25303
25703
  const assets = /* @__PURE__ */ new Map();
25304
- async function walk(current, prefix) {
25305
- for (const entry2 of await readdir(current, { withFileTypes: true })) {
25704
+ async function walk(current2, prefix) {
25705
+ for (const entry2 of await readdir(current2, { withFileTypes: true })) {
25306
25706
  if (entry2.isSymbolicLink()) throw new Error(t2("browser.assetSymlink"));
25307
25707
  const name = prefix + entry2.name;
25308
- if (entry2.isDirectory()) await walk(join(current, entry2.name), name + "/");
25309
- else if (entry2.isFile()) assets.set("/" + name, await readFile(join(current, entry2.name)));
25708
+ if (entry2.isDirectory()) await walk(join(current2, entry2.name), name + "/");
25709
+ else if (entry2.isFile()) assets.set("/" + name, await readFile(join(current2, entry2.name)));
25310
25710
  }
25311
25711
  }
25312
25712
  try {
@@ -25348,17 +25748,18 @@ function serveStatic(request, response, path3, assets) {
25348
25748
 
25349
25749
  // src/server/routes/project-routes.ts
25350
25750
  function projectRoutes(store, readChangelog) {
25751
+ const localized = (value) => value.ok ? value : failureDto(new RepositoryReadError(value.error.code));
25351
25752
  return [
25352
25753
  // Re-reading the session never contacts the registry; it returns the state the single startup check left.
25353
25754
  route({ method: "GET", path: "/api/v1/session", session: false, handle: () => ok(store.session) }),
25354
- route({ method: "GET", path: "/api/v1/status", session: true, handle: () => ({ status: store.latest.ok ? 200 : 503, body: store.latest }) }),
25755
+ route({ method: "GET", path: "/api/v1/status", session: true, handle: () => ({ status: store.latest.ok ? 200 : 503, body: localized(store.latest) }) }),
25355
25756
  route({ method: "POST", path: "/api/v1/status/refresh", session: true, handle: async ({ allowedOrigin }) => {
25356
25757
  if (!allowedOrigin) throw new HttpError(403, "FORBIDDEN", t2("server.refreshOrigin"));
25357
25758
  const value = await store.refresh();
25358
- return { status: value.ok ? 200 : 503, body: value };
25759
+ return { status: value.ok ? 200 : 503, body: localized(value) };
25359
25760
  } }),
25360
- route({ method: "GET", path: "/api/v1/changelog", session: true, query: changelogQueryV1, unreadable: t2("server.changelogUnreadable"), handle: async ({ query }) => {
25361
- const language = query.lang ?? defaultLanguage2;
25761
+ route({ method: "GET", path: "/api/v1/changelog", session: true, query: changelogQueryV1, unreadable: () => t2("server.changelogUnreadable"), handle: async ({ query }) => {
25762
+ const language = query.lang ?? getLanguage();
25362
25763
  const own2 = await readChangelog(language);
25363
25764
  const text2 = own2 ?? (language === defaultLanguage2 ? null : await readChangelog(defaultLanguage2));
25364
25765
  if (text2 === null) throw new HttpError(404, "NOT_FOUND", t2("server.changelogNotFound"));
@@ -25368,8 +25769,8 @@ function projectRoutes(store, readChangelog) {
25368
25769
  }
25369
25770
 
25370
25771
  // src/adapters/git/command-scope.ts
25371
- import { AsyncLocalStorage } from "node:async_hooks";
25372
- var storage = new AsyncLocalStorage();
25772
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
25773
+ var storage = new AsyncLocalStorage2();
25373
25774
  var withCommandScope = (run) => storage.run(/* @__PURE__ */ new Map(), run);
25374
25775
  function commandScoped(key, load) {
25375
25776
  const cache = storage.getStore();
@@ -25615,8 +26016,8 @@ async function publishConfig(root, text2, recheck, beforePublish, replace = fals
25615
26016
  const owner = await configDirectory(root);
25616
26017
  const canonical = await realpath(directory);
25617
26018
  const unchanged = async () => {
25618
- const current = await configDirectory(root);
25619
- if (!owner || !current || current.ino !== owner.ino || current.dev !== owner.dev || await realpath(directory) !== canonical) {
26019
+ const current2 = await configDirectory(root);
26020
+ if (!owner || !current2 || current2.ino !== owner.ino || current2.dev !== owner.dev || await realpath(directory) !== canonical) {
25620
26021
  throw new InitError("INPUT_CHANGED", t2("config.directoryChanged"));
25621
26022
  }
25622
26023
  };
@@ -25641,8 +26042,8 @@ async function publishConfig(root, text2, recheck, beforePublish, replace = fals
25641
26042
  else await link(temporary, join3(directory, "config.json"));
25642
26043
  } finally {
25643
26044
  await unchanged();
25644
- const current = await fileInfo(temporary);
25645
- if (current?.ino === tempOwner.ino && current.dev === tempOwner.dev) await unlink(temporary);
26045
+ const current2 = await fileInfo(temporary);
26046
+ if (current2?.ino === tempOwner.ino && current2.dev === tempOwner.dev) await unlink(temporary);
25646
26047
  }
25647
26048
  }
25648
26049
 
@@ -25897,7 +26298,7 @@ function createCheckoutReader(root, sessionId, inherited = process.env) {
25897
26298
  const raw = await readConfigFile(root);
25898
26299
  if (!raw || !("schemaVersion" in parseManagedConfig(raw))) throw new SpecPreviewError(t2("specReader.schemaRequired"));
25899
26300
  const head = await readHead();
25900
- const [current, dirty, authors, everyone] = await settled([
26301
+ const [current2, dirty, authors, everyone] = await settled([
25901
26302
  readWorkingPreviewState(root),
25902
26303
  head ? git(["status", "--porcelain=v1", "--", ".gitifact/spec", WIKI_DIR]) : Promise.resolve(""),
25903
26304
  head ? storeAuthors(head) : Promise.resolve(void 0),
@@ -25911,11 +26312,11 @@ function createCheckoutReader(root, sessionId, inherited = process.env) {
25911
26312
  if (!name || !email3 || !latest) throw new SpecPreviewError(t2("specReader.authorUnreadable"));
25912
26313
  tally(people, name, email3, latest);
25913
26314
  }
25914
- const features = current.specs.map(({ history: _history, ...feature2 }) => {
26315
+ const features = current2.specs.map(({ history: _history, ...feature2 }) => {
25915
26316
  const entry2 = authors?.folders.get(feature2.path.replace(/^\.(?:gitifact|tryce)\/(spec\/[^/]+)\/requirements\.md$/, "$1"));
25916
26317
  return { ...feature2, contributors: entry2 ? [...entry2.people.values()].sort((a, b) => b.commits - a.commits) : [], updatedAt: entry2?.latest ?? null };
25917
26318
  });
25918
- const documents = current.wiki.documents.map((doc) => ({ ...doc, updatedAt: authors?.pages.get(doc.path) ?? null }));
26319
+ const documents = current2.wiki.documents.map((doc) => ({ ...doc, updatedAt: authors?.pages.get(doc.path) ?? null }));
25919
26320
  if (await readHead() !== head) throw new SpecPreviewError(t2("specReader.projectChanged"));
25920
26321
  const checkout = browserSpecsV4.parse({
25921
26322
  contract: "browser-specs",
@@ -25934,18 +26335,69 @@ function createCheckoutReader(root, sessionId, inherited = process.env) {
25934
26335
  ...f.requirements.map((r) => ({ kind: "requirement", ref: r.id, title: r.title, where: f.title, body: r.body, featureId: f.id })),
25935
26336
  ...f.design ? [{ kind: "design", ref: f.id + ":design", title: f.design.title, where: f.title, body: f.design.body, featureId: f.id }] : []
25936
26337
  ]).concat(documents.map((d) => ({ kind: "document", ref: d.id, title: d.title, where: d.path.replace(/^\.gitifact\/wiki\//, ""), body: d.body, documentId: d.id })));
25937
- return { checkout, stamp: current.stamp, search };
26338
+ return { checkout, stamp: current2.stamp, search };
25938
26339
  }
25939
26340
  return () => {
25940
- let value = pending.get("");
26341
+ const language = getLanguage();
26342
+ let value = pending.get(language);
25941
26343
  if (!value) {
25942
- value = read().finally(() => pending.delete(""));
25943
- pending.set("", value);
26344
+ value = read().finally(() => pending.delete(language));
26345
+ pending.set(language, value);
25944
26346
  }
25945
26347
  return value;
25946
26348
  };
25947
26349
  }
25948
26350
 
26351
+ // src/server/history/merge-records.ts
26352
+ function remergeRecords(patch) {
26353
+ const touched = /* @__PURE__ */ new Set();
26354
+ for (const file2 of patch.split(/^diff --git /m).slice(1)) {
26355
+ const lines = file2.split("\n");
26356
+ const oldPath = lines.find((l) => l.startsWith("--- "))?.slice(4);
26357
+ const newPath = lines.find((l) => l.startsWith("+++ "))?.slice(4);
26358
+ if (!oldPath && !newPath) continue;
26359
+ const path3 = (newPath === "/dev/null" ? oldPath : newPath)?.replace(/^[ab]\//, "");
26360
+ if (!path3 || !recordPathPattern.test(path3) || !path3.endsWith(".md")) continue;
26361
+ const start = lines.findIndex((l) => l.startsWith("@@ "));
26362
+ if (start < 0 || !/^@@ -[01](?:,\d+)? \+[01](?:,\d+)? @@/.test(lines[start])) throw new SpecPreviewError(t2("specReader.historyUnreadable"));
26363
+ for (const side of ["-", "+"]) {
26364
+ const content = [];
26365
+ for (const line of lines.slice(start + 1)) {
26366
+ if (line.startsWith("@@ ")) throw new SpecPreviewError(t2("specReader.historyUnreadable"));
26367
+ if (line.startsWith(" ") || line.startsWith(side)) content.push({ text: line.slice(1).replace(/\r$/, ""), changed: line.startsWith(side) });
26368
+ }
26369
+ if (!path3.endsWith("/requirements.md")) {
26370
+ const source2 = content.map((l) => l.text).join("\n");
26371
+ const id = /^(?:id: "?|<!-- (?:gitifact|tryce)-(?:design|product|guide): )([SWPG]-[a-z2-7]{10})(?:"?$| -->$)/m.exec(source2)?.[1];
26372
+ if (id && content.some((l) => l.changed)) touched.add(id);
26373
+ continue;
26374
+ }
26375
+ let section = [];
26376
+ let fence;
26377
+ const finish = () => {
26378
+ if (!section[0]?.text.startsWith("## ")) return;
26379
+ const id = section.map((l) => /^<!-- (?:gitifact|tryce)-req: (R-[a-z2-7]{10}) -->$/.exec(l.text)?.[1]).find(Boolean);
26380
+ if (id && section.some((l) => l.changed)) touched.add(id);
26381
+ };
26382
+ for (const line of content) {
26383
+ if (!fence && /^## /.test(line.text)) {
26384
+ finish();
26385
+ section = [];
26386
+ }
26387
+ section.push(line);
26388
+ if (fence) {
26389
+ if (new RegExp(`^ {0,3}${fence.char}{${fence.size},}\\s*$`).test(line.text)) fence = void 0;
26390
+ } else {
26391
+ const match = /^ {0,3}(`{3,}|~{3,})/.exec(line.text);
26392
+ if (match) fence = { char: match[1][0], size: match[1].length };
26393
+ }
26394
+ }
26395
+ finish();
26396
+ }
26397
+ }
26398
+ return touched;
26399
+ }
26400
+
25949
26401
  // src/server/history/commit-changes.ts
25950
26402
  var HISTORY_PATHSPECS = [
25951
26403
  ...STORE_DIRS.flatMap((d) => [`:(glob)${d}/spec/*/requirements.md`, `:(glob)${d}/spec/*/design.md`, `:(glob)${d}/spec/*/history.jsonl`]),
@@ -25973,7 +26425,7 @@ function createCommitChanges(root, snapshot3) {
25973
26425
  if (!oldBlob || !newBlob || path3 === void 0) throw new SpecPreviewError(t2("specReader.historyUnreadable"));
25974
26426
  if (recordPathPattern.test(path3) || isLegacy(path3)) entries.push({ oldBlob, newBlob, path: path3 });
25975
26427
  }
25976
- return { commit, parent: parents.split(" ")[0] || void 0, author, email: email3, date: date5, committer, message, entries };
26428
+ return { commit, parent: parents.split(" ")[0] || void 0, parents: parents.split(" ").filter(Boolean), author, email: email3, date: date5, committer, message, entries };
25977
26429
  });
25978
26430
  }
25979
26431
  function wanted(entries) {
@@ -26044,9 +26496,56 @@ function createCommitChanges(root, snapshot3) {
26044
26496
  }
26045
26497
  return { commit: c.commit, events: eventsOf(c, comparePreviewBundles(before, after).changes), boundary };
26046
26498
  }
26499
+ async function merge2(c) {
26500
+ let touched;
26501
+ if (c.parents.length === 2) {
26502
+ const patch = reader.decode(await reader.run([
26503
+ "-c",
26504
+ "merge.conflictStyle=merge",
26505
+ "show",
26506
+ "--remerge-diff",
26507
+ "--format=",
26508
+ "--no-color",
26509
+ "--no-ext-diff",
26510
+ "--no-textconv",
26511
+ "--no-renames",
26512
+ "--src-prefix=a/",
26513
+ "--dst-prefix=b/",
26514
+ "--unified=1048576",
26515
+ c.commit,
26516
+ "--",
26517
+ ...HISTORY_PATHSPECS
26518
+ ]));
26519
+ touched = remergeRecords(patch);
26520
+ if (!touched.size) return { commit: c.commit, events: [], boundary: false };
26521
+ }
26522
+ const after = await snapshot3(c.commit);
26523
+ let boundary = false;
26524
+ const parents = [];
26525
+ for (const parent of c.parents) {
26526
+ try {
26527
+ parents.push(await snapshot3(parent));
26528
+ } catch (error62) {
26529
+ if (error62 instanceof LegacyBaselineError) {
26530
+ parents.push(emptyBundle());
26531
+ boundary = true;
26532
+ } else throw error62;
26533
+ }
26534
+ }
26535
+ const withoutReasons = (b) => ({ specs: b.specs.map((s) => ({ ...s, history: [] })), wiki: { ...b.wiki, history: [] } });
26536
+ const differences = parents.map((p) => comparePreviewBundles(withoutReasons(p), withoutReasons(after)).changes);
26537
+ const reasons = (b) => [...b.specs.flatMap((s) => s.history), ...b.wiki.history];
26538
+ const inherited = new Set(parents.flatMap((p) => reasons(p).map((r) => r.id)));
26539
+ const added = reasons(after).filter((r) => !inherited.has(r.id));
26540
+ const selected = differences[0].filter((change2) => touched ? touched.has(change2.id) : differences.every((d) => d.some((v) => v.id === change2.id)));
26541
+ return { commit: c.commit, events: eventsOf(c, selected.map((change2) => ({
26542
+ ...change2,
26543
+ reasons: added.filter((r) => [...r.requirements, ...r.designs ?? [], ...r.documents ?? []].includes(change2.id))
26544
+ }))), boundary };
26545
+ }
26047
26546
  async function batch(commits) {
26048
26547
  const names = /* @__PURE__ */ new Map();
26049
- for (const c of commits.filter((c2) => !c2.entries.some((e) => isLegacy(e.path) && !isZero(e.newBlob)))) {
26548
+ for (const c of commits.filter((c2) => c2.parents.length < 2 && !c2.entries.some((e) => isLegacy(e.path) && !isZero(e.newBlob)))) {
26050
26549
  const files = wanted(c.entries);
26051
26550
  const boundary = c.entries.some((e) => isLegacy(e.path) && !isZero(e.oldBlob));
26052
26551
  names.set(c, { after: files.map((f) => c.commit + ":" + f), before: c.parent && !boundary ? files.map((f) => c.parent + ":" + f) : [], boundary });
@@ -26059,6 +26558,10 @@ function createCommitChanges(root, snapshot3) {
26059
26558
  }
26060
26559
  const result = [];
26061
26560
  for (const c of commits) {
26561
+ if (c.parents.length > 1) {
26562
+ result.push(await merge2(c));
26563
+ continue;
26564
+ }
26062
26565
  let value;
26063
26566
  const want = names.get(c);
26064
26567
  if (want && blobs) {
@@ -26076,12 +26579,12 @@ function createCommitChanges(root, snapshot3) {
26076
26579
  return result;
26077
26580
  }
26078
26581
  return {
26079
- /** The commits of `head`'s first-parent history that touched records, newest first. One Git process. */
26582
+ /** All reachable record commits, children before parents even when author clocks differ. */
26080
26583
  async lineage(head) {
26081
- const text2 = reader.decode(await reader.run(["rev-list", "--first-parent", head, "--", ...HISTORY_PATHSPECS]));
26584
+ const text2 = reader.decode(await reader.run(["rev-list", "--full-history", "--date-order", head, "--", ...HISTORY_PATHSPECS]));
26082
26585
  return text2.split("\n").filter(Boolean);
26083
26586
  },
26084
- /** The changes of the given commits, in the given order; two Git processes per hundred commits. */
26587
+ /** Changes in the given order; ordinary commits are batched, merges also read their resolution diff. */
26085
26588
  async of(commits) {
26086
26589
  const out = [];
26087
26590
  for (let i = 0; i < commits.length; i += BATCH) {
@@ -26104,7 +26607,7 @@ function createCommitChanges(root, snapshot3) {
26104
26607
  import { DatabaseSync } from "node:sqlite";
26105
26608
  import { mkdir as mkdir3, rm } from "node:fs/promises";
26106
26609
  import { join as join5 } from "node:path";
26107
- var INDEX_FORMAT = 1;
26610
+ var INDEX_FORMAT = 2;
26108
26611
  var SCHEMA = `
26109
26612
  -- Commits already read, and whether the commit's parent was legacy JSON the history does not read past.
26110
26613
  CREATE TABLE commits (oid TEXT PRIMARY KEY, boundary INTEGER NOT NULL);
@@ -26114,7 +26617,7 @@ var SCHEMA = `
26114
26617
  id TEXT NOT NULL, kind TEXT NOT NULL, types TEXT NOT NULL, email TEXT NOT NULL, date TEXT NOT NULL,
26115
26618
  before_spec TEXT, after_spec TEXT, needle TEXT NOT NULL, row TEXT NOT NULL, detail TEXT NOT NULL);
26116
26619
  CREATE INDEX changes_by_commit ON changes (oid, ord);
26117
- -- The commits of one HEAD's first-parent history that touched records, newest first (pos 0).
26620
+ -- All reachable record commits of one HEAD, children before parents (pos 0 is newest).
26118
26621
  CREATE TABLE lineage (head TEXT NOT NULL, pos INTEGER NOT NULL, oid TEXT NOT NULL, PRIMARY KEY (head, pos));
26119
26622
  CREATE INDEX lineage_by_commit ON lineage (head, oid);
26120
26623
  CREATE TABLE heads (head TEXT PRIMARY KEY, seen INTEGER NOT NULL);
@@ -26295,12 +26798,13 @@ function createHistoryIndex(root, snapshot3) {
26295
26798
  }
26296
26799
  function ensure(head) {
26297
26800
  if (built.has(head)) return Promise.resolve();
26298
- let pending = building.get(head);
26801
+ const key = head + ":" + getLanguage();
26802
+ let pending = building.get(key);
26299
26803
  if (!pending) {
26300
26804
  pending = build(head).then(() => {
26301
26805
  built.add(head);
26302
- }).finally(() => building.delete(head));
26303
- building.set(head, pending);
26806
+ }).finally(() => building.delete(key));
26807
+ building.set(key, pending);
26304
26808
  }
26305
26809
  return pending;
26306
26810
  }
@@ -26381,8 +26885,8 @@ function createHistoryIndex(root, snapshot3) {
26381
26885
  /** Replaces the search rows of one worktree's checkout when it changed since they were written. */
26382
26886
  async syncCheckout(scope, stamp, documents) {
26383
26887
  await database.with((db) => {
26384
- const current = db.prepare("SELECT stamp FROM checkouts WHERE scope = ?").get(scope);
26385
- if (current?.stamp === stamp) return;
26888
+ const current2 = db.prepare("SELECT stamp FROM checkouts WHERE scope = ?").get(scope);
26889
+ if (current2?.stamp === stamp) return;
26386
26890
  transaction(db, () => {
26387
26891
  db.prepare("DELETE FROM search WHERE scope = ?").run(scope);
26388
26892
  const row = db.prepare("INSERT INTO search (scope, kind, ref, oid, payload, title, place, body) VALUES (?,?,?,?,?,?,?,?)");
@@ -26448,13 +26952,14 @@ function recordRoutes(root, sessionId, env) {
26448
26952
  const reader = specPreviewReader(root);
26449
26953
  const snapshots = /* @__PURE__ */ new Map();
26450
26954
  const snapshot3 = (oid3) => {
26451
- let value = snapshots.get(oid3);
26955
+ const key = oid3 + ":" + getLanguage();
26956
+ let value = snapshots.get(key);
26452
26957
  if (!value) {
26453
26958
  value = reader.readBundle(oid3).catch((e) => {
26454
- snapshots.delete(oid3);
26959
+ snapshots.delete(key);
26455
26960
  throw e;
26456
26961
  });
26457
- snapshots.set(oid3, value);
26962
+ snapshots.set(key, value);
26458
26963
  }
26459
26964
  if (snapshots.size > 128) snapshots.delete(snapshots.keys().next().value);
26460
26965
  return value;
@@ -26462,7 +26967,7 @@ function recordRoutes(root, sessionId, env) {
26462
26967
  const readCheckout = createCheckoutReader(root, sessionId, env);
26463
26968
  const history = createHistoryIndex(root, snapshot3);
26464
26969
  const scope = "checkout:" + root;
26465
- const unreadable = t2("server.specsUnreadable");
26970
+ const unreadable = () => t2("server.specsUnreadable");
26466
26971
  let synced;
26467
26972
  const checkout = async () => {
26468
26973
  const read = await readCheckout();
@@ -26552,18 +27057,19 @@ async function startBrowserServer(options) {
26552
27057
  ...recordRoutes(root, sessionId, options.env),
26553
27058
  ...assetRoutes(root)
26554
27059
  ];
26555
- let updatePending;
26556
27060
  let closing = false;
26557
27061
  let origin = "";
26558
27062
  const server = createServer({ requestTimeout: 5e3, headersTimeout: 5e3, maxHeaderSize: 8192 }, (request, response) => {
26559
- void (async () => {
27063
+ const header = request.headers["accept-language"];
27064
+ const language = typeof header === "string" ? resolveLanguage2(header.split(",")[0]?.split(";")[0]?.trim()) : getLanguage();
27065
+ void withLanguage(language, async () => {
26560
27066
  if (closing) return fail4(response, 503, "SERVER_CLOSING", t2("server.closing"));
26561
27067
  const admitted = admit(request, response, origin, !!options.dev);
26562
27068
  if (!admitted) return;
26563
27069
  if (admitted.api) return dispatch(routes, sessionId, admitted.url, admitted.path, request, response, admitted.allowedOrigin);
26564
27070
  return serveStatic(request, response, admitted.path, assets);
26565
- })().catch(() => {
26566
- if (!response.headersSent && !response.destroyed) fail4(response, 500, "INTERNAL_ERROR", t2("server.internal"));
27071
+ }).catch(() => {
27072
+ if (!response.headersSent && !response.destroyed) fail4(response, 500, "INTERNAL_ERROR", t2("server.internal", {}, language));
26567
27073
  else response.destroy();
26568
27074
  });
26569
27075
  });
@@ -26587,12 +27093,6 @@ async function startBrowserServer(options) {
26587
27093
  const address = server.address();
26588
27094
  if (!address || typeof address === "string") throw new Error(t2("server.addressUnknown"));
26589
27095
  origin = "http://127.0.0.1:" + address.port;
26590
- if (options.fetchLatest) {
26591
- store.setUpdate(checkingUpdate);
26592
- updatePending = resolveUpdate(store.session.cliVersion, options.fetchLatest, controller.signal, options.updateCheckTimeoutMs).then((state) => {
26593
- store.setUpdate(state);
26594
- });
26595
- }
26596
27096
  let resolveClosed;
26597
27097
  const closed = new Promise((resolve) => {
26598
27098
  resolveClosed = resolve;
@@ -26608,7 +27108,6 @@ async function startBrowserServer(options) {
26608
27108
  server.closeAllConnections();
26609
27109
  });
26610
27110
  await store.pending;
26611
- await updatePending;
26612
27111
  options.signal?.removeEventListener("abort", onAbort);
26613
27112
  options.signal?.removeEventListener("abort", closeOnAbort);
26614
27113
  resolveClosed();
@@ -26625,17 +27124,6 @@ async function startBrowserServer(options) {
26625
27124
  }, close, closed };
26626
27125
  }
26627
27126
 
26628
- // src/adapters/registry/latest-version.ts
26629
- var manifestUrl = "https://registry.npmjs.org/gitifact";
26630
- var fetchLatestVersion = async (signal) => {
26631
- const response = await fetch(manifestUrl, { signal, redirect: "error", headers: { Accept: "application/vnd.npm.install-v1+json" } });
26632
- if (!response.ok) throw new Error("registry status " + response.status);
26633
- const manifest = await response.json();
26634
- const latest = manifest["dist-tags"]?.latest;
26635
- if (typeof latest !== "string") throw new Error("registry answer has no latest tag");
26636
- return latest;
26637
- };
26638
-
26639
27127
  // src/commands/browser.ts
26640
27128
  function parsePort(value) {
26641
27129
  if (!/^\d+$/.test(value) || Number(value) > 65535) throw new InvalidArgumentError(t2("browser.invalidPort"));
@@ -26652,9 +27140,7 @@ async function runBrowser(options, version2) {
26652
27140
  port: options.port,
26653
27141
  dev: options.dev ?? false,
26654
27142
  signal: controller.signal,
26655
- cliVersion: version2,
26656
- // --no-update-check or GITIFACT_NO_UPDATE_CHECK keeps the server from contacting the npm registry.
26657
- ...updateCheckDisabled(process.env, options.updateCheck === false) ? {} : { fetchLatest: fetchLatestVersion }
27143
+ cliVersion: version2
26658
27144
  });
26659
27145
  if (!controller.signal.aborted) process.stdout.write(server.url + "/\n");
26660
27146
  await server.closed;
@@ -26824,7 +27310,7 @@ var info3 = (path3) => lstat5(path3).catch((e) => {
26824
27310
  if (e.code === "ENOENT") return void 0;
26825
27311
  throw e;
26826
27312
  });
26827
- function listDocTopics(lang = defaultLanguage2) {
27313
+ function listDocTopics(lang = getLanguage()) {
26828
27314
  const width = Math.max(...docTopics.map((topic) => topic.length));
26829
27315
  return docTopics.map((topic) => topic.padEnd(width) + " " + t2(`docs.summary.${topic}`, {}, lang)).join("\n") + "\n";
26830
27316
  }
@@ -26853,7 +27339,7 @@ async function readWikiPolicy(root) {
26853
27339
  }).join("\n").trim();
26854
27340
  return body || void 0;
26855
27341
  }
26856
- async function renderDoc(topic, controls = {}, lang = defaultLanguage2) {
27342
+ async function renderDoc(topic, controls = {}, lang = getLanguage()) {
26857
27343
  const read = controls.readDoc ?? readBundledDoc;
26858
27344
  const format = (await read(topic, lang)).trimEnd();
26859
27345
  if (topic !== "wiki") return format + "\n";
@@ -26892,11 +27378,16 @@ function boilerplateFor(path3) {
26892
27378
  return "# " + name + "\n\nProject-specific guidance for AI coding agents.\n";
26893
27379
  }
26894
27380
  var readBundledBlock = (lang) => readFile9(new URL("./i18n/" + lang + "/block.md", import.meta.url), "utf8");
26895
- async function renderAgentBlock(version2, controls = {}, lang = defaultLanguage2) {
27381
+ async function renderAgentBlock(version2, controls = {}, lang = getLanguage()) {
26896
27382
  const body = await (controls.readBlock ?? readBundledBlock)(lang);
26897
27383
  const filled = body.trimEnd().replace(/\{(version|language|topics)\}/g, (_whole, name) => name === "version" ? version2 : name === "language" ? lang : docTopics.join(", "));
26898
27384
  return AGENT_START + "\n" + filled + "\n" + AGENT_END;
26899
27385
  }
27386
+ function parseAgentBlock(text2) {
27387
+ const match = /^gitifact v(\S+)(?: · ([a-z][a-z-]*))? · .*schemaVersion (\d+)$/m.exec(text2);
27388
+ if (!match) return null;
27389
+ return { version: match[1], language: match[2] ?? "ko", schemaVersion: Number(match[3]) };
27390
+ }
26900
27391
  function findBlock(text2) {
26901
27392
  const start = text2.indexOf(AGENT_START);
26902
27393
  if (start === -1) {
@@ -26982,17 +27473,23 @@ async function planAgentDocs(root, options) {
26982
27473
  }
26983
27474
  return { mode: "remove", paths: paths2, writes, missing: [] };
26984
27475
  }
26985
- const block = await renderAgentBlock(options.version, options);
27476
+ const blocks = /* @__PURE__ */ new Map();
27477
+ const blockFor = async (previous) => {
27478
+ const stored = previous ? parseAgentBlock(previous)?.language : void 0;
27479
+ const language = explicitLanguage() ?? (stored === "ko" || stored === "en" ? stored : getLanguage());
27480
+ if (!blocks.has(language)) blocks.set(language, await renderAgentBlock(options.version, options, language));
27481
+ return blocks.get(language);
27482
+ };
26986
27483
  const { inject, create } = options.onlyExisting ? { inject: candidatePaths.filter((path3) => existing.has(path3) && findBlock(existing.get(path3)) && !isWrapperFile(existing.get(path3))), create: null } : resolveAgentPaths(options.agent, existing);
26987
27484
  for (const path3 of inject) {
26988
27485
  const previous = existing.get(path3);
26989
- const next = injectBlock(previous, block, boilerplateFor(path3));
27486
+ const next = injectBlock(previous, await blockFor(previous), boilerplateFor(path3));
26990
27487
  paths2.push(path3);
26991
27488
  if (next !== previous) writes.push({ path: path3, previous, next });
26992
27489
  }
26993
27490
  if (create) {
26994
27491
  paths2.push(create);
26995
- writes.push({ path: create, previous: null, next: injectBlock(null, block, boilerplateFor(create)) });
27492
+ writes.push({ path: create, previous: null, next: injectBlock(null, await blockFor(), boilerplateFor(create)) });
26996
27493
  }
26997
27494
  const missing2 = [];
26998
27495
  if (paths2.includes("AGENTS.md") && lacksClaudeFile(existing)) {
@@ -27013,17 +27510,59 @@ async function applyAgentDocs(root, plan, recheck) {
27013
27510
  return plan.writes.map((write) => write.path);
27014
27511
  }
27015
27512
 
27513
+ // src/shared/update-check.ts
27514
+ var updateCheckTimeoutMs = 3e3;
27515
+ var release2 = /^(\d+)\.(\d+)\.(\d+)$/;
27516
+ var running = /^(\d+)\.(\d+)\.(\d+)(-[0-9A-Za-z.-]+)?$/;
27517
+ function isNewerRelease(latest, current2) {
27518
+ const next = release2.exec(latest);
27519
+ const now = running.exec(current2);
27520
+ if (!next || !now) return false;
27521
+ for (let index = 1; index <= 3; index++) {
27522
+ const difference = Number(next[index]) - Number(now[index]);
27523
+ if (difference !== 0) return difference > 0;
27524
+ }
27525
+ return now[4] !== void 0;
27526
+ }
27527
+ var updateCheckDisabled = (env, flagged = false) => flagged || env.GITIFACT_NO_UPDATE_CHECK !== void 0 && env.GITIFACT_NO_UPDATE_CHECK !== "" && env.GITIFACT_NO_UPDATE_CHECK !== "0";
27528
+ var disabledUpdate = { status: "disabled", latestVersion: null };
27529
+ async function resolveUpdate(current2, fetchLatest, signal, timeoutMs = updateCheckTimeoutMs) {
27530
+ const controller = new AbortController();
27531
+ const abort = () => controller.abort();
27532
+ const timer = setTimeout(abort, timeoutMs);
27533
+ signal?.addEventListener("abort", abort, { once: true });
27534
+ if (signal?.aborted) controller.abort();
27535
+ try {
27536
+ const latest = await Promise.race([
27537
+ fetchLatest(controller.signal),
27538
+ new Promise((_resolve, reject) => {
27539
+ if (controller.signal.aborted) reject(new Error("aborted"));
27540
+ else controller.signal.addEventListener("abort", () => reject(new Error("aborted")), { once: true });
27541
+ })
27542
+ ]);
27543
+ if (typeof latest !== "string" || !release2.test(latest)) return { status: "unavailable", latestVersion: null };
27544
+ return { status: isNewerRelease(latest, current2) ? "available" : "up-to-date", latestVersion: latest };
27545
+ } catch {
27546
+ return { status: "unavailable", latestVersion: null };
27547
+ } finally {
27548
+ clearTimeout(timer);
27549
+ signal?.removeEventListener("abort", abort);
27550
+ }
27551
+ }
27552
+ var npmGlobalInstall = (version2) => "npm install -g gitifact@" + version2;
27553
+ var npxUpdate = (version2) => "npx --yes gitifact@" + version2 + " update";
27554
+
27016
27555
  // src/commands/spec-init.ts
27017
- async function initializeSpecProject(cwd, dryRun = false, env = process.env, beforePublish, agentDocs, readDoc = (name) => readBundledDoc(name, defaultLanguage2), update = disabledUpdate) {
27556
+ async function initializeSpecProject(cwd, dryRun = false, env = process.env, beforePublish, agentDocs, readDoc = (name) => readBundledDoc(name, getLanguage()), update = disabledUpdate) {
27018
27557
  const repo = initRepository(cwd, env);
27019
27558
  const first = await repo.inspect();
27020
27559
  const root = first.state.repository.rootPath;
27021
27560
  const docsPlan = agentDocs ? await planAgentDocs(root, agentDocs) : skippedAgentDocs;
27022
27561
  const result = async (config3, outcome) => {
27023
27562
  const checked = await update;
27024
- return projectInitV5.parse({
27563
+ return projectInitV6.parse({
27025
27564
  contract: "project-init",
27026
- version: 5,
27565
+ version: 6,
27027
27566
  ok: true,
27028
27567
  outcome,
27029
27568
  rootPath: root,
@@ -27032,7 +27571,7 @@ async function initializeSpecProject(cwd, dryRun = false, env = process.env, bef
27032
27571
  baseline: config3.baseline,
27033
27572
  agentDocs: { mode: docsPlan.mode, paths: docsPlan.paths },
27034
27573
  update: checked,
27035
- install: checked.status === "available" ? { npmGlobal: npmGlobalInstall(checked.latestVersion) } : null
27574
+ install: checked.status === "available" ? { npx: npxUpdate(checked.latestVersion), npmGlobal: npmGlobalInstall(checked.latestVersion) } : null
27036
27575
  });
27037
27576
  };
27038
27577
  const existing = async (text3) => {
@@ -27133,6 +27672,17 @@ async function writeWikiPolicy(root, readDoc) {
27133
27672
  await writeFile2(join10(root, ...WIKI_ENTRY_PATH.split("/")), text2, { flag: "wx" });
27134
27673
  }
27135
27674
 
27675
+ // src/adapters/registry/latest-version.ts
27676
+ var manifestUrl = "https://registry.npmjs.org/gitifact";
27677
+ var fetchLatestVersion = async (signal) => {
27678
+ const response = await fetch(manifestUrl, { signal, redirect: "error", headers: { Accept: "application/vnd.npm.install-v1+json" } });
27679
+ if (!response.ok) throw new Error("registry status " + response.status);
27680
+ const manifest = await response.json();
27681
+ const latest = manifest["dist-tags"]?.latest;
27682
+ if (typeof latest !== "string") throw new Error("registry answer has no latest tag");
27683
+ return latest;
27684
+ };
27685
+
27136
27686
  // src/commands/init.ts
27137
27687
  async function runInit(options, version2) {
27138
27688
  const cancel = new AbortController();
@@ -27149,7 +27699,7 @@ async function runInit(options, version2) {
27149
27699
  );
27150
27700
  if (!dto.ok) throw new Error("unreachable");
27151
27701
  const docs = dto.agentDocs.mode === "skip" ? t2("init.text.skipped") : dto.agentDocs.paths.join(", ") + (dto.agentDocs.mode === "remove" ? " (" + t2("init.text.blockRemoved") + ")" : dto.outcome === "planned" ? " (" + t2("init.text.blockPlanned") + ")" : " (" + t2("init.text.blockUpdated") + ")");
27152
- const newer = dto.install ? t2("init.text.updateAvailable", { version: dto.update.latestVersion, command: dto.install.npmGlobal }) + "\n" : "";
27702
+ const newer = dto.install ? t2("init.text.updateAvailable", { version: dto.update.latestVersion, command: dto.install.npx }) + "\n" : "";
27153
27703
  process.stdout.write(options.format === "text" ? `${dto.outcome}: ${dto.rootPath}/.gitifact/config.json
27154
27704
  ${t2("init.text.storage")}: schemaVersion ${dto.schemaVersion}
27155
27705
  ${t2("init.text.agentDocs")}: ${docs}
@@ -27158,7 +27708,7 @@ ${newer}` : JSON.stringify(dto) + "\n");
27158
27708
  cancel.abort();
27159
27709
  const known = error62 instanceof InitError || error62 instanceof RepositoryReadError;
27160
27710
  const failure2 = { code: known ? error62.code : "INIT_FAILED", message: known ? error62.message : t2("init.failed") };
27161
- process.stderr.write(options.format === "text" ? failure2.code + ": " + failure2.message + "\n" : JSON.stringify(projectInitV5.parse({ contract: "project-init", version: 5, ok: false, error: failure2 })) + "\n");
27711
+ process.stderr.write(options.format === "text" ? failure2.code + ": " + failure2.message + "\n" : JSON.stringify(projectInitV6.parse({ contract: "project-init", version: 6, ok: false, error: failure2 })) + "\n");
27162
27712
  process.exitCode = 1;
27163
27713
  }
27164
27714
  }
@@ -27263,13 +27813,13 @@ async function updateCommand(cwd, version2, env = process.env, controls = {}) {
27263
27813
  refreshBlocks(cwd, version2, env, controls)
27264
27814
  ]);
27265
27815
  const commit = controls.commit ? await commitBlocks(agentDocs, version2, env) : commitState("not-requested");
27266
- return updateV3.parse({
27816
+ return updateV4.parse({
27267
27817
  contract: "update",
27268
- version: 3,
27818
+ version: 4,
27269
27819
  ok: true,
27270
27820
  cliVersion: version2,
27271
27821
  update,
27272
- install: update.status === "available" ? { npmGlobal: npmGlobalInstall(update.latestVersion) } : null,
27822
+ install: update.status === "available" ? { npx: npxUpdate(update.latestVersion), npmGlobal: npmGlobalInstall(update.latestVersion) } : null,
27273
27823
  agentDocs: { state: agentDocs.state, paths: agentDocs.paths, missing: agentDocs.missing },
27274
27824
  commit
27275
27825
  });
@@ -27284,9 +27834,9 @@ async function runUpdate(options, version2) {
27284
27834
  }
27285
27835
  const lines = [t2("update.text.current", { version: dto.cliVersion })];
27286
27836
  lines.push(dto.update.status === "available" ? t2("update.text.available", { version: dto.update.latestVersion }) : dto.update.status === "up-to-date" ? t2("update.text.upToDate") : dto.update.status === "disabled" ? t2("update.text.disabled") : t2("update.text.unavailable"));
27287
- if (dto.install) lines.push(t2("update.text.install", { command: dto.install.npmGlobal }), t2("update.text.afterInstall"));
27288
- lines.push(dto.agentDocs.state === "refreshed" ? t2("update.text.blockRefreshed", { paths: dto.agentDocs.paths.join(", ") }) : dto.agentDocs.state === "current" ? t2("update.text.blockCurrent", { paths: dto.agentDocs.paths.join(", ") }) : dto.agentDocs.state === "no-block" ? t2("update.text.noBlock") : t2("update.text.notInitialized"));
27289
- if (dto.agentDocs.missing.length) lines.push(t2("update.text.missing", { paths: dto.agentDocs.missing.join(", ") }));
27837
+ if (dto.install) lines.push(t2("update.text.install", { command: dto.install.npx }), t2("update.text.afterInstall"));
27838
+ lines.push(dto.agentDocs.state === "refreshed" ? t2("update.text.blockRefreshed", { paths: dto.agentDocs.paths.join(", ") }) : dto.agentDocs.state === "current" ? t2("update.text.blockCurrent", { paths: dto.agentDocs.paths.join(", ") }) : dto.agentDocs.state === "no-block" ? t2("update.text.noBlock", { version: dto.cliVersion }) : t2("update.text.notInitialized"));
27839
+ if (dto.agentDocs.missing.length) lines.push(t2("update.text.missing", { paths: dto.agentDocs.missing.join(", "), version: dto.cliVersion }));
27290
27840
  const { commit } = dto;
27291
27841
  if (commit.state === "committed") lines.push(t2("update.text.committed", { commit: commit.commit.slice(0, 12), paths: commit.paths.join(", ") }));
27292
27842
  else if (commit.state === "nothing") lines.push(t2("update.text.commitNothing"));
@@ -27299,11 +27849,38 @@ async function runUpdate(options, version2) {
27299
27849
  } catch (error62) {
27300
27850
  const known = error62 instanceof InitError || error62 instanceof RepositoryReadError;
27301
27851
  const failure2 = { code: known ? error62.code : "UPDATE_FAILED", message: known ? error62.message : t2("update.failed") };
27302
- process.stderr.write(options.format === "text" ? failure2.code + ": " + failure2.message + "\n" : JSON.stringify(updateV3.parse({ contract: "update", version: 3, ok: false, error: failure2 })) + "\n");
27852
+ process.stderr.write(options.format === "text" ? failure2.code + ": " + failure2.message + "\n" : JSON.stringify(updateV4.parse({ contract: "update", version: 4, ok: false, error: failure2 })) + "\n");
27303
27853
  process.exitCode = 1;
27304
27854
  }
27305
27855
  }
27306
27856
 
27857
+ // src/commands/update-check.ts
27858
+ async function checkUpdateCommand(version2, env = process.env, controls = {}) {
27859
+ const update = updateCheckDisabled(env) ? disabledUpdate : await resolveUpdate(version2, controls.fetchLatest ?? fetchLatestVersion, void 0, controls.timeoutMs);
27860
+ return updateCheckV1.parse({
27861
+ contract: "update-check",
27862
+ version: 1,
27863
+ ok: true,
27864
+ cliVersion: version2,
27865
+ update,
27866
+ command: update.status === "available" ? npxUpdate(update.latestVersion) : null
27867
+ });
27868
+ }
27869
+ async function runUpdateCheck(format, version2) {
27870
+ const dto = await checkUpdateCommand(version2);
27871
+ if (format === "json") {
27872
+ process.stdout.write(JSON.stringify(dto) + "\n");
27873
+ return;
27874
+ }
27875
+ const state = dto.update;
27876
+ const lines = [
27877
+ t2("update.text.current", { version: dto.cliVersion }),
27878
+ state.status === "available" ? t2("update.text.available", { version: state.latestVersion }) : state.status === "up-to-date" ? t2("update.text.upToDate") : state.status === "disabled" ? t2("update.text.disabled") : t2("update.text.unavailable")
27879
+ ];
27880
+ if (dto.command) lines.push(t2("update.text.install", { command: dto.command }));
27881
+ process.stdout.write(lines.join("\n") + "\n");
27882
+ }
27883
+
27307
27884
  // src/commands/spec-preview.ts
27308
27885
  import { join as join15 } from "node:path";
27309
27886
  import { readFile as readFile12, stat as stat2 } from "node:fs/promises";
@@ -27345,11 +27922,11 @@ async function readPreviewContext(cwd) {
27345
27922
  var context = readPreviewContext;
27346
27923
  async function readFinalPreviewChanges(cwd) {
27347
27924
  const c = await context(cwd);
27348
- const current = await readWorkingPreviewState(cwd);
27349
- const delta = finalSpecPreviewChanges(c.bundle, current.bundle);
27925
+ const current2 = await readWorkingPreviewState(cwd);
27926
+ const delta = finalSpecPreviewChanges(c.bundle, current2.bundle);
27350
27927
  await c.recheck();
27351
- const pendingReasons = pendingPreviewReasons(c.bundle, current.bundle);
27352
- return { base: c.base, expected: expected(c.base, current.stamp), stamp: current.stamp, pendingReasons, ...delta };
27928
+ const pendingReasons = pendingPreviewReasons(c.bundle, current2.bundle);
27929
+ return { base: c.base, expected: expected(c.base, current2.stamp), stamp: current2.stamp, pendingReasons, ...delta };
27353
27930
  }
27354
27931
 
27355
27932
  // src/adapters/filesystem/agent-inputs.ts
@@ -27535,15 +28112,15 @@ async function specCommit(cwd, input2, dryRun) {
27535
28112
  if (await info5(busy)) fail7(t2("commit.busy", { path: busy }));
27536
28113
  if ((await staged()).length) fail7(t2("commit.existingStaging"));
27537
28114
  const indexHash = hash2(await optional2(indexPath) ?? Buffer.alloc(0));
27538
- const current = c.working;
27539
- if (request.expected !== void 0 && request.expected !== previewExpected(c.base, current.stamp)) fail7(t2("commit.staleInput"));
27540
- const reasons = request.reasons ?? pendingPreviewReasons(c.bundle, current.bundle).map((h) => ({ requirements: h.requirements, ...h.designs ? { designs: h.designs } : {}, ...h.documents ? { documents: h.documents } : {}, reason: h.reason }));
27541
- const prepared = prepareSpecPreview(c.bundle, current.bundle, c.files, current.files, reasons, () => generatePreviewId("H"));
28115
+ const current2 = c.working;
28116
+ if (request.expected !== void 0 && request.expected !== previewExpected(c.base, current2.stamp)) fail7(t2("commit.staleInput"));
28117
+ const reasons = request.reasons ?? pendingPreviewReasons(c.bundle, current2.bundle).map((h) => ({ requirements: h.requirements, ...h.designs ? { designs: h.designs } : {}, ...h.documents ? { documents: h.documents } : {}, reason: h.reason }));
28118
+ const prepared = prepareSpecPreview(c.bundle, current2.bundle, c.files, current2.files, reasons, () => generatePreviewId("H"));
27542
28119
  const blobId = (text2) => {
27543
28120
  const bytes = Buffer.from(text2);
27544
28121
  return createHash7(objectFormat).update(`blob ${bytes.length}\0`).update(bytes).digest("hex");
27545
28122
  };
27546
- const present = [...current.files.keys()];
28123
+ const present = [...current2.files.keys()];
27547
28124
  if (present.some((p) => p.includes("\n"))) fail7(t2("commit.newlinePath"));
27548
28125
  const stored = present.length ? (await git(["hash-object", "--stdin-paths"], void 0, Buffer.from(present.join("\n") + "\n"))).toString("utf8").split("\n") : [""];
27549
28126
  if (stored.length !== present.length + 1) fail7(t2("commit.storedShape"));
@@ -27552,14 +28129,14 @@ async function specCommit(cwd, input2, dryRun) {
27552
28129
  const finalId = (p) => writes.has(p) ? writes.get(p) === null ? void 0 : blobId(writes.get(p)) : workingIds.get(p);
27553
28130
  const pending = [.../* @__PURE__ */ new Set([...c.files.keys(), ...present, ...writes.keys()])].filter((p) => (c.files.has(p) ? blobId(c.files.get(p)) : void 0) !== finalId(p));
27554
28131
  if (pending.some((p) => !selected.includes(p))) fail7(t2("commit.selectPending"));
27555
- const finalFiles = new Map(current.files);
28132
+ const finalFiles = new Map(current2.files);
27556
28133
  for (const [path3, next] of writes) {
27557
28134
  if (next === null) finalFiles.delete(path3);
27558
28135
  else finalFiles.set(path3, next);
27559
28136
  }
27560
28137
  await checkLegacySelection(root, selected);
27561
28138
  const stageable = selected.filter((p) => !record2(p) || finalFiles.has(p) || c.files.has(p));
27562
- const known = /* @__PURE__ */ new Set([...current.specs.flatMap((s) => s.requirements.map((r) => r.id)), ...prepared.changes.filter((x) => x.kind === "requirement").map((x) => x.id)]);
28139
+ const known = /* @__PURE__ */ new Set([...current2.specs.flatMap((s) => s.requirements.map((r) => r.id)), ...prepared.changes.filter((x) => x.kind === "requirement").map((x) => x.id)]);
27563
28140
  if (references.some((r) => !known.has(r))) fail7(t2("commit.unknownRequirement"));
27564
28141
  const requirements = [.../* @__PURE__ */ new Set([...references, ...prepared.changes.filter((x) => x.kind === "requirement").map((x) => x.id)])].sort();
27565
28142
  const contextPaths = [.../* @__PURE__ */ new Set([...policyPaths(selected), ...extra])].sort();
@@ -27597,7 +28174,7 @@ async function specCommit(cwd, input2, dryRun) {
27597
28174
  if (e.code === "EEXIST") fail7(t2("commit.busy", { path: busy }));
27598
28175
  throw e;
27599
28176
  }
27600
- await previewTransaction(cwd, current.stamp, async () => ({ writes, data: {}, recheck: c.recheck }), rename4, async (published) => {
28177
+ await previewTransaction(cwd, current2.stamp, async () => ({ writes, data: {}, recheck: c.recheck }), rename4, async (published) => {
27601
28178
  let commitStarted = false;
27602
28179
  try {
27603
28180
  const lock = indexLock = await open4(indexPath + ".lock", "wx", 384);
@@ -27884,16 +28461,24 @@ async function runMigrate(options) {
27884
28461
  }
27885
28462
 
27886
28463
  // src/main.ts
27887
- var program2 = new Command().name("gitifact").description(t2("help.program")).version("0.6.2").allowExcessArguments(false).addHelpText("after", "\n" + t2("help.notYet")).action(() => program2.outputHelp());
28464
+ var languageArgs = process.argv.slice(2).slice(0, process.argv.slice(2).indexOf("--") < 0 ? void 0 : process.argv.slice(2).indexOf("--"));
28465
+ var selectedLanguage;
28466
+ for (let i = 0; i < languageArgs.length; i++) {
28467
+ const arg = languageArgs[i];
28468
+ const value = arg === "--lang" ? languageArgs[++i] : arg.startsWith("--lang=") ? arg.slice(7) : void 0;
28469
+ if (value === "ko" || value === "en") selectedLanguage = value;
28470
+ }
28471
+ configureLanguage(selectedLanguage ?? environmentLanguage(), selectedLanguage);
28472
+ var program2 = new Command().name("gitifact").addOption(new Option("--lang <language>", t2("help.lang")).choices(["ko", "en"])).description(t2("help.program")).version("0.7.1").allowExcessArguments(false).addHelpText("after", "\n" + t2("help.notYet")).action(() => program2.outputHelp());
27888
28473
  program2.command("status").description(t2("help.status")).allowExcessArguments(false).addOption(new Option("--format <format>", t2("help.format")).choices(["json", "text"]).default("json")).action(async (options) => {
27889
28474
  await runStatus(options.format);
27890
28475
  });
27891
- program2.command("browser").description(t2("help.browser")).allowExcessArguments(false).option("--port <port>", t2("help.browserPort"), parsePort, 0).option("--dev", t2("help.browserDev")).option("--no-update-check", t2("help.browserNoUpdateCheck")).action((options) => runBrowser(options, "0.6.2"));
27892
- program2.command("init").description(t2("help.init")).allowExcessArguments(false).option("--dry-run", t2("help.initDryRun")).addOption(new Option("--agent <agent>", t2("help.initAgent")).choices([...agentPresetNames])).addOption(new Option("--remove-agents", t2("help.initRemoveAgents")).conflicts("skipAgents")).option("--skip-agents", t2("help.initSkipAgents")).addOption(new Option("--format <format>", t2("help.format")).choices(["json", "text"]).default("json")).action((options) => runInit(options, "0.6.2"));
28476
+ program2.command("browser").description(t2("help.browser")).allowExcessArguments(false).option("--port <port>", t2("help.browserPort"), parsePort, 0).option("--dev", t2("help.browserDev")).action((options) => runBrowser(options, "0.7.1"));
28477
+ program2.command("init").description(t2("help.init")).allowExcessArguments(false).option("--dry-run", t2("help.initDryRun")).addOption(new Option("--agent <agent>", t2("help.initAgent")).choices([...agentPresetNames])).addOption(new Option("--remove-agents", t2("help.initRemoveAgents")).conflicts("skipAgents")).option("--skip-agents", t2("help.initSkipAgents")).addOption(new Option("--format <format>", t2("help.format")).choices(["json", "text"]).default("json")).action((options) => runInit(options, "0.7.1"));
27893
28478
  program2.command("docs").description(t2("help.docs")).argument("[topic]", t2("help.docsTopic")).allowExcessArguments(false).action(async (topic) => {
27894
28479
  await runDocs(topic);
27895
28480
  });
27896
- program2.command("update").description(t2("help.update")).allowExcessArguments(false).addOption(new Option("--format <format>", t2("help.format")).choices(["json", "text"]).default("json")).option("--commit", t2("help.updateCommit")).action((options) => runUpdate(options, "0.6.2"));
28481
+ program2.command("update").description(t2("help.update")).allowExcessArguments(false).addOption(new Option("--format <format>", t2("help.format")).choices(["json", "text"]).default("json")).option("--commit", t2("help.updateCommit")).addOption(new Option("--check", t2("help.updateCheck")).conflicts("commit")).action((options) => options.check ? runUpdateCheck(options.format, "0.7.1") : runUpdate(options, "0.7.1"));
27897
28482
  program2.command("migrate").description(t2("help.migrate")).allowExcessArguments(false).option("--dry-run", t2("help.migrateDryRun")).action(runMigrate);
27898
28483
  var spec = program2.command("spec").description(t2("help.spec"));
27899
28484
  spec.command("commit").description(t2("help.specCommit")).allowExcessArguments(false).requiredOption("--file <path>", t2("help.specCommitFile")).option("--dry-run", t2("help.specCommitDryRun")).action((o) => runSpecPreview("commit", o));