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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.github/workflows/ci.yml +12 -0
- package/CHANGELOG.md +18 -0
- package/benchmarks/issue290-pilot/README.md +199 -0
- package/benchmarks/issue290-pilot/analyze.py +299 -0
- package/benchmarks/issue290-pilot/analyze.test.py +146 -0
- package/benchmarks/issue290-pilot/check.py +19 -0
- package/benchmarks/issue290-pilot/evidence/archive.json +7 -0
- package/benchmarks/issue290-pilot/evidence/group-summary.json +684 -0
- package/benchmarks/issue290-pilot/evidence/inventory.json +3728 -0
- package/benchmarks/issue290-pilot/evidence/ledger.json +4644 -0
- package/benchmarks/issue290-pilot/evidence/replay/pairs.json +482 -0
- package/benchmarks/issue290-pilot/evidence/replay/provenance.json +24 -0
- package/benchmarks/issue290-pilot/evidence/replay/tables.md +37 -0
- package/benchmarks/issue290-pilot/evidence.tar.gz +0 -0
- package/benchmarks/issue290-pilot/export.py +110 -0
- package/benchmarks/issue290-pilot/export.test.py +65 -0
- package/benchmarks/issue290-pilot/fable-supplement.py +55 -0
- package/benchmarks/issue290-pilot/fixture.test.py +28 -0
- package/benchmarks/issue290-pilot/fixtures/polyglot/instructions.md +19 -0
- package/benchmarks/issue290-pilot/fixtures/polyglot/package.json +29 -0
- package/benchmarks/issue290-pilot/fixtures/polyglot/simple-linked-list.js +44 -0
- package/benchmarks/issue290-pilot/fixtures/polyglot/simple-linked-list.spec.js +131 -0
- package/benchmarks/issue290-pilot/fixtures/quixbugs/LICENSE +7 -0
- package/benchmarks/issue290-pilot/fixtures/quixbugs/cases.json +7 -0
- package/benchmarks/issue290-pilot/fixtures/quixbugs/find_in_sorted.py +31 -0
- package/benchmarks/issue290-pilot/fixtures/quixbugs/test_find_in_sorted.py +15 -0
- package/benchmarks/issue290-pilot/frozen-documents.json +41 -0
- package/benchmarks/issue290-pilot/grader.cjs +43 -0
- package/benchmarks/issue290-pilot/grader.test.cjs +10 -0
- package/benchmarks/issue290-pilot/hook.test.cjs +27 -0
- package/benchmarks/issue290-pilot/host-hook.cjs +35 -0
- package/benchmarks/issue290-pilot/metrics.test.py +131 -0
- package/benchmarks/issue290-pilot/preflight/amendment.md +32 -0
- package/benchmarks/issue290-pilot/preflight/environment.json +15 -0
- package/benchmarks/issue290-pilot/preflight/fable-amendment.md +20 -0
- package/benchmarks/issue290-pilot/preflight/fable-frozen-files.json +4 -0
- package/benchmarks/issue290-pilot/preflight/fable-infrastructure-amendment.md +27 -0
- package/benchmarks/issue290-pilot/preflight/fable-retry-frozen-files.json +4 -0
- package/benchmarks/issue290-pilot/preflight/fable-supplement-v1.py +47 -0
- package/benchmarks/issue290-pilot/preflight/frozen-files.json +18 -0
- package/benchmarks/issue290-pilot/preflight/graders.json +67 -0
- package/benchmarks/issue290-pilot/pricing-assumptions.json +29 -0
- package/benchmarks/issue290-pilot/protocol.json +32 -0
- package/benchmarks/issue290-pilot/read_metrics.py +166 -0
- package/benchmarks/issue290-pilot/replay.py +167 -0
- package/benchmarks/issue290-pilot/reporting.py +82 -0
- package/benchmarks/issue290-pilot/run.py +262 -0
- package/benchmarks/issue290-pilot/sources.json +10 -0
- package/benchmarks/issue290-pilot/verification/analyzer-tests.log +5 -0
- package/benchmarks/issue290-pilot/verification/final-gates.json +58 -0
- package/benchmarks/issue290-pilot/verification/fixture-tests.log +5 -0
- package/benchmarks/issue290-pilot/verification/gates.json +58 -0
- package/benchmarks/issue290-pilot/verification/npm-audit.json +84 -0
- package/benchmarks/issue290-pilot/verification/offline-correction.json +59 -0
- package/benchmarks/issue290-pilot/verification/post-report/helper-tests.log +22 -0
- package/benchmarks/issue290-pilot/verification/post-report/test-consistency.log +626 -0
- package/benchmarks/issue290-pilot/verification/post-report/test-references.log +6 -0
- package/benchmarks/issue290-pilot/verification/post-report/waza-tokens.log +710 -0
- package/benchmarks/issue290-pilot/verification/test-collision.log +28 -0
- package/benchmarks/issue290-pilot/verification/test-consistency.log +28 -0
- package/benchmarks/issue290-pilot/verification/test-docs-capabilities.log +131 -0
- package/benchmarks/issue290-pilot/verification/test-mechanism.log +1829 -0
- package/benchmarks/issue290-pilot/verification/test-mutations.log +16 -0
- package/benchmarks/issue290-pilot/verification/test-platform-compatibility.log +9 -0
- package/benchmarks/issue290-pilot/verification/test-plugin-openai.log +6 -0
- package/benchmarks/issue290-pilot/verification/test-plugin-submission.log +5 -0
- package/benchmarks/issue290-pilot/verification/test-references.log +6 -0
- package/benchmarks/issue290-pilot/verification/test-release.log +59 -0
- package/benchmarks/issue290-pilot/verification/test-repo-contract.log +5 -0
- package/benchmarks/issue290-pilot/verification/test-routing-invariants.log +785 -0
- package/benchmarks/issue290-pilot/verification/test-routing-skills.log +28 -0
- package/benchmarks/issue290-pilot/verification/test.log +3373 -0
- package/benchmarks/issue290-pilot/verification/verification-report.md +76 -0
- package/benchmarks/issue290-pilot/verification/waza-tokens.log +710 -0
- package/benchmarks/issue290-pilot/verification/with-dependencies/gates.json +26 -0
- package/benchmarks/issue290-pilot/verification/with-dependencies/test-collision.log +8 -0
- package/benchmarks/issue290-pilot/verification/with-dependencies/test-consistency.log +626 -0
- package/benchmarks/issue290-pilot/verification/with-dependencies/test-mechanism.log +1693 -0
- package/benchmarks/issue290-pilot/verification/with-dependencies/test-release.log +38 -0
- package/benchmarks/issue290-pilot/verification/with-dependencies/test-routing-skills.log +44 -0
- package/benchmarks/issue290-pilot/verification/with-dependencies/test.log +3253 -0
- package/ci/mechanism-379-self-evolve-workspace.test.js +151 -0
- package/ci/mechanism-383-doc-glob-line-terminators.test.js +131 -0
- package/docs/document-lifecycle.md +1 -1
- package/docs/platform-capabilities.md +4 -0
- package/harness-everything/SKILL.md +1 -1
- package/harness-everything/scripts/doc-check.js +1 -1
- package/harness-everything/scripts/doc-lifecycle.js +2 -2
- package/harness-everything/scripts/doc-obligations.js +2 -2
- package/hooks/scripts/lib/doc-reconciliation-core.js +2 -2
- package/hooks/scripts/lib/doc-reconciliation-provider.js +1 -1
- package/opencode-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
- package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-core.js +2 -2
- package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-provider.js +1 -1
- package/plugins/harness-everything/plugin.json +1 -1
- package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
- package/plugins/harness-everything/skills/harness-everything/scripts/doc-check.js +1 -1
- package/plugins/harness-everything/skills/harness-everything/scripts/doc-lifecycle.js +2 -2
- package/plugins/harness-everything/skills/harness-everything/scripts/doc-obligations.js +2 -2
- package/plugins/harness-everything/skills/self-evolve/SKILL.md +1 -1
- package/plugins/harness-everything/skills/self-evolve/references/memory-resolution.md +11 -0
- package/plugins/harness-everything/skills/self-evolve/scripts/lesson-candidate.js +9 -12
- package/plugins/harness-everything/skills/self-evolve/scripts/persist-memory.js +1 -14
- package/plugins/harness-everything/skills/self-evolve/scripts/register-dynamic-skill.js +5 -9
- package/plugins/harness-everything/skills/self-evolve/scripts/workspace-helper.js +98 -0
- package/sbom.cdx.json +7 -7
- package/self-evolve/SKILL.md +1 -1
- package/self-evolve/references/memory-resolution.md +11 -0
- package/self-evolve/scripts/lesson-candidate.js +9 -12
- package/self-evolve/scripts/persist-memory.js +1 -14
- package/self-evolve/scripts/register-dynamic-skill.js +5 -9
- 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 |
|
|
@@ -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 += '(
|
|
54
|
-
else { pattern += '
|
|
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 = '
|
|
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'],
|
package/package.json
CHANGED
|
@@ -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 += '(
|
|
54
|
-
else { pattern += '
|
|
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 = '
|
|
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.
|
|
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",
|
|
@@ -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 === '*') {
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
5
|
-
try {
|
|
6
|
-
({ getWorkspaceRoot } = require('../../scripts/lib/workspace'));
|
|
7
|
-
} catch (err) {
|
|
8
|
-
getWorkspaceRoot = () => null;
|
|
9
|
-
}
|
|
4
|
+
const { getWorkspaceRoot } = require('./workspace-helper');
|
|
10
5
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|