harness-everything 0.47.0 → 0.47.2

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 (116) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.github/workflows/ci.yml +12 -0
  4. package/CHANGELOG.md +18 -0
  5. package/benchmarks/issue290-pilot/README.md +199 -0
  6. package/benchmarks/issue290-pilot/analyze.py +299 -0
  7. package/benchmarks/issue290-pilot/analyze.test.py +146 -0
  8. package/benchmarks/issue290-pilot/check.py +19 -0
  9. package/benchmarks/issue290-pilot/evidence/archive.json +7 -0
  10. package/benchmarks/issue290-pilot/evidence/group-summary.json +684 -0
  11. package/benchmarks/issue290-pilot/evidence/inventory.json +3728 -0
  12. package/benchmarks/issue290-pilot/evidence/ledger.json +4644 -0
  13. package/benchmarks/issue290-pilot/evidence/replay/pairs.json +482 -0
  14. package/benchmarks/issue290-pilot/evidence/replay/provenance.json +24 -0
  15. package/benchmarks/issue290-pilot/evidence/replay/tables.md +37 -0
  16. package/benchmarks/issue290-pilot/evidence.tar.gz +0 -0
  17. package/benchmarks/issue290-pilot/export.py +110 -0
  18. package/benchmarks/issue290-pilot/export.test.py +65 -0
  19. package/benchmarks/issue290-pilot/fable-supplement.py +55 -0
  20. package/benchmarks/issue290-pilot/fixture.test.py +28 -0
  21. package/benchmarks/issue290-pilot/fixtures/polyglot/instructions.md +19 -0
  22. package/benchmarks/issue290-pilot/fixtures/polyglot/package.json +29 -0
  23. package/benchmarks/issue290-pilot/fixtures/polyglot/simple-linked-list.js +44 -0
  24. package/benchmarks/issue290-pilot/fixtures/polyglot/simple-linked-list.spec.js +131 -0
  25. package/benchmarks/issue290-pilot/fixtures/quixbugs/LICENSE +7 -0
  26. package/benchmarks/issue290-pilot/fixtures/quixbugs/cases.json +7 -0
  27. package/benchmarks/issue290-pilot/fixtures/quixbugs/find_in_sorted.py +31 -0
  28. package/benchmarks/issue290-pilot/fixtures/quixbugs/test_find_in_sorted.py +15 -0
  29. package/benchmarks/issue290-pilot/frozen-documents.json +41 -0
  30. package/benchmarks/issue290-pilot/grader.cjs +43 -0
  31. package/benchmarks/issue290-pilot/grader.test.cjs +10 -0
  32. package/benchmarks/issue290-pilot/hook.test.cjs +27 -0
  33. package/benchmarks/issue290-pilot/host-hook.cjs +35 -0
  34. package/benchmarks/issue290-pilot/metrics.test.py +131 -0
  35. package/benchmarks/issue290-pilot/preflight/amendment.md +32 -0
  36. package/benchmarks/issue290-pilot/preflight/environment.json +15 -0
  37. package/benchmarks/issue290-pilot/preflight/fable-amendment.md +20 -0
  38. package/benchmarks/issue290-pilot/preflight/fable-frozen-files.json +4 -0
  39. package/benchmarks/issue290-pilot/preflight/fable-infrastructure-amendment.md +27 -0
  40. package/benchmarks/issue290-pilot/preflight/fable-retry-frozen-files.json +4 -0
  41. package/benchmarks/issue290-pilot/preflight/fable-supplement-v1.py +47 -0
  42. package/benchmarks/issue290-pilot/preflight/frozen-files.json +18 -0
  43. package/benchmarks/issue290-pilot/preflight/graders.json +67 -0
  44. package/benchmarks/issue290-pilot/pricing-assumptions.json +29 -0
  45. package/benchmarks/issue290-pilot/protocol.json +32 -0
  46. package/benchmarks/issue290-pilot/read_metrics.py +166 -0
  47. package/benchmarks/issue290-pilot/replay.py +167 -0
  48. package/benchmarks/issue290-pilot/reporting.py +82 -0
  49. package/benchmarks/issue290-pilot/run.py +262 -0
  50. package/benchmarks/issue290-pilot/sources.json +10 -0
  51. package/benchmarks/issue290-pilot/verification/analyzer-tests.log +5 -0
  52. package/benchmarks/issue290-pilot/verification/final-gates.json +58 -0
  53. package/benchmarks/issue290-pilot/verification/fixture-tests.log +5 -0
  54. package/benchmarks/issue290-pilot/verification/gates.json +58 -0
  55. package/benchmarks/issue290-pilot/verification/npm-audit.json +84 -0
  56. package/benchmarks/issue290-pilot/verification/offline-correction.json +59 -0
  57. package/benchmarks/issue290-pilot/verification/post-report/helper-tests.log +22 -0
  58. package/benchmarks/issue290-pilot/verification/post-report/test-consistency.log +626 -0
  59. package/benchmarks/issue290-pilot/verification/post-report/test-references.log +6 -0
  60. package/benchmarks/issue290-pilot/verification/post-report/waza-tokens.log +710 -0
  61. package/benchmarks/issue290-pilot/verification/test-collision.log +28 -0
  62. package/benchmarks/issue290-pilot/verification/test-consistency.log +28 -0
  63. package/benchmarks/issue290-pilot/verification/test-docs-capabilities.log +131 -0
  64. package/benchmarks/issue290-pilot/verification/test-mechanism.log +1829 -0
  65. package/benchmarks/issue290-pilot/verification/test-mutations.log +16 -0
  66. package/benchmarks/issue290-pilot/verification/test-platform-compatibility.log +9 -0
  67. package/benchmarks/issue290-pilot/verification/test-plugin-openai.log +6 -0
  68. package/benchmarks/issue290-pilot/verification/test-plugin-submission.log +5 -0
  69. package/benchmarks/issue290-pilot/verification/test-references.log +6 -0
  70. package/benchmarks/issue290-pilot/verification/test-release.log +59 -0
  71. package/benchmarks/issue290-pilot/verification/test-repo-contract.log +5 -0
  72. package/benchmarks/issue290-pilot/verification/test-routing-invariants.log +785 -0
  73. package/benchmarks/issue290-pilot/verification/test-routing-skills.log +28 -0
  74. package/benchmarks/issue290-pilot/verification/test.log +3373 -0
  75. package/benchmarks/issue290-pilot/verification/verification-report.md +76 -0
  76. package/benchmarks/issue290-pilot/verification/waza-tokens.log +710 -0
  77. package/benchmarks/issue290-pilot/verification/with-dependencies/gates.json +26 -0
  78. package/benchmarks/issue290-pilot/verification/with-dependencies/test-collision.log +8 -0
  79. package/benchmarks/issue290-pilot/verification/with-dependencies/test-consistency.log +626 -0
  80. package/benchmarks/issue290-pilot/verification/with-dependencies/test-mechanism.log +1693 -0
  81. package/benchmarks/issue290-pilot/verification/with-dependencies/test-release.log +38 -0
  82. package/benchmarks/issue290-pilot/verification/with-dependencies/test-routing-skills.log +44 -0
  83. package/benchmarks/issue290-pilot/verification/with-dependencies/test.log +3253 -0
  84. package/ci/mechanism-379-self-evolve-workspace.test.js +151 -0
  85. package/ci/mechanism-383-doc-glob-line-terminators.test.js +131 -0
  86. package/docs/document-lifecycle.md +1 -1
  87. package/docs/platform-capabilities.md +4 -0
  88. package/harness-everything/SKILL.md +1 -1
  89. package/harness-everything/scripts/doc-check.js +1 -1
  90. package/harness-everything/scripts/doc-lifecycle.js +2 -2
  91. package/harness-everything/scripts/doc-obligations.js +2 -2
  92. package/hooks/scripts/lib/doc-reconciliation-core.js +2 -2
  93. package/hooks/scripts/lib/doc-reconciliation-provider.js +1 -1
  94. package/opencode-plugin/plugin.json +1 -1
  95. package/package.json +1 -1
  96. package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
  97. package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-core.js +2 -2
  98. package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-provider.js +1 -1
  99. package/plugins/harness-everything/plugin.json +1 -1
  100. package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
  101. package/plugins/harness-everything/skills/harness-everything/scripts/doc-check.js +1 -1
  102. package/plugins/harness-everything/skills/harness-everything/scripts/doc-lifecycle.js +2 -2
  103. package/plugins/harness-everything/skills/harness-everything/scripts/doc-obligations.js +2 -2
  104. package/plugins/harness-everything/skills/self-evolve/SKILL.md +1 -1
  105. package/plugins/harness-everything/skills/self-evolve/references/memory-resolution.md +11 -0
  106. package/plugins/harness-everything/skills/self-evolve/scripts/lesson-candidate.js +9 -12
  107. package/plugins/harness-everything/skills/self-evolve/scripts/persist-memory.js +1 -14
  108. package/plugins/harness-everything/skills/self-evolve/scripts/register-dynamic-skill.js +5 -9
  109. package/plugins/harness-everything/skills/self-evolve/scripts/workspace-helper.js +98 -0
  110. package/sbom.cdx.json +7 -7
  111. package/self-evolve/SKILL.md +1 -1
  112. package/self-evolve/references/memory-resolution.md +11 -0
  113. package/self-evolve/scripts/lesson-candidate.js +9 -12
  114. package/self-evolve/scripts/persist-memory.js +1 -14
  115. package/self-evolve/scripts/register-dynamic-skill.js +5 -9
  116. package/self-evolve/scripts/workspace-helper.js +98 -0
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ const assert = require('assert');
4
+ const fs = require('fs');
5
+ const os = require('os');
6
+ const path = require('path');
7
+ const { spawnSync } = require('child_process');
8
+ const ROOT = path.resolve(__dirname, '..');
9
+ const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-workspace-379-'));
10
+ const workspace = path.join(temp, 'workspace');
11
+ const stateHome = path.join(temp, 'state');
12
+ fs.mkdirSync(path.join(workspace, '.git'), { recursive: true });
13
+ const env = { ...process.env, HARNESS_STATE_HOME: stateHome };
14
+ for (const key of ['HARNESS_WORKSPACE_ROOT', 'CLAUDE_PROJECT_DIR', 'CODEX_PROJECT_DIR']) delete env[key];
15
+ const canonical = require('../scripts/lib/workspace');
16
+ const oldState = process.env.HARNESS_STATE_HOME;
17
+ process.env.HARNESS_STATE_HOME = stateHome;
18
+ const state = canonical.getWorkspaceStateDir(workspace);
19
+ if (oldState === undefined) delete process.env.HARNESS_STATE_HOME; else process.env.HARNESS_STATE_HOME = oldState;
20
+ const candidateId = 'lesson-0123456789abcdef01234567';
21
+ const candidate = { schemaVersion: 1, candidateId, state: 'proposed' };
22
+ const candidateDir = path.join(state, 'state', 'sessions', 'test', 'learning-candidates');
23
+ fs.mkdirSync(candidateDir, { recursive: true });
24
+ fs.writeFileSync(path.join(candidateDir, `${candidateId}.json`), JSON.stringify(candidate));
25
+ function layout(name, packaged = false, helper = true) {
26
+ const root = path.join(temp, name);
27
+ const scripts = path.join(root, packaged ? 'skills/self-evolve/scripts' : 'self-evolve/scripts');
28
+ fs.mkdirSync(path.dirname(scripts), { recursive: true });
29
+ fs.cpSync(path.join(ROOT, 'self-evolve/scripts'), scripts, { recursive: true });
30
+ if (packaged) {
31
+ fs.mkdirSync(path.join(root, '.codex-plugin'), { recursive: true });
32
+ fs.writeFileSync(path.join(root, '.codex-plugin/plugin.json'), JSON.stringify({ name: 'harness-everything', skills: './skills/' }));
33
+ } else {
34
+ fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({ name: 'harness-everything' }));
35
+ }
36
+ if (helper) {
37
+ fs.mkdirSync(path.join(root, 'scripts'), { recursive: true });
38
+ fs.cpSync(path.join(ROOT, 'scripts/lib'), path.join(root, 'scripts/lib'), { recursive: true });
39
+ }
40
+ return { root, scripts };
41
+ }
42
+ function run(l, file, args = [], extra = {}) {
43
+ return spawnSync(process.execPath, [path.join(l.scripts, file), ...args], { cwd: workspace, env, encoding: 'utf8', ...extra });
44
+ }
45
+ function error(result, code) {
46
+ assert.notStrictEqual(result.status, 0, result.stdout);
47
+ assert.match(result.stderr, new RegExp(code));
48
+ assert.doesNotMatch(result.stderr, /TypeError|path.join/);
49
+ }
50
+ let count = 0;
51
+ function test(name, fn) { fn(); console.log(`PASS ${name}`); count++; }
52
+ try {
53
+ for (const packaged of [false, true]) {
54
+ const l = layout(packaged ? 'plugin' : 'source', packaged);
55
+ test(`${packaged ? 'plugin' : 'source'} reads canonical candidates twice deterministically`, () => {
56
+ for (let i = 0; i < 2; i++) {
57
+ const result = run(l, 'lesson-candidate.js', ['list', '--workspace', workspace, '--session-id', 'test']);
58
+ assert.strictEqual(result.status, 0, result.stderr);
59
+ assert.deepStrictEqual(JSON.parse(result.stdout), [candidate]);
60
+ }
61
+ });
62
+ test(`${packaged ? 'plugin' : 'source'} empty session is []`, () => {
63
+ const result = run(l, 'lesson-candidate.js', ['list', '--workspace', workspace, '--session-id', 'empty']);
64
+ assert.strictEqual(result.status, 0, result.stderr);
65
+ assert.deepStrictEqual(JSON.parse(result.stdout), []);
66
+ });
67
+ test(`${packaged ? 'plugin' : 'source'} writers retain authorization boundary`, () => {
68
+ const result = run(l, 'persist-memory.js', ['Always verify Error.js before test changes.']);
69
+ assert.strictEqual(result.status, 3, result.stderr);
70
+ assert.match(result.stderr, /authorization/);
71
+ assert.strictEqual(fs.existsSync(path.join(workspace, 'memories')), false);
72
+ });
73
+ test(`${packaged ? 'plugin' : 'source'} register resolves helper before usage`, () => {
74
+ const result = run(l, 'register-dynamic-skill.js');
75
+ assert.doesNotMatch(result.stderr, /WORKSPACE_HELPER_UNAVAILABLE|Cannot register.*resolved git/);
76
+ assert.strictEqual(result.status, 0, result.stderr);
77
+ assert.match(result.stdout, /No self-evolved dynamic skills/);
78
+ });
79
+ }
80
+ const missing = layout('missing', true, false);
81
+ for (const command of ['list', 'show', 'evaluate', 'promote']) test(`missing helper ${command}`, () => {
82
+ const result = run(missing, 'lesson-candidate.js', [command, '--workspace', workspace, '--candidate-id', candidateId]);
83
+ error(result, 'WORKSPACE_HELPER_UNAVAILABLE');
84
+ assert.match(result.stderr, /workspace\.js/);
85
+ });
86
+ for (const file of ['persist-memory.js', 'register-dynamic-skill.js']) test(`missing helper ${file}`, () => {
87
+ error(run(missing, file, file === 'persist-memory.js' ? ['Always verify Error.js before test changes.'] : []), 'WORKSPACE_HELPER_UNAVAILABLE');
88
+ });
89
+ test('adversarial cwd helper is never imported', () => {
90
+ const hostile = path.join(temp, 'hostile-workspace');
91
+ const sentinel = path.join(temp, 'untrusted-helper-loaded');
92
+ fs.mkdirSync(path.join(hostile, '.git'), { recursive: true });
93
+ fs.mkdirSync(path.join(hostile, 'scripts/lib'), { recursive: true });
94
+ fs.writeFileSync(path.join(hostile, 'scripts/lib/workspace.js'), `require('fs').writeFileSync(${JSON.stringify(sentinel)}, 'loaded');`);
95
+ error(run(missing, 'lesson-candidate.js', ['list', '--workspace', hostile], { cwd: hostile }), 'WORKSPACE_HELPER_UNAVAILABLE');
96
+ assert.strictEqual(fs.existsSync(sentinel), false);
97
+ });
98
+ test('missing source helper never imports a hostile ancestor helper', () => {
99
+ const sourceMissing = layout('ancestor-attack/checkout', false, false);
100
+ const ancestorHelper = path.join(temp, 'ancestor-attack/scripts/lib/workspace.js');
101
+ const sentinel = path.join(temp, 'ancestor-helper-loaded');
102
+ fs.mkdirSync(path.dirname(ancestorHelper), { recursive: true });
103
+ fs.writeFileSync(ancestorHelper, `require('fs').writeFileSync(${JSON.stringify(sentinel)}, 'loaded'); module.exports = {getWorkspaceRoot: () => ${JSON.stringify(workspace)}, getWorkspaceStateDir: () => ${JSON.stringify(state)}, getWorkspaceKey: () => 'hostile'};`);
104
+ error(run(sourceMissing, 'lesson-candidate.js', ['list', '--workspace', workspace]), 'WORKSPACE_HELPER_UNAVAILABLE');
105
+ assert.strictEqual(fs.existsSync(sentinel), false);
106
+ });
107
+ test('source checkout named skills never falls through to a parent plugin marker', () => {
108
+ const namedSkills = layout('named-skills/skills', false, false);
109
+ const parent = path.dirname(namedSkills.root);
110
+ const sentinel = path.join(temp, 'false-plugin-helper-loaded');
111
+ fs.mkdirSync(path.join(parent, '.codex-plugin'), { recursive: true });
112
+ fs.mkdirSync(path.join(parent, 'scripts/lib'), { recursive: true });
113
+ fs.writeFileSync(path.join(parent, '.codex-plugin/plugin.json'), JSON.stringify({ name: 'harness-everything', skills: './skills/' }));
114
+ fs.writeFileSync(path.join(parent, 'scripts/lib/workspace.js'), `require('fs').writeFileSync(${JSON.stringify(sentinel)}, 'loaded'); module.exports = {getWorkspaceRoot: () => ${JSON.stringify(workspace)}, getWorkspaceStateDir: () => ${JSON.stringify(state)}, getWorkspaceKey: () => 'hostile'};`);
115
+ error(run(namedSkills, 'lesson-candidate.js', ['list', '--workspace', workspace]), 'WORKSPACE_HELPER_UNAVAILABLE');
116
+ assert.strictEqual(fs.existsSync(sentinel), false);
117
+ });
118
+ const broken = layout('broken');
119
+ fs.writeFileSync(path.join(broken.root, 'scripts/lib/workspace.js'), "throw new Error('resolver dependency failed');");
120
+ test('existing broken helper cause is visible', () => {
121
+ const result = run(broken, 'lesson-candidate.js', ['list', '--workspace', workspace]);
122
+ error(result, 'WORKSPACE_HELPER_UNAVAILABLE'); assert.match(result.stderr, /resolver dependency failed/);
123
+ });
124
+ const source = layout('unresolved');
125
+ for (const command of ['list', 'show', 'evaluate', 'promote']) test(`unresolved workspace ${command} cannot fall back to cwd`, () => {
126
+ error(run(source, 'lesson-candidate.js', [command, '--candidate-id', candidateId], { cwd: temp }), 'WORKSPACE_ROOT_UNRESOLVED');
127
+ });
128
+ for (const file of ['persist-memory.js', 'register-dynamic-skill.js']) test(`unresolved workspace ${file}`, () => {
129
+ error(run(source, file, file === 'persist-memory.js' ? ['Always verify Error.js before test changes.'] : [], { cwd: temp }), 'WORKSPACE_ROOT_UNRESOLVED');
130
+ });
131
+ const invalidState = layout('invalid-state');
132
+ fs.writeFileSync(path.join(invalidState.root, 'scripts/lib/workspace.js'), `module.exports = {getWorkspaceRoot: () => ${JSON.stringify(workspace)}, getWorkspaceStateDir: () => null, getWorkspaceKey: () => 'key'};`);
133
+ test('null canonical state is a state diagnostic', () => {
134
+ error(run(invalidState, 'lesson-candidate.js', ['list']), 'WORKSPACE_STATE_UNAVAILABLE');
135
+ });
136
+ test('candidate directory read errors are not an empty list', () => {
137
+ const session = path.join(state, 'state', 'sessions', 'blocked');
138
+ fs.mkdirSync(session, { recursive: true });
139
+ fs.writeFileSync(path.join(session, 'learning-candidates'), 'not a directory');
140
+ error(run(source, 'lesson-candidate.js', ['list', '--workspace', workspace, '--session-id', 'blocked']), 'WORKSPACE_STATE_UNAVAILABLE');
141
+ });
142
+ const blocked = path.join(temp, 'blocked-state'); fs.writeFileSync(blocked, 'not a directory');
143
+ for (const command of ['list', 'show', 'evaluate', 'promote']) test(`unusable state ${command}`, () => {
144
+ error(run(source, 'lesson-candidate.js', [command, '--workspace', workspace, '--candidate-id', candidateId], { env: { ...env, HARNESS_STATE_HOME: blocked } }), 'WORKSPACE_STATE_UNAVAILABLE');
145
+ });
146
+ test('no unintended workspace writes', () => {
147
+ assert.deepStrictEqual(fs.readdirSync(workspace), ['.git']);
148
+ assert.deepStrictEqual(JSON.parse(fs.readFileSync(path.join(candidateDir, `${candidateId}.json`))), candidate);
149
+ });
150
+ console.log(`${count} workspace resolver checks passed`);
151
+ } finally { fs.rmSync(temp, { recursive: true, force: true }); }
@@ -0,0 +1,131 @@
1
+ 'use strict';
2
+
3
+ // #383: preserve each consumer's existing glob dialect while fixing only **.
4
+ // Source contract: docs/document-lifecycle.md, project-docs configuration;
5
+ // harness-everything/references/doc-check.md, derived-view unsupported cases.
6
+ const assert = require('assert');
7
+ const fs = require('fs');
8
+ const os = require('os');
9
+ const path = require('path');
10
+ const vm = require('vm');
11
+ const { spawnSync } = require('child_process');
12
+
13
+ const ROOT = path.resolve(__dirname, '..');
14
+ const LEGS = [
15
+ path.join(ROOT, 'harness-everything', 'scripts'),
16
+ path.join(ROOT, 'plugins', 'harness-everything', 'skills', 'harness-everything', 'scripts')
17
+ ];
18
+ let failures = 0;
19
+ let checks = 0;
20
+ function check(name, run) {
21
+ try { run(); checks++; console.log(`PASS ${name}`); }
22
+ catch (error) { failures++; console.error(`FAIL ${name}: ${error.stack}`); }
23
+ }
24
+
25
+ for (const [legIndex, dir] of LEGS.entries()) {
26
+ const leg = legIndex === 0 ? 'source' : 'packaged';
27
+ for (const name of ['doc-lifecycle', 'doc-obligations', 'doc-check']) {
28
+ const source = fs.readFileSync(path.join(dir, `${name}.js`), 'utf8');
29
+ const declaration = source.match(/function globToRegExp\([^)]*\) \{[\s\S]*?\n\}/);
30
+ assert.ok(declaration, `${name}: matcher declaration exists`);
31
+ const matcher = vm.runInNewContext(`${declaration[0]}; globToRegExp`, {
32
+ normalizePath: value => String(value).replace(/\\/g, '/').replace(/^\.\//, '')
33
+ });
34
+ check(`${leg}/${name}: recursive line terminators, depths and negative controls`, () => {
35
+ for (const terminator of ['\n', '\r', '\u2028', '\u2029']) {
36
+ for (const pattern of ['docs/**/*.md', 'docs/**']) {
37
+ assert.strictEqual(matcher(pattern).test(`docs/a${terminator}b/file.md`), true, JSON.stringify({ pattern, terminator }));
38
+ }
39
+ }
40
+ assert.strictEqual(matcher('docs/**/*.md').test('docs/a/b/file.md'), true);
41
+ // doc-check has historically required the explicit slash after **;
42
+ // lifecycle and obligations give **/ zero-depth matching. Keep both.
43
+ assert.strictEqual(matcher('docs/**/*.md').test('docs/file.md'), name !== 'doc-check');
44
+ assert.strictEqual(matcher('docs/**').test('docs/file.md'), true);
45
+ assert.strictEqual(matcher('docs/*.md').test('docs/a/b.md'), false);
46
+ assert.strictEqual(matcher('docs/*.md').test('docs/a\nb.md'), true);
47
+ assert.strictEqual(matcher('docs/**/*.md').test('other/a/file.md'), false);
48
+ assert.strictEqual(matcher('docs/**/*.md').test('docs/a/file.txt'), false);
49
+ assert.strictEqual(matcher('docs.v1/**').test('docsXv1/file.md'), false);
50
+ assert.strictEqual(matcher('docs/?.md').test('docs/a.md'), name === 'doc-check');
51
+ assert.strictEqual(matcher('docs/?.md').test('docs/ab.md'), false);
52
+ assert.strictEqual(matcher('docs/?.md').test('docs/?.md'), true);
53
+ });
54
+ }
55
+
56
+ // LF/CR cannot be filesystem names on Windows; the in-memory matrix above
57
+ // covers them there. LS and PS exercise real paths on every supported host.
58
+ const terminators = process.platform === 'win32' ? ['\u2028', '\u2029'] : ['\n', '\r', '\u2028', '\u2029'];
59
+ for (const terminator of terminators) {
60
+ check(`${leg}/consumers: ${JSON.stringify(terminator)} scope and diagnostics`, () => {
61
+ const workspace = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-383-'));
62
+ try {
63
+ const relative = `docs/a${terminator}b/file.md`;
64
+ const config = {
65
+ governed: [{ path: 'docs/**/*.md', role: 'reference' }, { path: 'docs/**', role: 'adr' }],
66
+ domains: [], legacy: []
67
+ };
68
+ const absolute = path.join(workspace, relative);
69
+ fs.mkdirSync(path.dirname(absolute), { recursive: true });
70
+ fs.writeFileSync(absolute, '# Missing status\n');
71
+ const lifecycle = require(path.join(dir, 'doc-lifecycle'));
72
+ const docs = lifecycle.collectDocs(workspace, config);
73
+ assert.deepStrictEqual(docs.map(doc => [doc.path, doc.role]), [[relative, 'reference']], 'first matching scope retains precedence');
74
+ const lint = lifecycle.analyzeDocs(docs, config, { all: true });
75
+ assert.ok(lint.diagnostics.some(item => item.path === relative && item.code === 'missing-status'), 'scoped malformed document is diagnosed');
76
+
77
+ const { deriveDocFacts, deriveDocObligations } = require(path.join(dir, 'doc-obligations'));
78
+ const derived = deriveDocFacts({
79
+ base: { files: { [relative]: '---\nstatus: active\n---\nBefore\n' } },
80
+ candidate: { files: { [relative]: '---\nstatus: active\n---\nAfter\n' } }, config
81
+ });
82
+ assert.deepStrictEqual(derived.facts.governedPaths, [relative]);
83
+ const obligation = deriveDocObligations(derived.facts).find(item => item.capability === 'doc-lint');
84
+ assert.strictEqual(obligation.applicability, 'applicable');
85
+ assert.deepStrictEqual(obligation.scope.paths, [relative]);
86
+
87
+ fs.writeFileSync(path.join(workspace, 'config.json'), JSON.stringify({ derived: [{ path: 'docs/**/*.md', generator: 'never-run' }] }));
88
+ const result = spawnSync(process.execPath, [path.join(dir, 'doc-check.js'), 'compare-derived',
89
+ '--workspace', workspace, '--config', 'config.json', '--view', relative,
90
+ '--candidate', 'absent.md', '--generation-status', 'ok'], { encoding: 'utf8', cwd: os.tmpdir() });
91
+ assert.strictEqual(result.status, 2, result.stderr);
92
+ const output = JSON.parse(result.stdout);
93
+ assert.strictEqual(output.result, 'unsupported', 'declared recursive glob receives the correct diagnostic');
94
+ assert.strictEqual(output.reason, 'view matches a declared glob; only literal derived paths are supported');
95
+ assert.strictEqual(fs.existsSync(path.join(workspace, 'absent.md')), false, 'no generator or candidate is created');
96
+ } finally { fs.rmSync(workspace, { recursive: true, force: true }); }
97
+ });
98
+ }
99
+ }
100
+
101
+ // #383 F1: the doc-reconciliation core mirrors the ** branch; each copy must match line terminators too.
102
+ const CORE_LEGS = [
103
+ path.join(ROOT, 'hooks', 'scripts', 'lib', 'doc-reconciliation-core.js'),
104
+ path.join(ROOT, 'plugins', 'harness-everything', 'hooks', 'scripts', 'lib', 'doc-reconciliation-core.js')
105
+ ];
106
+ const binaryRecord = relative => ({ presence: 'present', path: relative, type: 'file', mode: '100644', storedBytes: Buffer.from([0xff, 0xfe, 0x41]) });
107
+ for (const [legIndex, corePath] of CORE_LEGS.entries()) {
108
+ const leg = legIndex === 0 ? 'source' : 'packaged';
109
+ const { deriveFiles, semanticRelevance } = require(corePath)._internals;
110
+ const config = { governed: [{ path: 'docs/**/*.md', role: 'reference' }] };
111
+ for (const terminator of ['\n', '\r', '
', '
']) {
112
+ const relative = `docs/a${terminator}b/file.md`;
113
+ check(`${leg}/doc-reconciliation-core: ${JSON.stringify(terminator)} is governed by docs/**/*.md`, () => {
114
+ assert.strictEqual(semanticRelevance(relative, config), 'document', JSON.stringify(relative));
115
+ });
116
+ check(`${leg}/doc-reconciliation-core: ${JSON.stringify(terminator)} undecodable record fails closed`, () => {
117
+ assert.throws(() => deriveFiles([binaryRecord(relative)], config), error => error.code === 'inventory-unsupported');
118
+ });
119
+ check(`${leg}/doc-reconciliation-core: ${JSON.stringify(terminator)} negative control (ungoverned path) is not fail-closed`, () => {
120
+ const other = `other/a${terminator}b/file.md`;
121
+ assert.strictEqual(semanticRelevance(other, config), null, JSON.stringify(other));
122
+ assert.deepStrictEqual(deriveFiles([binaryRecord(other)], config), {});
123
+ });
124
+ }
125
+ check(`${leg}/doc-reconciliation-core: positive control docs/ab/file.md fails closed`, () => {
126
+ assert.strictEqual(semanticRelevance('docs/ab/file.md', config), 'document');
127
+ assert.throws(() => deriveFiles([binaryRecord('docs/ab/file.md')], config), error => error.code === 'inventory-unsupported');
128
+ });
129
+ }
130
+ console.log(`${checks} checks passed; ${failures} failed`);
131
+ process.exitCode = failures ? 1 : 0;
@@ -114,7 +114,7 @@ Two *separate* gates apply. The **functional gate** requires contract tests, neg
114
114
  | `tickets` | `"tracker"` or a repo-relative path |
115
115
  | Unknown keys | Warning, not error |
116
116
 
117
- Globs are repo-relative POSIX paths supporting `*` (one segment) and `**` (any depth).
117
+ Globs are repo-relative POSIX paths supporting `*` (one segment) and `**` (any depth). Recursive `**` branches include line terminators in path components; `*` still cannot cross `/`.
118
118
 
119
119
  ## Roles and status (D6.1, D6.3)
120
120
 
@@ -43,6 +43,10 @@ upgrade any platform row's live-host status.
43
43
 
44
44
  The registered `doc-check check --conflict-assessment` mode runs document lifecycle lint across the full governed and legacy corpus and requires `--base` as an exact commit SHA. It emits `conflict-check` only when collection is complete and there are no lifecycle errors. Its event preserves the exact obligation scope separately from full-corpus paths and digests, config bytes and base commit, then checks those bindings against the workflow snapshot. The acceptance is limited to recorded and structural conflicts; prose contradictions remain with independent `quality-review`. Plain `check` never earns this credit, and OpenCode still refuses the capability. Deterministic mechanism coverage does not establish live-host behavior or change the platform matrix's live-verification status.
45
45
 
46
+ ### Self-evolve workspace resolution (mechanism only)
47
+
48
+ Source and packaged plugin self-evolve entrypoints use installation-relative canonical workspace helpers and the same workspace/session state path. Missing helpers, unresolved identities, and unusable state paths fail explicitly; only absent candidate directories yield an empty list. The [resolver contract](../self-evolve/references/memory-resolution.md#workspace-helper-and-diagnostics) and source/package subprocess regressions establish deterministic mechanism coverage, not a live-host upgrade. Existing memory authorization remains required.
49
+
46
50
  ## Current capability summary
47
51
 
48
52
  | Platform surface | Standalone skills | Plugin support | Hooks/lifecycle | Project scope | Global scope | Current evidence boundary |
@@ -4,7 +4,7 @@ description: "Route software/project work through Harness: classify tier/topolog
4
4
  license: Apache-2.0
5
5
  metadata:
6
6
  author: Miya Daniel
7
- version: 0.47.0
7
+ version: 0.47.1
8
8
  ---
9
9
 
10
10
  # Harness Everything
@@ -243,7 +243,7 @@ function parseCompareArgs(argv) {
243
243
 
244
244
  // Escape regex metacharacters, then translate `**`, `*` and `?`. Used only to detect a declared glob.
245
245
  function globToRegExp(pattern) {
246
- const body = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*\*/g, '\u0000').replace(/\*/g, '[^/]*').replace(/\?/g, '[^/]').replace(/\u0000/g, '.*');
246
+ const body = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*\*/g, '\u0000').replace(/\*/g, '[^/]*').replace(/\?/g, '[^/]').replace(/\u0000/g, '[\\s\\S]*');
247
247
  return new RegExp(`^${body}$`);
248
248
  }
249
249
 
@@ -202,10 +202,10 @@ function globToRegExp(glob) {
202
202
  const char = source[index];
203
203
  if (char === '*' && source[index + 1] === '*') {
204
204
  if (source[index + 2] === '/') {
205
- pattern += '(?:.*/)?';
205
+ pattern += '(?:[\\s\\S]*/)?';
206
206
  index += 2;
207
207
  } else {
208
- pattern += '.*';
208
+ pattern += '[\\s\\S]*';
209
209
  index++;
210
210
  }
211
211
  } else if (char === '*') {
@@ -70,10 +70,10 @@ function globToRegExp(glob) {
70
70
  const char = source[index];
71
71
  if (char === '*' && source[index + 1] === '*') {
72
72
  if (source[index + 2] === '/') {
73
- pattern += '(?:.*/)?';
73
+ pattern += '(?:[\\s\\S]*/)?';
74
74
  index += 2;
75
75
  } else {
76
- pattern += '.*';
76
+ pattern += '[\\s\\S]*';
77
77
  index++;
78
78
  }
79
79
  } else if (char === '*') {
@@ -50,8 +50,8 @@ function globToRegExp(glob) {
50
50
  for (let index = 0; index < source.length; index++) {
51
51
  const char = source[index];
52
52
  if (char === '*' && source[index + 1] === '*') {
53
- if (source[index + 2] === '/') { pattern += '(?:.*/)?'; index += 2; }
54
- else { pattern += '.*'; index += 1; }
53
+ if (source[index + 2] === '/') { pattern += '(?:[\\s\\S]*/)?'; index += 2; }
54
+ else { pattern += '[\\s\\S]*'; index += 1; }
55
55
  } else if (char === '*') pattern += '[^/]*';
56
56
  else pattern += char.replace(/[|\\{}()[\]^$+?.]/g, '\\$&');
57
57
  }
@@ -101,7 +101,7 @@ function isUncPathPrefix(token, command, index) {
101
101
  return token === '' && command[index] === '\\' && command[index + 1] === '\\';
102
102
  }
103
103
  // Canonical SHA-256 of harness-everything/scripts/doc-check.js (CRLF normalised to LF). Re-pin deliberately when that script changes.
104
- const SHIPPED_DOC_CHECK_DIGEST = '804a093b31afde07ea6ec40e39fc758b501966bfa2a15c53a5275ad3171b0755';
104
+ const SHIPPED_DOC_CHECK_DIGEST = '3ca386666d4dae9a98ffa27adf2f326bf171a783af90515375f0b328ca35f194';
105
105
  // This install's own doc-check runtime, relative to hooks/scripts/lib: the source checkout or the packaged plugin.
106
106
  const DOC_CHECK_INSTALL_LAYOUTS = [
107
107
  ['..', '..', '..', 'harness-everything', 'scripts', 'doc-check.js'],
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "harness-enforcement",
3
3
  "description": "Repository inventory for the opencode enforcement plugin and its standalone helper",
4
- "version": "0.47.0",
4
+ "version": "0.47.2",
5
5
  "runtime": "index.mjs",
6
6
  "hooks": {
7
7
  "verify": "hooks/verify.js"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harness-everything",
3
- "version": "0.47.0",
3
+ "version": "0.47.2",
4
4
  "description": "Harness OS: An Orchestrated Agent Operating System for AI-driven Development",
5
5
  "main": "harness-everything/scripts/bootstrap.js",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harness-everything",
3
- "version": "0.47.0",
3
+ "version": "0.47.2",
4
4
  "description": "Invariant-first engineering harness for routing, verification, and failure recovery while preserving agent autonomy.",
5
5
  "author": {
6
6
  "name": "Miya Daniel",
@@ -50,8 +50,8 @@ function globToRegExp(glob) {
50
50
  for (let index = 0; index < source.length; index++) {
51
51
  const char = source[index];
52
52
  if (char === '*' && source[index + 1] === '*') {
53
- if (source[index + 2] === '/') { pattern += '(?:.*/)?'; index += 2; }
54
- else { pattern += '.*'; index += 1; }
53
+ if (source[index + 2] === '/') { pattern += '(?:[\\s\\S]*/)?'; index += 2; }
54
+ else { pattern += '[\\s\\S]*'; index += 1; }
55
55
  } else if (char === '*') pattern += '[^/]*';
56
56
  else pattern += char.replace(/[|\\{}()[\]^$+?.]/g, '\\$&');
57
57
  }
@@ -101,7 +101,7 @@ function isUncPathPrefix(token, command, index) {
101
101
  return token === '' && command[index] === '\\' && command[index + 1] === '\\';
102
102
  }
103
103
  // Canonical SHA-256 of harness-everything/scripts/doc-check.js (CRLF normalised to LF). Re-pin deliberately when that script changes.
104
- const SHIPPED_DOC_CHECK_DIGEST = '804a093b31afde07ea6ec40e39fc758b501966bfa2a15c53a5275ad3171b0755';
104
+ const SHIPPED_DOC_CHECK_DIGEST = '3ca386666d4dae9a98ffa27adf2f326bf171a783af90515375f0b328ca35f194';
105
105
  // This install's own doc-check runtime, relative to hooks/scripts/lib: the source checkout or the packaged plugin.
106
106
  const DOC_CHECK_INSTALL_LAYOUTS = [
107
107
  ['..', '..', '..', 'harness-everything', 'scripts', 'doc-check.js'],
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "harness-everything",
4
- "version": "0.47.0",
4
+ "version": "0.47.2",
5
5
  "description": "Invariant-first engineering harness for routing, verification, and failure recovery while preserving agent autonomy.",
6
6
  "author": {
7
7
  "name": "Miya Daniel",
@@ -4,7 +4,7 @@ description: "Route software/project work through Harness: classify tier/topolog
4
4
  license: Apache-2.0
5
5
  metadata:
6
6
  author: Miya Daniel
7
- version: 0.47.0
7
+ version: 0.47.1
8
8
  ---
9
9
 
10
10
  # Harness Everything
@@ -243,7 +243,7 @@ function parseCompareArgs(argv) {
243
243
 
244
244
  // Escape regex metacharacters, then translate `**`, `*` and `?`. Used only to detect a declared glob.
245
245
  function globToRegExp(pattern) {
246
- const body = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*\*/g, '\u0000').replace(/\*/g, '[^/]*').replace(/\?/g, '[^/]').replace(/\u0000/g, '.*');
246
+ const body = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*\*/g, '\u0000').replace(/\*/g, '[^/]*').replace(/\?/g, '[^/]').replace(/\u0000/g, '[\\s\\S]*');
247
247
  return new RegExp(`^${body}$`);
248
248
  }
249
249
 
@@ -202,10 +202,10 @@ function globToRegExp(glob) {
202
202
  const char = source[index];
203
203
  if (char === '*' && source[index + 1] === '*') {
204
204
  if (source[index + 2] === '/') {
205
- pattern += '(?:.*/)?';
205
+ pattern += '(?:[\\s\\S]*/)?';
206
206
  index += 2;
207
207
  } else {
208
- pattern += '.*';
208
+ pattern += '[\\s\\S]*';
209
209
  index++;
210
210
  }
211
211
  } else if (char === '*') {
@@ -70,10 +70,10 @@ function globToRegExp(glob) {
70
70
  const char = source[index];
71
71
  if (char === '*' && source[index + 1] === '*') {
72
72
  if (source[index + 2] === '/') {
73
- pattern += '(?:.*/)?';
73
+ pattern += '(?:[\\s\\S]*/)?';
74
74
  index += 2;
75
75
  } else {
76
- pattern += '.*';
76
+ pattern += '[\\s\\S]*';
77
77
  index++;
78
78
  }
79
79
  } else if (char === '*') {
@@ -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