feat(#787): elevate Cline — .clinerules/ dir form, PreToolUse hook, AGENTS.md (#803)

* feat(#787): elevate Cline — .clinerules/ dir form, PreToolUse hook, AGENTS.md

Migrate the installer's Cline output from a single-file .clinerules to the
.clinerules/ directory form (.clinerules/gsd.md), which is the prerequisite for
Cline's v3.36 hooks (a path cannot be both a file and a directory). Add a
.clinerules/hooks/PreToolUse lifecycle hook implementing Cline's JSON stdin ->
{cancel,errorMessage,contextModification} protocol; it guards .planning/
artifacts and fails open. On global installs, merge GSD instructions into the
cross-tool ~/.agents/AGENTS.md target (marker-delimited, merge-safe). A legacy
single-file .clinerules is migrated in place; --uninstall removes the new
artifacts and strips the AGENTS.md GSD block.

Also fixes the uninstall targetDir for Cline local installs (it pointed at
./.cline instead of the project root) and re-runs writeManifest after the
Cline artifacts are written so they are hash-tracked.

Self-contained: implemented independently of the #782 Cline skills work.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#787): address review findings

- Scope PreToolUse hook path-walk to PATH_KEY fields only (eliminates false
  positive when doc body content mentions .planning/)
- Use lstatSync + isSymbolicLink() for migration guard so GSD never writes
  through a user's symlinked .clinerules into an external directory
- Add regression tests for both cases

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore(#787): set changeset pr: 803

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-06-07 15:20:55 -04:00
committed by GitHub
parent 74a818308e
commit eefef2ec19
6 changed files with 634 additions and 33 deletions

View File

@@ -142,17 +142,19 @@ describe('Cline install (local)', () => {
cleanup(tmpDir);
});
test('install creates .clinerules file', () => {
test('install creates .clinerules directory with gsd.md (#787 directory form)', () => {
install(false, 'cline');
const clinerules = path.join(tmpDir, '.clinerules');
assert.ok(fs.existsSync(clinerules), '.clinerules must exist after cline install');
const clinerulesDir = path.join(tmpDir, '.clinerules');
assert.ok(fs.existsSync(clinerulesDir), '.clinerules must exist after cline install');
assert.ok(fs.statSync(clinerulesDir).isDirectory(), '.clinerules must be a directory (#787)');
assert.ok(fs.existsSync(path.join(clinerulesDir, 'gsd.md')), '.clinerules/gsd.md must exist');
});
test('.clinerules contains GSD instructions', () => {
test('.clinerules/gsd.md contains GSD instructions', () => {
install(false, 'cline');
const clinerules = path.join(tmpDir, '.clinerules');
const content = fs.readFileSync(clinerules, 'utf8');
assert.ok(content.includes('GSD') || content.includes('gsd'), '.clinerules must reference GSD');
const ruleFile = path.join(tmpDir, '.clinerules', 'gsd.md');
const content = fs.readFileSync(ruleFile, 'utf8');
assert.ok(content.includes('GSD') || content.includes('gsd'), '.clinerules/gsd.md must reference GSD');
});
test('install creates gsd-core engine directory', () => {