diff --git a/.squad/agents/pao/history.md b/.squad/agents/pao/history.md index b3f609808..76ce8557a 100644 --- a/.squad/agents/pao/history.md +++ b/.squad/agents/pao/history.md @@ -109,6 +109,6 @@ Completed full PRD based on research findings. **Document:** `docs/research/jsdo 2. **`gh aw compile` demoted** from required step to troubleshooting note. Quick Start reduced from 6 to 5 steps. Lock files are generated automatically by `gh aw add`. Upgrading section left unchanged (correct there). 3. **Restricted secrets callout added** in Setup immediately after the `gh aw add` block, with `--approve` flag workaround. -**Decision record:** `.squad/decisions/inbox/pao-1761-gh-aw-path-verification.md` +**Decision record:** `.squad/decisions.md` — “2026-08-21: gitignore placement — colocation over root rules.” **PR:** Closes #1761 diff --git a/docs/src/content/docs/guide/gh-aw.md b/docs/src/content/docs/guide/gh-aw.md index cca07592b..f33793cb9 100644 --- a/docs/src/content/docs/guide/gh-aw.md +++ b/docs/src/content/docs/guide/gh-aw.md @@ -122,18 +122,15 @@ git commit -m "ci: add Squad agentic workflow" git push ``` -This stages everything `gh aw add` wrote — workflow source files and their -compiled `.lock.yml` files under `.github/workflows/`, the agentic-workflows -skill under `.github/skills/`, and the `.gitattributes` entry that marks lock -files as generated. No need to update this command as Squad's shipped files -evolve. +This command stages `.gitattributes` and files under `.github/workflows/` and +`.github/skills/`. > **Troubleshooting:** If the lock files are missing or you need to regenerate > them manually, run `gh aw compile`. This is only needed if something went wrong > during `gh aw add`. Downloaded workflow audit data is local diagnostic output and should not be -committed. If a `.gitignore` is missing from your workflow logs directory, add: +committed. If a `.gitignore` is missing from `.github/aw/logs/`, add one there: ```gitignore # Ignore all downloaded workflow logs diff --git a/test/gh-aw-plan-lifecycle.test.ts b/test/gh-aw-plan-lifecycle.test.ts index d3cc5e5bb..1d6f0156a 100644 --- a/test/gh-aw-plan-lifecycle.test.ts +++ b/test/gh-aw-plan-lifecycle.test.ts @@ -24,19 +24,64 @@ function readText(filePath: string): string { return readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n'); } -/** Slice a `## skill: \`name\`` block out of the workflow markdown. */ +/** Include the skill heading and stop before its end marker, the next skill, or EOF. */ function skillBlock(markdown: string, name: string): string { const marker = `## skill: \`${name}\``; - const start = markdown.indexOf(marker); - if (start === -1) throw new Error(`skill block "${name}" not found`); - const bodyStart = start + marker.length; - const nextSkillIdx = markdown.indexOf('\n## skill: `', bodyStart); - const endMarkerIdx = markdown.indexOf(`\n## end skill: \`${name}\``, bodyStart); - const candidates = [nextSkillIdx, endMarkerIdx].filter(index => index >= 0); - const endIdx = candidates.length === 0 ? -1 : Math.min(...candidates); - return endIdx === -1 ? markdown.slice(start) : markdown.slice(start, endIdx); + const endMarker = `## end skill: \`${name}\``; + const skillHeadings = [...markdown.matchAll(/^## skill:[^\r\n]*$/gm)]; + const requested = skillHeadings.filter(match => match[0] === marker); + if (requested.length !== 1) { + throw new Error(`skill block "${name}" must appear exactly once; found ${requested.length}`); + } + const start = requested[0].index!; + const nextSkill = skillHeadings.find(match => match.index! > start); + const matchingEnds = [...markdown.matchAll(/^## end skill:[^\r\n]*$/gm)].filter( + match => match[0] === endMarker && match.index! > start, + ); + if (matchingEnds.length > 1) { + throw new Error(`skill block "${name}" has duplicate end markers`); + } + const end = Math.min(nextSkill?.index ?? markdown.length, matchingEnds[0]?.index ?? markdown.length); + return markdown.slice(start, end); } +describe('skillBlock', () => { + const adjacentSkills = [ + '## skill: `requested`', + 'requested content', + '## skill: `following`', + 'following content', + '## end skill: `following`', + '## agent: `after-skills`', + ].join('\n'); + + it('includes the requested heading and content but excludes the following skill', () => { + const requested = skillBlock(adjacentSkills, 'requested'); + + expect(requested).toBe('## skill: `requested`\nrequested content\n'); + expect(requested).not.toContain('## skill: `following`'); + expect(requested).not.toContain('following content'); + expect(skillBlock(adjacentSkills, 'following')).toBe( + '## skill: `following`\nfollowing content\n', + ); + expect(skillBlock('## skill: `only`\nonly content', 'only')).toBe( + '## skill: `only`\nonly content', + ); + }); + + it('fails closed when the requested marker is missing or duplicated', () => { + expect(() => skillBlock(adjacentSkills, 'missing')).toThrow( + 'skill block "missing" must appear exactly once; found 0', + ); + expect(() => skillBlock(`${adjacentSkills}\n## skill: \`requested\``, 'requested')).toThrow( + 'skill block "requested" must appear exactly once; found 2', + ); + expect(() => skillBlock(`${adjacentSkills}\n## end skill: \`following\``, 'following')).toThrow( + 'skill block "following" has duplicate end markers', + ); + }); +}); + /** Slice a `## agent: \`name\`` block out of the workflow markdown. */ function agentBlock(markdown: string, name: string): string { const marker = `## agent: \`${name}\``; diff --git a/test/gh-aw-quality.test.ts b/test/gh-aw-quality.test.ts index aa13157e4..ae4bda202 100644 --- a/test/gh-aw-quality.test.ts +++ b/test/gh-aw-quality.test.ts @@ -1657,26 +1657,6 @@ describe('gh-aw: dispatch_workflow schema static gate (#1772)', () => { const scriptPath = join(process.cwd(), 'scripts', 'check-workflow-input-interpolation.mjs'); const fixturesDir = join(TEST_WORKSPACES_DIR, 'dispatch-schema-gate'); - function runGate(scanDir: string): { exitCode: number; stderr: string; stdout: string } { - // The script resolves SCAN_DIRS relative to repo root via import.meta.url. - // We invoke it with a patched environment variable so the fixture directory is - // scanned instead of the real workflows directory. - const result = spawnSync( - process.execPath, - [scriptPath], - { - env: { ...process.env, SQUAD_GATE_SCAN_OVERRIDE: scanDir }, - encoding: 'utf8', - cwd: process.cwd(), - } - ); - return { - exitCode: result.status ?? 1, - stderr: result.stderr ?? '', - stdout: result.stdout ?? '', - }; - } - function writeFixture(name: string, content: string): string { mkdirSync(fixturesDir, { recursive: true }); const path = join(fixturesDir, name);