harness-everything 0.46.3 → 0.47.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 (51) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +32 -1
  4. package/benchmarks/results/live-host/claude-341-outline-dispatch-2026-10-28/README.md +33 -8
  5. package/benchmarks/results/live-host/claude-341-outline-dispatch-2026-10-28/inventory/2741bd4514d2cd68f57d93a2b3ce94cbe597ca62d30e26f0ca58e882df5b03c6.md +128 -0
  6. package/benchmarks/results/live-host/claude-341-outline-dispatch-2026-10-28/sanitize-outline-capture.js +105 -0
  7. package/ci/mechanism-379-self-evolve-workspace.test.js +151 -0
  8. package/ci/mechanism-383-doc-glob-line-terminators.test.js +131 -0
  9. package/ci/mechanism-e1-derived-regenerate.test.js +333 -0
  10. package/ci/mechanism-outline-sanitizer.test.js +79 -0
  11. package/ci/mechanism-prb-outline-review.test.js +278 -31
  12. package/docs/doc-workflow-reconciliation.md +12 -8
  13. package/docs/document-lifecycle.md +1 -1
  14. package/docs/platform-capabilities.md +5 -1
  15. package/harness-everything/SKILL.md +1 -1
  16. package/harness-everything/scripts/doc-check.js +107 -9
  17. package/harness-everything/scripts/doc-lifecycle.js +51 -3
  18. package/harness-everything/scripts/doc-obligations.js +2 -2
  19. package/hooks/scripts/lib/doc-reconciliation-core.js +2 -2
  20. package/hooks/scripts/lib/doc-reconciliation-events.js +22 -0
  21. package/hooks/scripts/lib/doc-reconciliation-outline-capture.sanitized.jsonl +24 -0
  22. package/hooks/scripts/lib/doc-reconciliation-pilot.js +3 -1
  23. package/hooks/scripts/lib/doc-reconciliation-provider.js +168 -15
  24. package/hooks/scripts/lib/doc-reconciliation-qualification.json +20 -0
  25. package/opencode-plugin/plugin.json +1 -1
  26. package/package.json +1 -1
  27. package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
  28. package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-core.js +2 -2
  29. package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-events.js +22 -0
  30. package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-outline-capture.sanitized.jsonl +24 -0
  31. package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-pilot.js +3 -1
  32. package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-provider.js +168 -15
  33. package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-qualification.json +20 -0
  34. package/plugins/harness-everything/plugin.json +1 -1
  35. package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
  36. package/plugins/harness-everything/skills/harness-everything/scripts/doc-check.js +107 -9
  37. package/plugins/harness-everything/skills/harness-everything/scripts/doc-lifecycle.js +51 -3
  38. package/plugins/harness-everything/skills/harness-everything/scripts/doc-obligations.js +2 -2
  39. package/plugins/harness-everything/skills/self-evolve/SKILL.md +1 -1
  40. package/plugins/harness-everything/skills/self-evolve/references/memory-resolution.md +11 -0
  41. package/plugins/harness-everything/skills/self-evolve/scripts/lesson-candidate.js +9 -12
  42. package/plugins/harness-everything/skills/self-evolve/scripts/persist-memory.js +1 -14
  43. package/plugins/harness-everything/skills/self-evolve/scripts/register-dynamic-skill.js +5 -9
  44. package/plugins/harness-everything/skills/self-evolve/scripts/workspace-helper.js +98 -0
  45. package/sbom.cdx.json +7 -7
  46. package/self-evolve/SKILL.md +1 -1
  47. package/self-evolve/references/memory-resolution.md +11 -0
  48. package/self-evolve/scripts/lesson-candidate.js +9 -12
  49. package/self-evolve/scripts/persist-memory.js +1 -14
  50. package/self-evolve/scripts/register-dynamic-skill.js +5 -9
  51. package/self-evolve/scripts/workspace-helper.js +98 -0
@@ -18,14 +18,7 @@ for (const relative of [
18
18
  }
19
19
  }
20
20
 
21
- let getWorkspaceRoot;
22
- let getWorkspaceStateDir;
23
- try {
24
- ({ getWorkspaceRoot, getWorkspaceStateDir } = require('../../scripts/lib/workspace'));
25
- } catch (_) {
26
- getWorkspaceRoot = () => null;
27
- getWorkspaceStateDir = () => null;
28
- }
21
+ const { getWorkspaceRoot, getWorkspaceStateDir, workspaceError } = require('./workspace-helper');
29
22
 
30
23
  const SCHEMA_VERSION = 1;
31
24
  const TERMINAL = new Set(['rejected', 'validated', 'regressed', 'superseded']);
@@ -48,10 +41,10 @@ function readJson(filePath) {
48
41
  }
49
42
 
50
43
  function candidatePath(workspace, sessionId, candidateId) {
51
- if (!workspace || !getWorkspaceStateDir(workspace)) throw new Error('workspace state root is unavailable');
44
+ const stateDir = getWorkspaceStateDir(workspace);
52
45
  if (!/^lesson-[a-f0-9]{24}$/.test(String(candidateId || ''))) throw new Error('invalid candidate id');
53
46
  return path.join(
54
- getWorkspaceStateDir(workspace),
47
+ stateDir,
55
48
  'state',
56
49
  'sessions',
57
50
  safeSessionName(sessionId),
@@ -292,7 +285,10 @@ function observeCandidate(input) {
292
285
  function listCandidates({ workspace, sessionId }) {
293
286
  const base = path.join(getWorkspaceStateDir(workspace), 'state', 'sessions', safeSessionName(sessionId), 'learning-candidates');
294
287
  let files = [];
295
- try { files = fs.readdirSync(base).filter(name => /^lesson-[a-f0-9]{24}\.json$/.test(name)); } catch (_) { return []; }
288
+ try { files = fs.readdirSync(base).filter(name => /^lesson-[a-f0-9]{24}\.json$/.test(name)); } catch (error) {
289
+ if (error.code === 'ENOENT') return [];
290
+ throw workspaceError('WORKSPACE_STATE_UNAVAILABLE', `cannot read candidate directory ${base}`, error);
291
+ }
296
292
  return files.map(name => readJson(path.join(base, name))).filter(Boolean);
297
293
  }
298
294
 
@@ -303,7 +299,8 @@ function option(args, name) {
303
299
 
304
300
  function parseCli(argv) {
305
301
  const command = argv[0];
306
- const workspace = path.resolve(option(argv, '--workspace') || getWorkspaceRoot() || process.cwd());
302
+ const explicit = option(argv, '--workspace');
303
+ const workspace = getWorkspaceRoot(explicit ? { workspace_root: path.resolve(explicit) } : undefined);
307
304
  const base = {
308
305
  command,
309
306
  workspace,
@@ -4,20 +4,7 @@
4
4
  const crypto = require('crypto');
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
- let getWorkspaceRoot;
8
- let getWorkspaceStateDir;
9
- let getWorkspaceKey;
10
- try {
11
- ({ getWorkspaceRoot, getWorkspaceStateDir, getWorkspaceKey } = require('../../scripts/lib/workspace'));
12
- } catch (err) {
13
- getWorkspaceRoot = () => null;
14
- getWorkspaceStateDir = () => null;
15
- getWorkspaceKey = () => null;
16
- }
17
-
18
- // NOTE: this script ships standalone - it is copied whole into every install
19
- // target. When the canonical workspace resolver is not present, persistence
20
- // fails closed rather than guessing a project root (issue #42).
7
+ const { getWorkspaceRoot, getWorkspaceStateDir, getWorkspaceKey } = require('./workspace-helper');
21
8
 
22
9
  const SECRET_PATTERNS = [
23
10
  ['github-token', /\bgh[pousr]_[A-Za-z0-9]{20,}\b/],
@@ -1,16 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  const fs = require('fs');
3
3
  const path = require('path');
4
- let getWorkspaceRoot;
5
- try {
6
- ({ getWorkspaceRoot } = require('../../scripts/lib/workspace'));
7
- } catch (err) {
8
- getWorkspaceRoot = () => null;
9
- }
4
+ const { getWorkspaceRoot } = require('./workspace-helper');
10
5
 
11
- const workspaceRoot = getWorkspaceRoot();
12
- if (!workspaceRoot) {
13
- console.error('[self-evolve] Cannot register a dynamic skill without a resolved git workspace.');
6
+ let workspaceRoot;
7
+ try { workspaceRoot = getWorkspaceRoot(); }
8
+ catch (error) {
9
+ console.error(`[self-evolve] ${error.message}`);
14
10
  process.exit(1);
15
11
  }
16
12
 
@@ -0,0 +1,98 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ function workspaceError(code, message, cause) {
7
+ const error = new Error(`${code}: ${message}${cause ? ` (${cause.code || cause.name}: ${cause.message})` : ''}`, { cause });
8
+ error.code = code;
9
+ return error;
10
+ }
11
+
12
+ function readMarker(file) {
13
+ try { return JSON.parse(fs.readFileSync(file, 'utf8')); }
14
+ catch (error) {
15
+ if (error.code === 'ENOENT') return null;
16
+ throw workspaceError('WORKSPACE_HELPER_UNAVAILABLE', `cannot read installation marker ${file}`, error);
17
+ }
18
+ }
19
+
20
+ let helper;
21
+ function loadHelper() {
22
+ if (helper) return helper;
23
+ const sourceRoot = path.resolve(__dirname, '../..');
24
+ const sourceMarker = path.join(sourceRoot, 'package.json');
25
+ const source = readMarker(sourceMarker);
26
+ let installRoot;
27
+ // A source checkout may itself be named "skills". Its own package marker
28
+ // takes precedence, including when its helper is absent or broken.
29
+ if (source) {
30
+ if (source.name !== 'harness-everything') {
31
+ throw workspaceError('WORKSPACE_HELPER_UNAVAILABLE', `unrecognized source installation marker ${sourceMarker}`);
32
+ }
33
+ installRoot = sourceRoot;
34
+ } else {
35
+ const pluginRoot = path.dirname(sourceRoot);
36
+ const marker = path.join(pluginRoot, '.codex-plugin/plugin.json');
37
+ const plugin = path.basename(sourceRoot) === 'skills' ? readMarker(marker) : null;
38
+ if (!plugin || plugin.name !== 'harness-everything' || plugin.skills !== './skills/') {
39
+ throw workspaceError('WORKSPACE_HELPER_UNAVAILABLE', `unrecognized layout; expected ${sourceMarker} or packaged skills under ${marker}`);
40
+ }
41
+ installRoot = pluginRoot;
42
+ }
43
+ const candidate = path.join(installRoot, 'scripts/lib/workspace.js');
44
+ try {
45
+ const loaded = require(candidate);
46
+ for (const name of ['getWorkspaceRoot', 'getWorkspaceStateDir', 'getWorkspaceKey']) {
47
+ if (typeof loaded[name] !== 'function') throw new Error(`missing ${name} export`);
48
+ }
49
+ helper = loaded;
50
+ return helper;
51
+ } catch (error) {
52
+ throw workspaceError('WORKSPACE_HELPER_UNAVAILABLE', `cannot load selected installation helper ${candidate}`, error);
53
+ }
54
+ }
55
+
56
+ function getWorkspaceRoot(context) {
57
+ const loaded = loadHelper();
58
+ let root;
59
+ try { root = loaded.getWorkspaceRoot(context); }
60
+ catch (error) { throw workspaceError('WORKSPACE_ROOT_UNRESOLVED', 'canonical workspace resolution failed', error); }
61
+ if (typeof root !== 'string' || !root.trim()) {
62
+ throw workspaceError('WORKSPACE_ROOT_UNRESOLVED', 'canonical resolver returned no workspace identity');
63
+ }
64
+ return root;
65
+ }
66
+
67
+ function getWorkspaceStateDir(root) {
68
+ const loaded = loadHelper();
69
+ if (typeof root !== 'string' || !root.trim()) {
70
+ throw workspaceError('WORKSPACE_ROOT_UNRESOLVED', 'workspace identity is required for state access');
71
+ }
72
+ try {
73
+ const state = loaded.getWorkspaceStateDir(root);
74
+ if (typeof state !== 'string' || !state.trim()) throw new Error('canonical state path must be nonempty');
75
+ // Missing state is valid for a new workspace. Existing non-directory or
76
+ // inaccessible ancestors are not an empty candidate list.
77
+ let ancestor = state;
78
+ while (true) {
79
+ try {
80
+ if (!fs.statSync(ancestor).isDirectory()) throw new Error(`${ancestor} is not a directory`);
81
+ fs.accessSync(ancestor, fs.constants.R_OK);
82
+ break;
83
+ } catch (error) {
84
+ if (error.code !== 'ENOENT') throw error;
85
+ const parent = path.dirname(ancestor);
86
+ if (parent === ancestor) throw error;
87
+ ancestor = parent;
88
+ }
89
+ }
90
+ return state;
91
+ } catch (error) {
92
+ throw workspaceError('WORKSPACE_STATE_UNAVAILABLE', `cannot access canonical state for ${root}`, error);
93
+ }
94
+ }
95
+
96
+ function getWorkspaceKey(root) { return loadHelper().getWorkspaceKey(root); }
97
+
98
+ module.exports = { getWorkspaceRoot, getWorkspaceStateDir, getWorkspaceKey, workspaceError };
package/sbom.cdx.json CHANGED
@@ -2,23 +2,23 @@
2
2
  "$schema": "https://cyclonedx.org/schema/bom-1.7.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.7",
5
- "serialNumber": "urn:uuid:194f592d-6226-519c-a5b8-9f6750fb3c19",
5
+ "serialNumber": "urn:uuid:e76da8c1-2c84-5375-b676-2e0d5159ea62",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-10-28T05:22:07.321Z",
8
+ "timestamp": "2026-10-31T09:08:35.775Z",
9
9
  "tools": [
10
10
  {
11
11
  "vendor": "Harness Everything",
12
12
  "name": "generate-sbom.js",
13
- "version": "0.46.3"
13
+ "version": "0.47.1"
14
14
  }
15
15
  ],
16
16
  "component": {
17
17
  "type": "application",
18
- "bom-ref": "pkg:npm/harness-everything@0.46.3",
18
+ "bom-ref": "pkg:npm/harness-everything@0.47.1",
19
19
  "name": "harness-everything",
20
- "version": "0.46.3",
21
- "purl": "pkg:npm/harness-everything@0.46.3"
20
+ "version": "0.47.1",
21
+ "purl": "pkg:npm/harness-everything@0.47.1"
22
22
  }
23
23
  },
24
24
  "components": [
@@ -2033,7 +2033,7 @@
2033
2033
  ]
2034
2034
  },
2035
2035
  {
2036
- "ref": "pkg:npm/harness-everything@0.46.3",
2036
+ "ref": "pkg:npm/harness-everything@0.47.1",
2037
2037
  "dependsOn": [
2038
2038
  "pkg:npm/%40commitlint/cli@19.8.1",
2039
2039
  "pkg:npm/%40commitlint/config-conventional@19.8.1",
@@ -4,7 +4,7 @@ description: "Extract root causes from resolved problems and persist them as def
4
4
  license: Apache-2.0
5
5
  metadata:
6
6
  author: Miya Daniel
7
- version: 0.31.3
7
+ version: 0.47.1
8
8
  ---
9
9
 
10
10
  # Self-evolve
@@ -35,6 +35,17 @@ For a simple rule, inspect/list the session candidate, evaluate it with a genera
35
35
 
36
36
  For a reusable multi-step procedure, load `skill-creator/SKILL.md`, create the draft skill, and use `register-dynamic-skill.js` to update each platform's `manifest.json` `generated[]` registry. The host agent remains responsible for deciding which evidence is relevant; these scripts do not discover session history.
37
37
 
38
+ ## Workspace helper and diagnostics
39
+
40
+ | Contract | Behavior |
41
+ | --- | --- |
42
+ | Trusted imports | Select one layout using its installation marker: source `package.json` takes precedence; packaged `skills/` requires `.codex-plugin/plugin.json`. Load only that root’s `scripts/lib/workspace.js`; never fall through to ancestor/project/cwd code. |
43
+ | Identity and state | Use the canonical resolver and `HARNESS_STATE_HOME` precedence; source and plugin read the same workspace/session candidates. No alternate state root. |
44
+ | Missing helper | `WORKSPACE_HELPER_UNAVAILABLE` identifies the selected helper path or invalid layout marker and cause; absent or broken helpers fail without trying another layout. |
45
+ | Unresolved identity | `WORKSPACE_ROOT_UNRESOLVED` exits nonzero; do not substitute cwd when canonical resolution fails. |
46
+ | Unusable state | `WORKSPACE_STATE_UNAVAILABLE` exits nonzero for invalid state paths or failed reads; only a genuinely absent candidate directory returns `[]`. |
47
+ | Writes | Existing screening and workflow authorization still apply. Resolver failures never authorize persistence or state repair. |
48
+
38
49
  ## Candidate Lifecycle
39
50
 
40
51
  ```text
@@ -18,14 +18,7 @@ for (const relative of [
18
18
  }
19
19
  }
20
20
 
21
- let getWorkspaceRoot;
22
- let getWorkspaceStateDir;
23
- try {
24
- ({ getWorkspaceRoot, getWorkspaceStateDir } = require('../../scripts/lib/workspace'));
25
- } catch (_) {
26
- getWorkspaceRoot = () => null;
27
- getWorkspaceStateDir = () => null;
28
- }
21
+ const { getWorkspaceRoot, getWorkspaceStateDir, workspaceError } = require('./workspace-helper');
29
22
 
30
23
  const SCHEMA_VERSION = 1;
31
24
  const TERMINAL = new Set(['rejected', 'validated', 'regressed', 'superseded']);
@@ -48,10 +41,10 @@ function readJson(filePath) {
48
41
  }
49
42
 
50
43
  function candidatePath(workspace, sessionId, candidateId) {
51
- if (!workspace || !getWorkspaceStateDir(workspace)) throw new Error('workspace state root is unavailable');
44
+ const stateDir = getWorkspaceStateDir(workspace);
52
45
  if (!/^lesson-[a-f0-9]{24}$/.test(String(candidateId || ''))) throw new Error('invalid candidate id');
53
46
  return path.join(
54
- getWorkspaceStateDir(workspace),
47
+ stateDir,
55
48
  'state',
56
49
  'sessions',
57
50
  safeSessionName(sessionId),
@@ -292,7 +285,10 @@ function observeCandidate(input) {
292
285
  function listCandidates({ workspace, sessionId }) {
293
286
  const base = path.join(getWorkspaceStateDir(workspace), 'state', 'sessions', safeSessionName(sessionId), 'learning-candidates');
294
287
  let files = [];
295
- try { files = fs.readdirSync(base).filter(name => /^lesson-[a-f0-9]{24}\.json$/.test(name)); } catch (_) { return []; }
288
+ try { files = fs.readdirSync(base).filter(name => /^lesson-[a-f0-9]{24}\.json$/.test(name)); } catch (error) {
289
+ if (error.code === 'ENOENT') return [];
290
+ throw workspaceError('WORKSPACE_STATE_UNAVAILABLE', `cannot read candidate directory ${base}`, error);
291
+ }
296
292
  return files.map(name => readJson(path.join(base, name))).filter(Boolean);
297
293
  }
298
294
 
@@ -303,7 +299,8 @@ function option(args, name) {
303
299
 
304
300
  function parseCli(argv) {
305
301
  const command = argv[0];
306
- const workspace = path.resolve(option(argv, '--workspace') || getWorkspaceRoot() || process.cwd());
302
+ const explicit = option(argv, '--workspace');
303
+ const workspace = getWorkspaceRoot(explicit ? { workspace_root: path.resolve(explicit) } : undefined);
307
304
  const base = {
308
305
  command,
309
306
  workspace,
@@ -4,20 +4,7 @@
4
4
  const crypto = require('crypto');
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
- let getWorkspaceRoot;
8
- let getWorkspaceStateDir;
9
- let getWorkspaceKey;
10
- try {
11
- ({ getWorkspaceRoot, getWorkspaceStateDir, getWorkspaceKey } = require('../../scripts/lib/workspace'));
12
- } catch (err) {
13
- getWorkspaceRoot = () => null;
14
- getWorkspaceStateDir = () => null;
15
- getWorkspaceKey = () => null;
16
- }
17
-
18
- // NOTE: this script ships standalone - it is copied whole into every install
19
- // target. When the canonical workspace resolver is not present, persistence
20
- // fails closed rather than guessing a project root (issue #42).
7
+ const { getWorkspaceRoot, getWorkspaceStateDir, getWorkspaceKey } = require('./workspace-helper');
21
8
 
22
9
  const SECRET_PATTERNS = [
23
10
  ['github-token', /\bgh[pousr]_[A-Za-z0-9]{20,}\b/],
@@ -1,16 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  const fs = require('fs');
3
3
  const path = require('path');
4
- let getWorkspaceRoot;
5
- try {
6
- ({ getWorkspaceRoot } = require('../../scripts/lib/workspace'));
7
- } catch (err) {
8
- getWorkspaceRoot = () => null;
9
- }
4
+ const { getWorkspaceRoot } = require('./workspace-helper');
10
5
 
11
- const workspaceRoot = getWorkspaceRoot();
12
- if (!workspaceRoot) {
13
- console.error('[self-evolve] Cannot register a dynamic skill without a resolved git workspace.');
6
+ let workspaceRoot;
7
+ try { workspaceRoot = getWorkspaceRoot(); }
8
+ catch (error) {
9
+ console.error(`[self-evolve] ${error.message}`);
14
10
  process.exit(1);
15
11
  }
16
12
 
@@ -0,0 +1,98 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ function workspaceError(code, message, cause) {
7
+ const error = new Error(`${code}: ${message}${cause ? ` (${cause.code || cause.name}: ${cause.message})` : ''}`, { cause });
8
+ error.code = code;
9
+ return error;
10
+ }
11
+
12
+ function readMarker(file) {
13
+ try { return JSON.parse(fs.readFileSync(file, 'utf8')); }
14
+ catch (error) {
15
+ if (error.code === 'ENOENT') return null;
16
+ throw workspaceError('WORKSPACE_HELPER_UNAVAILABLE', `cannot read installation marker ${file}`, error);
17
+ }
18
+ }
19
+
20
+ let helper;
21
+ function loadHelper() {
22
+ if (helper) return helper;
23
+ const sourceRoot = path.resolve(__dirname, '../..');
24
+ const sourceMarker = path.join(sourceRoot, 'package.json');
25
+ const source = readMarker(sourceMarker);
26
+ let installRoot;
27
+ // A source checkout may itself be named "skills". Its own package marker
28
+ // takes precedence, including when its helper is absent or broken.
29
+ if (source) {
30
+ if (source.name !== 'harness-everything') {
31
+ throw workspaceError('WORKSPACE_HELPER_UNAVAILABLE', `unrecognized source installation marker ${sourceMarker}`);
32
+ }
33
+ installRoot = sourceRoot;
34
+ } else {
35
+ const pluginRoot = path.dirname(sourceRoot);
36
+ const marker = path.join(pluginRoot, '.codex-plugin/plugin.json');
37
+ const plugin = path.basename(sourceRoot) === 'skills' ? readMarker(marker) : null;
38
+ if (!plugin || plugin.name !== 'harness-everything' || plugin.skills !== './skills/') {
39
+ throw workspaceError('WORKSPACE_HELPER_UNAVAILABLE', `unrecognized layout; expected ${sourceMarker} or packaged skills under ${marker}`);
40
+ }
41
+ installRoot = pluginRoot;
42
+ }
43
+ const candidate = path.join(installRoot, 'scripts/lib/workspace.js');
44
+ try {
45
+ const loaded = require(candidate);
46
+ for (const name of ['getWorkspaceRoot', 'getWorkspaceStateDir', 'getWorkspaceKey']) {
47
+ if (typeof loaded[name] !== 'function') throw new Error(`missing ${name} export`);
48
+ }
49
+ helper = loaded;
50
+ return helper;
51
+ } catch (error) {
52
+ throw workspaceError('WORKSPACE_HELPER_UNAVAILABLE', `cannot load selected installation helper ${candidate}`, error);
53
+ }
54
+ }
55
+
56
+ function getWorkspaceRoot(context) {
57
+ const loaded = loadHelper();
58
+ let root;
59
+ try { root = loaded.getWorkspaceRoot(context); }
60
+ catch (error) { throw workspaceError('WORKSPACE_ROOT_UNRESOLVED', 'canonical workspace resolution failed', error); }
61
+ if (typeof root !== 'string' || !root.trim()) {
62
+ throw workspaceError('WORKSPACE_ROOT_UNRESOLVED', 'canonical resolver returned no workspace identity');
63
+ }
64
+ return root;
65
+ }
66
+
67
+ function getWorkspaceStateDir(root) {
68
+ const loaded = loadHelper();
69
+ if (typeof root !== 'string' || !root.trim()) {
70
+ throw workspaceError('WORKSPACE_ROOT_UNRESOLVED', 'workspace identity is required for state access');
71
+ }
72
+ try {
73
+ const state = loaded.getWorkspaceStateDir(root);
74
+ if (typeof state !== 'string' || !state.trim()) throw new Error('canonical state path must be nonempty');
75
+ // Missing state is valid for a new workspace. Existing non-directory or
76
+ // inaccessible ancestors are not an empty candidate list.
77
+ let ancestor = state;
78
+ while (true) {
79
+ try {
80
+ if (!fs.statSync(ancestor).isDirectory()) throw new Error(`${ancestor} is not a directory`);
81
+ fs.accessSync(ancestor, fs.constants.R_OK);
82
+ break;
83
+ } catch (error) {
84
+ if (error.code !== 'ENOENT') throw error;
85
+ const parent = path.dirname(ancestor);
86
+ if (parent === ancestor) throw error;
87
+ ancestor = parent;
88
+ }
89
+ }
90
+ return state;
91
+ } catch (error) {
92
+ throw workspaceError('WORKSPACE_STATE_UNAVAILABLE', `cannot access canonical state for ${root}`, error);
93
+ }
94
+ }
95
+
96
+ function getWorkspaceKey(root) { return loadHelper().getWorkspaceKey(root); }
97
+
98
+ module.exports = { getWorkspaceRoot, getWorkspaceStateDir, getWorkspaceKey, workspaceError };