archctx 0.4.5 → 0.4.6

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": "archcontext.practice-catalog-manifest/v1",
3
3
  "catalogVersion": "2026.06.0",
4
- "productVersion": "0.4.5",
4
+ "productVersion": "0.4.6",
5
5
  "generatedAt": "1970-01-01T00:00:00.000Z",
6
6
  "entries": [
7
7
  {
@@ -389,5 +389,5 @@
389
389
  "structurizr.dsl",
390
390
  "twelve-factor"
391
391
  ],
392
- "catalogDigest": "sha256:9687f9aabf24904bb08a174a9c0a83664d437bd0862bf0c7725faf2c11bbb913"
392
+ "catalogDigest": "sha256:12ce681507d3cf7bcec02fe367b1bb9161f3b469559a8f5d2db861d3a692c469"
393
393
  }
package/bin/archctx.mjs CHANGED
@@ -1088,7 +1088,7 @@ function productVersionManifest() {
1088
1088
  }
1089
1089
  };
1090
1090
  }
1091
- var ARCHCONTEXT_PRODUCT_NAME = "archctx", ARCHCONTEXT_PRODUCT_VERSION = "0.4.5", ARCHCONTEXT_PACKAGE_MANAGER = "bun@1.3.10", ARCHCONTEXT_NODE_RANGE = ">=24 <26", LOCAL_RUNTIME_RPC_SCHEMA_VERSION = "archcontext.runtime-rpc/v1", ARCHCONTEXT_SCHEMA_SET_VERSION = "2026-07-11.explorer-view-v2";
1091
+ var ARCHCONTEXT_PRODUCT_NAME = "archctx", ARCHCONTEXT_PRODUCT_VERSION = "0.4.6", ARCHCONTEXT_PACKAGE_MANAGER = "bun@1.3.10", ARCHCONTEXT_NODE_RANGE = ">=24 <26", LOCAL_RUNTIME_RPC_SCHEMA_VERSION = "archcontext.runtime-rpc/v1", ARCHCONTEXT_SCHEMA_SET_VERSION = "2026-07-11.explorer-view-v2";
1092
1092
 
1093
1093
  // packages/contracts/src/validator.ts
1094
1094
  function validateJsonSchema(schema, value) {
@@ -28492,6 +28492,71 @@ function auditGithubIssuesEnabledInManifestText(manifestText) {
28492
28492
  }
28493
28493
  var RUNTIME_RPC_VERSION = LOCAL_RUNTIME_RPC_SCHEMA_VERSION;
28494
28494
 
28495
+ class RuntimeUpdateInputError extends Error {
28496
+ }
28497
+ function decodeRuntimeWorktreeDigestProfile(value, field) {
28498
+ switch (value) {
28499
+ case "repository":
28500
+ case "architecture-documentation-projection":
28501
+ return value;
28502
+ default:
28503
+ throw new RuntimeUpdateInputError(`${field} must be repository or architecture-documentation-projection`);
28504
+ }
28505
+ }
28506
+ function decodeRuntimePlanUpdateInput(value) {
28507
+ const input = runtimeUpdateInputRecord(value, "plan_update input");
28508
+ if (typeof input.id !== "string" || input.id.length === 0) {
28509
+ throw new RuntimeUpdateInputError("plan_update id must be a non-empty string");
28510
+ }
28511
+ if (!Array.isArray(input.operations)) {
28512
+ throw new RuntimeUpdateInputError("plan_update operations must be an array");
28513
+ }
28514
+ let worktreeDigestPrecondition;
28515
+ if (input.worktreeDigestPrecondition !== undefined) {
28516
+ const precondition = runtimeUpdateInputRecord(input.worktreeDigestPrecondition, "plan_update worktreeDigestPrecondition");
28517
+ const profile = decodeRuntimeWorktreeDigestProfile(precondition.profile, "plan_update worktreeDigestPrecondition.profile");
28518
+ if (profile !== "architecture-documentation-projection") {
28519
+ throw new RuntimeUpdateInputError("plan_update worktreeDigestPrecondition.profile must be architecture-documentation-projection");
28520
+ }
28521
+ if (typeof precondition.expectedDigest !== "string" || precondition.expectedDigest.length === 0) {
28522
+ throw new RuntimeUpdateInputError("plan_update worktreeDigestPrecondition.expectedDigest must be a non-empty string");
28523
+ }
28524
+ worktreeDigestPrecondition = { profile, expectedDigest: precondition.expectedDigest };
28525
+ }
28526
+ return {
28527
+ id: input.id,
28528
+ operations: input.operations,
28529
+ ...input.reason === undefined ? {} : { reason: input.reason },
28530
+ ...worktreeDigestPrecondition === undefined ? {} : { worktreeDigestPrecondition }
28531
+ };
28532
+ }
28533
+ function decodeRuntimeApplyUpdateInput(value) {
28534
+ const input = runtimeUpdateInputRecord(value, "apply_update input");
28535
+ if (typeof input.id !== "string" || input.id.length === 0) {
28536
+ throw new RuntimeUpdateInputError("apply_update id must be a non-empty string");
28537
+ }
28538
+ if (typeof input.approved !== "boolean") {
28539
+ throw new RuntimeUpdateInputError("apply_update approved must be a boolean");
28540
+ }
28541
+ if (typeof input.expectedWorktreeDigest !== "string" || input.expectedWorktreeDigest.length === 0) {
28542
+ throw new RuntimeUpdateInputError("apply_update expectedWorktreeDigest must be a non-empty string");
28543
+ }
28544
+ const worktreeDigestProfile = input.worktreeDigestProfile === undefined ? undefined : decodeRuntimeWorktreeDigestProfile(input.worktreeDigestProfile, "apply_update worktreeDigestProfile");
28545
+ return {
28546
+ id: input.id,
28547
+ approved: input.approved,
28548
+ expectedWorktreeDigest: input.expectedWorktreeDigest,
28549
+ ...worktreeDigestProfile === undefined ? {} : { worktreeDigestProfile },
28550
+ ...input.projectionApplyReceipt === undefined ? {} : { projectionApplyReceipt: input.projectionApplyReceipt }
28551
+ };
28552
+ }
28553
+ function runtimeUpdateInputRecord(value, field) {
28554
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
28555
+ throw new RuntimeUpdateInputError(`${field} must be an object`);
28556
+ }
28557
+ return value;
28558
+ }
28559
+
28495
28560
  class ArchitectureLedgerReadModelStore {
28496
28561
  fallback;
28497
28562
  localStore;
@@ -28602,6 +28667,7 @@ class ArchctxDaemon {
28602
28667
  checkpointBaselines = new Map;
28603
28668
  checkpointCoalesced = new Map;
28604
28669
  changesets = new Map;
28670
+ changeSetWorktreeDigestProfiles = new Map;
28605
28671
  deferredArchitectureChangeFeedFailures = new Map;
28606
28672
  auditRunAbortControllers = new Map;
28607
28673
  landscape;
@@ -29591,6 +29657,7 @@ class ArchctxDaemon {
29591
29657
  operations: [{ op: "write_waiver", path, expectedHash, body }]
29592
29658
  });
29593
29659
  this.changesets.set(draft.id, draft);
29660
+ this.changeSetWorktreeDigestProfiles.set(draft.id, "repository");
29594
29661
  return okEnvelope("practices.waive", {
29595
29662
  schemaVersion: "archcontext.practice-waiver-plan/v1",
29596
29663
  waiver,
@@ -29762,21 +29829,36 @@ class ArchctxDaemon {
29762
29829
  };
29763
29830
  return okEnvelope("resource.read", result);
29764
29831
  }
29765
- async planUpdate(root, input) {
29832
+ async planUpdate(root, rawInput) {
29766
29833
  this.assertRunning();
29834
+ let input;
29835
+ try {
29836
+ input = decodeRuntimePlanUpdateInput(rawInput);
29837
+ } catch (error) {
29838
+ if (error instanceof RuntimeUpdateInputError) {
29839
+ return errorEnvelope("plan_update", "AC_SCHEMA_INVALID", error.message);
29840
+ }
29841
+ throw error;
29842
+ }
29767
29843
  const session = await this.openSession(root);
29768
29844
  const model = await this.readModelStore.validateModel(session.workspace);
29845
+ const worktreeDigestProfile = input.worktreeDigestPrecondition?.profile ?? "repository";
29846
+ const worktreeDigest = runtimeWorktreeDigest(root, worktreeDigestProfile);
29847
+ if (input.worktreeDigestPrecondition && input.worktreeDigestPrecondition.expectedDigest !== worktreeDigest) {
29848
+ throw new Error("Worktree digest changed before plan");
29849
+ }
29769
29850
  const draft = this.changeSetEngine.plan({
29770
29851
  id: input.id,
29771
29852
  base: {
29772
29853
  headSha: session.workspace.headSha,
29773
- worktreeDigest: session.snapshot.worktreeDigest,
29854
+ worktreeDigest,
29774
29855
  modelDigest: model.modelDigest
29775
29856
  },
29776
29857
  reason: input.reason ?? { taskSessionId: "task_runtime" },
29777
29858
  operations: input.operations
29778
29859
  });
29779
29860
  this.changesets.set(draft.id, draft);
29861
+ this.changeSetWorktreeDigestProfiles.set(draft.id, worktreeDigestProfile);
29780
29862
  return okEnvelope("plan_update", {
29781
29863
  draft,
29782
29864
  preview: this.changeSetEngine.preview(root, draft)
@@ -29905,18 +29987,31 @@ class ArchctxDaemon {
29905
29987
  return cached ? { ...cached.resource, queryDigest, cacheStatus: "stale" } : undefined;
29906
29988
  }
29907
29989
  }
29908
- async applyUpdate(root, input) {
29990
+ async applyUpdate(root, rawInput) {
29909
29991
  this.assertRunning();
29992
+ let input;
29993
+ try {
29994
+ input = decodeRuntimeApplyUpdateInput(rawInput);
29995
+ } catch (error) {
29996
+ if (error instanceof RuntimeUpdateInputError) {
29997
+ return errorEnvelope("apply_update", "AC_SCHEMA_INVALID", error.message);
29998
+ }
29999
+ throw error;
30000
+ }
29910
30001
  return this.withWriter(async () => {
29911
- if (!input.expectedWorktreeDigest)
29912
- throw new Error("apply_update requires expectedWorktreeDigest");
29913
- const current = computeWorktreeDigest2(root);
29914
- if (current !== input.expectedWorktreeDigest)
29915
- throw new Error("Worktree digest changed before apply");
29916
- const session = await this.openSession(root);
29917
30002
  const draft = this.changesets.get(input.id);
29918
30003
  if (!draft)
29919
30004
  throw new Error(`Unknown ChangeSet: ${input.id}`);
30005
+ const plannedProfile = this.changeSetWorktreeDigestProfiles.get(input.id);
30006
+ if (!plannedProfile)
30007
+ throw new Error("ChangeSet worktree digest profile missing before apply");
30008
+ const requestedProfile = input.worktreeDigestProfile ?? "repository";
30009
+ if (requestedProfile !== plannedProfile)
30010
+ throw new Error("ChangeSet worktree digest profile changed before apply");
30011
+ const current = runtimeWorktreeDigest(root, plannedProfile);
30012
+ if (current !== input.expectedWorktreeDigest)
30013
+ throw new Error("Worktree digest changed before apply");
30014
+ const session = await this.openSession(root);
29920
30015
  if (draft.base.headSha !== session.workspace.headSha)
29921
30016
  throw new Error("ChangeSet HEAD changed before apply");
29922
30017
  if (draft.base.worktreeDigest !== current)
@@ -32404,7 +32499,7 @@ class ArchctxRuntimeRpcServer {
32404
32499
  pid: process.pid,
32405
32500
  protocol: "http-loopback",
32406
32501
  version: 1,
32407
- product: productVersionManifest(),
32502
+ product: this.options.productManifest?.() ?? productVersionManifest(),
32408
32503
  composition: this.daemon.compositionReport()
32409
32504
  });
32410
32505
  return;
@@ -32719,6 +32814,16 @@ function shouldSkipGeneratedProjectionJob(metadata, input) {
32719
32814
  function isRuntimeAgentJobCursorStale(job, scope) {
32720
32815
  return job.worktree.headSha !== scope.worktree.headSha || job.worktree.worktreeDigest !== scope.worktree.worktreeDigest;
32721
32816
  }
32817
+ function runtimeWorktreeDigest(root, profile) {
32818
+ switch (profile) {
32819
+ case "repository":
32820
+ return computeWorktreeDigest2(root);
32821
+ case "architecture-documentation-projection":
32822
+ return architectureDocumentationProjectionWorktreeDigest(root, loadNativeModelFromArchContext(root));
32823
+ default:
32824
+ throw new RuntimeUpdateInputError(`unsupported worktree digest profile: ${String(profile)}`);
32825
+ }
32826
+ }
32722
32827
  function isArchContextGeneratedProjectionPath(path) {
32723
32828
  return path.replace(/\\/g, "/").startsWith(".archcontext/generated/");
32724
32829
  }
@@ -35773,14 +35878,19 @@ async function runProjectionProtocolCommand(args2, cwd, daemon) {
35773
35878
  const planned = await daemon.planUpdate(root, {
35774
35879
  id: changeSetId,
35775
35880
  reason: { taskSessionId: request.requestId },
35776
- operations: [architectureDocsRenderProjectionOperation(root, projection3.files)]
35881
+ operations: [architectureDocsRenderProjectionOperation(root, projection3.files)],
35882
+ worktreeDigestPrecondition: {
35883
+ profile: "architecture-documentation-projection",
35884
+ expectedDigest: request.expected.worktreeDigest
35885
+ }
35777
35886
  });
35778
35887
  if (!planned.ok)
35779
35888
  return planned;
35780
35889
  const applied = await daemon.applyUpdate(root, {
35781
35890
  id: changeSetId,
35782
35891
  approved: true,
35783
- expectedWorktreeDigest: computeWorktreeDigest(root),
35892
+ expectedWorktreeDigest: request.expected.worktreeDigest,
35893
+ worktreeDigestProfile: "architecture-documentation-projection",
35784
35894
  ...applyReceipt ? { projectionApplyReceipt: applyReceipt } : {}
35785
35895
  });
35786
35896
  if (!applied.ok)
@@ -37571,6 +37681,9 @@ async function doctorDaemon(cwd) {
37571
37681
  const health = await client.health().catch(() => {
37572
37682
  return;
37573
37683
  });
37684
+ const healthIssue = runtimeRpcCompatibilityIssueFromHealth(cwd, client, health);
37685
+ if (healthIssue)
37686
+ return incompatibleDaemonStatus(healthIssue);
37574
37687
  if (health?.ok === true) {
37575
37688
  const stalenessIssue = cliEntryStalenessIssue(cwd);
37576
37689
  if (stalenessIssue?.pidAlive)
@@ -37580,6 +37693,7 @@ async function doctorDaemon(cwd) {
37580
37693
  running: health?.ok === true,
37581
37694
  staleConnection: health?.ok !== true,
37582
37695
  rpcVersionCompatible: health?.schemaVersion === productVersionManifest().runtime.localRpc.schemaVersion,
37696
+ productVersionCompatible: health?.product?.product?.version === productVersionManifest().product.version,
37583
37697
  connection: client.connectionInfo(),
37584
37698
  health: health?.ok === true ? {
37585
37699
  composition: health.composition,
@@ -37723,6 +37837,9 @@ async function createOrStartRuntimeRpcClient(cwd) {
37723
37837
  const health = await startedClient.health().catch(() => {
37724
37838
  return;
37725
37839
  });
37840
+ const healthIssue = runtimeRpcCompatibilityIssueFromHealth(cwd, startedClient, health);
37841
+ if (healthIssue)
37842
+ throw new RuntimeVersionUnsupportedError(healthIssue);
37726
37843
  if (health?.ok === true)
37727
37844
  return startedClient;
37728
37845
  }
@@ -37812,6 +37929,7 @@ async function runDaemonCommand(args2, cwd) {
37812
37929
  running: true,
37813
37930
  product: health.product,
37814
37931
  rpcVersionCompatible: health.schemaVersion === RUNTIME_RPC_VERSION,
37932
+ productVersionCompatible: true,
37815
37933
  ...client.connectionInfo(),
37816
37934
  token: "stored-in-connection-file"
37817
37935
  });
@@ -37852,6 +37970,9 @@ async function startBackgroundDaemon(args2, cwd) {
37852
37970
  return errorEnvelope("daemon.start", "AC_RUNTIME_VERSION_UNSUPPORTED", runtimeVersionUnsupportedMessage(compatibilityIssue));
37853
37971
  }
37854
37972
  const discovered = await discoverRunningDaemonInfo(cwd, { recoverStale: true });
37973
+ if (discovered.compatibilityIssue?.pidAlive) {
37974
+ return errorEnvelope("daemon.start", "AC_RUNTIME_VERSION_UNSUPPORTED", runtimeVersionUnsupportedMessage(discovered.compatibilityIssue));
37975
+ }
37855
37976
  if (discovered.info) {
37856
37977
  return okEnvelope("daemon.start", {
37857
37978
  running: true,
@@ -37909,7 +38030,8 @@ async function startBackgroundDaemon(args2, cwd) {
37909
38030
  }
37910
38031
  }
37911
38032
  async function upgradeDaemon(args2, cwd) {
37912
- const issue = runtimeRpcCompatibilityIssue(cwd) ?? cliEntryStalenessIssue(cwd);
38033
+ const persistedIssue = runtimeRpcCompatibilityIssue(cwd) ?? cliEntryStalenessIssue(cwd);
38034
+ const issue = persistedIssue ?? (await discoverRunningDaemonInfo(cwd)).compatibilityIssue;
37913
38035
  if (!issue) {
37914
38036
  const started2 = await startBackgroundDaemon(args2, cwd);
37915
38037
  return started2.ok ? { ...started2, requestId: "daemon.upgrade", data: { ...started2.data, upgraded: false, reason: "runtime-compatible" } } : started2;
@@ -37930,6 +38052,10 @@ async function upgradeDaemon(args2, cwd) {
37930
38052
  previousStartedAt: issue.received,
37931
38053
  entrypointMtime: issue.expected,
37932
38054
  previousPid: issue.pid
38055
+ } : issue.reason === "product-version-mismatch" ? {
38056
+ previousProductVersion: issue.received,
38057
+ expectedProductVersion: issue.expected,
38058
+ previousPid: issue.pid
37933
38059
  } : {
37934
38060
  previousRpcSchemaVersion: issue.received,
37935
38061
  expectedRpcSchemaVersion: issue.expected,
@@ -37954,17 +38080,27 @@ async function discoverRunningDaemonInfo(cwd, options = {}) {
37954
38080
  if (!client) {
37955
38081
  return {
37956
38082
  info: undefined,
38083
+ compatibilityIssue: undefined,
37957
38084
  recovery: options.recoverStale ? recoverStaleDaemonControlFiles(cwd) : emptyRecovery(cwd)
37958
38085
  };
37959
38086
  }
37960
38087
  const health = await client.health().catch(() => {
37961
38088
  return;
37962
38089
  });
38090
+ const compatibilityIssue = runtimeRpcCompatibilityIssueFromHealth(cwd, client, health) ?? cliEntryStalenessIssue(cwd);
38091
+ if (compatibilityIssue) {
38092
+ return {
38093
+ info: undefined,
38094
+ compatibilityIssue,
38095
+ recovery: emptyRecovery(cwd)
38096
+ };
38097
+ }
37963
38098
  if (health?.ok === true) {
37964
- return { info: client.connectionInfo(), recovery: emptyRecovery(cwd) };
38099
+ return { info: client.connectionInfo(), compatibilityIssue: undefined, recovery: emptyRecovery(cwd) };
37965
38100
  }
37966
38101
  return {
37967
38102
  info: undefined,
38103
+ compatibilityIssue: undefined,
37968
38104
  recovery: options.recoverStale ? recoverStaleDaemonControlFiles(cwd, { removeUnhealthyConnection: true }) : emptyRecovery(cwd)
37969
38105
  };
37970
38106
  }
@@ -37972,26 +38108,40 @@ function runtimeRpcCompatibilityIssueFromHealth(cwd, client, health) {
37972
38108
  if (!(health && typeof health === "object"))
37973
38109
  return;
37974
38110
  const body = health;
37975
- if (body.ok !== false || body.error !== "runtime RPC version mismatch")
37976
- return;
37977
38111
  const connection = client.connectionInfo();
37978
38112
  const pid = typeof connection.pid === "number" ? connection.pid : undefined;
37979
- return {
37980
- reason: "rpc-version-mismatch",
37981
- expected: RUNTIME_RPC_VERSION,
37982
- received: typeof body.expected === "string" ? body.expected : "unknown",
38113
+ const location = {
37983
38114
  connectionPath: connection.connectionPath ?? defaultDaemonConnectionPath(cwd),
37984
38115
  lockPath: connection.lockPath ?? defaultDaemonLockPath(cwd),
37985
38116
  pid,
37986
38117
  pidAlive: pid !== undefined ? isPidAlive(pid) : false,
37987
38118
  upgradeCommand: "archctx daemon upgrade"
37988
38119
  };
38120
+ if (body.ok === true) {
38121
+ const expected = productVersionManifest().product.version;
38122
+ const received = typeof body.product?.product?.version === "string" ? body.product.product.version : "unknown";
38123
+ return received === expected ? undefined : {
38124
+ reason: "product-version-mismatch",
38125
+ expected,
38126
+ received,
38127
+ ...location
38128
+ };
38129
+ }
38130
+ if (body.ok !== false || body.error !== "runtime RPC version mismatch")
38131
+ return;
38132
+ return {
38133
+ reason: "rpc-version-mismatch",
38134
+ expected: RUNTIME_RPC_VERSION,
38135
+ received: typeof body.expected === "string" ? body.expected : "unknown",
38136
+ ...location
38137
+ };
37989
38138
  }
37990
38139
  function incompatibleDaemonStatus(issue) {
37991
38140
  return {
37992
38141
  running: issue.pidAlive,
37993
38142
  protocol: "http-loopback",
37994
- rpcVersionCompatible: false,
38143
+ rpcVersionCompatible: issue.reason === "product-version-mismatch",
38144
+ productVersionCompatible: issue.reason !== "product-version-mismatch",
37995
38145
  connectionPath: issue.connectionPath,
37996
38146
  lockPath: issue.lockPath,
37997
38147
  pid: issue.pid,
@@ -38008,6 +38158,9 @@ function runtimeVersionUnsupportedMessage(issue) {
38008
38158
  if (issue.reason === "stale-daemon-entry") {
38009
38159
  return `archctxd (pid ${issue.pid ?? "unknown"}, started ${issue.received}) was spawned from an older copy of the archctx entrypoint than the one running this command (modified ${issue.expected}); run ${issue.upgradeCommand} to replace the local daemon.`;
38010
38160
  }
38161
+ if (issue.reason === "product-version-mismatch") {
38162
+ return `archctxd product version ${issue.received} does not exactly match this CLI (${issue.expected}); run ${issue.upgradeCommand} to replace the local daemon.`;
38163
+ }
38011
38164
  return `archctxd RPC version ${issue.received} is incompatible with this CLI (${issue.expected}); run ${issue.upgradeCommand} to replace the local daemon.`;
38012
38165
  }
38013
38166
  async function waitForPidExit(pid, timeoutMs) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "archctx",
3
- "version": "0.4.5",
3
+ "version": "0.4.6",
4
4
  "description": "Local architecture context CLI for agentic coding workflows.",
5
5
  "private": false,
6
6
  "type": "module",