harness-everything 0.36.1 → 0.36.3
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/CHANGELOG.md +17 -0
- package/ci/mechanism-2aw-project-docs-config.test.js +450 -10
- package/ci/mechanism-335-baseline-evidence.test.js +355 -7
- package/docs/doc-evidence-baseline-binding.md +28 -7
- package/docs/document-lifecycle.md +60 -8
- package/harness-everything/SKILL.md +1 -1
- package/harness-everything/scripts/doc-evidence.js +69 -9
- package/harness-everything/scripts/project-docs-resolver.js +30 -13
- package/multi-agent-workspace/SKILL.md +1 -1
- package/multi-agent-workspace/scripts/project-docs-resolver.js +30 -13
- 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/plugin.json +1 -1
- package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
- package/plugins/harness-everything/skills/harness-everything/scripts/doc-evidence.js +69 -9
- package/plugins/harness-everything/skills/harness-everything/scripts/project-docs-resolver.js +30 -13
- package/plugins/harness-everything/skills/multi-agent-workspace/SKILL.md +1 -1
- package/plugins/harness-everything/skills/multi-agent-workspace/scripts/project-docs-resolver.js +30 -13
- package/plugins/harness-everything/skills/to-spec/SKILL.md +1 -1
- package/plugins/harness-everything/skills/to-spec/scripts/check-project-docs.js +136 -4
- package/plugins/harness-everything/skills/to-spec/scripts/project-docs-resolver.js +30 -13
- package/sbom.cdx.json +7 -7
- package/to-spec/SKILL.md +1 -1
- package/to-spec/scripts/check-project-docs.js +136 -4
- package/to-spec/scripts/project-docs-resolver.js +30 -13
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"name": "harness-everything",
|
|
11
11
|
"source": "./",
|
|
12
12
|
"description": "Harness OS: an orchestrated agent operating system — tier routing, circuit breaker guardrails, verification gates, WAL session memory, and self-evolving workspace rules.",
|
|
13
|
-
"version": "0.36.
|
|
13
|
+
"version": "0.36.3",
|
|
14
14
|
"author": {
|
|
15
15
|
"name": "Miya Daniel"
|
|
16
16
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "harness-everything",
|
|
3
3
|
"description": "Harness OS: an orchestrated agent operating system — tier routing, circuit breaker guardrails, verification gates, WAL session memory, and self-evolving workspace rules.",
|
|
4
|
-
"version": "0.36.
|
|
4
|
+
"version": "0.36.3",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Miya Daniel"
|
|
7
7
|
},
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.36.3](https://github.com/dyphn1/Harness-everything/compare/v0.36.2...v0.36.3) (2026-10-20)
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
* **project-docs:** reject non-string required fields and unsafe init paths ([#331](https://github.com/dyphn1/Harness-everything/issues/331)) ([685df07](https://github.com/dyphn1/Harness-everything/commit/685df076a84a7c336a4ce3c9e06f97ebae2bd8b8))
|
|
10
|
+
* **project-docs:** stop host-only init from overwriting malformed manifests ([#331](https://github.com/dyphn1/Harness-everything/issues/331)) ([93df52c](https://github.com/dyphn1/Harness-everything/commit/93df52c94f2f55df828a0176fb7bf533c412fc6c))
|
|
11
|
+
|
|
12
|
+
## [0.36.2](https://github.com/dyphn1/Harness-everything/compare/v0.36.1...v0.36.2) (2026-10-20)
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
* **doc-lifecycle:** honor poisoned shared destinations inside metadata ([2c69247](https://github.com/dyphn1/Harness-everything/commit/2c69247c3a2e108df945fc91c68fb060ac04e058)), closes [#340](https://github.com/dyphn1/Harness-everything/issues/340)
|
|
17
|
+
* **doc-lifecycle:** keep rejected destination snapshots single-read ([f49b253](https://github.com/dyphn1/Harness-everything/commit/f49b25318f4022d241d91051e1daa51ef61cdff2)), closes [#340](https://github.com/dyphn1/Harness-everything/issues/340)
|
|
18
|
+
* **doc-lifecycle:** single-read the whole-baseline and destination digests ([063a00b](https://github.com/dyphn1/Harness-everything/commit/063a00b4f97c147299a63d7b9b82fc2cbae87bc0)), closes [#340](https://github.com/dyphn1/Harness-everything/issues/340)
|
|
19
|
+
|
|
5
20
|
## [0.36.1](https://github.com/dyphn1/Harness-everything/compare/v0.36.0...v0.36.1) (2026-10-20)
|
|
6
21
|
|
|
7
22
|
### Bug Fixes
|
|
@@ -66,10 +81,12 @@ All notable changes to this project are documented in this file.
|
|
|
66
81
|
* **doc-evidence:** defer the candidate-inventory prototype check to applicable baseline-only obligations and treat a throwing prototype trap as unknown inventory, so unrelated obligations are never aborted
|
|
67
82
|
* **doc-evidence:** bind baseline-annotated mixed scopes and rename destinations to the captured candidate snapshot for evidence binding, the audit identity, and stale diagnostics, so a later-throwing or later-changing digest accessor cannot disagree with the digest that bound evidence
|
|
68
83
|
* **doc-evidence:** carry the metadata-validated rename destination digest and baseline-path source-absence proof through capture and binding, so a candidate inventory that answers consistently during validation is never re-inspected and cannot abort the check or drift the audit key
|
|
84
|
+
* **doc-evidence:** keep rejected rename destination snapshots single-read, so a throwing or unusable destination digest getter is never re-read and its later values never reach the capture or audit identity, including shared destinations where a poisoned destination is honored inside metadata validation and wins in the audit
|
|
69
85
|
* **doc-evidence:** restrict the validated source-absence audit shortcut to applicable obligations and require every accepted entry sharing a rename destination to agree on the validated own digest, so inapplicable audit identity keeps the legacy read and contradictory shared-destination snapshots fail closed
|
|
70
86
|
* **doc-evidence:** gate the rename destination snapshot on applicability and guard baselinePaths entry inspection, so inapplicable audit identity reports the first legacy digest value and a throwing entry getter or descriptor trap fails the entry closed instead of escaping the checker
|
|
71
87
|
* **doc-evidence:** capture the validated per-path baseline digest during metadata and drive the audit mirror, stale-baseline diagnostics, and binding from that snapshot, so a stateful baseline getter cannot bind a record against a value it started returning only after validation and a throwing inspection is a metadata error instead of an escape
|
|
72
88
|
* **doc-evidence:** key the baseline audit mirror by the metadata-validated path snapshot instead of re-reading `baselinePaths[].path`, so a stateful path accessor read during validation can never make a passing result describe a different baseline identity
|
|
89
|
+
* **doc-evidence:** read the whole-baseline `baseline.digest` and the rename destination's own digest exactly once and validate that captured scalar, so a stateful getter cannot pass validation as one value while evidence binds its later value; identity and destination inspection failures fail closed as structured metadata errors instead of escaping the checker
|
|
73
90
|
|
|
74
91
|
## [0.34.0](https://github.com/dyphn1/Harness-everything/compare/v0.33.0...v0.34.0) (2026-10-17)
|
|
75
92
|
|
|
@@ -8,6 +8,14 @@ const resolverPath = path.join(__dirname, '..', 'multi-agent-workspace', 'script
|
|
|
8
8
|
const toSpecResolverPath = path.join(__dirname, '..', 'to-spec', 'scripts', 'project-docs-resolver.js');
|
|
9
9
|
const harnessResolverPath = path.join(__dirname, '..', 'harness-everything', 'scripts', 'project-docs-resolver.js');
|
|
10
10
|
const toSpecCheckPath = path.join(__dirname, '..', 'to-spec', 'scripts', 'check-project-docs.js');
|
|
11
|
+
const packagedToSpecCheckPath = path.join(__dirname, '..', 'plugins', 'harness-everything', 'skills', 'to-spec', 'scripts', 'check-project-docs.js');
|
|
12
|
+
const packagedToSpecResolverPath = path.join(__dirname, '..', 'plugins', 'harness-everything', 'skills', 'to-spec', 'scripts', 'project-docs-resolver.js');
|
|
13
|
+
const packagedMultiAgentResolverPath = path.join(__dirname, '..', 'plugins', 'harness-everything', 'skills', 'multi-agent-workspace', 'scripts', 'project-docs-resolver.js');
|
|
14
|
+
const packagedHarnessResolverPath = path.join(__dirname, '..', 'plugins', 'harness-everything', 'skills', 'harness-everything', 'scripts', 'project-docs-resolver.js');
|
|
15
|
+
const toSpecCliLegs = Object.freeze([
|
|
16
|
+
['source', toSpecCheckPath],
|
|
17
|
+
['packaged', packagedToSpecCheckPath]
|
|
18
|
+
]);
|
|
11
19
|
const resolver = require(resolverPath);
|
|
12
20
|
const toSpecResolver = require(toSpecResolverPath);
|
|
13
21
|
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-2aw-project-docs-'));
|
|
@@ -342,8 +350,12 @@ checkCase('2aw. project file zone fields drive existing zone resolution', () =>
|
|
|
342
350
|
checkCase('2aw. to-spec ships the same standalone resolver implementation', () => {
|
|
343
351
|
const source = fs.readFileSync(toSpecResolverPath, 'utf8');
|
|
344
352
|
assert.strictEqual(source, fs.readFileSync(resolverPath, 'utf8'));
|
|
353
|
+
assert.strictEqual(source, fs.readFileSync(packagedToSpecResolverPath, 'utf8'));
|
|
354
|
+
assert.strictEqual(source, fs.readFileSync(packagedMultiAgentResolverPath, 'utf8'));
|
|
355
|
+
assert.strictEqual(source, fs.readFileSync(packagedHarnessResolverPath, 'utf8'));
|
|
345
356
|
assert.ok(!source.includes("require('../../multi-agent-workspace"), 'standalone resolver must not defer to a sibling skill');
|
|
346
357
|
assert.strictEqual(typeof toSpecResolver.resolveDocsConfig, 'function');
|
|
358
|
+
assert.strictEqual(typeof toSpecResolver.normalizeHostProjectDocs, 'function');
|
|
347
359
|
});
|
|
348
360
|
|
|
349
361
|
checkCase('2aw dogfood. harness-everything ships the byte-identical standalone resolver', () => {
|
|
@@ -628,12 +640,12 @@ checkCase('2aw owner-p2. init rejects whitespace-only required values consistent
|
|
|
628
640
|
assert.strictEqual(fs.readFileSync(hostFile2, 'utf8'), host2Before, 'whitespace-only host init must write nothing');
|
|
629
641
|
});
|
|
630
642
|
|
|
631
|
-
function runToSpecCheck(root) {
|
|
632
|
-
return spawnSync(process.execPath, [
|
|
643
|
+
function runToSpecCheck(root, scriptPath = toSpecCheckPath) {
|
|
644
|
+
return spawnSync(process.execPath, [scriptPath, 'check', '--workspace', root], { encoding: 'utf8' });
|
|
633
645
|
}
|
|
634
646
|
|
|
635
|
-
function runToSpecInit(root, args) {
|
|
636
|
-
return spawnSync(process.execPath, [
|
|
647
|
+
function runToSpecInit(root, args, scriptPath = toSpecCheckPath) {
|
|
648
|
+
return spawnSync(process.execPath, [scriptPath, 'init', '--workspace', root, ...args], { encoding: 'utf8' });
|
|
637
649
|
}
|
|
638
650
|
|
|
639
651
|
checkCase('2aw owner-p2. init fills only missing fields in an existing valid project file and preserves the rest', () => {
|
|
@@ -701,24 +713,188 @@ checkCase('2aw owner-p2. init leaves malformed or schema-invalid project files u
|
|
|
701
713
|
assert.strictEqual(fs.readFileSync(invalidFile, 'utf8'), invalidBefore, 'invalid schema file untouched');
|
|
702
714
|
});
|
|
703
715
|
|
|
716
|
+
checkCase('2aw issue331. host-only init rejects malformed JSON without losing a registered custom skill', () => {
|
|
717
|
+
const malformed = '{\n "package": "harness-everything",\n "skills": [{"id":"custom-skill","dirPath":"custom-skill"}],\n "agents": [],\n "projectDocs": {"governed": [';
|
|
718
|
+
for (const [leg, scriptPath] of toSpecCliLegs) {
|
|
719
|
+
const root = workspace(`issue331-${leg}-host-init-malformed`);
|
|
720
|
+
const hostFile = writeHostConfig(root, 'claude', null, malformed);
|
|
721
|
+
const before = fs.readFileSync(hostFile);
|
|
722
|
+
const result = runToSpecInit(root, ['--doc-location', 'docs/', '--tracker', 'GitHub Issues', '--issue-definition', 'Title and acceptance'], scriptPath);
|
|
723
|
+
assert.strictEqual(result.status, 1, `${leg} malformed host manifest must fail init: ${result.stdout}${result.stderr}`);
|
|
724
|
+
assert.match(`${result.stdout}\n${result.stderr}`, /\.claude\/harness-everything\/manifest\.json/);
|
|
725
|
+
assert.match(`${result.stdout}\n${result.stderr}`, /invalid JSON/i);
|
|
726
|
+
assert.match(`${result.stdout}\n${result.stderr}`, /repair|fix/i);
|
|
727
|
+
assert.deepStrictEqual(fs.readFileSync(hostFile), before, 'malformed host manifest stays byte-identical');
|
|
728
|
+
}
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
checkCase('2aw issue331. host-only init rejects schema-invalid projectDocs without writing', () => {
|
|
732
|
+
for (const [leg, scriptPath] of toSpecCliLegs) {
|
|
733
|
+
const root = workspace(`issue331-${leg}-host-init-invalid-schema`);
|
|
734
|
+
const hostFile = writeHostConfig(root, 'claude', { governed: 'not-an-array', domains: ['GRPH'] });
|
|
735
|
+
const before = fs.readFileSync(hostFile);
|
|
736
|
+
const result = runToSpecInit(root, ['--doc-location', 'docs/', '--tracker', 'GitHub Issues', '--issue-definition', 'Title and acceptance'], scriptPath);
|
|
737
|
+
assert.strictEqual(result.status, 1, `${leg} schema-invalid host projectDocs must fail init: ${result.stdout}${result.stderr}`);
|
|
738
|
+
assert.match(`${result.stdout}\n${result.stderr}`, /\.claude\/harness-everything\/manifest\.json/);
|
|
739
|
+
assert.match(`${result.stdout}\n${result.stderr}`, /invalid-governed/);
|
|
740
|
+
assert.match(`${result.stdout}\n${result.stderr}`, /repair|fix/i);
|
|
741
|
+
assert.deepStrictEqual(fs.readFileSync(hostFile), before, 'schema-invalid host manifest stays byte-identical');
|
|
742
|
+
}
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
for (const [leg, scriptPath] of toSpecCliLegs) {
|
|
746
|
+
checkCase(`2aw issue331. ${leg} host-only init accepts legacy string domains without changing its on-disk value`, () => {
|
|
747
|
+
const root = workspace(`issue331-${leg}-legacy-host-domains`);
|
|
748
|
+
const hostFile = writeHostConfig(root, 'claude', {
|
|
749
|
+
domains: 'records/domain',
|
|
750
|
+
governed: [{ path: 'docs/prd/**', role: 'prd' }]
|
|
751
|
+
});
|
|
752
|
+
const before = JSON.parse(fs.readFileSync(hostFile, 'utf8'));
|
|
753
|
+
const result = runToSpecInit(root, ['--doc-location', 'docs/', '--tracker', 'GitHub Issues', '--issue-definition', 'Title and acceptance'], scriptPath);
|
|
754
|
+
assert.strictEqual(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
|
755
|
+
const after = JSON.parse(fs.readFileSync(hostFile, 'utf8'));
|
|
756
|
+
assert.deepStrictEqual(after.projectDocs.domains, before.projectDocs.domains, 'deprecated string domains remains unchanged on disk');
|
|
757
|
+
assert.strictEqual(after.projectDocs.docLocation, 'docs/');
|
|
758
|
+
assert.strictEqual(after.projectDocs.tracker, 'GitHub Issues');
|
|
759
|
+
assert.strictEqual(after.projectDocs.issueDefinition, 'Title and acceptance');
|
|
760
|
+
const check = runToSpecCheck(root, scriptPath);
|
|
761
|
+
assert.strictEqual(check.status, 0, `check accepts host legacy value after init: ${check.stdout}\n${check.stderr}`);
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
checkCase(`2aw issue331. ${leg} complete legacy host permits a no-op init`, () => {
|
|
765
|
+
const root = workspace(`issue331-${leg}-legacy-host-complete`);
|
|
766
|
+
const hostFile = writeHostConfig(root, 'claude', {
|
|
767
|
+
domains: 'records/domain',
|
|
768
|
+
docLocation: 'docs/', tracker: 'GitHub Issues', issueDefinition: 'Title and acceptance'
|
|
769
|
+
});
|
|
770
|
+
const before = fs.readFileSync(hostFile);
|
|
771
|
+
const result = runToSpecInit(root, ['--doc-location', 'docs/', '--tracker', 'GitHub Issues', '--issue-definition', 'Title and acceptance'], scriptPath);
|
|
772
|
+
assert.strictEqual(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
|
773
|
+
assert.deepStrictEqual(fs.readFileSync(hostFile), before, 'complete host config remains byte-identical');
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
checkCase(`2aw issue331. ${leg} directory manifest reports a read failure and leaves contents intact`, () => {
|
|
777
|
+
const root = workspace(`issue331-${leg}-directory-manifest`);
|
|
778
|
+
const manifestPath = path.join(root, '.claude', 'harness-everything', 'manifest.json');
|
|
779
|
+
fs.mkdirSync(manifestPath, { recursive: true });
|
|
780
|
+
const marker = path.join(manifestPath, 'registration.json');
|
|
781
|
+
fs.writeFileSync(marker, '{"skills":[{"id":"custom-skill"}]}', 'utf8');
|
|
782
|
+
const before = fs.readFileSync(marker);
|
|
783
|
+
const result = runToSpecInit(root, ['--doc-location', 'docs/', '--tracker', 'GitHub Issues', '--issue-definition', 'Title and acceptance'], scriptPath);
|
|
784
|
+
const output = `${result.stdout}\n${result.stderr}`;
|
|
785
|
+
assert.strictEqual(result.status, 1, `directory manifest must fail init: ${output}`);
|
|
786
|
+
assert.match(output, /\.claude\/harness-everything\/manifest\.json/);
|
|
787
|
+
assert.match(output, /cannot be read/i);
|
|
788
|
+
assert.doesNotMatch(output, /invalid JSON/i);
|
|
789
|
+
assert.match(output, /repair|restore|readable/i);
|
|
790
|
+
assert.deepStrictEqual(fs.readFileSync(marker), before, 'directory contents remain byte-identical');
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
checkCase('2aw issue331. modern array domains remain accepted on source and packaged CLIs', () => {
|
|
795
|
+
for (const [leg, scriptPath] of toSpecCliLegs) {
|
|
796
|
+
const root = workspace(`issue331-${leg}-modern-host-domains`);
|
|
797
|
+
const hostFile = writeHostConfig(root, 'claude', {
|
|
798
|
+
domains: ['GRPH'], governed: [{ path: 'docs/prd/**', role: 'prd' }]
|
|
799
|
+
});
|
|
800
|
+
const before = JSON.parse(fs.readFileSync(hostFile, 'utf8'));
|
|
801
|
+
const result = runToSpecInit(root, ['--doc-location', 'docs/', '--tracker', 'GitHub Issues', '--issue-definition', 'Title and acceptance'], scriptPath);
|
|
802
|
+
assert.strictEqual(result.status, 0, `${leg}: ${result.stdout}\n${result.stderr}`);
|
|
803
|
+
const after = JSON.parse(fs.readFileSync(hostFile, 'utf8'));
|
|
804
|
+
assert.deepStrictEqual(after.projectDocs.domains, before.projectDocs.domains, 'modern domains array is preserved');
|
|
805
|
+
assert.strictEqual(runToSpecCheck(root, scriptPath).status, 0, 'modern host config passes check after init');
|
|
806
|
+
}
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
checkCase('2aw issue331. project-file domains string remains invalid on source and packaged CLIs', () => {
|
|
810
|
+
for (const [leg, scriptPath] of toSpecCliLegs) {
|
|
811
|
+
const root = workspace(`issue331-${leg}-project-domains-string`);
|
|
812
|
+
const projectFile = writeProjectConfig(root, {
|
|
813
|
+
governed: [{ path: 'docs/prd/**', role: 'prd' }],
|
|
814
|
+
domains: 'records/domain'
|
|
815
|
+
});
|
|
816
|
+
const before = fs.readFileSync(projectFile);
|
|
817
|
+
const result = runToSpecInit(root, ['--doc-location', 'docs/', '--tracker', 'GitHub Issues', '--issue-definition', 'Title and acceptance'], scriptPath);
|
|
818
|
+
const output = `${result.stdout}\n${result.stderr}`;
|
|
819
|
+
assert.strictEqual(result.status, 1, `${leg} project-file legacy value must remain invalid: ${output}`);
|
|
820
|
+
assert.match(output, /invalid-domains/);
|
|
821
|
+
assert.deepStrictEqual(fs.readFileSync(projectFile), before, 'invalid project file remains byte-identical');
|
|
822
|
+
}
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
checkCase('2aw issue331. source and packaged host validators still reject malformed governed globs without writes', () => {
|
|
826
|
+
for (const [leg, scriptPath] of toSpecCliLegs) {
|
|
827
|
+
const root = workspace(`issue331-${leg}-invalid-host-glob`);
|
|
828
|
+
const hostFile = writeHostConfig(root, 'claude', {
|
|
829
|
+
governed: [{ path: 'docs//prd/**', role: 'prd' }], domains: ['GRPH']
|
|
830
|
+
});
|
|
831
|
+
const before = fs.readFileSync(hostFile);
|
|
832
|
+
const result = runToSpecInit(root, ['--doc-location', 'docs/', '--tracker', 'GitHub Issues', '--issue-definition', 'Title and acceptance'], scriptPath);
|
|
833
|
+
const output = `${result.stdout}\n${result.stderr}`;
|
|
834
|
+
assert.strictEqual(result.status, 1, `${leg} malformed host glob must fail: ${output}`);
|
|
835
|
+
assert.match(output, /invalid-governed-path|failed schema validation/i);
|
|
836
|
+
assert.deepStrictEqual(fs.readFileSync(hostFile), before, 'invalid host config remains byte-identical');
|
|
837
|
+
}
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
checkCase('2aw issue331. malformed host aborts a multi-host init before any manifest changes', () => {
|
|
841
|
+
for (const [leg, scriptPath] of toSpecCliLegs) {
|
|
842
|
+
const root = workspace(`issue331-${leg}-host-init-multi-host-atomic`);
|
|
843
|
+
const validHost = writeHostConfig(root, 'claude', {
|
|
844
|
+
governed: [{ path: 'docs/prd/**', role: 'prd' }], domains: ['GRPH'], docLocation: 'host-docs/'
|
|
845
|
+
});
|
|
846
|
+
const malformedHost = writeHostConfig(root, 'codex', null, '{"skills":[{"id":"custom-skill"}], broken');
|
|
847
|
+
const validBefore = fs.readFileSync(validHost);
|
|
848
|
+
const malformedBefore = fs.readFileSync(malformedHost);
|
|
849
|
+
const result = runToSpecInit(root, ['--doc-location', 'host-docs/', '--tracker', 'GitHub Issues', '--issue-definition', 'Title and acceptance'], scriptPath);
|
|
850
|
+
assert.strictEqual(result.status, 1, `${leg} malformed host must abort the full plan: ${result.stdout}${result.stderr}`);
|
|
851
|
+
assert.match(`${result.stdout}\n${result.stderr}`, /\.codex\/harness-everything\/manifest\.json/);
|
|
852
|
+
assert.deepStrictEqual(fs.readFileSync(validHost), validBefore, 'valid host is not written before malformed peer is rejected');
|
|
853
|
+
assert.deepStrictEqual(fs.readFileSync(malformedHost), malformedBefore, 'malformed host remains byte-identical');
|
|
854
|
+
}
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
checkCase('2aw issue331. absent host manifest still receives the requested projectDocs fields', () => {
|
|
858
|
+
for (const [leg, scriptPath] of toSpecCliLegs) {
|
|
859
|
+
const root = workspace(`issue331-${leg}-host-init-absent`);
|
|
860
|
+
const hostDir = path.join(root, '.claude', 'harness-everything');
|
|
861
|
+
fs.mkdirSync(hostDir, { recursive: true });
|
|
862
|
+
const hostFile = path.join(hostDir, 'manifest.json');
|
|
863
|
+
const result = runToSpecInit(root, ['--doc-location', 'docs/', '--tracker', 'GitHub Issues', '--issue-definition', 'Title and acceptance'], scriptPath);
|
|
864
|
+
assert.strictEqual(result.status, 0, `${leg}: ${result.stdout}\n${result.stderr}`);
|
|
865
|
+
const created = JSON.parse(fs.readFileSync(hostFile, 'utf8'));
|
|
866
|
+
assert.deepStrictEqual(created.projectDocs.docLocation, 'docs/');
|
|
867
|
+
assert.strictEqual(created.projectDocs.tracker, 'GitHub Issues');
|
|
868
|
+
assert.strictEqual(created.projectDocs.issueDefinition, 'Title and acceptance');
|
|
869
|
+
}
|
|
870
|
+
});
|
|
871
|
+
|
|
704
872
|
checkCase('2aw owner-p2. host-only init preserves existing host projectDocs keys and fills only missing fields', () => {
|
|
705
873
|
const root = workspace('owner-p2-init-host-preserve');
|
|
706
|
-
const hostFile =
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
874
|
+
const hostFile = path.join(root, '.claude', 'harness-everything', 'manifest.json');
|
|
875
|
+
fs.mkdirSync(path.dirname(hostFile), { recursive: true });
|
|
876
|
+
const registration = { id: 'custom-skill', dirPath: 'custom-skill', installedAt: '2026-01-01T00:00:00.000Z' };
|
|
877
|
+
fs.writeFileSync(hostFile, `${JSON.stringify({
|
|
878
|
+
package: 'harness-everything', skills: [registration], agents: [], projectDocs: {
|
|
879
|
+
governed: [{ path: 'docs/prd/**', role: 'prd' }],
|
|
880
|
+
domains: ['GRPH'],
|
|
881
|
+
docLocation: 'host-docs/',
|
|
882
|
+
updatedAt: '2026-01-01T00:00:00.000Z'
|
|
883
|
+
}
|
|
884
|
+
}, null, 2)}\n`, 'utf8');
|
|
712
885
|
const before = JSON.parse(fs.readFileSync(hostFile, 'utf8'));
|
|
713
886
|
const result = runToSpecInit(root, ['--doc-location', 'host-docs/', '--tracker', 'host-tracker', '--issue-definition', 'host-template']);
|
|
714
887
|
assert.strictEqual(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
|
715
888
|
const after = JSON.parse(fs.readFileSync(hostFile, 'utf8'));
|
|
889
|
+
assert.deepStrictEqual(after.skills, before.skills, 'custom skill registrations preserved');
|
|
716
890
|
assert.deepStrictEqual(after.projectDocs.governed, before.projectDocs.governed, 'host governed governance keys preserved');
|
|
717
891
|
assert.deepStrictEqual(after.projectDocs.domains, before.projectDocs.domains, 'host domains preserved');
|
|
718
892
|
assert.strictEqual(after.projectDocs.docLocation, 'host-docs/', 'configured docLocation preserved');
|
|
719
893
|
assert.strictEqual(after.projectDocs.tracker, 'host-tracker', 'missing tracker filled');
|
|
720
894
|
assert.strictEqual(after.projectDocs.issueDefinition, 'host-template', 'missing issueDefinition filled');
|
|
721
895
|
assert.strictEqual(after.projectDocs.updatedAt, before.projectDocs.updatedAt, 'existing updatedAt preserved');
|
|
896
|
+
const check = runToSpecCheck(root);
|
|
897
|
+
assert.strictEqual(check.status, 0, `check succeeds after host-only init: ${check.stdout}\n${check.stderr}`);
|
|
722
898
|
const conflicting = runToSpecInit(root, ['--doc-location', 'host-docs/', '--tracker', 'different-tracker', '--issue-definition', 'host-template']);
|
|
723
899
|
assert.strictEqual(conflicting.status, 1, `conflicting host flag must fail: ${conflicting.stdout}${conflicting.stderr}`);
|
|
724
900
|
assert.match(`${conflicting.stdout}\n${conflicting.stderr}`, /tracker/);
|
|
@@ -756,4 +932,268 @@ checkCase('2aw fix. to-spec CLI refuses conflicting hosts and invalid project ov
|
|
|
756
932
|
assert.ok(!rejected.stdout.includes('host-docs'));
|
|
757
933
|
});
|
|
758
934
|
|
|
935
|
+
for (const [leg, scriptPath] of toSpecCliLegs) {
|
|
936
|
+
for (const [field, value] of [['docLocation', 42], ['tracker', {}], ['issueDefinition', []]]) {
|
|
937
|
+
checkCase(`2aw issue331 p2. ${leg} host non-string ${field} fails init without writing the manifest`, () => {
|
|
938
|
+
const root = workspace(`issue331-p2-${leg}-type-${field}`);
|
|
939
|
+
const hostConfig = { docLocation: 'docs/', tracker: 'GitHub Issues', issueDefinition: 'Title and acceptance' };
|
|
940
|
+
hostConfig[field] = value;
|
|
941
|
+
const hostFile = writeHostConfig(root, 'claude', hostConfig);
|
|
942
|
+
const before = fs.readFileSync(hostFile);
|
|
943
|
+
const result = runToSpecInit(root, ['--doc-location', 'docs/', '--tracker', 'GitHub Issues', '--issue-definition', 'Title and acceptance'], scriptPath);
|
|
944
|
+
const output = `${result.stdout}\n${result.stderr}`;
|
|
945
|
+
assert.strictEqual(result.status, 1, `${leg} non-string host ${field} must fail init: ${output}`);
|
|
946
|
+
assert.match(output, /\.claude\/harness-everything\/manifest\.json/);
|
|
947
|
+
assert.match(output, new RegExp(field));
|
|
948
|
+
assert.deepStrictEqual(fs.readFileSync(hostFile), before, `non-string host ${field} manifest stays byte-identical`);
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
checkCase(`2aw issue331 p2. ${leg} multi-host init rejects a non-string peer and leaves both manifests byte-identical`, () => {
|
|
953
|
+
const root = workspace(`issue331-p2-${leg}-multi-host`);
|
|
954
|
+
const validFile = writeHostConfig(root, 'claude', { governed: [{ path: 'docs/prd/**', role: 'prd' }], domains: ['GRPH'] });
|
|
955
|
+
const invalidFile = writeHostConfig(root, 'codex', { docLocation: 42 });
|
|
956
|
+
const validBefore = fs.readFileSync(validFile);
|
|
957
|
+
const invalidBefore = fs.readFileSync(invalidFile);
|
|
958
|
+
const result = runToSpecInit(root, ['--doc-location', 'docs/', '--tracker', 'GitHub Issues', '--issue-definition', 'Title and acceptance'], scriptPath);
|
|
959
|
+
const output = `${result.stdout}\n${result.stderr}`;
|
|
960
|
+
assert.strictEqual(result.status, 1, `${leg} non-string codex docLocation must abort the full plan: ${output}`);
|
|
961
|
+
assert.match(output, /\.codex\/harness-everything\/manifest\.json/);
|
|
962
|
+
assert.deepStrictEqual(fs.readFileSync(validFile), validBefore, 'valid claude host is not written before the non-string peer is rejected');
|
|
963
|
+
assert.deepStrictEqual(fs.readFileSync(invalidFile), invalidBefore, 'non-string codex host stays byte-identical');
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
checkCase(`2aw issue331 p2. ${leg} init still fills absent and empty-string host fields`, () => {
|
|
967
|
+
const root = workspace(`issue331-p2-${leg}-empty-control`);
|
|
968
|
+
const hostFile = writeHostConfig(root, 'claude', { docLocation: '', tracker: ' ', governed: [{ path: 'docs/prd/**', role: 'prd' }] });
|
|
969
|
+
const result = runToSpecInit(root, ['--doc-location', 'docs/', '--tracker', 'GitHub Issues', '--issue-definition', 'Title and acceptance'], scriptPath);
|
|
970
|
+
assert.strictEqual(result.status, 0, `${leg}: ${result.stdout}\n${result.stderr}`);
|
|
971
|
+
const after = JSON.parse(fs.readFileSync(hostFile, 'utf8'));
|
|
972
|
+
assert.strictEqual(after.projectDocs.docLocation, 'docs/', 'empty-string docLocation filled');
|
|
973
|
+
assert.strictEqual(after.projectDocs.tracker, 'GitHub Issues', 'whitespace-only tracker filled');
|
|
974
|
+
assert.strictEqual(after.projectDocs.issueDefinition, 'Title and acceptance', 'absent issueDefinition filled');
|
|
975
|
+
assert.deepStrictEqual(after.projectDocs.governed, [{ path: 'docs/prd/**', role: 'prd' }], 'governed preserved');
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
checkCase(`2aw issue331 p2. ${leg} dangling manifest symlink to a target outside the workspace fails init without creating it`, () => {
|
|
979
|
+
const root = workspace(`issue331-p2-${leg}-dangling-link`);
|
|
980
|
+
const outside = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-2aw-p2-outside-'));
|
|
981
|
+
try {
|
|
982
|
+
const target = path.join(outside, 'escaped-manifest.json');
|
|
983
|
+
const hostDir = path.join(root, '.claude', 'harness-everything');
|
|
984
|
+
fs.mkdirSync(hostDir, { recursive: true });
|
|
985
|
+
const linkFile = path.join(hostDir, 'manifest.json');
|
|
986
|
+
try {
|
|
987
|
+
fs.symlinkSync(target, linkFile);
|
|
988
|
+
} catch (error) {
|
|
989
|
+
if (error.code === 'EPERM') {
|
|
990
|
+
console.log(` SKIP ${leg} dangling-symlink case: symlink creation is not permitted on this host (${error.code})`);
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
throw error;
|
|
994
|
+
}
|
|
995
|
+
const validFile = writeHostConfig(root, 'codex', { governed: [{ path: 'docs/prd/**', role: 'prd' }], domains: ['GRPH'] });
|
|
996
|
+
const validBefore = fs.readFileSync(validFile);
|
|
997
|
+
const result = runToSpecInit(root, ['--doc-location', 'docs/', '--tracker', 'GitHub Issues', '--issue-definition', 'Title and acceptance'], scriptPath);
|
|
998
|
+
const output = `${result.stdout}\n${result.stderr}`;
|
|
999
|
+
assert.strictEqual(result.status, 1, `${leg} dangling manifest symlink must fail init: ${output}`);
|
|
1000
|
+
assert.match(output, /\.claude\/harness-everything\/manifest\.json/);
|
|
1001
|
+
assert.ok(!fs.existsSync(target), 'symlink target outside the workspace must not be created');
|
|
1002
|
+
assert.ok(fs.lstatSync(linkFile).isSymbolicLink(), 'manifest path must remain a symlink');
|
|
1003
|
+
assert.strictEqual(fs.readlinkSync(linkFile), target, 'symlink must still point at its original target');
|
|
1004
|
+
assert.ok(!fs.existsSync(linkFile), 'symlink must remain dangling');
|
|
1005
|
+
assert.deepStrictEqual(fs.readFileSync(validFile), validBefore, 'second valid host manifest stays byte-identical');
|
|
1006
|
+
} finally {
|
|
1007
|
+
fs.rmSync(outside, { recursive: true, force: true });
|
|
1008
|
+
}
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
function trySymlink(target, linkPath, label, type) {
|
|
1013
|
+
try {
|
|
1014
|
+
fs.symlinkSync(target, linkPath, type);
|
|
1015
|
+
return true;
|
|
1016
|
+
} catch (error) {
|
|
1017
|
+
if (error.code !== 'EPERM') throw error;
|
|
1018
|
+
console.log(` SKIP ${label}: symlink creation is not permitted on this host (${error.code})`);
|
|
1019
|
+
return false;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
for (const [leg, scriptPath] of toSpecCliLegs) {
|
|
1024
|
+
checkCase(`2aw issue331 p2b. ${leg} host null docLocation fails init without writing the manifest`, () => {
|
|
1025
|
+
const root = workspace(`issue331-p2b-${leg}-null-doc-location`);
|
|
1026
|
+
const hostFile = writeHostConfig(root, 'claude', { docLocation: null, tracker: 'GitHub Issues', issueDefinition: 'Title and acceptance' });
|
|
1027
|
+
const before = fs.readFileSync(hostFile);
|
|
1028
|
+
const result = runToSpecInit(root, ['--doc-location', 'docs/', '--tracker', 'GitHub Issues', '--issue-definition', 'Title and acceptance'], scriptPath);
|
|
1029
|
+
const output = `${result.stdout}\n${result.stderr}`;
|
|
1030
|
+
assert.strictEqual(result.status, 1, `${leg} null host docLocation must fail init: ${output}`);
|
|
1031
|
+
assert.match(output, /\.claude\/harness-everything\/manifest\.json/);
|
|
1032
|
+
assert.deepStrictEqual(fs.readFileSync(hostFile), before, 'null host docLocation manifest stays byte-identical');
|
|
1033
|
+
});
|
|
1034
|
+
|
|
1035
|
+
for (const [field, value] of [['tracker', 0], ['issueDefinition', false]]) {
|
|
1036
|
+
checkCase(`2aw issue331 p2b. ${leg} host ${field} ${JSON.stringify(value)} fails init without writing the manifest`, () => {
|
|
1037
|
+
const root = workspace(`issue331-p2b-${leg}-${field}-scalar`);
|
|
1038
|
+
const hostConfig = { docLocation: 'docs/', tracker: 'GitHub Issues', issueDefinition: 'Title and acceptance' };
|
|
1039
|
+
hostConfig[field] = value;
|
|
1040
|
+
const hostFile = writeHostConfig(root, 'claude', hostConfig);
|
|
1041
|
+
const before = fs.readFileSync(hostFile);
|
|
1042
|
+
const result = runToSpecInit(root, ['--doc-location', 'docs/', '--tracker', 'GitHub Issues', '--issue-definition', 'Title and acceptance'], scriptPath);
|
|
1043
|
+
const output = `${result.stdout}\n${result.stderr}`;
|
|
1044
|
+
assert.strictEqual(result.status, 1, `${leg} non-string host ${field} must fail init: ${output}`);
|
|
1045
|
+
assert.match(output, /\.claude\/harness-everything\/manifest\.json/);
|
|
1046
|
+
assert.match(output, new RegExp(field));
|
|
1047
|
+
assert.deepStrictEqual(fs.readFileSync(hostFile), before, `${field} scalar host manifest stays byte-identical`);
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
checkCase(`2aw issue331 p2b. ${leg} host manifest symlink to a regular manifest outside the workspace fails init without changing the target`, () => {
|
|
1052
|
+
const root = workspace(`issue331-p2b-${leg}-outside-file-link`);
|
|
1053
|
+
const outside = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-2aw-p2b-outside-'));
|
|
1054
|
+
try {
|
|
1055
|
+
const target = path.join(outside, 'manifest.json');
|
|
1056
|
+
fs.writeFileSync(target, JSON.stringify({ projectDocs: { docLocation: 'docs/', tracker: 'GitHub Issues' } }, null, 2), 'utf8');
|
|
1057
|
+
const linkFile = path.join(root, '.claude', 'harness-everything', 'manifest.json');
|
|
1058
|
+
fs.mkdirSync(path.dirname(linkFile), { recursive: true });
|
|
1059
|
+
if (!trySymlink(target, linkFile, `${leg} outside file-link case`, 'file')) return;
|
|
1060
|
+
const before = fs.readFileSync(target);
|
|
1061
|
+
const result = runToSpecInit(root, ['--doc-location', 'docs/', '--tracker', 'GitHub Issues', '--issue-definition', 'Title and acceptance'], scriptPath);
|
|
1062
|
+
const output = `${result.stdout}\n${result.stderr}`;
|
|
1063
|
+
assert.strictEqual(result.status, 1, `${leg} host symlink to an outside manifest must fail init: ${output}`);
|
|
1064
|
+
assert.match(output, /\.claude\/harness-everything\/manifest\.json/);
|
|
1065
|
+
assert.deepStrictEqual(fs.readFileSync(target), before, 'outside manifest bytes stay unchanged');
|
|
1066
|
+
} finally {
|
|
1067
|
+
fs.rmSync(outside, { recursive: true, force: true });
|
|
1068
|
+
}
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1071
|
+
checkCase(`2aw issue331 p2c. ${leg} absent host manifest under a parent symlink to outside fails init without creating it`, () => {
|
|
1072
|
+
const root = workspace(`issue331-p2c-${leg}-absent-leaf-outside-parent`);
|
|
1073
|
+
const outside = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-2aw-p2c-outside-'));
|
|
1074
|
+
try {
|
|
1075
|
+
const hostDir = path.join(root, '.claude');
|
|
1076
|
+
fs.mkdirSync(hostDir, { recursive: true });
|
|
1077
|
+
const linkDir = path.join(hostDir, 'harness-everything');
|
|
1078
|
+
if (!trySymlink(outside, linkDir, `${leg} absent-leaf outside-parent case`, 'dir')) return;
|
|
1079
|
+
const result = runToSpecInit(root, ['--doc-location', 'docs/', '--tracker', 'GitHub Issues', '--issue-definition', 'Title and acceptance'], scriptPath);
|
|
1080
|
+
const output = `${result.stdout}\n${result.stderr}`;
|
|
1081
|
+
assert.strictEqual(result.status, 1, `${leg} absent manifest under an escaping parent must fail init: ${output}`);
|
|
1082
|
+
assert.match(output, /nearest existing parent resolves outside the workspace/);
|
|
1083
|
+
assert.match(output, /regular file that resolves inside this workspace/);
|
|
1084
|
+
assert.deepStrictEqual(fs.readdirSync(outside), [], 'no manifest is created outside the workspace');
|
|
1085
|
+
} finally {
|
|
1086
|
+
fs.rmSync(outside, { recursive: true, force: true });
|
|
1087
|
+
}
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
checkCase(`2aw issue331 p2c. ${leg} FIFO host manifest fails init before any read or write`, () => {
|
|
1091
|
+
if (process.platform === 'win32') {
|
|
1092
|
+
console.log(`SKIP ${leg} FIFO case: mkfifo is unavailable on win32`);
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
const root = workspace(`issue331-p2c-${leg}-fifo`);
|
|
1096
|
+
const fifo = path.join(root, '.claude', 'harness-everything', 'manifest.json');
|
|
1097
|
+
fs.mkdirSync(path.dirname(fifo), { recursive: true });
|
|
1098
|
+
execFileSync('mkfifo', [fifo]);
|
|
1099
|
+
const result = runToSpecInit(root, ['--doc-location', 'docs/', '--tracker', 'GitHub Issues', '--issue-definition', 'Title and acceptance'], scriptPath);
|
|
1100
|
+
const output = `${result.stdout}\n${result.stderr}`;
|
|
1101
|
+
assert.strictEqual(result.status, 1, `${leg} FIFO manifest must fail init: ${output}`);
|
|
1102
|
+
assert.match(output, /not a regular file/);
|
|
1103
|
+
assert.ok(fs.lstatSync(fifo).isFIFO(), 'FIFO is left in place');
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1106
|
+
checkCase(`2aw issue331 p2b. ${leg} host manifest symlink to a regular manifest inside the workspace fills the target and keeps the link`, () => {
|
|
1107
|
+
const root = workspace(`issue331-p2b-${leg}-inside-file-link`);
|
|
1108
|
+
const target = path.join(root, 'shared', 'manifest.json');
|
|
1109
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
1110
|
+
fs.writeFileSync(target, JSON.stringify({ projectDocs: { governed: [{ path: 'docs/prd/**', role: 'prd' }], domains: ['GRPH'] } }, null, 2), 'utf8');
|
|
1111
|
+
const linkFile = path.join(root, '.claude', 'harness-everything', 'manifest.json');
|
|
1112
|
+
fs.mkdirSync(path.dirname(linkFile), { recursive: true });
|
|
1113
|
+
if (!trySymlink(target, linkFile, `${leg} inside file-link case`, 'file')) return;
|
|
1114
|
+
const result = runToSpecInit(root, ['--doc-location', 'docs/', '--tracker', 'GitHub Issues', '--issue-definition', 'Title and acceptance'], scriptPath);
|
|
1115
|
+
assert.strictEqual(result.status, 0, `${leg}: ${result.stdout}\n${result.stderr}`);
|
|
1116
|
+
assert.ok(fs.lstatSync(linkFile).isSymbolicLink(), 'host manifest path must remain a symlink');
|
|
1117
|
+
const after = JSON.parse(fs.readFileSync(target, 'utf8'));
|
|
1118
|
+
assert.strictEqual(after.projectDocs.docLocation, 'docs/', 'in-workspace target gets docLocation');
|
|
1119
|
+
assert.strictEqual(after.projectDocs.tracker, 'GitHub Issues', 'in-workspace target gets tracker');
|
|
1120
|
+
assert.strictEqual(after.projectDocs.issueDefinition, 'Title and acceptance', 'in-workspace target gets issueDefinition');
|
|
1121
|
+
assert.deepStrictEqual(after.projectDocs.governed, [{ path: 'docs/prd/**', role: 'prd' }], 'governed preserved');
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
checkCase(`2aw issue331 p2b. ${leg} host manifest path that is a directory fails init and leaves a valid peer unwritten`, () => {
|
|
1125
|
+
const root = workspace(`issue331-p2b-${leg}-manifest-directory`);
|
|
1126
|
+
const dirManifest = path.join(root, '.claude', 'harness-everything', 'manifest.json');
|
|
1127
|
+
fs.mkdirSync(dirManifest, { recursive: true });
|
|
1128
|
+
const peerFile = writeHostConfig(root, 'codex', { governed: [{ path: 'docs/prd/**', role: 'prd' }], domains: ['GRPH'] });
|
|
1129
|
+
const peerBefore = fs.readFileSync(peerFile);
|
|
1130
|
+
const result = runToSpecInit(root, ['--doc-location', 'docs/', '--tracker', 'GitHub Issues', '--issue-definition', 'Title and acceptance'], scriptPath);
|
|
1131
|
+
const output = `${result.stdout}\n${result.stderr}`;
|
|
1132
|
+
assert.strictEqual(result.status, 1, `${leg} directory host manifest must fail init: ${output}`);
|
|
1133
|
+
assert.match(output, /\.claude\/harness-everything\/manifest\.json/);
|
|
1134
|
+
assert.deepStrictEqual(fs.readFileSync(peerFile), peerBefore, 'valid peer host manifest stays byte-identical');
|
|
1135
|
+
});
|
|
1136
|
+
|
|
1137
|
+
checkCase(`2aw issue331 p2b. ${leg} host directory symlink to an outside directory fails init and leaves the outside manifest unchanged`, () => {
|
|
1138
|
+
const root = workspace(`issue331-p2b-${leg}-outside-dir-link`);
|
|
1139
|
+
const outside = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-2aw-p2b-outside-'));
|
|
1140
|
+
try {
|
|
1141
|
+
const outsideDir = path.join(outside, 'harness-everything');
|
|
1142
|
+
fs.mkdirSync(outsideDir, { recursive: true });
|
|
1143
|
+
const target = path.join(outsideDir, 'manifest.json');
|
|
1144
|
+
fs.writeFileSync(target, JSON.stringify({ projectDocs: { docLocation: 'docs/', tracker: 'GitHub Issues' } }, null, 2), 'utf8');
|
|
1145
|
+
fs.mkdirSync(path.join(root, '.claude'), { recursive: true });
|
|
1146
|
+
if (!trySymlink(outsideDir, path.join(root, '.claude', 'harness-everything'), `${leg} outside dir-link case`, 'dir')) return;
|
|
1147
|
+
const before = fs.readFileSync(target);
|
|
1148
|
+
const result = runToSpecInit(root, ['--doc-location', 'docs/', '--tracker', 'GitHub Issues', '--issue-definition', 'Title and acceptance'], scriptPath);
|
|
1149
|
+
const output = `${result.stdout}\n${result.stderr}`;
|
|
1150
|
+
assert.strictEqual(result.status, 1, `${leg} host directory symlink to outside must fail init: ${output}`);
|
|
1151
|
+
assert.match(output, /\.claude\/harness-everything\/manifest\.json/);
|
|
1152
|
+
assert.deepStrictEqual(fs.readFileSync(target), before, 'outside manifest bytes stay unchanged');
|
|
1153
|
+
} finally {
|
|
1154
|
+
fs.rmSync(outside, { recursive: true, force: true });
|
|
1155
|
+
}
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
checkCase(`2aw issue331 p2b. ${leg} dangling authoritative project-file symlink to outside fails init without creating the target`, () => {
|
|
1159
|
+
const root = workspace(`issue331-p2b-${leg}-project-dangling`);
|
|
1160
|
+
const outside = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-2aw-p2b-outside-'));
|
|
1161
|
+
try {
|
|
1162
|
+
const target = path.join(outside, 'created-project-docs.json');
|
|
1163
|
+
const projectLink = configPath(root);
|
|
1164
|
+
fs.mkdirSync(path.dirname(projectLink), { recursive: true });
|
|
1165
|
+
if (!trySymlink(target, projectLink, `${leg} dangling project-link case`, 'file')) return;
|
|
1166
|
+
const result = runToSpecInit(root, ['--doc-location', 'docs/', '--tracker', 'GitHub Issues', '--issue-definition', 'Title and acceptance'], scriptPath);
|
|
1167
|
+
const output = `${result.stdout}\n${result.stderr}`;
|
|
1168
|
+
assert.strictEqual(result.status, 1, `${leg} dangling project-file symlink must fail init: ${output}`);
|
|
1169
|
+
assert.ok(!fs.existsSync(target), 'project-file symlink target outside the workspace must not be created');
|
|
1170
|
+
assert.strictEqual(fs.readlinkSync(projectLink), target, 'project-file symlink must still point at its original target');
|
|
1171
|
+
for (const home of Object.values(HOSTS)) {
|
|
1172
|
+
assert.ok(!fs.existsSync(path.join(root, home, 'harness-everything', 'manifest.json')), 'no host manifest may be written when the project file is unsafe');
|
|
1173
|
+
}
|
|
1174
|
+
} finally {
|
|
1175
|
+
fs.rmSync(outside, { recursive: true, force: true });
|
|
1176
|
+
}
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
for (const [label, resolverFile] of [
|
|
1181
|
+
['to-spec', toSpecResolverPath],
|
|
1182
|
+
['multi-agent-workspace', resolverPath],
|
|
1183
|
+
['harness-everything', harnessResolverPath],
|
|
1184
|
+
['packaged to-spec', packagedToSpecResolverPath],
|
|
1185
|
+
['packaged multi-agent-workspace', packagedMultiAgentResolverPath],
|
|
1186
|
+
['packaged harness-everything', packagedHarnessResolverPath]
|
|
1187
|
+
]) {
|
|
1188
|
+
checkCase(`2aw issue331 p2b. ${label} resolver rejects non-string docLocation and accepts whitespace-only docLocation`, () => {
|
|
1189
|
+
const { validateDocsConfig } = require(resolverFile);
|
|
1190
|
+
assert.strictEqual(typeof validateDocsConfig, 'function', 'validateDocsConfig export is required');
|
|
1191
|
+
const rejected = validateDocsConfig({ docLocation: 42, domains: ['GRPH'] });
|
|
1192
|
+
assert.strictEqual(rejected.valid, false, `${label}: numeric docLocation must be invalid: ${JSON.stringify(rejected.diagnostics)}`);
|
|
1193
|
+
assert.ok(JSON.stringify(rejected.diagnostics).includes('docLocation'), `diagnostic must name docLocation: ${JSON.stringify(rejected.diagnostics)}`);
|
|
1194
|
+
const accepted = validateDocsConfig({ docLocation: ' ', domains: ['GRPH'] });
|
|
1195
|
+
assert.strictEqual(accepted.valid, true, `${label}: whitespace-only docLocation must stay fillable: ${JSON.stringify(accepted.diagnostics)}`);
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
|
|
759
1199
|
helper.finish();
|