Skip to content

Commit 2fd8f4c

Browse files
authored
fix(skill-nudge): require complete Codex managed section (#90)
* fix(skill-nudge): require complete Codex managed section * docs(skill-nudge): document helper contracts --------- Co-authored-by: ahndohun <19940813+ahndohun@users.noreply.github.com>
1 parent 33b7848 commit 2fd8f4c

2 files changed

Lines changed: 22 additions & 6 deletions

File tree

src/lib/skill-nudge.test.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, expect, it } from 'vitest';
2-
import { MANAGED_SECTION_BEGIN, TARGETS } from './agent-targets.js';
2+
import { MANAGED_SECTION_BEGIN, MANAGED_SECTION_END, TARGETS } from './agent-targets.js';
33
import type { OutputMode } from './output.js';
44
import {
55
SKILL_NUDGE_COMMANDS,
@@ -36,10 +36,17 @@ describe('isVerifySkillInstalled', () => {
3636

3737
it('true when AGENTS.md exists AND carries our BEGIN sentinel', () => {
3838
const existsSync = (p: string) => p.endsWith('AGENTS.md');
39-
const readFileSync = () => `# project\n${MANAGED_SECTION_BEGIN}\n...skill...\n`;
39+
const readFileSync = () =>
40+
`# project\n${MANAGED_SECTION_BEGIN}\n...skill...\n${MANAGED_SECTION_END}\n`;
4041
expect(isVerifySkillInstalled('/proj', { existsSync, readFileSync })).toBe(true);
4142
});
4243

44+
it('false when AGENTS.md has only the BEGIN sentinel without a complete managed section', () => {
45+
const existsSync = (p: string) => p.endsWith('AGENTS.md');
46+
const readFileSync = () => `# project\n${MANAGED_SECTION_BEGIN}\n...partial skill...\n`;
47+
expect(isVerifySkillInstalled('/proj', { existsSync, readFileSync })).toBe(false);
48+
});
49+
4350
it('false when only a bare AGENTS.md (no sentinel) exists', () => {
4451
const existsSync = (p: string) => p.endsWith('AGENTS.md');
4552
const readFileSync = () => '# my project\nNothing TestSprite here.\n';

src/lib/skill-nudge.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { existsSync, readFileSync } from 'node:fs';
22
import { join } from 'node:path';
3-
import { MANAGED_SECTION_BEGIN, TARGETS } from './agent-targets.js';
3+
import { MANAGED_SECTION_BEGIN, MANAGED_SECTION_END, TARGETS } from './agent-targets.js';
44
import { defaultCredentialsPath, readProfile } from './credentials.js';
55
import type { OutputMode } from './output.js';
66

@@ -44,8 +44,8 @@ export interface SkillPresenceDeps {
4444
* True if the `testsprite-verify` skill is installed for ANY supported agent in
4545
* `dir`. own-file targets (claude/cursor/cline/antigravity): the landing file
4646
* exists. managed-section target (codex / AGENTS.md): the file exists AND
47-
* carries our BEGIN sentinel — a user-authored AGENTS.md without the sentinel
48-
* does NOT count as our skill.
47+
* carries one complete managed section — a user-authored AGENTS.md without the
48+
* sentinels, or with a truncated section, does NOT count as our skill.
4949
*
5050
* The TARGETS table is the single source of truth for landing paths, so this
5151
* stays in lockstep with `agent install` without re-listing paths. Best-effort:
@@ -59,7 +59,7 @@ export function isVerifySkillInstalled(dir: string, deps: SkillPresenceDeps = {}
5959
if (!exists(full)) continue;
6060
if (spec.mode === 'managed-section') {
6161
try {
62-
if (read(full).includes(MANAGED_SECTION_BEGIN)) return true;
62+
if (hasCompleteManagedSection(read(full))) return true;
6363
} catch {
6464
// unreadable AGENTS.md → treat this target as absent, keep checking
6565
}
@@ -70,6 +70,14 @@ export function isVerifySkillInstalled(dir: string, deps: SkillPresenceDeps = {}
7070
return false;
7171
}
7272

73+
/** True when a managed-section file contains an ordered TestSprite BEGIN/END pair. */
74+
function hasCompleteManagedSection(content: string): boolean {
75+
const begin = content.indexOf(MANAGED_SECTION_BEGIN);
76+
if (begin === -1) return false;
77+
const end = content.indexOf(MANAGED_SECTION_END, begin + MANAGED_SECTION_BEGIN.length);
78+
return end !== -1;
79+
}
80+
7381
export interface SkillNudgeContext {
7482
/** Full command path, e.g. "test run" / "auth whoami". */
7583
commandPath: string;
@@ -133,6 +141,7 @@ export function maybeEmitSkillNudge(ctx: SkillNudgeContext): void {
133141
}
134142
}
135143

144+
/** Interpret common env-var spellings for an enabled opt-out flag. */
136145
function isTruthyEnv(v: string | undefined): boolean {
137146
if (v === undefined) return false;
138147
const s = v.trim().toLowerCase();

0 commit comments

Comments
 (0)