archgraph-argo 0.20.4 → 0.20.5

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.
@@ -38,7 +38,13 @@ const REQUIRED_TOOL_NAMES = [
38
38
  // `includeBootstrap` is true for the standalone CLI (it calls initializeWorkspace
39
39
  // to bootstrap the workspace); the initializeWorkspace MCP tool passes false
40
40
  // because it already bootstrapped — avoids re-entrancy through ensureWorkspaceBootstrap.
41
- async function buildHarnessReport({ checkOnly = false, workspaceRoot, includeBootstrap = true }) {
41
+ async function buildHarnessReport(options = {}) {
42
+ const { checkOnly = false, workspaceRoot, includeBootstrap = true } = options;
43
+ const { withAlignmentLock } = require('./graph-rag/semanticAlignmentLock.js');
44
+ return withAlignmentLock(workspaceRoot, () => runHarnessReport({ checkOnly, workspaceRoot, includeBootstrap }));
45
+ }
46
+
47
+ async function runHarnessReport({ checkOnly, workspaceRoot, includeBootstrap }) {
42
48
  const reportPath = path.join(workspaceRoot, '.argo', 'temp', 'argo-harness-init-report.json');
43
49
  const report = {
44
50
  status: 'ok',
@@ -0,0 +1,76 @@
1
+ 'use strict';
2
+
3
+ // Cross-process, single-flight lock for the heavy semantic-index alignment
4
+ // (workspace bootstrap + semantic backfill + Neo4j sync + EA projection). Any
5
+ // invocation that can rebuild the index -- the MCP startup preheat, the
6
+ // query-path auto-alignment, and an explicit `argo init` (initializeWorkspace),
7
+ // in this process or another -- acquires this lock, so they serialize instead of
8
+ // racing (duplicate embeddings, concurrent Neo4j upserts, concurrent .qea
9
+ // rebuild, readiness/report races).
10
+ //
11
+ // The lock is a regular file created with `wx` under <workspace>/.argo/temp. A
12
+ // holder that crashes leaves a stale file; a lock older than `staleMs` is stolen.
13
+
14
+ const fs = require('node:fs');
15
+ const path = require('node:path');
16
+
17
+ const LOCK_RELATIVE_PATH = path.join('.argo', 'temp', 'semantic-alignment.lock');
18
+ const DEFAULT_WAIT_MS = 10 * 60 * 1000;
19
+ const DEFAULT_STALE_MS = 15 * 60 * 1000;
20
+
21
+ function alignmentLockPath(repositoryRoot) {
22
+ return path.join(repositoryRoot, ...LOCK_RELATIVE_PATH.split(path.sep));
23
+ }
24
+
25
+ function sleep(ms) {
26
+ return new Promise(resolve => setTimeout(resolve, ms));
27
+ }
28
+
29
+ async function withAlignmentLock(repositoryRoot, action, options = {}) {
30
+ const waitMs = Number.isFinite(options.waitMs) && options.waitMs > 0 ? options.waitMs : DEFAULT_WAIT_MS;
31
+ const staleMs = Number.isFinite(options.staleMs) && options.staleMs > 0 ? options.staleMs : DEFAULT_STALE_MS;
32
+ const file = alignmentLockPath(repositoryRoot);
33
+ fs.mkdirSync(path.dirname(file), { recursive: true });
34
+ const deadline = Date.now() + waitMs;
35
+ let fd = null;
36
+ for (;;) {
37
+ try {
38
+ fd = fs.openSync(file, 'wx');
39
+ break;
40
+ } catch (error) {
41
+ if (!error || error.code !== 'EEXIST') {
42
+ throw error;
43
+ }
44
+ let stale = false;
45
+ try {
46
+ stale = (Date.now() - fs.statSync(file).mtimeMs) > staleMs;
47
+ } catch {
48
+ stale = true;
49
+ }
50
+ if (stale) {
51
+ try { fs.unlinkSync(file); } catch { /* raced with another stealer */ }
52
+ continue;
53
+ }
54
+ if (Date.now() >= deadline) {
55
+ const timeout = new Error('SEMANTIC_ALIGNMENT_LOCK_TIMEOUT');
56
+ timeout.category = 'SEMANTIC_ALIGNMENT_LOCK_TIMEOUT';
57
+ timeout.message = 'Another semantic alignment is already running and did not finish in time.';
58
+ timeout.action = 'Wait for the running alignment (argo init) to finish, then retry.';
59
+ throw timeout;
60
+ }
61
+ await sleep(250);
62
+ }
63
+ }
64
+ try {
65
+ try { fs.writeSync(fd, String(process.pid)); } catch { /* best-effort owner marker */ }
66
+ return await action();
67
+ } finally {
68
+ try { fs.closeSync(fd); } catch { /* ignore */ }
69
+ try { fs.unlinkSync(file); } catch { /* already gone */ }
70
+ }
71
+ }
72
+
73
+ module.exports = {
74
+ withAlignmentLock,
75
+ alignmentLockPath,
76
+ };
@@ -34,6 +34,18 @@ function isSemanticReady(repositoryRoot) {
34
34
  }
35
35
  }
36
36
 
37
+ // A project is "initialized" once its canonical graph exists. The startup
38
+ // preheat must NEVER bootstrap a brand-new project: creating files / running a
39
+ // full backfill for an unknown workspace in the background is surprising and
40
+ // would race an explicit `argo init`. New projects initialize explicitly.
41
+ function isProjectInitialized(repositoryRoot) {
42
+ try {
43
+ return fs.existsSync(path.join(repositoryRoot, 'design', 'KG', 'SystemArchitecture.json'));
44
+ } catch {
45
+ return false;
46
+ }
47
+ }
48
+
37
49
  function alignmentError() {
38
50
  const error = new Error('SEMANTIC_AUTO_ALIGNMENT_FAILED');
39
51
  error.category = 'SEMANTIC_AUTO_ALIGNMENT_FAILED';
@@ -79,7 +91,7 @@ function runSemanticAlignment(repositoryRoot = getWorkspaceRoot()) {
79
91
  // otherwise it starts the rebuild in the background so the first query rarely
80
92
  // pays for it. Safe to call repeatedly (guarded + de-duplicated).
81
93
  function preheatSemanticAlignment(repositoryRoot = getWorkspaceRoot()) {
82
- if (preheated || !repositoryRoot || isSemanticReady(repositoryRoot)) {
94
+ if (preheated || !repositoryRoot || !isProjectInitialized(repositoryRoot) || isSemanticReady(repositoryRoot)) {
83
95
  return;
84
96
  }
85
97
  preheated = true;
@@ -91,5 +103,6 @@ module.exports = {
91
103
  runSemanticAlignment,
92
104
  preheatSemanticAlignment,
93
105
  isSemanticReady,
106
+ isProjectInitialized,
94
107
  readinessRecordPath,
95
108
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "archgraph-argo",
3
- "version": "0.20.4",
3
+ "version": "0.20.5",
4
4
  "description": "Deploy the ArchGraph ARGO toolchain, skills, and rules (schema, scripts, argo-init skill, global rule) with one command.",
5
5
  "license": "MIT",
6
6
  "bin": {