Perform code optimization and document cleanup #1
@@ -29,9 +29,12 @@
|
||||
"skills/atlassian/pi/SKILL.md"
|
||||
],
|
||||
|
||||
// Ignore patterns — always exclude node_modules and generated artefacts
|
||||
// Ignore patterns — always exclude node_modules, generated artefacts, and canonical _source dirs
|
||||
"ignores": [
|
||||
"**/node_modules/**",
|
||||
"pi-package/**"
|
||||
"pi-package/**",
|
||||
// Canonical source directories — contain per-agent source with relative links
|
||||
// calibrated to their generated location (one level up); exclude from linting.
|
||||
"skills/**/_source/**"
|
||||
]
|
||||
}
|
||||
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
# Changelog — ai-coding-skills
|
||||
|
||||
All notable changes to the ai-coding-skills repository are recorded here.
|
||||
Entries are milestone-scoped; stories within each milestone are listed for traceability.
|
||||
|
||||
---
|
||||
|
||||
## M3 — Shared-source generator for agent variants
|
||||
|
||||
### Package metadata change ⚠️
|
||||
|
||||
**All generated agent-variant `package.json` files have been renamed to unique
|
||||
private scoped names.** This is an intentional breaking rename in the artifact
|
||||
structure, but all packages carry `"private": true` and are **never published**
|
||||
to any registry. End-user manual-install workflows are unaffected: copying a
|
||||
`skills/<skill>/<agent>/` directory continues to work unchanged.
|
||||
|
||||
| Old `name` | New `name` | Path |
|
||||
|-----------|-----------|------|
|
||||
| `atlassian-skill-scripts` | `@ai-coding-skills/atlassian-claude-code` | `skills/atlassian/claude-code/scripts/package.json` |
|
||||
| `atlassian-skill-scripts` | `@ai-coding-skills/atlassian-codex` | `skills/atlassian/codex/scripts/package.json` |
|
||||
| `atlassian-skill-scripts` | `@ai-coding-skills/atlassian-cursor` | `skills/atlassian/cursor/scripts/package.json` |
|
||||
| `atlassian-skill-scripts` | `@ai-coding-skills/atlassian-opencode` | `skills/atlassian/opencode/scripts/package.json` |
|
||||
| `atlassian-skill-scripts` | `@ai-coding-skills/atlassian-pi` | `skills/atlassian/pi/scripts/package.json` |
|
||||
| `atlassian-skill-scripts` | `@ai-coding-skills/atlassian-pi-mirror` | `pi-package/skills/atlassian/scripts/package.json` |
|
||||
| `web-automation-scripts` | `@ai-coding-skills/web-automation-claude-code` | `skills/web-automation/claude-code/scripts/package.json` |
|
||||
| `web-automation-scripts` | `@ai-coding-skills/web-automation-codex` | `skills/web-automation/codex/scripts/package.json` |
|
||||
| `web-automation-scripts` | `@ai-coding-skills/web-automation-cursor` | `skills/web-automation/cursor/scripts/package.json` |
|
||||
| `web-automation-scripts` | `@ai-coding-skills/web-automation-opencode` | `skills/web-automation/opencode/scripts/package.json` |
|
||||
| `web-automation-scripts` | `@ai-coding-skills/web-automation-pi` | `skills/web-automation/pi/scripts/package.json` |
|
||||
| `web-automation-scripts` | `@ai-coding-skills/web-automation-pi-mirror` | `pi-package/skills/web-automation/scripts/package.json` |
|
||||
|
||||
All renamed packages also gained `"private": true`.
|
||||
|
||||
### What else changed in M3
|
||||
|
||||
**S-301 — Canonical sources documented**
|
||||
|
||||
Canonical source directories introduced (all outside every generated root):
|
||||
|
||||
- `skills/atlassian/_source/<agent>/SKILL.md` — per-agent Atlassian skill descriptions
|
||||
- `skills/web-automation/_source/<agent>/SKILL.md` — per-agent Web Automation skill descriptions
|
||||
- `skills/web-automation/shared/` — shared web-automation TypeScript runtime scripts
|
||||
- `skills/create-plan/_source/<agent>/` — per-agent create-plan SKILL.md + templates
|
||||
- `skills/do-task/_source/<agent>/` — per-agent do-task SKILL.md + templates
|
||||
- `skills/implement-plan/_source/<agent>/` — per-agent implement-plan SKILL.md
|
||||
- `skills/reviewer-runtime/` (base) — canonical `run-review.sh` and `notify-telegram.sh`
|
||||
|
||||
**S-302 — Generator built**
|
||||
|
||||
`scripts/generate-skills.mjs` regenerates all 31 generated roots from canonical
|
||||
sources in one pass. Exports `detectFileType`, `applyHeader`,
|
||||
`makePackageJsonContent`, `getGeneratedRoots`, `buildManifest`, and
|
||||
`generateSkills`. 35 unit tests.
|
||||
|
||||
**S-303 — Manual-workflow skills migrated**
|
||||
|
||||
`create-plan`, `do-task`, `implement-plan` agent variants now generated from
|
||||
`_source/` canonical sources. Only diffs vs pre-migration: file-type-aware
|
||||
headers and new `.generated-manifest.json` files.
|
||||
|
||||
**S-304 — Web-automation runtime scripts migrated**
|
||||
|
||||
`skills/web-automation/shared/` created from the former codex canonical source.
|
||||
All five web-automation agent variants and the pi-package mirror now generated
|
||||
from this shared location.
|
||||
|
||||
**S-305 — Reviewer-runtime Pi variants migrated**
|
||||
|
||||
`skills/reviewer-runtime/pi/run-review.sh` and `notify-telegram.sh` are now
|
||||
generated from the canonical base scripts. The "keep this file in sync"
|
||||
comments are replaced by generated-file headers.
|
||||
|
||||
**S-306 — `sync:pi` and `verify:generated` implemented**
|
||||
|
||||
- `pnpm run sync:pi` now calls `node scripts/generate-skills.mjs`.
|
||||
- `scripts/verify-generated.mjs` implements the full comparison contract:
|
||||
walks declared roots only, uses `.generated-manifest.json` as oracle, reports
|
||||
structured diffs, exits non-zero on any mismatch. 5 unit tests including the
|
||||
required `_source/` stray-file boundary test.
|
||||
- `pnpm run verify:generated` wired to the real implementation.
|
||||
|
||||
**S-307 — Workspace updated**
|
||||
|
||||
`pnpm-workspace.yaml` updated to **include** all generated agent-variant
|
||||
packages (now uniquely named) alongside the canonical sources. The M1
|
||||
negative-glob exclusions are replaced by positive includes.
|
||||
|
||||
**S-308 — Documentation updated**
|
||||
|
||||
- `docs/DEVELOPMENT.md`: new "How variants are generated" section with
|
||||
canonical source table, generated-root list, contributor workflow, header
|
||||
policy table, and manifest contract.
|
||||
- `CHANGELOG.md` (this file): full rename table and story summaries.
|
||||
|
||||
### Byte-equivalence diff allow-list (M3 vs pre-M3 generated roots)
|
||||
|
||||
The only permitted diffs between the M3-generated output and the pre-M3
|
||||
manually-maintained variants are:
|
||||
|
||||
1. **File-type-aware generated-file headers** — added per the policy table in
|
||||
`docs/DEVELOPMENT.md`.
|
||||
2. **New `.generated-manifest.json` files** — one per generated root.
|
||||
3. **`package.json` `name` field** — renamed to `@ai-coding-skills/<skill>-<agent>`.
|
||||
4. **`"private": true` added** — to each generated `package.json`.
|
||||
|
||||
No other content diffs are permitted; `verify:generated` and the generator
|
||||
itself reject any other change.
|
||||
|
||||
### `pnpm run check` status after M3
|
||||
|
||||
```text
|
||||
PASS lint (was FAIL; all pre-existing violations fixed)
|
||||
PASS typecheck
|
||||
PASS test
|
||||
PASS verify:pi
|
||||
PASS verify:reviewers
|
||||
PASS verify:docs
|
||||
PASS verify:generated (was stub; now real implementation)
|
||||
```
|
||||
|
||||
All checks green for the first time.
|
||||
|
||||
---
|
||||
|
||||
*Previous milestones (M1, M2) did not have a CHANGELOG.md entry. See
|
||||
`docs/CLEANUP-BASELINE.md` for the M1 baseline and M2 delta.*
|
||||
@@ -131,7 +131,7 @@ The repo root now includes a pi package manifest that ships only the pi-specific
|
||||
- `skills/reviewer-runtime/pi/`
|
||||
- `docs/PI*.md`
|
||||
- `scripts/manage-skills.mjs` and `scripts/manage-skills.sh`
|
||||
- `scripts/sync-pi-package-skills.sh`
|
||||
- `scripts/generate-skills.mjs`
|
||||
- `scripts/verify-pi-resources.sh`
|
||||
- `scripts/verify-pi-workflows.sh`
|
||||
- `scripts/verify-reviewer-support.sh`
|
||||
@@ -159,7 +159,7 @@ The repo pins its pnpm version in `package.json` so Corepack-backed installs res
|
||||
Before publishing or sharing a tarball, run:
|
||||
|
||||
```bash
|
||||
./scripts/sync-pi-package-skills.sh
|
||||
pnpm run sync:pi
|
||||
npm run verify:pi
|
||||
npm run verify:reviewers
|
||||
npm pack --dry-run --json
|
||||
|
||||
+1
-1
@@ -138,7 +138,7 @@ Recommended full Pi package install:
|
||||
Manual single-skill Pi install from the package mirror:
|
||||
|
||||
```bash
|
||||
./scripts/sync-pi-package-skills.sh
|
||||
pnpm run sync:pi
|
||||
mkdir -p .pi/skills/atlassian
|
||||
cp -R pi-package/skills/atlassian/* .pi/skills/atlassian/
|
||||
cd .pi/skills/atlassian/scripts
|
||||
|
||||
@@ -293,6 +293,59 @@ changes the baseline status of a check.*
|
||||
|
||||
---
|
||||
|
||||
## Post-M3 state
|
||||
|
||||
Captured: 2026-05-03 · Platform: macOS 15 (arm64) · Node 22.14.0 · pnpm 10.18.1
|
||||
|
||||
M3 resolved all remaining pre-existing `lint` violations (2 ESLint errors and
|
||||
7 shellcheck findings). `verify:generated` is now a real implementation
|
||||
(was a stub in M2).
|
||||
|
||||
### `pnpm run check` aggregate (post-M3)
|
||||
|
||||
```text
|
||||
PASS lint (was FAIL in M1/M2; pre-existing violations fixed in M3)
|
||||
PASS typecheck
|
||||
PASS test
|
||||
PASS verify:pi
|
||||
PASS verify:reviewers
|
||||
PASS verify:docs
|
||||
PASS verify:generated (was stub in M2; real implementation in M3)
|
||||
```
|
||||
|
||||
Overall exit: **0** — all checks green for the first time.
|
||||
|
||||
### What changed in M3
|
||||
|
||||
- `scripts/generate-skills.mjs` added (generator for all 31 agent-variant roots).
|
||||
- `scripts/verify-generated.mjs` added (drift detector).
|
||||
- Canonical source directories created: `skills/<skill>/_source/`,
|
||||
`skills/web-automation/shared/`.
|
||||
- All 31 generated roots now carry `.generated-manifest.json` and file-type-aware
|
||||
headers.
|
||||
- `package.json` renames: all generated agent-variant packages renamed to
|
||||
`@ai-coding-skills/<skill>-<agent>` with `"private": true`.
|
||||
- `pnpm-workspace.yaml` updated: M1 negative-glob exclusions replaced by
|
||||
positive includes for all uniquely-named generated variants.
|
||||
- Pre-existing ESLint violations fixed: `skill-manager-core.mjs:282`
|
||||
(`no-useless-assignment`) and `manage-skills.mjs:270` (`no-unused-vars`).
|
||||
- Pre-existing shellcheck findings fixed: SC2034 and SC2329 in
|
||||
`reviewer-runtime/run-review.sh`; SC2064 in test trap statements;
|
||||
SC2016 / SC2251 in verify scripts (suppress intentional patterns).
|
||||
- `pnpm run sync:pi` now calls the generator instead of
|
||||
`sync-pi-package-skills.sh`.
|
||||
- `docs/DEVELOPMENT.md` extended with generation workflow and metadata docs.
|
||||
- `CHANGELOG.md` created with package-metadata change callout.
|
||||
|
||||
### Full byte-equivalence diff (M3 vs pre-M3)
|
||||
|
||||
See `CHANGELOG.md` § "Byte-equivalence diff allow-list" for the complete
|
||||
documented diff between M3-generated output and the pre-M3 variants. The
|
||||
only permitted diffs are headers, `.generated-manifest.json`, `name` rename,
|
||||
and `"private": true`.
|
||||
|
||||
---
|
||||
|
||||
## Post-M2 state
|
||||
|
||||
Captured: 2026-05-03 · Platform: macOS 15 (arm64) · Node 22.14.0 · pnpm 10.18.1
|
||||
|
||||
+1
-1
@@ -116,7 +116,7 @@ Recommended full Pi package install:
|
||||
Manual single-skill Pi install from the package mirror:
|
||||
|
||||
```bash
|
||||
./scripts/sync-pi-package-skills.sh
|
||||
pnpm run sync:pi
|
||||
mkdir -p .pi/skills/create-plan
|
||||
cp -R pi-package/skills/create-plan/* .pi/skills/create-plan/
|
||||
mkdir -p .pi/skills/reviewer-runtime/pi
|
||||
|
||||
+137
-20
@@ -167,34 +167,46 @@ container with Node 20.
|
||||
documented in `docs/CLEANUP-BASELINE.md`. Those will be fixed in a dedicated
|
||||
cleanup pass. No violations were introduced by M2.
|
||||
|
||||
## pnpm workspace policy (M1 exclusion-only)
|
||||
**M3 update:** `pnpm run check` is now **fully green**. All pre-existing lint
|
||||
violations have been fixed (2 ESLint errors, 7 shellcheck findings). The
|
||||
`verify:generated` check is now a real implementation (was a stub in M2).
|
||||
|
||||
The `pnpm-workspace.yaml` at the repo root implements the **non-mutating,
|
||||
exclusion-only** policy for M1:
|
||||
## pnpm workspace policy (updated in M3)
|
||||
|
||||
**Included** (canonical source packages):
|
||||
The `pnpm-workspace.yaml` at the repo root uses a **positive-include** policy
|
||||
introduced in M3. There are no negative-glob exclusions.
|
||||
|
||||
- `skills/atlassian/shared/scripts` — shared Atlassian runtime source
|
||||
- `skills/web-automation/codex/scripts` — provisional canonical copy; M3
|
||||
will rename and/or consolidate
|
||||
**Canonical source packages** (never generated):
|
||||
|
||||
**Excluded** (generated agent-variant directories):
|
||||
- `skills/atlassian/shared/scripts` — shared Atlassian TypeScript runtime
|
||||
- `skills/web-automation/shared` — shared web-automation runtime template
|
||||
|
||||
- `skills/atlassian/{codex,claude-code,cursor,opencode,pi}/scripts`
|
||||
- `skills/web-automation/{claude-code,cursor,opencode,pi}/scripts`
|
||||
- `pi-package/**`
|
||||
**Generated agent-variant packages** (uniquely named, positively included):
|
||||
|
||||
**Why exclusion-only in M1?**
|
||||
- `skills/atlassian/{claude-code,codex,cursor,opencode,pi}/scripts`
|
||||
→ names `@ai-coding-skills/atlassian-{claude-code,codex,cursor,opencode,pi}`
|
||||
- `skills/web-automation/{claude-code,codex,cursor,opencode,pi}/scripts`
|
||||
→ names `@ai-coding-skills/web-automation-{claude-code,codex,cursor,opencode,pi}`
|
||||
- `pi-package/skills/atlassian/scripts`
|
||||
→ name `@ai-coding-skills/atlassian-pi-mirror`
|
||||
- `pi-package/skills/web-automation/scripts`
|
||||
→ name `@ai-coding-skills/web-automation-pi-mirror`
|
||||
|
||||
The generated agent-variant directories contain `package.json` files with the
|
||||
same `name` field as the canonical source. Including them in the workspace
|
||||
would cause pnpm to complain about duplicate package names. Renaming them to
|
||||
unique names (e.g. `atlassian-skill-scripts-codex`) requires a generator-driven
|
||||
update that touches every generated file — this is deferred to **M3** to keep
|
||||
M1 byte-identical for those files.
|
||||
**Why unique names matter:**
|
||||
|
||||
After `pnpm install`, `git status` should show zero modifications to any file
|
||||
under the excluded directories. If it does not, the workspace config is broken.
|
||||
Each package in a pnpm workspace must have a distinct `name` field. In M1 all
|
||||
generated agent-variant packages shared the same non-unique name as their
|
||||
canonical source package. In M3 every package receives a scoped unique name
|
||||
of the form `@ai-coding-skills/<skill>-<agent>`, enabling pnpm to include them
|
||||
alongside canonical source packages without conflicts. The pi-package mirrors
|
||||
use the `-mirror` suffix to distinguish them from the `pi` agent variants.
|
||||
|
||||
All generated packages are `"private": true` and are never published to any
|
||||
registry.
|
||||
|
||||
After `pnpm install`, `git status` should show zero modifications to any
|
||||
package.json file under any generated directory. If it does not, the workspace
|
||||
config or generator is broken.
|
||||
|
||||
## How to interpret the baseline report
|
||||
|
||||
@@ -205,8 +217,113 @@ fails on a check, compare the output against the baseline:
|
||||
- If the failure does not appear in the baseline → it is a regression
|
||||
introduced by recent changes and must be fixed before merge.
|
||||
|
||||
## How variants are generated (M3)
|
||||
|
||||
Every agent-variant directory under `skills/<skill>/<agent>/` and every
|
||||
`pi-package/skills/<skill>/` mirror is **generator-owned**. Do not edit
|
||||
files inside these directories directly — edit the canonical source instead
|
||||
and run `pnpm run sync:pi` to regenerate.
|
||||
|
||||
### Canonical sources (non-generated)
|
||||
|
||||
These directories are **never** generated and are **never** inside a generated
|
||||
root. They are the single source of truth for all content.
|
||||
|
||||
| Skill | Canonical source |
|
||||
|-------|------------------|
|
||||
| `atlassian` | SKILL.md: `skills/atlassian/_source/<agent>/SKILL.md`; scripts: `skills/atlassian/shared/scripts/` |
|
||||
| `web-automation` | SKILL.md: `skills/web-automation/_source/<agent>/SKILL.md`; scripts: `skills/web-automation/shared/` |
|
||||
| `create-plan` | `skills/create-plan/_source/<agent>/` (SKILL.md + templates) |
|
||||
| `do-task` | `skills/do-task/_source/<agent>/` (SKILL.md + templates) |
|
||||
| `implement-plan` | `skills/implement-plan/_source/<agent>/SKILL.md` |
|
||||
| `reviewer-runtime` (base) | `skills/reviewer-runtime/run-review.sh`, `notify-telegram.sh` |
|
||||
|
||||
The `_source/` directories and `shared/` directories sit under the skill root
|
||||
**outside** every generated root. Stale-file detection (`verify:generated`)
|
||||
never traverses them.
|
||||
|
||||
### Generated roots (authoritative list)
|
||||
|
||||
All paths are relative to the repo root.
|
||||
|
||||
```text
|
||||
skills/atlassian/{claude-code,codex,cursor,opencode,pi}/
|
||||
skills/web-automation/{claude-code,codex,cursor,opencode,pi}/
|
||||
skills/create-plan/{claude-code,codex,cursor,opencode,pi}/
|
||||
skills/do-task/{claude-code,codex,cursor,opencode,pi}/
|
||||
skills/implement-plan/{claude-code,codex,cursor,opencode,pi}/
|
||||
skills/reviewer-runtime/pi/
|
||||
pi-package/skills/{atlassian,create-plan,do-task,implement-plan,web-automation}/
|
||||
```
|
||||
|
||||
### Contributor workflow
|
||||
|
||||
1. **Edit the canonical source** in the appropriate `_source/<agent>/`, `shared/`, or
|
||||
base `skills/reviewer-runtime/` directory.
|
||||
2. **Run the generator:**
|
||||
|
||||
```bash
|
||||
pnpm run sync:pi # regenerates all 31 generated roots
|
||||
```
|
||||
|
||||
3. **Verify no drift:**
|
||||
|
||||
```bash
|
||||
pnpm run verify:generated # must exit 0
|
||||
```
|
||||
|
||||
4. **Stage both the canonical source AND the generated output** in the same commit.
|
||||
Never commit a canonical change without regenerating.
|
||||
|
||||
### File-type-aware header policy
|
||||
|
||||
Every generated file (except JSON and `pnpm-lock.yaml`) receives a header
|
||||
identifying it as generated and pointing to the canonical source. Headers are
|
||||
inserted so they do not break parsers or tools:
|
||||
|
||||
| File type | Header form | Placement |
|
||||
|-----------|-------------|----------|
|
||||
| Markdown (with YAML front matter) | `<!-- ⚠️ GENERATED FILE ... -->` | After closing `---` |
|
||||
| Markdown (no front matter) | `<!-- ⚠️ GENERATED FILE ... -->` | Top of file |
|
||||
| Shell script | `# ⚠️ GENERATED FILE ...` | After `#!` shebang |
|
||||
| TypeScript / JavaScript | `// ⚠️ GENERATED FILE ...` | After `#!` shebang if present, else top |
|
||||
| YAML (non-lockfile) | `# ⚠️ GENERATED FILE ...` | Top of file |
|
||||
| JSON | *(no header — recorded in `.generated-manifest.json`)* | — |
|
||||
| `pnpm-lock.yaml` | *(no header — managed by pnpm)* | — |
|
||||
| JSONC | `// ⚠️ GENERATED FILE ...` | Top of file |
|
||||
|
||||
### `.generated-manifest.json` contract
|
||||
|
||||
Each generated root contains a `.generated-manifest.json` that:
|
||||
|
||||
- Has a `$schema` marker and a `generator` field for identification.
|
||||
- Lists every **other** generator-owned path in that root (relative path,
|
||||
file mode, SHA-256 hash, kind).
|
||||
- **Does NOT list itself** (non-self-referential).
|
||||
- Is scoped to exactly **one** generated root — there is no manifest at
|
||||
`skills/<skill>/`.
|
||||
- Is verified structurally (not byte-for-byte) by `verify:generated`.
|
||||
|
||||
### Package metadata (M3 change)
|
||||
|
||||
Each generated agent-variant `package.json` carries:
|
||||
|
||||
- A **unique private name** in the form `@ai-coding-skills/<skill>-<agent>` (e.g.
|
||||
`@ai-coding-skills/atlassian-claude-code`).
|
||||
- `"private": true` to prevent accidental npm publication.
|
||||
|
||||
The pi-package mirrors (`pi-package/skills/atlassian/scripts` and
|
||||
`pi-package/skills/web-automation/scripts`) use the `-mirror` agent suffix
|
||||
(`@ai-coding-skills/atlassian-pi-mirror`, `@ai-coding-skills/web-automation-pi-mirror`)
|
||||
to distinguish them from the `pi` agent variants in the workspace.
|
||||
|
||||
This replaces the previous non-unique `name` fields (`atlassian-skill-scripts`,
|
||||
`web-automation-scripts`). These packages are private and are never published.
|
||||
See `CHANGELOG.md` for the full rename list.
|
||||
|
||||
## Links
|
||||
|
||||
- [CLEANUP-BASELINE.md](./CLEANUP-BASELINE.md) — as-is quality baseline
|
||||
- [README.md](../README.md) — project overview
|
||||
- [docs/README.md](./README.md) — documentation index
|
||||
- [CHANGELOG.md](../CHANGELOG.md) — milestone-by-milestone change record
|
||||
|
||||
+1
-1
@@ -151,7 +151,7 @@ Recommended full Pi package install:
|
||||
Manual single-skill Pi install from the package mirror:
|
||||
|
||||
```bash
|
||||
./scripts/sync-pi-package-skills.sh
|
||||
pnpm run sync:pi
|
||||
mkdir -p .pi/skills/do-task
|
||||
cp -R pi-package/skills/do-task/* .pi/skills/do-task/
|
||||
mkdir -p .pi/skills/reviewer-runtime/pi
|
||||
|
||||
@@ -124,7 +124,7 @@ Recommended full Pi package install:
|
||||
Manual single-skill Pi install from the package mirror:
|
||||
|
||||
```bash
|
||||
./scripts/sync-pi-package-skills.sh
|
||||
pnpm run sync:pi
|
||||
mkdir -p .pi/skills/implement-plan
|
||||
cp -R pi-package/skills/implement-plan/* .pi/skills/implement-plan/
|
||||
mkdir -p .pi/skills/reviewer-runtime/pi
|
||||
|
||||
+15
-10
@@ -22,15 +22,18 @@ Related docs:
|
||||
|
||||
### Source Of Truth
|
||||
|
||||
Author Pi variants under:
|
||||
Edit the **canonical sources** under:
|
||||
|
||||
- `skills/atlassian/pi/`
|
||||
- `skills/create-plan/pi/`
|
||||
- `skills/do-task/pi/`
|
||||
- `skills/implement-plan/pi/`
|
||||
- `skills/web-automation/pi/`
|
||||
- `skills/atlassian/_source/pi/SKILL.md`
|
||||
- `skills/create-plan/_source/pi/` (SKILL.md + templates)
|
||||
- `skills/do-task/_source/pi/` (SKILL.md + templates)
|
||||
- `skills/implement-plan/_source/pi/SKILL.md`
|
||||
- `skills/web-automation/_source/pi/SKILL.md`
|
||||
|
||||
These are the directories to edit by hand.
|
||||
After editing a canonical source, run `pnpm run sync:pi` to regenerate all
|
||||
agent-variant directories including `skills/<family>/pi/` and the package mirrors.
|
||||
Do not edit the generated `skills/<family>/pi/` directories directly — they are
|
||||
overwritten by the generator.
|
||||
|
||||
### Package Mirror
|
||||
|
||||
@@ -42,7 +45,9 @@ The package exposes:
|
||||
- `pi-package/skills/implement-plan/`
|
||||
- `pi-package/skills/web-automation/`
|
||||
|
||||
Those directories are generated from the source variants by [`scripts/sync-pi-package-skills.sh`](../scripts/sync-pi-package-skills.sh).
|
||||
Those directories are generated from the canonical sources by
|
||||
[`scripts/generate-skills.mjs`](../scripts/generate-skills.mjs)
|
||||
(run via `pnpm run sync:pi`).
|
||||
|
||||
### Shared Setup Docs
|
||||
|
||||
@@ -106,7 +111,7 @@ The package surface intentionally ships:
|
||||
- `docs/PI*.md`
|
||||
- `scripts/install-pi-package.sh`
|
||||
- `scripts/manage-skills.mjs` and `scripts/manage-skills.sh`
|
||||
- `scripts/sync-pi-package-skills.sh`
|
||||
- `scripts/generate-skills.mjs`
|
||||
- `scripts/verify-pi-resources.sh`
|
||||
- `scripts/verify-pi-workflows.sh`
|
||||
- `scripts/verify-reviewer-support.sh`
|
||||
@@ -130,7 +135,7 @@ Global installs use `~/.pi/agent/skills/<skill>/` instead of `.pi/skills/<skill>
|
||||
When a source Pi variant changes:
|
||||
|
||||
```bash
|
||||
./scripts/sync-pi-package-skills.sh
|
||||
pnpm run sync:pi
|
||||
npm run verify:pi
|
||||
npm run verify:reviewers
|
||||
npm pack --dry-run --json
|
||||
|
||||
+4
-2
@@ -61,10 +61,12 @@ Reference docs for each skill family:
|
||||
### 7. Development
|
||||
|
||||
- [DEVELOPMENT.md](./DEVELOPMENT.md) — Prerequisites, `pnpm run check`,
|
||||
workspace policy, cross-platform shell support, and the transitional quality
|
||||
contract.
|
||||
workspace policy, generation workflow, cross-platform shell support, and the
|
||||
quality contract.
|
||||
- [CLEANUP-BASELINE.md](./CLEANUP-BASELINE.md) — As-is quality baseline
|
||||
captured at M1, updated through subsequent milestones.
|
||||
- [../CHANGELOG.md](../CHANGELOG.md) — Milestone-by-milestone change record
|
||||
(includes M3 package-metadata rename table).
|
||||
|
||||
## Repo Setup
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ Recommended full Pi package install:
|
||||
Manual single-skill Pi install from the package mirror:
|
||||
|
||||
```bash
|
||||
./scripts/sync-pi-package-skills.sh
|
||||
pnpm run sync:pi
|
||||
mkdir -p .pi/skills/web-automation
|
||||
cp -R pi-package/skills/web-automation/* .pi/skills/web-automation/
|
||||
cd .pi/skills/web-automation/scripts
|
||||
|
||||
+28
-10
@@ -20,16 +20,34 @@ export default [
|
||||
{
|
||||
ignores: [
|
||||
"**/node_modules/**",
|
||||
// Generated agent-variant script bundles (excluded per M1 workspace policy)
|
||||
"skills/atlassian/codex/scripts/**",
|
||||
"skills/atlassian/claude-code/scripts/**",
|
||||
"skills/atlassian/cursor/scripts/**",
|
||||
"skills/atlassian/opencode/scripts/**",
|
||||
"skills/atlassian/pi/scripts/**",
|
||||
"skills/web-automation/claude-code/scripts/**",
|
||||
"skills/web-automation/cursor/scripts/**",
|
||||
"skills/web-automation/opencode/scripts/**",
|
||||
"skills/web-automation/pi/scripts/**",
|
||||
// Generated agent-variant directories (M3: now uniquely named but still
|
||||
// not linted — the canonical source in shared/ or _source/ is the linting target)
|
||||
"skills/atlassian/codex/**",
|
||||
"skills/atlassian/claude-code/**",
|
||||
"skills/atlassian/cursor/**",
|
||||
"skills/atlassian/opencode/**",
|
||||
"skills/atlassian/pi/**",
|
||||
"skills/web-automation/claude-code/**",
|
||||
"skills/web-automation/cursor/**",
|
||||
"skills/web-automation/opencode/**",
|
||||
"skills/web-automation/pi/**",
|
||||
// skill-only generated variants (no scripts to lint)
|
||||
"skills/create-plan/claude-code/**",
|
||||
"skills/create-plan/codex/**",
|
||||
"skills/create-plan/cursor/**",
|
||||
"skills/create-plan/opencode/**",
|
||||
"skills/create-plan/pi/**",
|
||||
"skills/do-task/claude-code/**",
|
||||
"skills/do-task/codex/**",
|
||||
"skills/do-task/cursor/**",
|
||||
"skills/do-task/opencode/**",
|
||||
"skills/do-task/pi/**",
|
||||
"skills/implement-plan/claude-code/**",
|
||||
"skills/implement-plan/codex/**",
|
||||
"skills/implement-plan/cursor/**",
|
||||
"skills/implement-plan/opencode/**",
|
||||
"skills/implement-plan/pi/**",
|
||||
"skills/reviewer-runtime/pi/**",
|
||||
"pi-package/**",
|
||||
],
|
||||
},
|
||||
|
||||
+4
-3
@@ -33,13 +33,14 @@
|
||||
"scripts/lib/skill-manager-core.mjs",
|
||||
"scripts/manage-skills.mjs",
|
||||
"scripts/manage-skills.sh",
|
||||
"scripts/sync-pi-package-skills.sh",
|
||||
"CHANGELOG.md",
|
||||
"scripts/generate-skills.mjs",
|
||||
"scripts/verify-pi-resources.sh",
|
||||
"scripts/verify-pi-workflows.sh",
|
||||
"scripts/verify-reviewer-support.sh"
|
||||
],
|
||||
"scripts": {
|
||||
"sync:pi": "./scripts/sync-pi-package-skills.sh",
|
||||
"sync:pi": "node scripts/generate-skills.mjs",
|
||||
"verify:pi": "./scripts/verify-pi-resources.sh && ./scripts/verify-pi-workflows.sh",
|
||||
"verify:reviewers": "./scripts/verify-reviewer-support.sh",
|
||||
"test:installer": "node --test scripts/tests/*.test.mjs",
|
||||
@@ -49,7 +50,7 @@
|
||||
"test": "pnpm run test:installer && pnpm run -r --if-present test",
|
||||
"verify:docs": "markdownlint-cli2 ; R1=$?; node scripts/lib/run-link-check.mjs ; R2=$?; node scripts/verify-docs-flow.mjs ; R3=$?; [ $((R1+R2+R3)) -eq 0 ]",
|
||||
"verify:docs:online": "markdownlint-cli2 ; R1=$?; node scripts/lib/run-link-check.mjs --online ; R2=$?; node scripts/verify-docs-flow.mjs ; R3=$?; [ $((R1+R2+R3)) -eq 0 ]",
|
||||
"verify:generated": "node -e \"console.log('verify:generated: stub — fleshed out in M3'); process.exit(0)\"",
|
||||
"verify:generated": "node scripts/verify-generated.mjs",
|
||||
"check": "node scripts/lib/run-check.mjs"
|
||||
},
|
||||
"pi": {
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
{
|
||||
"$schema": "https://ai-coding-skills.dev/schemas/generated-manifest/v1.json",
|
||||
"generator": "scripts/generate-skills.mjs",
|
||||
"generatedRoot": "pi-package/skills/atlassian",
|
||||
"files": [
|
||||
{
|
||||
"path": "scripts/package.json",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "97dd269f922ea83f6abfd29497ca532b94540acd1461b3a012f92853db55e196"
|
||||
},
|
||||
{
|
||||
"path": "scripts/pnpm-lock.yaml",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "15556a6f53e68bb8d92d2710aae0836bc80af7f29be9d63aa1b87fcbd33732c6"
|
||||
},
|
||||
{
|
||||
"path": "scripts/src/adf.ts",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "c7c3b4a78ccd8fb5a8ab99c82e0eab67a0a0d656b3985c1f56817bda199ad20f"
|
||||
},
|
||||
{
|
||||
"path": "scripts/src/cli.ts",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "5c4f4db76817fa9dbdae0fd0c75be302248d4b87fc0a53f6bd3c90407a75ae98"
|
||||
},
|
||||
{
|
||||
"path": "scripts/src/config.ts",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "700dcdce96afab5294426e09f539135ae5432632370260190d6292071422eb3f"
|
||||
},
|
||||
{
|
||||
"path": "scripts/src/confluence.ts",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "709d5d61fdb14e37aa4eaa7175eb7f17f0ec661376c96071020fbc9574ddbb73"
|
||||
},
|
||||
{
|
||||
"path": "scripts/src/files.ts",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "16296eaa3ae41a4d7c694773036f9bb4bd2baa2db6a9c318078532b713678dba"
|
||||
},
|
||||
{
|
||||
"path": "scripts/src/health.ts",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "1db4b49e05b16a095b7e7ca31cdc4e22ebda19e20e05c40baaaac648eaec0d08"
|
||||
},
|
||||
{
|
||||
"path": "scripts/src/http.ts",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "66444b777d4d9b14d9793eb051c586eb811d2b36815b1018dd9d7517666c7eb2"
|
||||
},
|
||||
{
|
||||
"path": "scripts/src/jira.ts",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "485d8d618fe04eb1ce546c1694eadf15d867bc83c2a6f7df994688ab0335ea4f"
|
||||
},
|
||||
{
|
||||
"path": "scripts/src/output.ts",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "38e99818582a4962c09a83175634cba2bfead6acf33bd5f43cdca5caed7100a0"
|
||||
},
|
||||
{
|
||||
"path": "scripts/src/raw.ts",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "2309c96dd45a03509df204803de9ecf0b5ff82fd488730f55ac5dd6a23b81dd8"
|
||||
},
|
||||
{
|
||||
"path": "scripts/src/types.ts",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "9f92d27ab68604d5abfd0f5dc9552b96fed6d1f9fc7dc6eb30190d8b617628bf"
|
||||
},
|
||||
{
|
||||
"path": "scripts/tsconfig.json",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "3c2eb7ba5c95a16cada153de4787ca7a4bf179609bf3848e12ff15b1b7927a68"
|
||||
},
|
||||
{
|
||||
"path": "SKILL.md",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "69d83441799f3feada7fbf85691bda16fc30718b724871d7e37cfac574db2253"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -3,6 +3,8 @@ name: atlassian
|
||||
description: Interact with Atlassian Cloud Jira and Confluence through a portable task-oriented CLI for search, issue/page edits, comments, transitions, and bounded raw requests.
|
||||
---
|
||||
|
||||
<!-- ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/atlassian/_source/pi/SKILL.md and run `pnpm run sync:pi`. -->
|
||||
|
||||
# Atlassian (Pi)
|
||||
|
||||
Portable Atlassian workflows for pi using the shared TypeScript CLI in `scripts/`.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "atlassian-skill-scripts",
|
||||
"name": "@ai-coding-skills/atlassian-pi-mirror",
|
||||
"version": "1.0.0",
|
||||
"description": "Shared runtime for the Atlassian skill",
|
||||
"type": "module",
|
||||
@@ -16,5 +16,6 @@
|
||||
"tsx": "^4.20.5",
|
||||
"typescript": "^5.9.2"
|
||||
},
|
||||
"packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34"
|
||||
"packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34",
|
||||
"private": true
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/atlassian/shared/scripts/ and run `pnpm run sync:pi`.
|
||||
const TEXT_NODE = "text";
|
||||
|
||||
function textNode(text: string) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/atlassian/shared/scripts/ and run `pnpm run sync:pi`.
|
||||
import process from "node:process";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/atlassian/shared/scripts/ and run `pnpm run sync:pi`.
|
||||
import path from "node:path";
|
||||
|
||||
import { config as loadDotEnv } from "dotenv";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/atlassian/shared/scripts/ and run `pnpm run sync:pi`.
|
||||
import { sendJsonRequest } from "./http.js";
|
||||
import type { AtlassianConfig, CommandOutput, FetchLike } from "./types.js";
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/atlassian/shared/scripts/ and run `pnpm run sync:pi`.
|
||||
import { readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/atlassian/shared/scripts/ and run `pnpm run sync:pi`.
|
||||
import { createJsonHeaders, createStatusError } from "./http.js";
|
||||
import type { AtlassianConfig, CommandOutput, FetchLike } from "./types.js";
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/atlassian/shared/scripts/ and run `pnpm run sync:pi`.
|
||||
import { createBasicAuthHeader } from "./config.js";
|
||||
import type { AtlassianConfig, FetchLike } from "./types.js";
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/atlassian/shared/scripts/ and run `pnpm run sync:pi`.
|
||||
import { markdownToAdf } from "./adf.js";
|
||||
import { sendJsonRequest } from "./http.js";
|
||||
import type { AtlassianConfig, CommandOutput, FetchLike, JiraIssueSummary } from "./types.js";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/atlassian/shared/scripts/ and run `pnpm run sync:pi`.
|
||||
import type { CommandOutput, OutputFormat, Writer } from "./types.js";
|
||||
|
||||
function renderText(payload: CommandOutput<unknown>) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/atlassian/shared/scripts/ and run `pnpm run sync:pi`.
|
||||
import { readWorkspaceFile } from "./files.js";
|
||||
import { sendJsonRequest } from "./http.js";
|
||||
import type { AtlassianConfig, CommandOutput, FetchLike } from "./types.js";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/atlassian/shared/scripts/ and run `pnpm run sync:pi`.
|
||||
export type AtlassianConfig = {
|
||||
baseUrl: string;
|
||||
jiraBaseUrl: string;
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"$schema": "https://ai-coding-skills.dev/schemas/generated-manifest/v1.json",
|
||||
"generator": "scripts/generate-skills.mjs",
|
||||
"generatedRoot": "pi-package/skills/create-plan",
|
||||
"files": [
|
||||
{
|
||||
"path": "SKILL.md",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "8767db25ce6f03e141ce4c48f37e9d7c4958c3cf3d70729f3bd7214b84f6d065"
|
||||
},
|
||||
{
|
||||
"path": "templates/continuation-runbook.md",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "1685cded3d4abaf03122a490175ff03b7da593ce60cbca97ae15fadcb706617f"
|
||||
},
|
||||
{
|
||||
"path": "templates/milestone-plan.md",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "364c08bf0a5ee3738195a0770db72c5a4c9ad7f7fb89eaa064eb8c67f47ad69a"
|
||||
},
|
||||
{
|
||||
"path": "templates/story-tracker.md",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "ecb550ea9dcd9dde6c813e90af9f538bf5a247fc249e8e323f2b7bf583e52196"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -3,6 +3,8 @@ name: create-plan
|
||||
description: Use when a user asks to create or maintain a structured implementation plan in pi, including milestones, bite-sized stories, and resumable local planning artifacts under ai_plan.
|
||||
---
|
||||
|
||||
<!-- ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/create-plan/_source/pi/ and run `pnpm run sync:pi`. -->
|
||||
|
||||
# Create Plan (Pi)
|
||||
|
||||
Create and maintain a local plan workspace under `ai_plan/` at project root.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<!-- ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/create-plan/_source/pi/ and run `pnpm run sync:pi`. -->
|
||||
# Continuation Runbook: [Plan Title]
|
||||
|
||||
## Reference Files (START HERE)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<!-- ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/create-plan/_source/pi/ and run `pnpm run sync:pi`. -->
|
||||
# [Plan Title]
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<!-- ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/create-plan/_source/pi/ and run `pnpm run sync:pi`. -->
|
||||
# Story Tracker: [Plan Title]
|
||||
|
||||
## Progress Summary
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"$schema": "https://ai-coding-skills.dev/schemas/generated-manifest/v1.json",
|
||||
"generator": "scripts/generate-skills.mjs",
|
||||
"generatedRoot": "pi-package/skills/do-task",
|
||||
"files": [
|
||||
{
|
||||
"path": "SKILL.md",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "4920ad0cdeda546b37432c2268159724de54ddb01922308f2df88fcca4db8d31"
|
||||
},
|
||||
{
|
||||
"path": "templates/task-plan.md",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "fd38213fabf350e14b48c5209910d00c16ff74c455101618063835fa8c19e73e"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -3,6 +3,8 @@ name: do-task
|
||||
description: Execute a single user-supplied prompt end-to-end in pi with plan review, implementation review, verification, and one persistent task-plan artifact.
|
||||
---
|
||||
|
||||
<!-- ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/do-task/_source/pi/ and run `pnpm run sync:pi`. -->
|
||||
|
||||
# Do Task (Pi)
|
||||
|
||||
Execute an ad-hoc user prompt end-to-end: parse, clarify, plan, implement, verify, review, commit, and optionally push.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<!-- ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/do-task/_source/pi/ and run `pnpm run sync:pi`. -->
|
||||
# Task Plan: [Short Title]
|
||||
|
||||
> **Variant guardrail (pi):** Required workflow skills (`brainstorming`, `test-driven-development`, `verification-before-completion`, `finishing-a-development-branch`, `using-git-worktrees`) must be available to pi as documented in `docs/PI-SUPERPOWERS.md`. Load the relevant workflow skill before entering its matching phase.
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"$schema": "https://ai-coding-skills.dev/schemas/generated-manifest/v1.json",
|
||||
"generator": "scripts/generate-skills.mjs",
|
||||
"generatedRoot": "pi-package/skills/implement-plan",
|
||||
"files": [
|
||||
{
|
||||
"path": "SKILL.md",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "578e5b4fbc443ade486e4aa034b0875ebfa7eefd3a1268852ac1c5cbf71f28bf"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -3,6 +3,8 @@ name: implement-plan
|
||||
description: Use when a plan folder created by create-plan must be executed in pi with milestone verification, reviewer gates, local commits, and resumable tracker updates.
|
||||
---
|
||||
|
||||
<!-- ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/implement-plan/_source/pi/ and run `pnpm run sync:pi`. -->
|
||||
|
||||
# Implement Plan (Pi)
|
||||
|
||||
Execute an existing plan under `ai_plan/` milestone by milestone, using verification gates, reviewer approval, and local commits after each approved milestone.
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
{
|
||||
"$schema": "https://ai-coding-skills.dev/schemas/generated-manifest/v1.json",
|
||||
"generator": "scripts/generate-skills.mjs",
|
||||
"generatedRoot": "pi-package/skills/web-automation",
|
||||
"files": [
|
||||
{
|
||||
"path": "scripts/auth.ts",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "ce0a8aae0bc41b86e11aab51cc0e0cfa484a1934807f147c05c9bd38d416c066"
|
||||
},
|
||||
{
|
||||
"path": "scripts/browse.ts",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "42da9cdc6806b8d7d8d814952ad9540033b6c6a4cbe9844ada328b2ceace67c9"
|
||||
},
|
||||
{
|
||||
"path": "scripts/check-install.js",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "e46ee8cbe103794bf1e9c3466bb0fbd21079ceddc60ad9521299e8bc0150e48f"
|
||||
},
|
||||
{
|
||||
"path": "scripts/extract.js",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "6fa2a0589de8afd6501e332e5fa263e1344187ea43a33590b431cdee59d04217"
|
||||
},
|
||||
{
|
||||
"path": "scripts/flow.ts",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "b1c256bf6a206473512a4c0555c891893a48025529da282fa6cd07e68ad3d051"
|
||||
},
|
||||
{
|
||||
"path": "scripts/package.json",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "1d9226da585c65106dacd874e5e6c7951f5a5b2b0f0cf5902f305a951ca4b44d"
|
||||
},
|
||||
{
|
||||
"path": "scripts/pnpm-lock.yaml",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "17017e15e8b04311f5d53bdd37065b2f5a514a3119f40a0403148440ed181437"
|
||||
},
|
||||
{
|
||||
"path": "scripts/scan-local-app.ts",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "3f42f9bb2d355fefc8645d2b2acfa3107bd87f9c2579b2631c94132bed0abea4"
|
||||
},
|
||||
{
|
||||
"path": "scripts/scrape.ts",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "a1a3d81d57d9e8ab1854ce3cb230bdd39ae1087ec50c9fe82cc58f5f2663ebeb"
|
||||
},
|
||||
{
|
||||
"path": "scripts/test-full.ts",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "76a647e840753621445c36894bff62e163f6a2e4d0860fa8e64d8df45fe21e08"
|
||||
},
|
||||
{
|
||||
"path": "scripts/test-minimal.ts",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "59e0b2319d3f7521b2a8a4fca2d779afaa157bf2d160160fdec8cb56bea30b4f"
|
||||
},
|
||||
{
|
||||
"path": "scripts/test-profile.ts",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "6cf0141581a9275bfa8a070a36212cef5f6417d64df3df3e614ec682008376b9"
|
||||
},
|
||||
{
|
||||
"path": "scripts/tsconfig.json",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "5f9a83c8caab167eb20defbb5afde58f2bb573a300af99654997dcb3372408e0"
|
||||
},
|
||||
{
|
||||
"path": "scripts/turndown-plugin-gfm.d.ts",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "c5001c059b160eff18a4097a8a0a7b96689b4ebc374543c7d5bf6e40b0d8a5ac"
|
||||
},
|
||||
{
|
||||
"path": "SKILL.md",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "7ff56c1c50697439875f4dd0a7f7697962c8ba2105a4f66ab7b170f5dcc762bd"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -3,6 +3,8 @@ name: web-automation
|
||||
description: Browse and scrape web pages using Playwright-compatible CloakBrowser. Use when automating web workflows, extracting rendered page content, handling authenticated sessions, or running multi-step browser flows.
|
||||
---
|
||||
|
||||
<!-- ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/web-automation/_source/pi/SKILL.md and run `pnpm run sync:pi`. -->
|
||||
|
||||
# Web Automation with CloakBrowser (Pi)
|
||||
|
||||
Automated web browsing and scraping for pi using the shared runtime bundle in `scripts/`.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
// ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/web-automation/shared/ and run `pnpm run sync:pi`.
|
||||
|
||||
/**
|
||||
* Authentication handler for web automation
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
// ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/web-automation/shared/ and run `pnpm run sync:pi`.
|
||||
|
||||
/**
|
||||
* Browser launcher using CloakBrowser with persistent profile
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
// ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/web-automation/shared/ and run `pnpm run sync:pi`.
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
@@ -18,11 +19,9 @@ async function main() {
|
||||
try {
|
||||
await import("cloakbrowser");
|
||||
await import("playwright-core");
|
||||
await import("better-sqlite3");
|
||||
await import("esbuild");
|
||||
} catch (error) {
|
||||
fail(
|
||||
"Missing dependency/config: web-automation requires cloakbrowser, playwright-core, better-sqlite3, and esbuild.",
|
||||
"Missing dependency/config: web-automation requires cloakbrowser and playwright-core.",
|
||||
error instanceof Error ? error.message : String(error)
|
||||
);
|
||||
}
|
||||
@@ -34,7 +33,6 @@ async function main() {
|
||||
}
|
||||
|
||||
process.stdout.write("OK: cloakbrowser + playwright-core installed\n");
|
||||
process.stdout.write("OK: better-sqlite3 + esbuild installed\n");
|
||||
process.stdout.write("OK: CloakBrowser integration detected in browse.ts\n");
|
||||
}
|
||||
|
||||
|
||||
Executable → Regular
+1
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
// ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/web-automation/shared/ and run `pnpm run sync:pi`.
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
// ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/web-automation/shared/ and run `pnpm run sync:pi`.
|
||||
|
||||
import parseArgs from 'minimist';
|
||||
import type { Page } from 'playwright-core';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "web-automation-scripts",
|
||||
"name": "@ai-coding-skills/web-automation-pi-mirror",
|
||||
"version": "1.0.0",
|
||||
"description": "Web browsing and scraping scripts using CloakBrowser",
|
||||
"type": "module",
|
||||
@@ -32,5 +32,6 @@
|
||||
"tsx": "^4.7.0",
|
||||
"typescript": "^5.3.0"
|
||||
},
|
||||
"packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34"
|
||||
"packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34",
|
||||
"private": true
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
// ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/web-automation/shared/ and run `pnpm run sync:pi`.
|
||||
|
||||
import { mkdirSync, writeFileSync } from 'fs';
|
||||
import { dirname, resolve } from 'path';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
// ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/web-automation/shared/ and run `pnpm run sync:pi`.
|
||||
|
||||
/**
|
||||
* Web scraper that extracts content to markdown
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/web-automation/shared/ and run `pnpm run sync:pi`.
|
||||
import { launchPersistentContext } from 'cloakbrowser';
|
||||
import { homedir } from 'os';
|
||||
import { join } from 'path';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/web-automation/shared/ and run `pnpm run sync:pi`.
|
||||
import { launch } from 'cloakbrowser';
|
||||
|
||||
async function test() {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/web-automation/shared/ and run `pnpm run sync:pi`.
|
||||
import { launchPersistentContext } from 'cloakbrowser';
|
||||
import { homedir } from 'os';
|
||||
import { join } from 'path';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/web-automation/shared/ and run `pnpm run sync:pi`.
|
||||
declare module 'turndown-plugin-gfm' {
|
||||
import TurndownService from 'turndown';
|
||||
|
||||
|
||||
Generated
+390
@@ -27,6 +27,166 @@ importers:
|
||||
specifier: 3.8.3
|
||||
version: 3.8.3
|
||||
|
||||
pi-package/skills/atlassian/scripts:
|
||||
dependencies:
|
||||
commander:
|
||||
specifier: ^13.1.0
|
||||
version: 13.1.0
|
||||
dotenv:
|
||||
specifier: ^16.4.7
|
||||
version: 16.6.1
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^24.3.0
|
||||
version: 24.12.2
|
||||
tsx:
|
||||
specifier: ^4.20.5
|
||||
version: 4.21.0
|
||||
typescript:
|
||||
specifier: ^5.9.2
|
||||
version: 5.9.3
|
||||
|
||||
pi-package/skills/web-automation/scripts:
|
||||
dependencies:
|
||||
'@mozilla/readability':
|
||||
specifier: ^0.5.0
|
||||
version: 0.5.0
|
||||
better-sqlite3:
|
||||
specifier: ^12.6.2
|
||||
version: 12.9.0
|
||||
cloakbrowser:
|
||||
specifier: ^0.3.22
|
||||
version: 0.3.26(playwright-core@1.59.1)
|
||||
jsdom:
|
||||
specifier: ^24.0.0
|
||||
version: 24.1.3
|
||||
minimist:
|
||||
specifier: ^1.2.8
|
||||
version: 1.2.8
|
||||
playwright-core:
|
||||
specifier: ^1.59.1
|
||||
version: 1.59.1
|
||||
turndown:
|
||||
specifier: ^7.1.2
|
||||
version: 7.2.4
|
||||
turndown-plugin-gfm:
|
||||
specifier: ^1.0.2
|
||||
version: 1.0.2
|
||||
devDependencies:
|
||||
'@types/jsdom':
|
||||
specifier: ^21.1.6
|
||||
version: 21.1.7
|
||||
'@types/minimist':
|
||||
specifier: ^1.2.5
|
||||
version: 1.2.5
|
||||
'@types/turndown':
|
||||
specifier: ^5.0.4
|
||||
version: 5.0.6
|
||||
esbuild:
|
||||
specifier: 0.27.0
|
||||
version: 0.27.0
|
||||
tsx:
|
||||
specifier: ^4.7.0
|
||||
version: 4.21.0
|
||||
typescript:
|
||||
specifier: ^5.3.0
|
||||
version: 5.9.3
|
||||
|
||||
skills/atlassian/claude-code/scripts:
|
||||
dependencies:
|
||||
commander:
|
||||
specifier: ^13.1.0
|
||||
version: 13.1.0
|
||||
dotenv:
|
||||
specifier: ^16.4.7
|
||||
version: 16.6.1
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^24.3.0
|
||||
version: 24.12.2
|
||||
tsx:
|
||||
specifier: ^4.20.5
|
||||
version: 4.21.0
|
||||
typescript:
|
||||
specifier: ^5.9.2
|
||||
version: 5.9.3
|
||||
|
||||
skills/atlassian/codex/scripts:
|
||||
dependencies:
|
||||
commander:
|
||||
specifier: ^13.1.0
|
||||
version: 13.1.0
|
||||
dotenv:
|
||||
specifier: ^16.4.7
|
||||
version: 16.6.1
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^24.3.0
|
||||
version: 24.12.2
|
||||
tsx:
|
||||
specifier: ^4.20.5
|
||||
version: 4.21.0
|
||||
typescript:
|
||||
specifier: ^5.9.2
|
||||
version: 5.9.3
|
||||
|
||||
skills/atlassian/cursor/scripts:
|
||||
dependencies:
|
||||
commander:
|
||||
specifier: ^13.1.0
|
||||
version: 13.1.0
|
||||
dotenv:
|
||||
specifier: ^16.4.7
|
||||
version: 16.6.1
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^24.3.0
|
||||
version: 24.12.2
|
||||
tsx:
|
||||
specifier: ^4.20.5
|
||||
version: 4.21.0
|
||||
typescript:
|
||||
specifier: ^5.9.2
|
||||
version: 5.9.3
|
||||
|
||||
skills/atlassian/opencode/scripts:
|
||||
dependencies:
|
||||
commander:
|
||||
specifier: ^13.1.0
|
||||
version: 13.1.0
|
||||
dotenv:
|
||||
specifier: ^16.4.7
|
||||
version: 16.6.1
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^24.3.0
|
||||
version: 24.12.2
|
||||
tsx:
|
||||
specifier: ^4.20.5
|
||||
version: 4.21.0
|
||||
typescript:
|
||||
specifier: ^5.9.2
|
||||
version: 5.9.3
|
||||
|
||||
skills/atlassian/pi/scripts:
|
||||
dependencies:
|
||||
commander:
|
||||
specifier: ^13.1.0
|
||||
version: 13.1.0
|
||||
dotenv:
|
||||
specifier: ^16.4.7
|
||||
version: 16.6.1
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^24.3.0
|
||||
version: 24.12.2
|
||||
tsx:
|
||||
specifier: ^4.20.5
|
||||
version: 4.21.0
|
||||
typescript:
|
||||
specifier: ^5.9.2
|
||||
version: 5.9.3
|
||||
|
||||
skills/atlassian/shared/scripts:
|
||||
dependencies:
|
||||
commander:
|
||||
@@ -46,6 +206,52 @@ importers:
|
||||
specifier: ^5.9.2
|
||||
version: 5.9.3
|
||||
|
||||
skills/web-automation/claude-code/scripts:
|
||||
dependencies:
|
||||
'@mozilla/readability':
|
||||
specifier: ^0.5.0
|
||||
version: 0.5.0
|
||||
better-sqlite3:
|
||||
specifier: ^12.6.2
|
||||
version: 12.9.0
|
||||
cloakbrowser:
|
||||
specifier: ^0.3.22
|
||||
version: 0.3.26(playwright-core@1.59.1)
|
||||
jsdom:
|
||||
specifier: ^24.0.0
|
||||
version: 24.1.3
|
||||
minimist:
|
||||
specifier: ^1.2.8
|
||||
version: 1.2.8
|
||||
playwright-core:
|
||||
specifier: ^1.59.1
|
||||
version: 1.59.1
|
||||
turndown:
|
||||
specifier: ^7.1.2
|
||||
version: 7.2.4
|
||||
turndown-plugin-gfm:
|
||||
specifier: ^1.0.2
|
||||
version: 1.0.2
|
||||
devDependencies:
|
||||
'@types/jsdom':
|
||||
specifier: ^21.1.6
|
||||
version: 21.1.7
|
||||
'@types/minimist':
|
||||
specifier: ^1.2.5
|
||||
version: 1.2.5
|
||||
'@types/turndown':
|
||||
specifier: ^5.0.4
|
||||
version: 5.0.6
|
||||
esbuild:
|
||||
specifier: 0.27.0
|
||||
version: 0.27.0
|
||||
tsx:
|
||||
specifier: ^4.7.0
|
||||
version: 4.21.0
|
||||
typescript:
|
||||
specifier: ^5.3.0
|
||||
version: 5.9.3
|
||||
|
||||
skills/web-automation/codex/scripts:
|
||||
dependencies:
|
||||
'@mozilla/readability':
|
||||
@@ -92,6 +298,190 @@ importers:
|
||||
specifier: ^5.3.0
|
||||
version: 5.9.3
|
||||
|
||||
skills/web-automation/cursor/scripts:
|
||||
dependencies:
|
||||
'@mozilla/readability':
|
||||
specifier: ^0.5.0
|
||||
version: 0.5.0
|
||||
better-sqlite3:
|
||||
specifier: ^12.6.2
|
||||
version: 12.9.0
|
||||
cloakbrowser:
|
||||
specifier: ^0.3.22
|
||||
version: 0.3.26(playwright-core@1.59.1)
|
||||
jsdom:
|
||||
specifier: ^24.0.0
|
||||
version: 24.1.3
|
||||
minimist:
|
||||
specifier: ^1.2.8
|
||||
version: 1.2.8
|
||||
playwright-core:
|
||||
specifier: ^1.59.1
|
||||
version: 1.59.1
|
||||
turndown:
|
||||
specifier: ^7.1.2
|
||||
version: 7.2.4
|
||||
turndown-plugin-gfm:
|
||||
specifier: ^1.0.2
|
||||
version: 1.0.2
|
||||
devDependencies:
|
||||
'@types/jsdom':
|
||||
specifier: ^21.1.6
|
||||
version: 21.1.7
|
||||
'@types/minimist':
|
||||
specifier: ^1.2.5
|
||||
version: 1.2.5
|
||||
'@types/turndown':
|
||||
specifier: ^5.0.4
|
||||
version: 5.0.6
|
||||
esbuild:
|
||||
specifier: 0.27.0
|
||||
version: 0.27.0
|
||||
tsx:
|
||||
specifier: ^4.7.0
|
||||
version: 4.21.0
|
||||
typescript:
|
||||
specifier: ^5.3.0
|
||||
version: 5.9.3
|
||||
|
||||
skills/web-automation/opencode/scripts:
|
||||
dependencies:
|
||||
'@mozilla/readability':
|
||||
specifier: ^0.5.0
|
||||
version: 0.5.0
|
||||
better-sqlite3:
|
||||
specifier: ^12.6.2
|
||||
version: 12.9.0
|
||||
cloakbrowser:
|
||||
specifier: ^0.3.22
|
||||
version: 0.3.26(playwright-core@1.59.1)
|
||||
jsdom:
|
||||
specifier: ^24.0.0
|
||||
version: 24.1.3
|
||||
minimist:
|
||||
specifier: ^1.2.8
|
||||
version: 1.2.8
|
||||
playwright-core:
|
||||
specifier: ^1.59.1
|
||||
version: 1.59.1
|
||||
turndown:
|
||||
specifier: ^7.1.2
|
||||
version: 7.2.4
|
||||
turndown-plugin-gfm:
|
||||
specifier: ^1.0.2
|
||||
version: 1.0.2
|
||||
devDependencies:
|
||||
'@types/jsdom':
|
||||
specifier: ^21.1.6
|
||||
version: 21.1.7
|
||||
'@types/minimist':
|
||||
specifier: ^1.2.5
|
||||
version: 1.2.5
|
||||
'@types/turndown':
|
||||
specifier: ^5.0.4
|
||||
version: 5.0.6
|
||||
esbuild:
|
||||
specifier: 0.27.0
|
||||
version: 0.27.0
|
||||
tsx:
|
||||
specifier: ^4.7.0
|
||||
version: 4.21.0
|
||||
typescript:
|
||||
specifier: ^5.3.0
|
||||
version: 5.9.3
|
||||
|
||||
skills/web-automation/pi/scripts:
|
||||
dependencies:
|
||||
'@mozilla/readability':
|
||||
specifier: ^0.5.0
|
||||
version: 0.5.0
|
||||
better-sqlite3:
|
||||
specifier: ^12.6.2
|
||||
version: 12.9.0
|
||||
cloakbrowser:
|
||||
specifier: ^0.3.22
|
||||
version: 0.3.26(playwright-core@1.59.1)
|
||||
jsdom:
|
||||
specifier: ^24.0.0
|
||||
version: 24.1.3
|
||||
minimist:
|
||||
specifier: ^1.2.8
|
||||
version: 1.2.8
|
||||
playwright-core:
|
||||
specifier: ^1.59.1
|
||||
version: 1.59.1
|
||||
turndown:
|
||||
specifier: ^7.1.2
|
||||
version: 7.2.4
|
||||
turndown-plugin-gfm:
|
||||
specifier: ^1.0.2
|
||||
version: 1.0.2
|
||||
devDependencies:
|
||||
'@types/jsdom':
|
||||
specifier: ^21.1.6
|
||||
version: 21.1.7
|
||||
'@types/minimist':
|
||||
specifier: ^1.2.5
|
||||
version: 1.2.5
|
||||
'@types/turndown':
|
||||
specifier: ^5.0.4
|
||||
version: 5.0.6
|
||||
esbuild:
|
||||
specifier: 0.27.0
|
||||
version: 0.27.0
|
||||
tsx:
|
||||
specifier: ^4.7.0
|
||||
version: 4.21.0
|
||||
typescript:
|
||||
specifier: ^5.3.0
|
||||
version: 5.9.3
|
||||
|
||||
skills/web-automation/shared:
|
||||
dependencies:
|
||||
'@mozilla/readability':
|
||||
specifier: ^0.5.0
|
||||
version: 0.5.0
|
||||
better-sqlite3:
|
||||
specifier: ^12.6.2
|
||||
version: 12.9.0
|
||||
cloakbrowser:
|
||||
specifier: ^0.3.22
|
||||
version: 0.3.26(playwright-core@1.59.1)
|
||||
jsdom:
|
||||
specifier: ^24.0.0
|
||||
version: 24.1.3
|
||||
minimist:
|
||||
specifier: ^1.2.8
|
||||
version: 1.2.8
|
||||
playwright-core:
|
||||
specifier: ^1.59.1
|
||||
version: 1.59.1
|
||||
turndown:
|
||||
specifier: ^7.1.2
|
||||
version: 7.2.4
|
||||
turndown-plugin-gfm:
|
||||
specifier: ^1.0.2
|
||||
version: 1.0.2
|
||||
devDependencies:
|
||||
'@types/jsdom':
|
||||
specifier: ^21.1.6
|
||||
version: 21.1.7
|
||||
'@types/minimist':
|
||||
specifier: ^1.2.5
|
||||
version: 1.2.5
|
||||
'@types/turndown':
|
||||
specifier: ^5.0.4
|
||||
version: 5.0.6
|
||||
esbuild:
|
||||
specifier: 0.27.0
|
||||
version: 0.27.0
|
||||
tsx:
|
||||
specifier: ^4.7.0
|
||||
version: 4.21.0
|
||||
typescript:
|
||||
specifier: ^5.3.0
|
||||
version: 5.9.3
|
||||
|
||||
packages:
|
||||
|
||||
'@asamuzakjp/css-color@3.2.0':
|
||||
|
||||
+31
-33
@@ -1,41 +1,39 @@
|
||||
# pnpm workspace — non-mutating, exclusion-only policy (M1)
|
||||
# pnpm workspace — includes canonical sources + uniquely-named generated variants (M3)
|
||||
#
|
||||
# IMPORTANT: This file implements the M1 exclusion-only policy described in
|
||||
# docs/DEVELOPMENT.md. Only canonical *source* packages are included as
|
||||
# workspace members. All generated agent-variant directories are excluded via
|
||||
# negative globs so pnpm never hoists, modifies, or renames their
|
||||
# package.json files.
|
||||
# M3 update: every generated agent-variant package.json now carries a unique
|
||||
# private name (@ai-coding-skills/<skill>-<agent>), which allows pnpm to include
|
||||
# ALL generated roots alongside the canonical sources. The M1 negative-glob
|
||||
# exclusions are replaced by explicit positive includes.
|
||||
#
|
||||
# Canonical source packages (M1):
|
||||
# - skills/atlassian/shared/scripts → the shared Atlassian runtime
|
||||
# - skills/web-automation/codex/scripts → provisional canonical copy until
|
||||
# M3 finalises the web-automation source-of-truth and renames packages
|
||||
# Canonical source packages (never generated):
|
||||
# - skills/atlassian/shared/scripts → atlassian-skill-scripts (source + tests)
|
||||
# - skills/web-automation/shared → web-automation-scripts (source template)
|
||||
#
|
||||
# Generated agent-variant directories (excluded):
|
||||
# skills/*/codex (except web-automation/codex above)
|
||||
# skills/*/claude-code
|
||||
# skills/*/cursor
|
||||
# skills/*/opencode
|
||||
# skills/*/pi
|
||||
# pi-package/**
|
||||
#
|
||||
# The package-name collision between atlassian variants is sidestepped in M1
|
||||
# by this exclusion. A generator-driven rename to give each variant a unique
|
||||
# name is deferred to M3.
|
||||
# Generated agent-variant directories (now uniquely named, included):
|
||||
# - skills/atlassian/{claude-code,codex,cursor,opencode,pi}/scripts
|
||||
# - skills/web-automation/{claude-code,codex,cursor,opencode,pi}/scripts
|
||||
# - pi-package/skills/atlassian/scripts (@ai-coding-skills/atlassian-pi-mirror)
|
||||
# - pi-package/skills/web-automation/scripts (@ai-coding-skills/web-automation-pi-mirror)
|
||||
|
||||
packages:
|
||||
# ── Canonical source packages ────────────────────────────────────────────
|
||||
- "skills/atlassian/shared/scripts"
|
||||
- "skills/web-automation/codex/scripts"
|
||||
- "skills/web-automation/shared"
|
||||
|
||||
# ── Exclude generated agent-variant directories ──────────────────────────
|
||||
- "!skills/atlassian/codex/scripts"
|
||||
- "!skills/atlassian/claude-code/scripts"
|
||||
- "!skills/atlassian/cursor/scripts"
|
||||
- "!skills/atlassian/opencode/scripts"
|
||||
- "!skills/atlassian/pi/scripts"
|
||||
- "!skills/web-automation/claude-code/scripts"
|
||||
- "!skills/web-automation/cursor/scripts"
|
||||
- "!skills/web-automation/opencode/scripts"
|
||||
- "!skills/web-automation/pi/scripts"
|
||||
- "!pi-package/**"
|
||||
# ── Generated atlassian agent-variant packages ───────────────────────────
|
||||
- "skills/atlassian/claude-code/scripts"
|
||||
- "skills/atlassian/codex/scripts"
|
||||
- "skills/atlassian/cursor/scripts"
|
||||
- "skills/atlassian/opencode/scripts"
|
||||
- "skills/atlassian/pi/scripts"
|
||||
|
||||
# ── Generated web-automation agent-variant packages ──────────────────────
|
||||
- "skills/web-automation/claude-code/scripts"
|
||||
- "skills/web-automation/codex/scripts"
|
||||
- "skills/web-automation/cursor/scripts"
|
||||
- "skills/web-automation/opencode/scripts"
|
||||
- "skills/web-automation/pi/scripts"
|
||||
|
||||
# ── Generated pi-package mirrors ─────────────────────────────────────────
|
||||
- "pi-package/skills/atlassian/scripts"
|
||||
- "pi-package/skills/web-automation/scripts"
|
||||
|
||||
@@ -0,0 +1,600 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* generate-skills.mjs — shared-source generator for agent variants (M3, S-302)
|
||||
*
|
||||
* Generates every agent-variant directory (`skills/<skill>/<agent>/`) and
|
||||
* `pi-package/skills/<skill>/` mirror from canonical sources. Generated files
|
||||
* carry file-type-aware headers; each generated root gets a non-self-referential
|
||||
* `.generated-manifest.json`.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/generate-skills.mjs # regenerate everything
|
||||
* pnpm run sync:pi # same via pnpm alias
|
||||
*
|
||||
* Exported helpers (used by verify-generated.mjs and tests):
|
||||
* detectFileType(filePath) → string
|
||||
* applyHeader(content, fileType, canonicalHint) → string
|
||||
* makePackageJsonContent(sourcePkg, skillName, agentName) → object
|
||||
* getGeneratedRoots(repoRoot?) → string[]
|
||||
* buildManifest(generatedRootAbs, generatedRootRel) → Promise<object>
|
||||
* generateSkills(repoRoot, options?) → Promise<{generatedRoots: string[]}>
|
||||
*/
|
||||
|
||||
import {
|
||||
lstat,
|
||||
mkdir,
|
||||
readdir,
|
||||
readFile,
|
||||
rm,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import crypto from "node:crypto";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
const REPO_ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname), "..");
|
||||
|
||||
// ── Constants ──────────────────────────────────────────────────────────────
|
||||
|
||||
const MANIFEST_SCHEMA =
|
||||
"https://ai-coding-skills.dev/schemas/generated-manifest/v1.json";
|
||||
const MANIFEST_GENERATOR = "scripts/generate-skills.mjs";
|
||||
const MANIFEST_FILENAME = ".generated-manifest.json";
|
||||
|
||||
const AGENTS = ["claude-code", "codex", "cursor", "opencode", "pi"];
|
||||
|
||||
/**
|
||||
* Canonical list of all generated roots, relative to repo root.
|
||||
* Verifier uses this to know which directories to walk.
|
||||
*/
|
||||
const GENERATED_ROOTS = [
|
||||
// atlassian agent variants
|
||||
"skills/atlassian/claude-code",
|
||||
"skills/atlassian/codex",
|
||||
"skills/atlassian/cursor",
|
||||
"skills/atlassian/opencode",
|
||||
"skills/atlassian/pi",
|
||||
// web-automation agent variants
|
||||
"skills/web-automation/claude-code",
|
||||
"skills/web-automation/codex",
|
||||
"skills/web-automation/cursor",
|
||||
"skills/web-automation/opencode",
|
||||
"skills/web-automation/pi",
|
||||
// create-plan agent variants
|
||||
"skills/create-plan/claude-code",
|
||||
"skills/create-plan/codex",
|
||||
"skills/create-plan/cursor",
|
||||
"skills/create-plan/opencode",
|
||||
"skills/create-plan/pi",
|
||||
// do-task agent variants
|
||||
"skills/do-task/claude-code",
|
||||
"skills/do-task/codex",
|
||||
"skills/do-task/cursor",
|
||||
"skills/do-task/opencode",
|
||||
"skills/do-task/pi",
|
||||
// implement-plan agent variants
|
||||
"skills/implement-plan/claude-code",
|
||||
"skills/implement-plan/codex",
|
||||
"skills/implement-plan/cursor",
|
||||
"skills/implement-plan/opencode",
|
||||
"skills/implement-plan/pi",
|
||||
// reviewer-runtime pi variant
|
||||
"skills/reviewer-runtime/pi",
|
||||
// pi-package mirrors
|
||||
"pi-package/skills/atlassian",
|
||||
"pi-package/skills/create-plan",
|
||||
"pi-package/skills/do-task",
|
||||
"pi-package/skills/implement-plan",
|
||||
"pi-package/skills/web-automation",
|
||||
];
|
||||
|
||||
// ── File-type detection ────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Classify a file path into a header policy category.
|
||||
* @param {string} filePath - Relative or absolute path to the file.
|
||||
* @returns {'markdown'|'shell'|'ts'|'js'|'json'|'yaml'|'jsonc'|'unknown'}
|
||||
*/
|
||||
export function detectFileType(filePath) {
|
||||
const base = path.basename(filePath);
|
||||
const ext = path.extname(base).toLowerCase();
|
||||
|
||||
if (ext === ".md") return "markdown";
|
||||
if (ext === ".sh") return "shell";
|
||||
if (ext === ".ts") return "ts";
|
||||
if (ext === ".js") return "js";
|
||||
if (ext === ".jsonc") return "jsonc";
|
||||
if (ext === ".json") return "json";
|
||||
if (ext === ".yaml" || ext === ".yml") return "yaml";
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
// ── Header insertion ──────────────────────────────────────────────────────
|
||||
|
||||
const HEADER_MSG = (hint) =>
|
||||
`⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in ${hint} and run \`pnpm run sync:pi\`.`;
|
||||
|
||||
/**
|
||||
* Insert a file-type-aware generated-file header into content.
|
||||
*
|
||||
* Policy:
|
||||
* markdown → HTML comment after YAML front matter (or at top if no front matter)
|
||||
* shell → # comment after shebang (never before it)
|
||||
* ts/js → // comment at top
|
||||
* jsonc → // comment at top
|
||||
* yaml → # comment at top (pnpm-lock.yaml skipped by caller)
|
||||
* json → no header (recorded in .generated-manifest.json)
|
||||
* unknown → no header
|
||||
*
|
||||
* @param {string} content - Original file content.
|
||||
* @param {string} fileType - Output of detectFileType().
|
||||
* @param {string} canonicalHint - Human-readable hint to canonical source location.
|
||||
* @returns {string} Content with header inserted, or original if no header applies.
|
||||
*/
|
||||
export function applyHeader(content, fileType, canonicalHint) {
|
||||
const msg = HEADER_MSG(canonicalHint);
|
||||
|
||||
switch (fileType) {
|
||||
case "markdown": {
|
||||
// Insert HTML comment after YAML front matter closing ---, or at top
|
||||
if (content.startsWith("---\n")) {
|
||||
const closingIdx = content.indexOf("\n---\n", 4);
|
||||
if (closingIdx !== -1) {
|
||||
const after = closingIdx + 5; // length of "\n---\n"
|
||||
const before = content.slice(0, after);
|
||||
const rest = content.slice(after);
|
||||
// Ensure the comment is on its own line, with a blank line before/after
|
||||
return `${before}\n<!-- ${msg} -->\n${rest}`;
|
||||
}
|
||||
}
|
||||
return `<!-- ${msg} -->\n${content}`;
|
||||
}
|
||||
|
||||
case "shell": {
|
||||
// Insert # comment AFTER shebang line (never before it)
|
||||
const lines = content.split("\n");
|
||||
if (lines[0].startsWith("#!")) {
|
||||
return [lines[0], `# ${msg}`, ...lines.slice(1)].join("\n");
|
||||
}
|
||||
return `# ${msg}\n${content}`;
|
||||
}
|
||||
|
||||
case "ts":
|
||||
case "js":
|
||||
case "jsonc": {
|
||||
// Insert after shebang if present (TypeScript requires #! on line 1)
|
||||
const lines = content.split("\n");
|
||||
if (lines[0].startsWith("#!")) {
|
||||
return [lines[0], `// ${msg}`, ...lines.slice(1)].join("\n");
|
||||
}
|
||||
return `// ${msg}\n${content}`;
|
||||
}
|
||||
|
||||
case "yaml": {
|
||||
return `# ${msg}\n${content}`;
|
||||
}
|
||||
|
||||
case "json":
|
||||
case "unknown":
|
||||
default:
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
// ── package.json transformation ───────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Produce a modified package.json object with a unique scoped name and
|
||||
* `"private": true` for an agent-variant generated root.
|
||||
*
|
||||
* @param {object} sourcePkg - Parsed source package.json object.
|
||||
* @param {string} skillName - Skill identifier (e.g. "atlassian").
|
||||
* @param {string} agentName - Agent identifier (e.g. "claude-code").
|
||||
* @returns {object} New object (source is not mutated).
|
||||
*/
|
||||
export function makePackageJsonContent(sourcePkg, skillName, agentName) {
|
||||
return {
|
||||
...sourcePkg,
|
||||
name: `@ai-coding-skills/${skillName}-${agentName}`,
|
||||
private: true,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Generated-root list ───────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Return the authoritative list of generated roots as repo-relative paths.
|
||||
* Callers (verify-generated.mjs) use this to know which directories to walk.
|
||||
*
|
||||
* @returns {string[]} Sorted array of relative paths.
|
||||
*/
|
||||
export function getGeneratedRoots() {
|
||||
return [...GENERATED_ROOTS];
|
||||
}
|
||||
|
||||
// ── Manifest construction ─────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Walk a directory recursively and return all file paths (abs).
|
||||
* Skips node_modules.
|
||||
*/
|
||||
async function walkDir(dir) {
|
||||
const results = [];
|
||||
let entries;
|
||||
try {
|
||||
entries = await readdir(dir, { withFileTypes: true });
|
||||
} catch {
|
||||
return results;
|
||||
}
|
||||
for (const entry of entries) {
|
||||
if (entry.name === "node_modules") continue;
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
const sub = await walkDir(full);
|
||||
results.push(...sub);
|
||||
} else if (entry.isFile()) {
|
||||
results.push(full);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a .generated-manifest.json object for a generated root.
|
||||
*
|
||||
* The manifest lists every file in the generated root EXCEPT itself.
|
||||
* Files are sorted by relative path for stable canonical serialization.
|
||||
*
|
||||
* @param {string} generatedRootAbs - Absolute path to the generated root directory.
|
||||
* @param {string} generatedRootRel - Repo-relative path (e.g. "skills/create-plan/pi").
|
||||
* @returns {Promise<object>} Manifest object (not yet serialized to disk).
|
||||
*/
|
||||
export async function buildManifest(generatedRootAbs, generatedRootRel) {
|
||||
const allFiles = await walkDir(generatedRootAbs);
|
||||
const entries = [];
|
||||
|
||||
for (const absPath of allFiles) {
|
||||
const relPath = path.relative(generatedRootAbs, absPath).replace(/\\/g, "/");
|
||||
|
||||
// Non-self-referential: the manifest never lists itself
|
||||
if (relPath === MANIFEST_FILENAME) continue;
|
||||
|
||||
const contentBuf = await readFile(absPath);
|
||||
const sha256 = crypto.createHash("sha256").update(contentBuf).digest("hex");
|
||||
|
||||
const st = await lstat(absPath);
|
||||
const mode = (st.mode & 0o777).toString(8).padStart(3, "0");
|
||||
|
||||
entries.push({
|
||||
path: relPath,
|
||||
kind: "file",
|
||||
mode,
|
||||
sha256,
|
||||
});
|
||||
}
|
||||
|
||||
entries.sort((a, b) => a.path.localeCompare(b.path));
|
||||
|
||||
return {
|
||||
$schema: MANIFEST_SCHEMA,
|
||||
generator: MANIFEST_GENERATOR,
|
||||
generatedRoot: generatedRootRel,
|
||||
files: entries,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Core generation helpers ───────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Copy a single file from source to destination, inserting a header.
|
||||
* Skips header for:
|
||||
* - pnpm-lock.yaml (managed by pnpm, would be stripped on next install)
|
||||
* - JSON files (per header policy, no in-file header)
|
||||
* - node_modules (never copied)
|
||||
*/
|
||||
async function copyWithHeader(srcAbs, dstAbs, canonicalHint) {
|
||||
const basename = path.basename(srcAbs);
|
||||
// Skip pnpm-lock.yaml header (pnpm regenerates without comment)
|
||||
const skipHeader = basename === "pnpm-lock.yaml";
|
||||
|
||||
const raw = await readFile(srcAbs, "utf8");
|
||||
const fileType = skipHeader ? "unknown" : detectFileType(srcAbs);
|
||||
const content = applyHeader(raw, fileType, canonicalHint);
|
||||
|
||||
await mkdir(path.dirname(dstAbs), { recursive: true });
|
||||
await writeFile(dstAbs, content, "utf8");
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively copy a directory tree, adding headers to text files.
|
||||
*/
|
||||
async function copyDirWithHeaders(srcDir, dstDir, canonicalHint) {
|
||||
const entries = await readdir(srcDir, { withFileTypes: true });
|
||||
await mkdir(dstDir, { recursive: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.name === "node_modules") continue;
|
||||
const src = path.join(srcDir, entry.name);
|
||||
const dst = path.join(dstDir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
await copyDirWithHeaders(src, dst, canonicalHint);
|
||||
} else if (entry.isFile()) {
|
||||
await copyWithHeader(src, dst, canonicalHint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a .generated-manifest.json file into a generated root.
|
||||
*/
|
||||
async function writeManifest(generatedRootAbs, generatedRootRel) {
|
||||
const manifest = await buildManifest(generatedRootAbs, generatedRootRel);
|
||||
const dstPath = path.join(generatedRootAbs, MANIFEST_FILENAME);
|
||||
await writeFile(dstPath, JSON.stringify(manifest, null, 2) + "\n", "utf8");
|
||||
}
|
||||
|
||||
// ── Skill-family generators ───────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Generate one agent variant for a "skills-only" skill
|
||||
* (create-plan, do-task, implement-plan).
|
||||
*
|
||||
* Canonical source: skills/<skill>/_source/<agent>/
|
||||
* Generated root: skills/<skill>/<agent>/
|
||||
*
|
||||
* @param {string} writeRoot - Root directory to write output into (defaults to repoRoot).
|
||||
*/
|
||||
async function generateSkillOnlyVariant(repoRoot, writeRoot, skillName, agentName, generatedRootRel) {
|
||||
const sourceDir = path.join(repoRoot, "skills", skillName, "_source", agentName);
|
||||
const targetDir = path.join(writeRoot, generatedRootRel);
|
||||
const canonicalHint = `skills/${skillName}/_source/${agentName}/`;
|
||||
|
||||
// Clear previous generated content (preserve node_modules if any)
|
||||
await clearGeneratedRoot(targetDir);
|
||||
|
||||
// Copy all files from source with headers
|
||||
await copyDirWithHeaders(sourceDir, targetDir, canonicalHint);
|
||||
|
||||
// Write manifest
|
||||
await writeManifest(targetDir, generatedRootRel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate one agent variant for a "scripts+skill" skill
|
||||
* (atlassian, web-automation).
|
||||
*
|
||||
* For atlassian:
|
||||
* - SKILL.md from skills/atlassian/_source/<agent>/SKILL.md
|
||||
* - scripts/* from skills/atlassian/shared/scripts/
|
||||
* (only src/, tsconfig.json, pnpm-lock.yaml — not tests/ or scripts/sync-*)
|
||||
*
|
||||
* For web-automation:
|
||||
* - SKILL.md from skills/web-automation/_source/<agent>/SKILL.md
|
||||
* - scripts/* from skills/web-automation/shared/
|
||||
*
|
||||
* @param {string} [packageAgentName] - Override the agent name used only for the
|
||||
* package.json `name` field. Defaults to `agentName`. Use this to give
|
||||
* pi-package mirrors a distinct name (e.g. "pi-mirror") so workspace package
|
||||
* names are unique even when two roots share the same source agent.
|
||||
*/
|
||||
async function generateScriptsSkillVariant(
|
||||
repoRoot,
|
||||
writeRoot,
|
||||
skillName,
|
||||
agentName,
|
||||
generatedRootRel,
|
||||
config,
|
||||
packageAgentName,
|
||||
) {
|
||||
const targetDir = path.join(writeRoot, generatedRootRel);
|
||||
await clearGeneratedRoot(targetDir);
|
||||
|
||||
// 1. Copy SKILL.md from per-agent canonical source
|
||||
const skillMdSrc = path.join(repoRoot, "skills", skillName, "_source", agentName, "SKILL.md");
|
||||
const skillMdDst = path.join(targetDir, "SKILL.md");
|
||||
const skillMdHint = `skills/${skillName}/_source/${agentName}/SKILL.md`;
|
||||
await copyWithHeader(skillMdSrc, skillMdDst, skillMdHint);
|
||||
|
||||
// 2. Copy scripts
|
||||
const scriptsTargetDir = path.join(targetDir, "scripts");
|
||||
await mkdir(scriptsTargetDir, { recursive: true });
|
||||
|
||||
const canonicalScripts = path.join(repoRoot, config.canonicalScripts);
|
||||
const scriptsHint = `${config.canonicalScripts}/`;
|
||||
|
||||
for (const entry of config.scriptFiles) {
|
||||
const srcPath = path.join(canonicalScripts, entry);
|
||||
const dstPath = path.join(scriptsTargetDir, entry); // scriptsTargetDir already uses writeRoot
|
||||
|
||||
// Check if source exists
|
||||
let st;
|
||||
try {
|
||||
st = await lstat(srcPath);
|
||||
} catch {
|
||||
continue; // skip if file doesn't exist in canonical
|
||||
}
|
||||
|
||||
if (st.isDirectory()) {
|
||||
await copyDirWithHeaders(srcPath, dstPath, scriptsHint);
|
||||
} else {
|
||||
await copyWithHeader(srcPath, dstPath, scriptsHint);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Generate modified package.json
|
||||
const srcPkgPath = path.join(canonicalScripts, "package.json");
|
||||
const srcPkg = JSON.parse(await readFile(srcPkgPath, "utf8"));
|
||||
|
||||
let targetPkg = makePackageJsonContent(srcPkg, skillName, packageAgentName ?? agentName);
|
||||
|
||||
// For atlassian variants, strip test/sync scripts (not in agent variants)
|
||||
if (skillName === "atlassian") {
|
||||
const agentScripts = {};
|
||||
for (const [k, v] of Object.entries(targetPkg.scripts ?? {})) {
|
||||
if (k !== "test" && k !== "sync:agents") agentScripts[k] = v;
|
||||
}
|
||||
targetPkg = { ...targetPkg, scripts: agentScripts };
|
||||
}
|
||||
|
||||
const dstPkgPath = path.join(scriptsTargetDir, "package.json");
|
||||
await writeFile(dstPkgPath, JSON.stringify(targetPkg, null, 2) + "\n", "utf8");
|
||||
|
||||
// 4. Write manifest
|
||||
await writeManifest(targetDir, generatedRootRel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the reviewer-runtime pi variant from the non-Pi canonical scripts.
|
||||
*
|
||||
* Canonical source: skills/reviewer-runtime/{run-review.sh,notify-telegram.sh}
|
||||
* Generated root: skills/reviewer-runtime/pi/
|
||||
*
|
||||
* The pi variant is byte-identical to the canonical except for:
|
||||
* - A generated # comment after the shebang (replaces old "keep in sync" comment)
|
||||
*
|
||||
* @param {string} writeRoot - Root directory to write output into (defaults to repoRoot).
|
||||
*/
|
||||
async function generateReviewerRuntimePi(repoRoot, writeRoot) {
|
||||
const srcDir = path.join(repoRoot, "skills", "reviewer-runtime");
|
||||
const dstDir = path.join(writeRoot, "skills", "reviewer-runtime", "pi");
|
||||
const canonicalHint = "skills/reviewer-runtime/";
|
||||
|
||||
// Clear old generated content (preserve tests/ which is canonical)
|
||||
await clearGeneratedRoot(dstDir);
|
||||
|
||||
for (const fname of ["run-review.sh", "notify-telegram.sh"]) {
|
||||
const srcPath = path.join(srcDir, fname);
|
||||
const dstPath = path.join(dstDir, fname);
|
||||
|
||||
const raw = await readFile(srcPath, "utf8");
|
||||
let content = applyHeader(raw, "shell", `${canonicalHint}${fname}`);
|
||||
|
||||
await mkdir(path.dirname(dstPath), { recursive: true });
|
||||
await writeFile(dstPath, content, "utf8");
|
||||
|
||||
// Preserve executable bit
|
||||
const st = await lstat(srcPath);
|
||||
if (st.mode & 0o100) {
|
||||
const { chmod } = await import("node:fs/promises");
|
||||
await chmod(dstPath, 0o755);
|
||||
}
|
||||
}
|
||||
|
||||
// Write manifest (generatedRootRel is always relative to repo root, not writeRoot)
|
||||
await writeManifest(dstDir, "skills/reviewer-runtime/pi");
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear generated content in a root, preserving:
|
||||
* - node_modules (installed by pnpm)
|
||||
* - .generated-manifest.json (will be rewritten after generation)
|
||||
*/
|
||||
async function clearGeneratedRoot(rootDir) {
|
||||
let entries;
|
||||
try {
|
||||
entries = await readdir(rootDir, { withFileTypes: true });
|
||||
} catch {
|
||||
return; // dir doesn't exist yet — nothing to clear
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.name === "node_modules") continue;
|
||||
if (entry.name === MANIFEST_FILENAME) continue;
|
||||
await rm(path.join(rootDir, entry.name), { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
// ── Skill configurations ──────────────────────────────────────────────────
|
||||
|
||||
const SCRIPTS_SKILL_CONFIGS = {
|
||||
atlassian: {
|
||||
canonicalScripts: "skills/atlassian/shared/scripts",
|
||||
// Files to copy from canonicalScripts into each agent's scripts/ dir
|
||||
scriptFiles: ["src", "tsconfig.json", "pnpm-lock.yaml"],
|
||||
},
|
||||
"web-automation": {
|
||||
canonicalScripts: "skills/web-automation/shared",
|
||||
scriptFiles: [
|
||||
"auth.ts",
|
||||
"browse.ts",
|
||||
"check-install.js",
|
||||
"extract.js",
|
||||
"flow.ts",
|
||||
"scan-local-app.ts",
|
||||
"scrape.ts",
|
||||
"test-full.ts",
|
||||
"test-minimal.ts",
|
||||
"test-profile.ts",
|
||||
"tsconfig.json",
|
||||
"turndown-plugin-gfm.d.ts",
|
||||
"pnpm-lock.yaml",
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// ── Main generator ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Regenerate all agent variants from canonical sources.
|
||||
*
|
||||
* @param {string} repoRoot - Absolute path to repo root (canonical sources are read from here).
|
||||
* @param {object} [options]
|
||||
* @param {boolean} [options.dryRun] - If true, don't write files (future use).
|
||||
* @param {string} [options.targetRoot] - Write generated output here instead of `repoRoot`.
|
||||
* Canonical sources are always read from `repoRoot`. Use this to generate into a
|
||||
* temp directory for drift detection without modifying on-disk files.
|
||||
* @returns {Promise<{generatedRoots: string[]}>}
|
||||
*/
|
||||
export async function generateSkills(repoRoot = REPO_ROOT, options = {}) {
|
||||
const { dryRun = false, targetRoot } = options;
|
||||
const writeRoot = targetRoot ?? repoRoot;
|
||||
if (dryRun) {
|
||||
return { generatedRoots: GENERATED_ROOTS };
|
||||
}
|
||||
|
||||
const skillOnlySkills = ["create-plan", "do-task", "implement-plan"];
|
||||
|
||||
// 1. Generate skill-only skills (create-plan, do-task, implement-plan)
|
||||
for (const skillName of skillOnlySkills) {
|
||||
for (const agentName of AGENTS) {
|
||||
const rootRel = `skills/${skillName}/${agentName}`;
|
||||
await generateSkillOnlyVariant(repoRoot, writeRoot, skillName, agentName, rootRel);
|
||||
}
|
||||
// pi-package mirror (same source as pi variant)
|
||||
const piPackageRel = `pi-package/skills/${skillName}`;
|
||||
await generateSkillOnlyVariant(repoRoot, writeRoot, skillName, "pi", piPackageRel);
|
||||
}
|
||||
|
||||
// 2. Generate scripts skills (atlassian, web-automation)
|
||||
for (const [skillName, config] of Object.entries(SCRIPTS_SKILL_CONFIGS)) {
|
||||
for (const agentName of AGENTS) {
|
||||
const rootRel = `skills/${skillName}/${agentName}`;
|
||||
await generateScriptsSkillVariant(repoRoot, writeRoot, skillName, agentName, rootRel, config);
|
||||
}
|
||||
// pi-package mirror: same source as pi variant but a distinct package name
|
||||
// ("pi-mirror" suffix) so the workspace has no duplicate package names.
|
||||
const piPackageRel = `pi-package/skills/${skillName}`;
|
||||
await generateScriptsSkillVariant(repoRoot, writeRoot, skillName, "pi", piPackageRel, config, "pi-mirror");
|
||||
}
|
||||
|
||||
// 3. Generate reviewer-runtime pi variant
|
||||
await generateReviewerRuntimePi(repoRoot, writeRoot);
|
||||
|
||||
return { generatedRoots: GENERATED_ROOTS };
|
||||
}
|
||||
|
||||
// ── CLI entry point ────────────────────────────────────────────────────────
|
||||
|
||||
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
try {
|
||||
const result = await generateSkills(REPO_ROOT);
|
||||
console.log(
|
||||
`Generated ${result.generatedRoots.length} roots from canonical sources.`,
|
||||
);
|
||||
process.exit(0);
|
||||
} catch (err) {
|
||||
console.error("generate-skills: fatal error:", err.message ?? err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -47,13 +47,20 @@ const SKIP_PATHS = new Set([
|
||||
"pi-package",
|
||||
]);
|
||||
|
||||
// Also skip any _source/ subdirectory within skills — canonical source files
|
||||
// use relative paths calibrated to the generated location (one level shallower).
|
||||
const SKIP_SEGMENT = "_source";
|
||||
|
||||
function shouldSkip(absPath) {
|
||||
const rel = path.relative(REPO_ROOT, absPath);
|
||||
for (const skip of SKIP_PATHS) {
|
||||
if (rel === skip || rel.startsWith(skip + path.sep)) return true;
|
||||
}
|
||||
const parts = rel.split(path.sep);
|
||||
return parts.includes("node_modules");
|
||||
if (parts.includes("node_modules")) return true;
|
||||
// Skip canonical _source/ directories (links calibrated to generated location)
|
||||
if (parts.includes(SKIP_SEGMENT)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function collectMarkdownFiles(dir) {
|
||||
|
||||
@@ -279,7 +279,7 @@ async function findClaudeCodeSuperpowersPluginRoots(homeDir) {
|
||||
|
||||
async function findCursorSuperpowersPluginRoots(homeDir) {
|
||||
const pluginRoot = path.join(homeDir, ".cursor", "plugins", "cache", "cursor-public", "superpowers");
|
||||
let entries = [];
|
||||
let entries;
|
||||
try {
|
||||
entries = await readdir(pluginRoot, { withFileTypes: true });
|
||||
} catch (error) {
|
||||
@@ -598,7 +598,10 @@ export async function executeOperation(op) {
|
||||
if (op.action === "unsupported" || op.status === "skipped") return { ...op, status: "skipped" };
|
||||
if (op.kind === "package-skill") return { ...op, status: "included" };
|
||||
if (op.kind === "sync-pi-package") {
|
||||
runCommand(path.join(op.repoRoot, "scripts", "sync-pi-package-skills.sh"), [], { cwd: op.repoRoot });
|
||||
// Use the canonical generator (pnpm run sync:pi / node scripts/generate-skills.mjs).
|
||||
// The legacy sync-pi-package-skills.sh is retired in M3; it bypassed the
|
||||
// generator and copied skills/*/pi into pi-package directly, corrupting manifests.
|
||||
runCommand(process.execPath, [path.join(op.repoRoot, "scripts", "generate-skills.mjs")], { cwd: op.repoRoot });
|
||||
return { ...op, status: "ok" };
|
||||
}
|
||||
if (op.kind === "pi-package") {
|
||||
|
||||
@@ -267,7 +267,6 @@ async function main() {
|
||||
const removeAnswer = await rl.question(`Remove Superpowers for ${prompt.clientId}/${prompt.scope} too? (yes/no) [no]: `);
|
||||
if (removeAnswer.trim().toLowerCase() === "yes") {
|
||||
const scope = resolveClientScope(prompt.clientId, prompt.scope, process.cwd());
|
||||
const client = CLIENTS[prompt.clientId];
|
||||
const target = `${scope.skillsRoot}/superpowers`;
|
||||
plan.operations.push({ kind: "superpowers", clientId: prompt.clientId, scope: prompt.scope, action: "remove", target, skillsRoot: scope.skillsRoot });
|
||||
}
|
||||
|
||||
@@ -0,0 +1,350 @@
|
||||
/**
|
||||
* Unit tests for generate-skills.mjs — RED phase of TDD.
|
||||
*
|
||||
* Tests cover:
|
||||
* - detectFileType: classification of files by extension
|
||||
* - applyHeader: insertion per file-type-aware policy
|
||||
* - makePackageJsonContent: unique name + private:true
|
||||
* - getGeneratedRoots: returns the canonical generated-root list
|
||||
*/
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtemp, mkdir, writeFile, rm } from "node:fs/promises";
|
||||
import crypto from "node:crypto";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const SCRIPTS_DIR = path.resolve(__dirname, "..");
|
||||
|
||||
const {
|
||||
detectFileType,
|
||||
applyHeader,
|
||||
makePackageJsonContent,
|
||||
getGeneratedRoots,
|
||||
buildManifest,
|
||||
} = await import(`${SCRIPTS_DIR}/generate-skills.mjs`);
|
||||
|
||||
// ── detectFileType ────────────────────────────────────────────────────────
|
||||
|
||||
test("detectFileType: .md files → markdown", () => {
|
||||
assert.equal(detectFileType("SKILL.md"), "markdown");
|
||||
assert.equal(detectFileType("templates/milestone-plan.md"), "markdown");
|
||||
assert.equal(detectFileType("README.md"), "markdown");
|
||||
});
|
||||
|
||||
test("detectFileType: .sh files → shell", () => {
|
||||
assert.equal(detectFileType("run-review.sh"), "shell");
|
||||
assert.equal(detectFileType("scripts/install.sh"), "shell");
|
||||
});
|
||||
|
||||
test("detectFileType: .ts and .d.ts files → ts", () => {
|
||||
assert.equal(detectFileType("src/cli.ts"), "ts");
|
||||
assert.equal(detectFileType("turndown-plugin-gfm.d.ts"), "ts");
|
||||
assert.equal(detectFileType("auth.ts"), "ts");
|
||||
});
|
||||
|
||||
test("detectFileType: .js files → js", () => {
|
||||
assert.equal(detectFileType("check-install.js"), "js");
|
||||
assert.equal(detectFileType("extract.js"), "js");
|
||||
});
|
||||
|
||||
test("detectFileType: .json files → json", () => {
|
||||
assert.equal(detectFileType("package.json"), "json");
|
||||
assert.equal(detectFileType("tsconfig.json"), "json");
|
||||
});
|
||||
|
||||
test("detectFileType: .yaml and .yml files → yaml", () => {
|
||||
assert.equal(detectFileType("pnpm-lock.yaml"), "yaml");
|
||||
assert.equal(detectFileType("other.yml"), "yaml");
|
||||
});
|
||||
|
||||
test("detectFileType: .jsonc files → jsonc", () => {
|
||||
assert.equal(detectFileType(".markdownlint.jsonc"), "jsonc");
|
||||
});
|
||||
|
||||
test("detectFileType: unknown extension → unknown", () => {
|
||||
assert.equal(detectFileType("Makefile"), "unknown");
|
||||
assert.equal(detectFileType("somefile"), "unknown");
|
||||
});
|
||||
|
||||
// ── applyHeader ───────────────────────────────────────────────────────────
|
||||
|
||||
test("applyHeader: markdown with YAML front matter inserts HTML comment after closing ---", () => {
|
||||
const content = "---\nname: create-plan\n---\n\n# Create Plan\n\nBody.\n";
|
||||
const result = applyHeader(content, "markdown", "skills/create-plan/_source/claude-code/SKILL.md");
|
||||
|
||||
// Front matter block preserved verbatim at start
|
||||
assert.ok(result.startsWith("---\nname: create-plan\n---\n"), "front matter at start");
|
||||
|
||||
// HTML comment present
|
||||
assert.ok(result.includes("<!-- ⚠️"), "HTML comment present");
|
||||
|
||||
// Comment comes after front matter closer and before the title
|
||||
const commentIdx = result.indexOf("<!-- ⚠️");
|
||||
const titleIdx = result.indexOf("# Create Plan");
|
||||
assert.ok(commentIdx !== -1 && titleIdx !== -1, "both positions found");
|
||||
assert.ok(commentIdx < titleIdx, "comment before title");
|
||||
|
||||
// Comment must NOT appear before the first ---
|
||||
assert.ok(commentIdx > 3, "comment not before front matter");
|
||||
});
|
||||
|
||||
test("applyHeader: markdown without front matter inserts HTML comment at top", () => {
|
||||
const content = "# Heading\n\nContent\n";
|
||||
const result = applyHeader(content, "markdown", "skills/create-plan/_source/claude-code/templates/plan.md");
|
||||
assert.ok(result.startsWith("<!-- ⚠️"), "comment at very top");
|
||||
assert.ok(result.includes("# Heading"), "original content preserved");
|
||||
});
|
||||
|
||||
test("applyHeader: shell with shebang inserts # comment after shebang", () => {
|
||||
const content = "#!/usr/bin/env bash\nset -euo pipefail\necho hello\n";
|
||||
const result = applyHeader(content, "shell", "skills/reviewer-runtime/run-review.sh");
|
||||
|
||||
assert.ok(result.startsWith("#!/usr/bin/env bash\n"), "shebang preserved at top");
|
||||
assert.ok(result.includes("# ⚠️"), "hash comment present");
|
||||
|
||||
const shebangEnd = result.indexOf("\n") + 1;
|
||||
const commentStart = result.indexOf("# ⚠️");
|
||||
assert.ok(commentStart === shebangEnd, "comment immediately after shebang line");
|
||||
});
|
||||
|
||||
test("applyHeader: ts file inserts // comment at top", () => {
|
||||
const content = "import path from 'path';\nexport const x = 1;\n";
|
||||
const result = applyHeader(content, "ts", "skills/atlassian/shared/scripts/src/cli.ts");
|
||||
assert.ok(result.startsWith("// ⚠️"), "// comment at top");
|
||||
assert.ok(result.includes("import path from 'path';"), "original content preserved");
|
||||
});
|
||||
|
||||
test("applyHeader: ts file with shebang inserts // comment after shebang", () => {
|
||||
const content = "#!/usr/bin/env npx tsx\nimport foo from 'foo';\n";
|
||||
const result = applyHeader(content, "ts", "auth.ts");
|
||||
assert.ok(result.startsWith("#!/usr/bin/env npx tsx\n"), "shebang preserved at top");
|
||||
assert.ok(result.includes("// ⚠️"), "// comment present");
|
||||
const shebangEnd = result.indexOf("\n") + 1;
|
||||
const commentStart = result.indexOf("// ⚠️");
|
||||
assert.ok(commentStart === shebangEnd, "// comment immediately after shebang");
|
||||
});
|
||||
|
||||
test("applyHeader: js file with shebang inserts // comment after shebang", () => {
|
||||
const content = "#!/usr/bin/env node\nconst x = 1;\n";
|
||||
const result = applyHeader(content, "js", "check-install.js");
|
||||
assert.ok(result.startsWith("#!/usr/bin/env node\n"), "shebang preserved at top");
|
||||
assert.ok(result.includes("// ⚠️"), "// comment present");
|
||||
});
|
||||
|
||||
test("applyHeader: js file inserts // comment at top", () => {
|
||||
const content = "const x = require('x');\n";
|
||||
const result = applyHeader(content, "js", "check-install.js");
|
||||
assert.ok(result.startsWith("// ⚠️"), "// comment at top");
|
||||
});
|
||||
|
||||
test("applyHeader: yaml file inserts # comment at top", () => {
|
||||
const content = "lockfileVersion: '9.0'\n\npackages:\n";
|
||||
const result = applyHeader(content, "yaml", "pnpm-lock.yaml");
|
||||
assert.ok(result.startsWith("# ⚠️"), "# comment at top");
|
||||
});
|
||||
|
||||
test("applyHeader: jsonc file inserts // comment at top", () => {
|
||||
const content = "// existing comment\n{\n \"key\": true\n}\n";
|
||||
const result = applyHeader(content, "jsonc", ".markdownlint.jsonc");
|
||||
assert.ok(result.startsWith("// ⚠️"), "// comment at top");
|
||||
});
|
||||
|
||||
test("applyHeader: json file returns content unchanged (no header)", () => {
|
||||
const content = '{\n "name": "test"\n}\n';
|
||||
const result = applyHeader(content, "json", "package.json");
|
||||
assert.equal(result, content, "JSON file must not be modified");
|
||||
});
|
||||
|
||||
test("applyHeader: unknown type returns content unchanged", () => {
|
||||
const content = "raw content\n";
|
||||
const result = applyHeader(content, "unknown", "Makefile");
|
||||
assert.equal(result, content);
|
||||
});
|
||||
|
||||
test("applyHeader: header contains canonical source hint", () => {
|
||||
const hint = "skills/create-plan/_source/pi/SKILL.md";
|
||||
const content = "---\nname: create-plan\n---\n\n# Title\n";
|
||||
const result = applyHeader(content, "markdown", hint);
|
||||
assert.ok(result.includes(hint), "canonical hint appears in header");
|
||||
});
|
||||
|
||||
test("applyHeader: never inserts header before shebang", () => {
|
||||
const content = "#!/usr/bin/env bash\nset -euo pipefail\n";
|
||||
const result = applyHeader(content, "shell", "run.sh");
|
||||
assert.ok(result.startsWith("#!/"), "shebang still first");
|
||||
});
|
||||
|
||||
// ── makePackageJsonContent ────────────────────────────────────────────────
|
||||
|
||||
test("makePackageJsonContent: renames name to scoped unique form", () => {
|
||||
const src = { name: "atlassian-skill-scripts", version: "1.0.0" };
|
||||
const result = makePackageJsonContent(src, "atlassian", "claude-code");
|
||||
assert.equal(result.name, "@ai-coding-skills/atlassian-claude-code");
|
||||
});
|
||||
|
||||
test("makePackageJsonContent: adds private:true", () => {
|
||||
const src = { name: "web-automation-scripts", version: "1.0.0" };
|
||||
const result = makePackageJsonContent(src, "web-automation", "codex");
|
||||
assert.equal(result.private, true);
|
||||
});
|
||||
|
||||
test("makePackageJsonContent: preserves all other top-level fields", () => {
|
||||
const src = {
|
||||
name: "atlassian-skill-scripts",
|
||||
version: "1.0.0",
|
||||
type: "module",
|
||||
scripts: { typecheck: "tsc --noEmit" },
|
||||
dependencies: { commander: "^13.1.0" },
|
||||
devDependencies: { tsx: "^4.20.5" },
|
||||
packageManager: "pnpm@10.18.1",
|
||||
};
|
||||
const result = makePackageJsonContent(src, "atlassian", "cursor");
|
||||
assert.equal(result.version, "1.0.0");
|
||||
assert.equal(result.type, "module");
|
||||
assert.deepEqual(result.scripts, { typecheck: "tsc --noEmit" });
|
||||
assert.deepEqual(result.dependencies, { commander: "^13.1.0" });
|
||||
assert.equal(result.packageManager, "pnpm@10.18.1");
|
||||
});
|
||||
|
||||
test("makePackageJsonContent: pi-package mirror uses -pi agent suffix", () => {
|
||||
const src = { name: "atlassian-skill-scripts" };
|
||||
const result = makePackageJsonContent(src, "atlassian", "pi");
|
||||
assert.equal(result.name, "@ai-coding-skills/atlassian-pi");
|
||||
});
|
||||
|
||||
test("makePackageJsonContent: does not mutate the source object", () => {
|
||||
const src = { name: "original", version: "1.0.0" };
|
||||
makePackageJsonContent(src, "atlassian", "codex");
|
||||
assert.equal(src.name, "original", "source object not mutated");
|
||||
});
|
||||
|
||||
// ── getGeneratedRoots ─────────────────────────────────────────────────────
|
||||
|
||||
test("getGeneratedRoots: returns at least one root per agent per skills skill", () => {
|
||||
const roots = getGeneratedRoots();
|
||||
assert.ok(Array.isArray(roots), "returns array");
|
||||
assert.ok(roots.length > 0, "non-empty");
|
||||
|
||||
const agents = ["claude-code", "codex", "cursor", "opencode", "pi"];
|
||||
const skillsWithAgents = ["create-plan", "do-task", "implement-plan", "atlassian", "web-automation"];
|
||||
for (const skill of skillsWithAgents) {
|
||||
for (const agent of agents) {
|
||||
const expected = `skills/${skill}/${agent}`;
|
||||
assert.ok(roots.includes(expected), `missing generated root: ${expected}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("getGeneratedRoots: includes pi-package mirrors for all skills", () => {
|
||||
const roots = getGeneratedRoots();
|
||||
const piPackageSkills = ["atlassian", "create-plan", "do-task", "implement-plan", "web-automation"];
|
||||
for (const skill of piPackageSkills) {
|
||||
const expected = `pi-package/skills/${skill}`;
|
||||
assert.ok(roots.includes(expected), `missing pi-package mirror root: ${expected}`);
|
||||
}
|
||||
});
|
||||
|
||||
test("getGeneratedRoots: includes reviewer-runtime/pi", () => {
|
||||
const roots = getGeneratedRoots();
|
||||
assert.ok(roots.includes("skills/reviewer-runtime/pi"), "reviewer-runtime/pi missing");
|
||||
});
|
||||
|
||||
test("getGeneratedRoots: does not include _source or shared directories", () => {
|
||||
const roots = getGeneratedRoots();
|
||||
for (const r of roots) {
|
||||
assert.ok(!r.includes("_source"), `root should not contain _source: ${r}`);
|
||||
assert.ok(!r.endsWith("/shared"), `root should not be shared: ${r}`);
|
||||
assert.ok(!r.includes("shared/scripts"), `root should not contain shared/scripts: ${r}`);
|
||||
}
|
||||
});
|
||||
|
||||
// ── buildManifest ─────────────────────────────────────────────────────────
|
||||
|
||||
test("buildManifest: returns object with $schema and generator markers", async () => {
|
||||
const dir = await mkdtemp(path.join(tmpdir(), "manifest-test-"));
|
||||
try {
|
||||
await writeFile(path.join(dir, "SKILL.md"), "---\nname: test\n---\n# Test\n");
|
||||
await mkdir(path.join(dir, "templates"), { recursive: true });
|
||||
await writeFile(path.join(dir, "templates", "plan.md"), "# Plan\n");
|
||||
// Write .generated-manifest.json itself (should be excluded from listing)
|
||||
await writeFile(path.join(dir, ".generated-manifest.json"), "{}");
|
||||
|
||||
const manifest = await buildManifest(dir, "skills/test/claude-code");
|
||||
|
||||
assert.ok(manifest.$schema, "$schema field present");
|
||||
assert.ok(manifest.generator, "generator field present");
|
||||
assert.equal(manifest.generatedRoot, "skills/test/claude-code");
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("buildManifest: does not include .generated-manifest.json in files list", async () => {
|
||||
const dir = await mkdtemp(path.join(tmpdir(), "manifest-self-"));
|
||||
try {
|
||||
await writeFile(path.join(dir, "SKILL.md"), "# skill\n");
|
||||
await writeFile(path.join(dir, ".generated-manifest.json"), "{}");
|
||||
|
||||
const manifest = await buildManifest(dir, "skills/test/pi");
|
||||
|
||||
const paths = manifest.files.map((f) => f.path);
|
||||
assert.ok(!paths.includes(".generated-manifest.json"), "manifest must not list itself");
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("buildManifest: files list includes path, kind, mode, sha256", async () => {
|
||||
const dir = await mkdtemp(path.join(tmpdir(), "manifest-fields-"));
|
||||
try {
|
||||
await writeFile(path.join(dir, "SKILL.md"), "# skill\n");
|
||||
|
||||
const manifest = await buildManifest(dir, "skills/test/codex");
|
||||
|
||||
assert.equal(manifest.files.length, 1);
|
||||
const entry = manifest.files[0];
|
||||
assert.equal(entry.path, "SKILL.md");
|
||||
assert.equal(entry.kind, "file");
|
||||
assert.ok(typeof entry.mode === "string" && entry.mode.match(/^\d{3}$/), "mode is 3-digit octal string");
|
||||
assert.ok(typeof entry.sha256 === "string" && entry.sha256.length === 64, "sha256 is 64-char hex");
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("buildManifest: files are sorted by path", async () => {
|
||||
const dir = await mkdtemp(path.join(tmpdir(), "manifest-sorted-"));
|
||||
try {
|
||||
await mkdir(path.join(dir, "templates"), { recursive: true });
|
||||
await writeFile(path.join(dir, "SKILL.md"), "# skill\n");
|
||||
await writeFile(path.join(dir, "templates", "z.md"), "z\n");
|
||||
await writeFile(path.join(dir, "templates", "a.md"), "a\n");
|
||||
|
||||
const manifest = await buildManifest(dir, "skills/test/cursor");
|
||||
|
||||
const paths = manifest.files.map((f) => f.path);
|
||||
const sorted = [...paths].sort();
|
||||
assert.deepEqual(paths, sorted, "files must be sorted by path");
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("buildManifest: sha256 matches actual file content", async () => {
|
||||
const dir = await mkdtemp(path.join(tmpdir(), "manifest-hash-"));
|
||||
try {
|
||||
const content = "---\nname: test\n---\n# Title\n";
|
||||
await writeFile(path.join(dir, "SKILL.md"), content);
|
||||
|
||||
const manifest = await buildManifest(dir, "skills/test/opencode");
|
||||
|
||||
const expected = crypto.createHash("sha256").update(content, "utf8").digest("hex");
|
||||
assert.equal(manifest.files[0].sha256, expected, "sha256 matches file content");
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,348 @@
|
||||
/**
|
||||
* Unit tests for verify-generated.mjs — RED phase of TDD.
|
||||
*
|
||||
* Key contract from acceptance criteria:
|
||||
* - A stray file under skills/<skill>/_source/ or skills/<skill>/shared/
|
||||
* does NOT cause verify:generated to flag it as stale.
|
||||
* - A stray file under skills/<skill>/<agent>/ (other than
|
||||
* .generated-manifest.json) DOES cause verify:generated to flag it.
|
||||
*/
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtemp, mkdir, writeFile, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const SCRIPTS_DIR = path.resolve(__dirname, "..");
|
||||
|
||||
const { verifyGenerated } = await import(`${SCRIPTS_DIR}/verify-generated.mjs`);
|
||||
|
||||
// ── Stray-file detection boundary tests ──────────────────────────────────
|
||||
|
||||
test("verifyGenerated: stray file in _source/ is NOT flagged as stale", async () => {
|
||||
// This uses the real repo's canonical source - if stray file under _source
|
||||
// is added, verify-generated should not complain about it.
|
||||
|
||||
// We test this by verifying the function signature accepts a repoRoot and
|
||||
// generatedRoots override, and that the verifier only walks declared roots.
|
||||
// (Full integration test against the real repo runs in pnpm run verify:generated)
|
||||
|
||||
const dir = await mkdtemp(path.join(tmpdir(), "vg-stray-source-"));
|
||||
try {
|
||||
// Create a fake skill structure with _source/ and a generated root
|
||||
const skillName = "test-skill";
|
||||
const agentName = "claude-code";
|
||||
|
||||
const sourceDir = path.join(dir, "skills", skillName, "_source", agentName);
|
||||
const agentDir = path.join(dir, "skills", skillName, agentName);
|
||||
const sharedDir = path.join(dir, "skills", skillName, "shared");
|
||||
|
||||
await mkdir(sourceDir, { recursive: true });
|
||||
await mkdir(sharedDir, { recursive: true });
|
||||
await mkdir(agentDir, { recursive: true });
|
||||
|
||||
const skillContent = "---\nname: test-skill\n---\n\n# Test Skill\n";
|
||||
await writeFile(path.join(sourceDir, "SKILL.md"), skillContent);
|
||||
|
||||
// Add a STRAY file in _source/ — this should NOT trigger stale detection
|
||||
await writeFile(path.join(sourceDir, "STRAY.md"), "stray content");
|
||||
|
||||
// Add a STRAY file in shared/ — this should NOT trigger stale detection
|
||||
await writeFile(path.join(sharedDir, "shared-stray.txt"), "shared stray");
|
||||
|
||||
// The generated root is EMPTY (no manifest, no files) — but we're testing
|
||||
// that _source/ and shared/ stray files don't appear in stale detection.
|
||||
// We can test this indirectly: verifyGenerated with no declared roots for
|
||||
// this dir returns no stale errors about _source/ or shared/.
|
||||
const result = await verifyGenerated(dir, {
|
||||
// Override generated roots to be empty for this minimal test
|
||||
generatedRootsOverride: [],
|
||||
});
|
||||
|
||||
// No stale errors from _source/ or shared/
|
||||
const sourceErrors = result.errors.filter(
|
||||
(e) => e.includes("_source") || e.includes("shared"),
|
||||
);
|
||||
assert.equal(sourceErrors.length, 0, `unexpected stale errors from _source/shared: ${JSON.stringify(sourceErrors)}`);
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("verifyGenerated: stray file in agent dir IS flagged as stale", async () => {
|
||||
const dir = await mkdtemp(path.join(tmpdir(), "vg-stray-agent-"));
|
||||
try {
|
||||
const skillName = "test-skill";
|
||||
const agentName = "claude-code";
|
||||
|
||||
const sourceDir = path.join(dir, "skills", skillName, "_source", agentName);
|
||||
const agentDir = path.join(dir, "skills", skillName, agentName);
|
||||
|
||||
await mkdir(sourceDir, { recursive: true });
|
||||
await mkdir(agentDir, { recursive: true });
|
||||
|
||||
const skillContent = "---\nname: test-skill\n---\n\n# Test Skill\n";
|
||||
await writeFile(path.join(sourceDir, "SKILL.md"), skillContent);
|
||||
|
||||
// Generate the agent dir with proper content + manifest
|
||||
const headerLine =
|
||||
`<!-- ⚠️ GENERATED FILE – do not edit directly. ` +
|
||||
`Edit the canonical source in skills/${skillName}/_source/${agentName}/SKILL.md ` +
|
||||
`and run \`pnpm run sync:pi\`. -->`;
|
||||
const generatedContent = `---\nname: test-skill\n---\n\n${headerLine}\n\n# Test Skill\n`;
|
||||
await writeFile(path.join(agentDir, "SKILL.md"), generatedContent);
|
||||
|
||||
// Add a STRAY file in the agent dir — this SHOULD be flagged
|
||||
await writeFile(path.join(agentDir, "STRAY.md"), "stray content");
|
||||
|
||||
// Write a manifest that does NOT include STRAY.md
|
||||
const { buildManifest } = await import(`${SCRIPTS_DIR}/generate-skills.mjs`);
|
||||
const manifest = await buildManifest(agentDir, `skills/${skillName}/${agentName}`);
|
||||
// Remove STRAY.md from manifest (simulate pre-stray-add manifest)
|
||||
manifest.files = manifest.files.filter((f) => f.path !== "STRAY.md");
|
||||
await writeFile(
|
||||
path.join(agentDir, ".generated-manifest.json"),
|
||||
JSON.stringify(manifest, null, 2) + "\n",
|
||||
);
|
||||
|
||||
const result = await verifyGenerated(dir, {
|
||||
generatedRootsOverride: [`skills/${skillName}/${agentName}`],
|
||||
});
|
||||
|
||||
assert.equal(result.ok, false, "should fail when stray file present");
|
||||
const strayError = result.errors.some((e) => e.includes("STRAY.md"));
|
||||
assert.ok(strayError, `STRAY.md should appear in errors: ${JSON.stringify(result.errors)}`);
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("verifyGenerated: .generated-manifest.json is excluded from stale-file detection", async () => {
|
||||
// Even though .generated-manifest.json is in the generated root, it should
|
||||
// not be considered a "stale file" just because it's not in the files list
|
||||
const dir = await mkdtemp(path.join(tmpdir(), "vg-manifest-self-"));
|
||||
try {
|
||||
const skillName = "test-skill";
|
||||
const agentName = "pi";
|
||||
|
||||
const sourceDir = path.join(dir, "skills", skillName, "_source", agentName);
|
||||
const agentDir = path.join(dir, "skills", skillName, agentName);
|
||||
|
||||
await mkdir(sourceDir, { recursive: true });
|
||||
await mkdir(agentDir, { recursive: true });
|
||||
|
||||
const skillContent = "---\nname: test-skill\n---\n\n# Test Skill\n";
|
||||
await writeFile(path.join(sourceDir, "SKILL.md"), skillContent);
|
||||
|
||||
const headerLine =
|
||||
`<!-- ⚠️ GENERATED FILE – do not edit directly. ` +
|
||||
`Edit the canonical source in skills/${skillName}/_source/${agentName}/SKILL.md ` +
|
||||
`and run \`pnpm run sync:pi\`. -->`;
|
||||
const generatedContent = `---\nname: test-skill\n---\n\n${headerLine}\n\n# Test Skill\n`;
|
||||
await writeFile(path.join(agentDir, "SKILL.md"), generatedContent);
|
||||
|
||||
// Write manifest (will include SKILL.md, not itself)
|
||||
const { buildManifest } = await import(`${SCRIPTS_DIR}/generate-skills.mjs`);
|
||||
const manifest = await buildManifest(agentDir, `skills/${skillName}/${agentName}`);
|
||||
await writeFile(
|
||||
path.join(agentDir, ".generated-manifest.json"),
|
||||
JSON.stringify(manifest, null, 2) + "\n",
|
||||
);
|
||||
|
||||
const result = await verifyGenerated(dir, {
|
||||
generatedRootsOverride: [`skills/${skillName}/${agentName}`],
|
||||
});
|
||||
|
||||
// Should pass — .generated-manifest.json is excluded from stale detection
|
||||
const manifestErrors = result.errors.filter((e) => e.includes(".generated-manifest.json"));
|
||||
assert.equal(manifestErrors.length, 0, `manifest file should not appear as stale: ${JSON.stringify(manifestErrors)}`);
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("verifyGenerated: missing file from manifest is flagged as deleted", async () => {
|
||||
const dir = await mkdtemp(path.join(tmpdir(), "vg-missing-file-"));
|
||||
try {
|
||||
const skillName = "test-skill";
|
||||
const agentName = "cursor";
|
||||
|
||||
const sourceDir = path.join(dir, "skills", skillName, "_source", agentName);
|
||||
const agentDir = path.join(dir, "skills", skillName, agentName);
|
||||
|
||||
await mkdir(sourceDir, { recursive: true });
|
||||
await mkdir(agentDir, { recursive: true });
|
||||
|
||||
const skillContent = "---\nname: test-skill\n---\n\n# Test Skill\n";
|
||||
await writeFile(path.join(sourceDir, "SKILL.md"), skillContent);
|
||||
await writeFile(path.join(agentDir, "SKILL.md"), "generated content\n");
|
||||
|
||||
// Manifest claims templates/plan.md exists, but the file doesn't
|
||||
const manifest = {
|
||||
$schema: "https://ai-coding-skills.dev/schemas/generated-manifest/v1.json",
|
||||
generator: "scripts/generate-skills.mjs",
|
||||
generatedRoot: `skills/${skillName}/${agentName}`,
|
||||
files: [
|
||||
{ path: "SKILL.md", kind: "file", mode: "644", sha256: "aaa" },
|
||||
{ path: "templates/plan.md", kind: "file", mode: "644", sha256: "bbb" },
|
||||
],
|
||||
};
|
||||
await writeFile(
|
||||
path.join(agentDir, ".generated-manifest.json"),
|
||||
JSON.stringify(manifest, null, 2) + "\n",
|
||||
);
|
||||
|
||||
const result = await verifyGenerated(dir, {
|
||||
generatedRootsOverride: [`skills/${skillName}/${agentName}`],
|
||||
});
|
||||
|
||||
assert.equal(result.ok, false, "should fail on missing file");
|
||||
const missingError = result.errors.some((e) => e.includes("templates/plan.md"));
|
||||
assert.ok(missingError, `missing file should appear in errors: ${JSON.stringify(result.errors)}`);
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("verifyGenerated: content mismatch is flagged", async () => {
|
||||
const dir = await mkdtemp(path.join(tmpdir(), "vg-content-mismatch-"));
|
||||
try {
|
||||
const skillName = "test-skill";
|
||||
const agentName = "opencode";
|
||||
|
||||
const sourceDir = path.join(dir, "skills", skillName, "_source", agentName);
|
||||
const agentDir = path.join(dir, "skills", skillName, agentName);
|
||||
|
||||
await mkdir(sourceDir, { recursive: true });
|
||||
await mkdir(agentDir, { recursive: true });
|
||||
|
||||
await writeFile(path.join(sourceDir, "SKILL.md"), "---\nname: test-skill\n---\n\n# Original\n");
|
||||
// Agent dir has DIFFERENT content than what manifest says
|
||||
await writeFile(path.join(agentDir, "SKILL.md"), "---\nname: test-skill\n---\n\n# Modified!\n");
|
||||
|
||||
const manifest = {
|
||||
$schema: "https://ai-coding-skills.dev/schemas/generated-manifest/v1.json",
|
||||
generator: "scripts/generate-skills.mjs",
|
||||
generatedRoot: `skills/${skillName}/${agentName}`,
|
||||
files: [
|
||||
{
|
||||
path: "SKILL.md",
|
||||
kind: "file",
|
||||
mode: "644",
|
||||
// SHA of the ORIGINAL content (not what's on disk)
|
||||
sha256: "0000000000000000000000000000000000000000000000000000000000000000",
|
||||
},
|
||||
],
|
||||
};
|
||||
await writeFile(
|
||||
path.join(agentDir, ".generated-manifest.json"),
|
||||
JSON.stringify(manifest, null, 2) + "\n",
|
||||
);
|
||||
|
||||
const result = await verifyGenerated(dir, {
|
||||
generatedRootsOverride: [`skills/${skillName}/${agentName}`],
|
||||
});
|
||||
|
||||
assert.equal(result.ok, false, "should fail on content mismatch");
|
||||
const mismatchError = result.errors.some((e) => e.includes("SKILL.md"));
|
||||
assert.ok(mismatchError, `content mismatch should appear in errors: ${JSON.stringify(result.errors)}`);
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("verifyGenerated: manifest entry with wrong sha256 is flagged even when paths match", async () => {
|
||||
const dir = await mkdtemp(path.join(tmpdir(), "vg-manifest-sha-"));
|
||||
try {
|
||||
const skillName = "test-skill";
|
||||
const agentName = "codex";
|
||||
|
||||
const sourceDir = path.join(dir, "skills", skillName, "_source", agentName);
|
||||
const agentDir = path.join(dir, "skills", skillName, agentName);
|
||||
|
||||
await mkdir(sourceDir, { recursive: true });
|
||||
await mkdir(agentDir, { recursive: true });
|
||||
|
||||
const content = "---\nname: test-skill\n---\n\n# Test Skill\n";
|
||||
await writeFile(path.join(sourceDir, "SKILL.md"), content);
|
||||
await writeFile(path.join(agentDir, "SKILL.md"), content);
|
||||
|
||||
// Manifest has correct path but deliberately wrong sha256 (simulates corrupted metadata)
|
||||
const manifest = {
|
||||
$schema: "https://ai-coding-skills.dev/schemas/generated-manifest/v1.json",
|
||||
generator: "scripts/generate-skills.mjs",
|
||||
generatedRoot: `skills/${skillName}/${agentName}`,
|
||||
files: [
|
||||
{
|
||||
path: "SKILL.md",
|
||||
kind: "file",
|
||||
mode: "644",
|
||||
sha256: "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
|
||||
},
|
||||
],
|
||||
};
|
||||
await writeFile(
|
||||
path.join(agentDir, ".generated-manifest.json"),
|
||||
JSON.stringify(manifest, null, 2) + "\n",
|
||||
);
|
||||
|
||||
const result = await verifyGenerated(dir, {
|
||||
generatedRootsOverride: [`skills/${skillName}/${agentName}`],
|
||||
});
|
||||
|
||||
// Even though file paths match, the sha256 mismatch in the manifest should be detected
|
||||
assert.equal(result.ok, false, "should fail on sha256 mismatch in manifest");
|
||||
const sha256Error = result.errors.some(
|
||||
(e) => e.includes("sha256") || e.includes("SKILL.md"),
|
||||
);
|
||||
assert.ok(sha256Error, `sha256 mismatch should appear in errors: ${JSON.stringify(result.errors)}`);
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("verifyGenerated: manifest entry with wrong mode is flagged even when paths match", async () => {
|
||||
const dir = await mkdtemp(path.join(tmpdir(), "vg-manifest-mode-"));
|
||||
try {
|
||||
const skillName = "test-skill";
|
||||
const agentName = "cursor";
|
||||
|
||||
const agentDir = path.join(dir, "skills", skillName, agentName);
|
||||
await mkdir(agentDir, { recursive: true });
|
||||
|
||||
const content = "# Test Skill\n";
|
||||
await writeFile(path.join(agentDir, "SKILL.md"), content);
|
||||
|
||||
// Build a correct manifest first (with real sha256)
|
||||
const { buildManifest } = await import(`${SCRIPTS_DIR}/generate-skills.mjs`);
|
||||
const correctManifest = await buildManifest(agentDir, `skills/${skillName}/${agentName}`);
|
||||
|
||||
// Tamper: change the mode field for the SKILL.md entry
|
||||
const tamperedManifest = {
|
||||
...correctManifest,
|
||||
files: correctManifest.files.map((f) =>
|
||||
f.path === "SKILL.md" ? { ...f, mode: "777" } : f,
|
||||
),
|
||||
};
|
||||
await writeFile(
|
||||
path.join(agentDir, ".generated-manifest.json"),
|
||||
JSON.stringify(tamperedManifest, null, 2) + "\n",
|
||||
);
|
||||
|
||||
const result = await verifyGenerated(dir, {
|
||||
generatedRootsOverride: [`skills/${skillName}/${agentName}`],
|
||||
});
|
||||
|
||||
// The mode mismatch in the manifest should be detected by diffManifests
|
||||
assert.equal(result.ok, false, "should fail on mode mismatch in manifest");
|
||||
const modeError = result.errors.some(
|
||||
(e) => e.includes("mode") || e.includes("SKILL.md"),
|
||||
);
|
||||
assert.ok(modeError, `mode mismatch should appear in errors: ${JSON.stringify(result.errors)}`);
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,421 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* verify-generated.mjs — drift detector for generator-owned files (M3, S-306)
|
||||
*
|
||||
* Verifies that every declared generated root on disk matches the content that
|
||||
* the generator would produce from the current canonical sources.
|
||||
*
|
||||
* Two verification modes:
|
||||
*
|
||||
* Production mode (default, no generatedRootsOverride):
|
||||
* Calls generateSkills() into a temp directory and compares the freshly
|
||||
* generated output file-by-file against the on-disk generated roots.
|
||||
* Detects ALL forms of drift:
|
||||
* (a) Direct edits to generated files
|
||||
* (b) Canonical source changes without running `pnpm run sync:pi`
|
||||
* (c) Stale files added to a generated root
|
||||
* (d) Files deleted from a generated root
|
||||
*
|
||||
* Test mode (generatedRootsOverride set):
|
||||
* Uses `.generated-manifest.json` as the oracle (manifest-based checks).
|
||||
* Suitable for unit tests that use artificial generated-root fixtures
|
||||
* without a full canonical source tree.
|
||||
*
|
||||
* Walk scope: only the declared generated roots returned by generate-skills.mjs.
|
||||
* - `skills/<skill>/_source/` and `skills/<skill>/shared/` are NEVER walked.
|
||||
* - `.generated-manifest.json` is excluded from content comparison.
|
||||
* - `node_modules/` is always excluded.
|
||||
*
|
||||
* Exit codes:
|
||||
* 0 — all generated roots match
|
||||
* 1 — one or more mismatches detected
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/verify-generated.mjs
|
||||
* pnpm run verify:generated
|
||||
*
|
||||
* Exported:
|
||||
* verifyGenerated(repoRoot, options?) → Promise<{ok: boolean, errors: string[]}>
|
||||
*/
|
||||
|
||||
import crypto from "node:crypto";
|
||||
import { lstat, mkdtemp, readdir, readFile, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
import { buildManifest, generateSkills, getGeneratedRoots } from "./generate-skills.mjs";
|
||||
|
||||
const REPO_ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname), "..");
|
||||
const MANIFEST_FILENAME = ".generated-manifest.json";
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
async function sha256File(absPath) {
|
||||
const buf = await readFile(absPath);
|
||||
return crypto.createHash("sha256").update(buf).digest("hex");
|
||||
}
|
||||
|
||||
async function pathExists(p) {
|
||||
try {
|
||||
await lstat(p);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk a directory and return all file relative paths (excluding node_modules
|
||||
* and the manifest file itself, which is handled separately).
|
||||
*/
|
||||
async function walkRootFiles(rootDir) {
|
||||
const results = [];
|
||||
let entries;
|
||||
try {
|
||||
entries = await readdir(rootDir, { withFileTypes: true });
|
||||
} catch {
|
||||
return results;
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.name === "node_modules") continue;
|
||||
if (entry.name === MANIFEST_FILENAME) continue; // manifest handled separately
|
||||
|
||||
const full = path.join(rootDir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
const sub = await walkSubDir(full, entry.name);
|
||||
results.push(...sub);
|
||||
} else if (entry.isFile()) {
|
||||
results.push(entry.name);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
async function walkSubDir(dir, prefix) {
|
||||
const results = [];
|
||||
let entries;
|
||||
try {
|
||||
entries = await readdir(dir, { withFileTypes: true });
|
||||
} catch {
|
||||
return results;
|
||||
}
|
||||
for (const entry of entries) {
|
||||
if (entry.name === "node_modules") continue;
|
||||
const relPath = `${prefix}/${entry.name}`;
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
const sub = await walkSubDir(full, relPath);
|
||||
results.push(...sub);
|
||||
} else if (entry.isFile()) {
|
||||
results.push(relPath);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and parse the .generated-manifest.json from a generated root.
|
||||
* Returns null if missing or malformed.
|
||||
*/
|
||||
async function loadManifest(rootDir) {
|
||||
const manifestPath = path.join(rootDir, MANIFEST_FILENAME);
|
||||
try {
|
||||
const raw = await readFile(manifestPath, "utf8");
|
||||
const obj = JSON.parse(raw);
|
||||
// Structural validation: must have $schema and generator markers
|
||||
if (!obj.$schema || !obj.generator || !Array.isArray(obj.files)) {
|
||||
return null;
|
||||
}
|
||||
return obj;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify structural equality of two manifests (ignoring ephemeral fields).
|
||||
* Returns array of difference descriptions, or empty array if equal.
|
||||
*/
|
||||
function diffManifests(expected, actual, rootRel) {
|
||||
const diffs = [];
|
||||
|
||||
if (expected.$schema !== actual.$schema) {
|
||||
diffs.push(`${rootRel}/.generated-manifest.json: $schema mismatch`);
|
||||
}
|
||||
if (expected.generator !== actual.generator) {
|
||||
diffs.push(`${rootRel}/.generated-manifest.json: generator mismatch`);
|
||||
}
|
||||
if (expected.generatedRoot !== actual.generatedRoot) {
|
||||
diffs.push(
|
||||
`${rootRel}/.generated-manifest.json: generatedRoot mismatch ` +
|
||||
`(expected ${expected.generatedRoot}, got ${actual.generatedRoot})`,
|
||||
);
|
||||
}
|
||||
|
||||
// Compare files arrays structurally — paths, kind, mode, and sha256
|
||||
const expByPath = new Map(expected.files.map((f) => [f.path, f]));
|
||||
const actByPath = new Map(actual.files.map((f) => [f.path, f]));
|
||||
|
||||
for (const p of expByPath.keys()) {
|
||||
if (!actByPath.has(p)) {
|
||||
diffs.push(`${rootRel}/.generated-manifest.json: expected file entry missing: ${p}`);
|
||||
}
|
||||
}
|
||||
for (const p of actByPath.keys()) {
|
||||
if (!expByPath.has(p)) {
|
||||
diffs.push(`${rootRel}/.generated-manifest.json: unexpected file entry: ${p}`);
|
||||
}
|
||||
}
|
||||
|
||||
// For entries present in both, compare kind, mode, and sha256
|
||||
for (const [p, expEntry] of expByPath) {
|
||||
const actEntry = actByPath.get(p);
|
||||
if (!actEntry) continue; // missing already reported above
|
||||
|
||||
if (expEntry.kind !== actEntry.kind) {
|
||||
diffs.push(
|
||||
`${rootRel}/.generated-manifest.json: ${p}: kind mismatch ` +
|
||||
`(expected ${expEntry.kind}, got ${actEntry.kind})`,
|
||||
);
|
||||
}
|
||||
if (expEntry.mode !== actEntry.mode) {
|
||||
diffs.push(
|
||||
`${rootRel}/.generated-manifest.json: ${p}: mode mismatch ` +
|
||||
`(expected ${expEntry.mode}, got ${actEntry.mode})`,
|
||||
);
|
||||
}
|
||||
if (expEntry.sha256 !== actEntry.sha256) {
|
||||
diffs.push(
|
||||
`${rootRel}/.generated-manifest.json: ${p}: sha256 mismatch ` +
|
||||
`(expected ${expEntry.sha256.slice(0, 8)}…, got ${actEntry.sha256.slice(0, 8)}…)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return diffs;
|
||||
}
|
||||
|
||||
// ── Core verifier ─────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Verify all declared generated roots against freshly generated output.
|
||||
*
|
||||
* Calls generateSkills() into a temp directory and compares the resulting
|
||||
* files to the on-disk roots. Detects all drift types:
|
||||
* - Canonical source changed without running `pnpm run sync:pi`
|
||||
* - Generated file edited directly
|
||||
* - Stale file added to generated root
|
||||
* - Generated file deleted from root
|
||||
*
|
||||
* Does NOT walk _source/, shared/, or node_modules/.
|
||||
*
|
||||
* @param {string} repoRoot - Absolute path to repo root.
|
||||
* @param {string[]} rootsRelative - Generated root paths to verify.
|
||||
* @param {string[]} errors - Error array to append to.
|
||||
*/
|
||||
async function verifyWithFreshGeneration(repoRoot, rootsRelative, errors) {
|
||||
const tmpDir = await mkdtemp(path.join(tmpdir(), "verify-gen-"));
|
||||
try {
|
||||
await generateSkills(repoRoot, { targetRoot: tmpDir });
|
||||
|
||||
for (const rootRel of rootsRelative) {
|
||||
const freshRootAbs = path.join(tmpDir, rootRel);
|
||||
const onDiskRootAbs = path.join(repoRoot, rootRel);
|
||||
|
||||
if (!(await pathExists(onDiskRootAbs))) {
|
||||
errors.push(`generated root missing: ${rootRel}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const freshFiles = await walkRootFiles(freshRootAbs);
|
||||
const onDiskFiles = await walkRootFiles(onDiskRootAbs);
|
||||
|
||||
const freshSet = new Set(freshFiles);
|
||||
const diskSet = new Set(onDiskFiles);
|
||||
|
||||
// Files the generator produces but are absent on disk
|
||||
for (const f of freshFiles) {
|
||||
if (!diskSet.has(f)) {
|
||||
errors.push(`${rootRel}/${f}: missing (expected per canonical sources — run \`pnpm run sync:pi\`)`);
|
||||
}
|
||||
}
|
||||
|
||||
// On-disk files the generator would NOT produce (stale)
|
||||
for (const f of onDiskFiles) {
|
||||
if (!freshSet.has(f)) {
|
||||
errors.push(`${rootRel}/${f}: stale (not produced from canonical sources)`);
|
||||
}
|
||||
}
|
||||
|
||||
// Content comparison for files present in both sets
|
||||
for (const f of freshFiles) {
|
||||
if (!diskSet.has(f)) continue; // missing already reported
|
||||
const freshHash = await sha256File(path.join(freshRootAbs, f));
|
||||
const diskHash = await sha256File(path.join(onDiskRootAbs, f));
|
||||
if (freshHash !== diskHash) {
|
||||
errors.push(
|
||||
`${rootRel}/${f}: content drift ` +
|
||||
`(on-disk sha256=${diskHash.slice(0, 8)}…, expected=${freshHash.slice(0, 8)}… — run \`pnpm run sync:pi\`)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate .generated-manifest.json on disk matches what the generator produced
|
||||
const freshManifestPath = path.join(freshRootAbs, MANIFEST_FILENAME);
|
||||
const diskManifestPath = path.join(onDiskRootAbs, MANIFEST_FILENAME);
|
||||
if (await pathExists(freshManifestPath)) {
|
||||
if (!(await pathExists(diskManifestPath))) {
|
||||
errors.push(`${rootRel}/${MANIFEST_FILENAME}: missing`);
|
||||
} else {
|
||||
const freshManifest = await loadManifest(freshRootAbs);
|
||||
const diskManifest = await loadManifest(onDiskRootAbs);
|
||||
if (!diskManifest) {
|
||||
errors.push(`${rootRel}/${MANIFEST_FILENAME}: missing or malformed`);
|
||||
} else if (freshManifest) {
|
||||
const manifestDiffs = diffManifests(freshManifest, diskManifest, rootRel);
|
||||
errors.push(...manifestDiffs);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await rm(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify using .generated-manifest.json as the oracle (test / no-canonical-sources mode).
|
||||
*
|
||||
* Used when generatedRootsOverride is set: the test fixture creates generated
|
||||
* roots with manifests but does not provide a full canonical source tree.
|
||||
*
|
||||
* For each root:
|
||||
* 1. Load the on-disk .generated-manifest.json (report missing manifest).
|
||||
* 2. For every file listed in the manifest: verify existence, mode, sha256.
|
||||
* 3. Walk the root for any files NOT listed in the manifest (stale files).
|
||||
* 4. Verify the manifest's own structural fields match expected schema.
|
||||
*
|
||||
* @param {string} repoRoot - Absolute path to repo root.
|
||||
* @param {string[]} rootsRelative - Generated root paths to verify.
|
||||
* @param {string[]} errors - Error array to append to.
|
||||
*/
|
||||
async function verifyWithManifest(repoRoot, rootsRelative, errors) {
|
||||
for (const rootRel of rootsRelative) {
|
||||
const rootAbs = path.join(repoRoot, rootRel);
|
||||
|
||||
// Check the root directory exists
|
||||
if (!(await pathExists(rootAbs))) {
|
||||
errors.push(`generated root missing: ${rootRel}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Load on-disk manifest
|
||||
const manifest = await loadManifest(rootAbs);
|
||||
if (!manifest) {
|
||||
errors.push(`${rootRel}/.generated-manifest.json: missing or malformed`);
|
||||
// Can't verify files without a manifest; report all on-disk files as stale
|
||||
const onDisk = await walkRootFiles(rootAbs);
|
||||
for (const f of onDisk) {
|
||||
errors.push(`${rootRel}/${f}: stale (no valid manifest)`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Build expected manifest from current disk state (structural check)
|
||||
const expectedManifest = await buildManifest(rootAbs, rootRel);
|
||||
const manifestDiffs = diffManifests(expectedManifest, manifest, rootRel);
|
||||
errors.push(...manifestDiffs);
|
||||
|
||||
// Build maps for file checks
|
||||
const manifestByPath = new Map(manifest.files.map((f) => [f.path, f]));
|
||||
|
||||
// Check each file listed in manifest
|
||||
for (const entry of manifest.files) {
|
||||
const fileAbs = path.join(rootAbs, entry.path);
|
||||
|
||||
if (!(await pathExists(fileAbs))) {
|
||||
errors.push(`${rootRel}/${entry.path}: missing (listed in manifest)`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check content hash
|
||||
const actualHash = await sha256File(fileAbs);
|
||||
if (actualHash !== entry.sha256) {
|
||||
errors.push(
|
||||
`${rootRel}/${entry.path}: content mismatch ` +
|
||||
`(manifest sha256=${entry.sha256.slice(0, 8)}…, actual=${actualHash.slice(0, 8)}…)`,
|
||||
);
|
||||
}
|
||||
|
||||
// Check mode
|
||||
const st = await lstat(fileAbs);
|
||||
const actualMode = (st.mode & 0o777).toString(8).padStart(3, "0");
|
||||
if (actualMode !== entry.mode) {
|
||||
errors.push(
|
||||
`${rootRel}/${entry.path}: mode mismatch ` +
|
||||
`(manifest=${entry.mode}, actual=${actualMode})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Detect stale files: on-disk files not listed in manifest
|
||||
const onDiskFiles = await walkRootFiles(rootAbs);
|
||||
for (const relPath of onDiskFiles) {
|
||||
if (!manifestByPath.has(relPath)) {
|
||||
errors.push(`${rootRel}/${relPath}: stale (not in manifest)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify all declared generated roots in a repo.
|
||||
*
|
||||
* In production mode (no generatedRootsOverride): generates into a temp
|
||||
* directory and compares file-by-file against on-disk roots. This detects
|
||||
* both direct edits to generated files AND changes to canonical sources that
|
||||
* were not followed by `pnpm run sync:pi`.
|
||||
*
|
||||
* In test mode (generatedRootsOverride set): uses manifest-based checks.
|
||||
* Suitable for unit tests that provide artificial generated-root fixtures
|
||||
* without a full canonical source tree.
|
||||
*
|
||||
* @param {string} [repoRoot] - Absolute path to repo root.
|
||||
* @param {object} [options]
|
||||
* @param {string[]} [options.generatedRootsOverride] - Override root list (test mode only).
|
||||
* @returns {Promise<{ok: boolean, errors: string[]}>}
|
||||
*/
|
||||
export async function verifyGenerated(repoRoot = REPO_ROOT, options = {}) {
|
||||
const rootsRelative = options.generatedRootsOverride ?? getGeneratedRoots();
|
||||
const errors = [];
|
||||
|
||||
if (options.generatedRootsOverride) {
|
||||
// Test mode: no canonical source tree available — use manifest oracle
|
||||
await verifyWithManifest(repoRoot, rootsRelative, errors);
|
||||
} else {
|
||||
// Production mode: generate fresh from canonical sources and compare
|
||||
await verifyWithFreshGeneration(repoRoot, rootsRelative, errors);
|
||||
}
|
||||
|
||||
return { ok: errors.length === 0, errors };
|
||||
}
|
||||
|
||||
// ── CLI entry point ────────────────────────────────────────────────────────
|
||||
|
||||
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
const result = await verifyGenerated(REPO_ROOT);
|
||||
|
||||
if (result.ok) {
|
||||
console.log("verify:generated: all generated roots are up to date.");
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.error("verify:generated: drift detected:\n");
|
||||
for (const err of result.errors) {
|
||||
console.error(` ✗ ${err}`);
|
||||
}
|
||||
console.error(`\n${result.errors.length} error(s). Run \`pnpm run sync:pi\` to regenerate.`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,6 @@ REQUIRED_FILES=(
|
||||
"skills/reviewer-runtime/pi/run-review.sh"
|
||||
"skills/reviewer-runtime/pi/notify-telegram.sh"
|
||||
"scripts/install-pi-package.sh"
|
||||
"scripts/sync-pi-package-skills.sh"
|
||||
"pi-package/skills/atlassian/SKILL.md"
|
||||
"pi-package/skills/create-plan/SKILL.md"
|
||||
"pi-package/skills/do-task/SKILL.md"
|
||||
@@ -39,13 +38,13 @@ done
|
||||
test -x skills/reviewer-runtime/pi/run-review.sh
|
||||
test -x skills/reviewer-runtime/pi/notify-telegram.sh
|
||||
test -x scripts/install-pi-package.sh
|
||||
test -x scripts/sync-pi-package-skills.sh
|
||||
find skills/web-automation/pi/scripts -type f -print -quit | grep -q .
|
||||
find skills/atlassian/pi/scripts -type f -print -quit | grep -q .
|
||||
|
||||
for file in skills/create-plan/pi/SKILL.md skills/do-task/pi/SKILL.md skills/implement-plan/pi/SKILL.md; do
|
||||
grep -q 'docs/PI-SUPERPOWERS.md' "$file"
|
||||
grep -q 'docs/PI-COMMON-REVIEWER.md' "$file"
|
||||
# shellcheck disable=SC2016
|
||||
grep -q 'Reviewer CLI: `codex`, `claude`, `cursor`, `opencode`, `pi`, or `skip`' "$file"
|
||||
grep -q 'pi --no-session --no-skills --no-prompt-templates --no-extensions --no-context-files' "$file"
|
||||
grep -q -- '--tools read,grep,find,ls -p' "$file"
|
||||
@@ -54,11 +53,13 @@ done
|
||||
|
||||
grep -q 'reviewer model is configured independently' docs/PI-COMMON-REVIEWER.md
|
||||
grep -q 'provider-qualified model IDs' docs/PI-COMMON-REVIEWER.md
|
||||
# shellcheck disable=SC2016
|
||||
grep -q 'MUST NOT include `write`, `edit`, or `bash`' docs/PI-COMMON-REVIEWER.md
|
||||
grep -q 'Reviewer CLI | codex \\| claude \\| cursor \\| opencode \\| pi' skills/do-task/pi/templates/task-plan.md
|
||||
|
||||
grep -q 'pi-package/skills/atlassian/scripts' skills/atlassian/pi/SKILL.md
|
||||
grep -q 'pi-package/skills/web-automation/scripts' skills/web-automation/pi/SKILL.md
|
||||
# shellcheck disable=SC2016
|
||||
grep -q 'local checkout package install keeps the runtime in `pi-package/skills/<skill>/scripts`' docs/PI.md
|
||||
|
||||
grep -q 'install-pi-package.sh --global' docs/PI.md
|
||||
@@ -81,6 +82,8 @@ for family in atlassian create-plan do-task implement-plan web-automation; do
|
||||
diff -qr \
|
||||
--exclude '.DS_Store' \
|
||||
--exclude 'node_modules' \
|
||||
--exclude '.generated-manifest.json' \
|
||||
--exclude 'package.json' \
|
||||
"$source_dir" "$mirror_dir" >/dev/null
|
||||
|
||||
while IFS= read -r -d '' source_path; do
|
||||
@@ -93,10 +96,14 @@ for family in atlassian create-plan do-task implement-plan web-automation; do
|
||||
-mindepth 1 -print0)
|
||||
done
|
||||
|
||||
! grep -nE 'update_plan|plan mode|sub-agent|subagents' \
|
||||
# SC2251: restructured to avoid ! outside condition
|
||||
if grep -nE 'update_plan|plan mode|sub-agent|subagents' \
|
||||
skills/create-plan/pi/SKILL.md \
|
||||
skills/do-task/pi/SKILL.md \
|
||||
skills/implement-plan/pi/SKILL.md
|
||||
skills/implement-plan/pi/SKILL.md; then
|
||||
echo "Error: pi SKILL.md files must not contain Codex-specific terms" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
node <<'EOF'
|
||||
const fs = require("fs");
|
||||
@@ -129,7 +136,7 @@ for (const requiredFile of [
|
||||
"pi-package/skills",
|
||||
"docs/PI-COMMON-REVIEWER.md",
|
||||
"scripts/install-pi-package.sh",
|
||||
"scripts/sync-pi-package-skills.sh",
|
||||
"scripts/generate-skills.mjs",
|
||||
]) {
|
||||
if (!pkg.files.includes(requiredFile)) {
|
||||
console.error(`package.json files must include ${requiredFile}`);
|
||||
|
||||
@@ -13,6 +13,7 @@ WORKFLOW_FILES=(
|
||||
for file in "${WORKFLOW_FILES[@]}"; do
|
||||
test -f "$file"
|
||||
grep -q 'docs/PI-SUPERPOWERS.md' "$file"
|
||||
# shellcheck disable=SC2016
|
||||
grep -q 'Reviewer CLI: `codex`, `claude`, `cursor`, `opencode`, `pi`, or `skip`' "$file"
|
||||
grep -q 'pi --no-session --no-skills --no-prompt-templates --no-extensions --no-context-files' "$file"
|
||||
grep -q -- '--tools read,grep,find,ls -p' "$file"
|
||||
@@ -21,6 +22,7 @@ done
|
||||
|
||||
grep -q 'reviewer model is configured independently' docs/PI-COMMON-REVIEWER.md
|
||||
grep -q 'provider-qualified model IDs' docs/PI-COMMON-REVIEWER.md
|
||||
# shellcheck disable=SC2016
|
||||
grep -q 'MUST NOT include `write`, `edit`, or `bash`' docs/PI-COMMON-REVIEWER.md
|
||||
|
||||
if command -v pi >/dev/null 2>&1; then
|
||||
@@ -43,6 +45,10 @@ grep -q 'Reviewer CLI | codex \\| claude \\| cursor \\| opencode \\| pi' skills/
|
||||
test -x skills/reviewer-runtime/pi/run-review.sh
|
||||
test -x skills/reviewer-runtime/pi/notify-telegram.sh
|
||||
|
||||
! rg -n 'update_plan|plan mode|sub-agent|subagents' "${WORKFLOW_FILES[@]}"
|
||||
# SC2251: restructured to avoid ! outside condition
|
||||
if rg -n 'update_plan|plan mode|sub-agent|subagents' "${WORKFLOW_FILES[@]}"; then
|
||||
echo "Error: pi workflow SKILL.md files must not contain Codex-specific terms" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "pi workflow skill docs verified"
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
name: atlassian
|
||||
description: Interact with Atlassian Cloud Jira and Confluence through a portable task-oriented CLI for search, issue/page edits, comments, transitions, and bounded raw requests.
|
||||
---
|
||||
|
||||
# Atlassian (Claude Code)
|
||||
|
||||
Portable Atlassian workflows for Claude Code using a shared TypeScript CLI.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Node.js 20+
|
||||
- `pnpm`
|
||||
- Atlassian Cloud account access
|
||||
- `ATLASSIAN_BASE_URL`
|
||||
- `ATLASSIAN_EMAIL`
|
||||
- `ATLASSIAN_API_TOKEN`
|
||||
|
||||
The `ATLASSIAN_*` values may come from the shell environment or a `.env` file in `~/.claude/skills/atlassian/scripts`.
|
||||
|
||||
## First-Time Setup
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.claude/skills/atlassian
|
||||
cp -R skills/atlassian/claude-code/* ~/.claude/skills/atlassian/
|
||||
cd ~/.claude/skills/atlassian/scripts
|
||||
pnpm install
|
||||
```
|
||||
|
||||
## Prerequisite Check (MANDATORY)
|
||||
|
||||
```bash
|
||||
cd ~/.claude/skills/atlassian/scripts
|
||||
node -e "require.resolve('commander');require.resolve('dotenv');console.log('OK: runtime dependencies installed')"
|
||||
node -e 'require("dotenv").config({ path: ".env" }); const required = ["ATLASSIAN_BASE_URL", "ATLASSIAN_EMAIL", "ATLASSIAN_API_TOKEN"]; const missing = required.filter((key) => !(process.env[key] || "").trim()); if (missing.length) { console.error("Missing required Atlassian config: " + missing.join(", ")); process.exit(1); } console.log("OK: Atlassian config present")'
|
||||
pnpm atlassian health
|
||||
```
|
||||
|
||||
If any check fails, stop and return:
|
||||
|
||||
`Missing dependency/config: atlassian requires installed CLI dependencies and valid Atlassian Cloud credentials. Configure ATLASSIAN_* in the shell environment or scripts/.env, then retry.`
|
||||
|
||||
## Supported Commands
|
||||
|
||||
- `pnpm atlassian health`
|
||||
- `pnpm atlassian jira-search --jql "..."`
|
||||
- `pnpm atlassian jira-get --issue ABC-123`
|
||||
- `pnpm atlassian jira-create ... [--dry-run]`
|
||||
- `pnpm atlassian jira-update ... [--dry-run]`
|
||||
- `pnpm atlassian jira-comment ... [--dry-run]`
|
||||
- `pnpm atlassian jira-transitions --issue ABC-123`
|
||||
- `pnpm atlassian jira-transition ... [--dry-run]`
|
||||
- `pnpm atlassian conf-search --query "..."`
|
||||
- `pnpm atlassian conf-get --page 12345`
|
||||
- `pnpm atlassian conf-create ... [--dry-run]`
|
||||
- `pnpm atlassian conf-update ... [--dry-run]`
|
||||
- `pnpm atlassian conf-comment ... [--dry-run]`
|
||||
- `pnpm atlassian conf-children --page 12345`
|
||||
- `pnpm atlassian raw --product jira|confluence --method GET|POST|PUT --path ...`
|
||||
|
||||
## Usage Examples
|
||||
|
||||
- `pnpm atlassian jira-search --jql "project = ENG ORDER BY updated DESC" --max-results 10`
|
||||
- `pnpm atlassian conf-comment --page 12345 --body-file comment.storage.html --dry-run`
|
||||
- `pnpm atlassian raw --product jira --method GET --path "/rest/api/3/issue/ENG-123"`
|
||||
|
||||
## Safety Rules
|
||||
|
||||
- Default output is JSON; only switch to text output when the user needs a human-readable summary.
|
||||
- Use `--dry-run` before any write unless the user clearly asked for the mutation.
|
||||
- Treat `raw` as an escape hatch, not the default API surface.
|
||||
- `--body-file` must stay inside the current workspace.
|
||||
- Confluence write bodies should be storage-format inputs in v1.
|
||||
|
||||
## Notes
|
||||
|
||||
- Atlassian Cloud is the primary supported platform in v1.
|
||||
- The portable CLI exists so the same skill works consistently across multiple agent environments.
|
||||
@@ -0,0 +1,81 @@
|
||||
---
|
||||
name: atlassian
|
||||
description: Interact with Atlassian Cloud Jira and Confluence through a portable task-oriented CLI for search, issue/page edits, comments, transitions, and bounded raw requests.
|
||||
---
|
||||
|
||||
# Atlassian (Codex)
|
||||
|
||||
Portable Atlassian workflows for Codex using a shared TypeScript CLI.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Node.js 20+
|
||||
- `pnpm`
|
||||
- Atlassian Cloud account access
|
||||
- `ATLASSIAN_BASE_URL`
|
||||
- `ATLASSIAN_EMAIL`
|
||||
- `ATLASSIAN_API_TOKEN`
|
||||
|
||||
The `ATLASSIAN_*` values may come from the shell environment or a `.env` file in `~/.codex/skills/atlassian/scripts`.
|
||||
|
||||
## First-Time Setup
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.codex/skills/atlassian
|
||||
cp -R skills/atlassian/codex/* ~/.codex/skills/atlassian/
|
||||
cd ~/.codex/skills/atlassian/scripts
|
||||
pnpm install
|
||||
```
|
||||
|
||||
## Prerequisite Check (MANDATORY)
|
||||
|
||||
Run before using the skill:
|
||||
|
||||
```bash
|
||||
cd ~/.codex/skills/atlassian/scripts
|
||||
node -e "require.resolve('commander');require.resolve('dotenv');console.log('OK: runtime dependencies installed')"
|
||||
node -e 'require("dotenv").config({ path: ".env" }); const required = ["ATLASSIAN_BASE_URL", "ATLASSIAN_EMAIL", "ATLASSIAN_API_TOKEN"]; const missing = required.filter((key) => !(process.env[key] || "").trim()); if (missing.length) { console.error("Missing required Atlassian config: " + missing.join(", ")); process.exit(1); } console.log("OK: Atlassian config present")'
|
||||
pnpm atlassian health
|
||||
```
|
||||
|
||||
If any check fails, stop and return:
|
||||
|
||||
`Missing dependency/config: atlassian requires installed CLI dependencies and valid Atlassian Cloud credentials. Configure ATLASSIAN_* in the shell environment or scripts/.env, then retry.`
|
||||
|
||||
## Supported Commands
|
||||
|
||||
- `pnpm atlassian health`
|
||||
- `pnpm atlassian jira-search --jql "..."`
|
||||
- `pnpm atlassian jira-get --issue ABC-123`
|
||||
- `pnpm atlassian jira-create ... [--dry-run]`
|
||||
- `pnpm atlassian jira-update ... [--dry-run]`
|
||||
- `pnpm atlassian jira-comment ... [--dry-run]`
|
||||
- `pnpm atlassian jira-transitions --issue ABC-123`
|
||||
- `pnpm atlassian jira-transition ... [--dry-run]`
|
||||
- `pnpm atlassian conf-search --query "..."`
|
||||
- `pnpm atlassian conf-get --page 12345`
|
||||
- `pnpm atlassian conf-create ... [--dry-run]`
|
||||
- `pnpm atlassian conf-update ... [--dry-run]`
|
||||
- `pnpm atlassian conf-comment ... [--dry-run]`
|
||||
- `pnpm atlassian conf-children --page 12345`
|
||||
- `pnpm atlassian raw --product jira|confluence --method GET|POST|PUT --path ...`
|
||||
|
||||
## Usage Examples
|
||||
|
||||
- `pnpm atlassian jira-search --jql "project = ENG ORDER BY updated DESC" --max-results 10`
|
||||
- `pnpm atlassian conf-update --page 12345 --title "Runbook" --body-file page.storage.html --dry-run`
|
||||
- `pnpm atlassian raw --product confluence --method POST --path "/wiki/api/v2/pages" --body-file page.json --dry-run`
|
||||
|
||||
## Safety Rules
|
||||
|
||||
- Default output is JSON; prefer that for agent workflows.
|
||||
- Use `--dry-run` before any mutating command unless the user clearly wants the write to happen immediately.
|
||||
- Jira long-text fields are converted to ADF locally.
|
||||
- Confluence page bodies are storage-first in v1.
|
||||
- `--body-file` must point to workspace-scoped files only; do not use arbitrary system paths.
|
||||
- `raw` is for explicit edge cases only and does not allow `DELETE`.
|
||||
|
||||
## Notes
|
||||
|
||||
- Atlassian Cloud is the only first-class target in v1.
|
||||
- This skill exists so Codex, Claude Code, Cursor Agent, and OpenCode can share the same command surface even when MCP access differs.
|
||||
@@ -0,0 +1,93 @@
|
||||
---
|
||||
name: atlassian
|
||||
description: Interact with Atlassian Cloud Jira and Confluence through a portable task-oriented CLI for search, issue/page edits, comments, transitions, and bounded raw requests.
|
||||
---
|
||||
|
||||
# Atlassian (Cursor Agent CLI)
|
||||
|
||||
Portable Atlassian workflows for Cursor Agent CLI using a shared TypeScript CLI.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Cursor Agent CLI skill discovery via `.cursor/skills/` or `~/.cursor/skills/`
|
||||
- Node.js 20+
|
||||
- `pnpm`
|
||||
- Atlassian Cloud account access
|
||||
- `ATLASSIAN_BASE_URL`
|
||||
- `ATLASSIAN_EMAIL`
|
||||
- `ATLASSIAN_API_TOKEN`
|
||||
|
||||
The `ATLASSIAN_*` values may come from the shell environment or a `.env` file in the installed `scripts/` folder.
|
||||
|
||||
## First-Time Setup
|
||||
|
||||
Repo-local install:
|
||||
|
||||
```bash
|
||||
mkdir -p .cursor/skills/atlassian
|
||||
cp -R skills/atlassian/cursor/* .cursor/skills/atlassian/
|
||||
cd .cursor/skills/atlassian/scripts
|
||||
pnpm install
|
||||
```
|
||||
|
||||
Global install:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.cursor/skills/atlassian
|
||||
cp -R skills/atlassian/cursor/* ~/.cursor/skills/atlassian/
|
||||
cd ~/.cursor/skills/atlassian/scripts
|
||||
pnpm install
|
||||
```
|
||||
|
||||
## Prerequisite Check (MANDATORY)
|
||||
|
||||
Repo-local form:
|
||||
|
||||
```bash
|
||||
cursor-agent --version
|
||||
cd .cursor/skills/atlassian/scripts
|
||||
node -e "require.resolve('commander');require.resolve('dotenv');console.log('OK: runtime dependencies installed')"
|
||||
node -e 'require("dotenv").config({ path: ".env" }); const required = ["ATLASSIAN_BASE_URL", "ATLASSIAN_EMAIL", "ATLASSIAN_API_TOKEN"]; const missing = required.filter((key) => !(process.env[key] || "").trim()); if (missing.length) { console.error("Missing required Atlassian config: " + missing.join(", ")); process.exit(1); } console.log("OK: Atlassian config present")'
|
||||
pnpm atlassian health
|
||||
```
|
||||
|
||||
If any check fails, stop and return:
|
||||
|
||||
`Missing dependency/config: atlassian requires installed CLI dependencies and valid Atlassian Cloud credentials. Configure ATLASSIAN_* in the shell environment or scripts/.env, then retry.`
|
||||
|
||||
## Supported Commands
|
||||
|
||||
- `pnpm atlassian health`
|
||||
- `pnpm atlassian jira-search --jql "..."`
|
||||
- `pnpm atlassian jira-get --issue ABC-123`
|
||||
- `pnpm atlassian jira-create ... [--dry-run]`
|
||||
- `pnpm atlassian jira-update ... [--dry-run]`
|
||||
- `pnpm atlassian jira-comment ... [--dry-run]`
|
||||
- `pnpm atlassian jira-transitions --issue ABC-123`
|
||||
- `pnpm atlassian jira-transition ... [--dry-run]`
|
||||
- `pnpm atlassian conf-search --query "..."`
|
||||
- `pnpm atlassian conf-get --page 12345`
|
||||
- `pnpm atlassian conf-create ... [--dry-run]`
|
||||
- `pnpm atlassian conf-update ... [--dry-run]`
|
||||
- `pnpm atlassian conf-comment ... [--dry-run]`
|
||||
- `pnpm atlassian conf-children --page 12345`
|
||||
- `pnpm atlassian raw --product jira|confluence --method GET|POST|PUT --path ...`
|
||||
|
||||
## Usage Examples
|
||||
|
||||
- `pnpm atlassian jira-get --issue ENG-123`
|
||||
- `pnpm atlassian conf-search --query "title ~ \\\"Runbook\\\"" --max-results 10 --start-at 0`
|
||||
- `pnpm atlassian raw --product confluence --method POST --path "/wiki/api/v2/pages" --body-file page.json --dry-run`
|
||||
|
||||
## Safety Rules
|
||||
|
||||
- Prefer JSON output for agent use.
|
||||
- Use `--dry-run` before writes unless the user explicitly wants the change applied.
|
||||
- Keep `--body-file` inputs within the current workspace.
|
||||
- Use `raw` only for user-requested unsupported endpoints.
|
||||
- `raw` does not allow `DELETE`.
|
||||
|
||||
## Notes
|
||||
|
||||
- Cursor discovers this skill from `.cursor/skills/` or `~/.cursor/skills/`.
|
||||
- Atlassian Cloud is the supported platform in v1.
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
name: atlassian
|
||||
description: Interact with Atlassian Cloud Jira and Confluence through a portable task-oriented CLI for search, issue/page edits, comments, transitions, and bounded raw requests.
|
||||
---
|
||||
|
||||
# Atlassian (OpenCode)
|
||||
|
||||
Portable Atlassian workflows for OpenCode using a shared TypeScript CLI.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Node.js 20+
|
||||
- `pnpm`
|
||||
- Atlassian Cloud account access
|
||||
- `ATLASSIAN_BASE_URL`
|
||||
- `ATLASSIAN_EMAIL`
|
||||
- `ATLASSIAN_API_TOKEN`
|
||||
|
||||
The `ATLASSIAN_*` values may come from the shell environment or a `.env` file in `~/.config/opencode/skills/atlassian/scripts`.
|
||||
|
||||
## First-Time Setup
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.config/opencode/skills/atlassian
|
||||
cp -R skills/atlassian/opencode/* ~/.config/opencode/skills/atlassian/
|
||||
cd ~/.config/opencode/skills/atlassian/scripts
|
||||
pnpm install
|
||||
```
|
||||
|
||||
## Prerequisite Check (MANDATORY)
|
||||
|
||||
```bash
|
||||
cd ~/.config/opencode/skills/atlassian/scripts
|
||||
node -e "require.resolve('commander');require.resolve('dotenv');console.log('OK: runtime dependencies installed')"
|
||||
node -e 'require("dotenv").config({ path: ".env" }); const required = ["ATLASSIAN_BASE_URL", "ATLASSIAN_EMAIL", "ATLASSIAN_API_TOKEN"]; const missing = required.filter((key) => !(process.env[key] || "").trim()); if (missing.length) { console.error("Missing required Atlassian config: " + missing.join(", ")); process.exit(1); } console.log("OK: Atlassian config present")'
|
||||
pnpm atlassian health
|
||||
```
|
||||
|
||||
If any check fails, stop and return:
|
||||
|
||||
`Missing dependency/config: atlassian requires installed CLI dependencies and valid Atlassian Cloud credentials. Configure ATLASSIAN_* in the shell environment or scripts/.env, then retry.`
|
||||
|
||||
## Supported Commands
|
||||
|
||||
- `pnpm atlassian health`
|
||||
- `pnpm atlassian jira-search --jql "..."`
|
||||
- `pnpm atlassian jira-get --issue ABC-123`
|
||||
- `pnpm atlassian jira-create ... [--dry-run]`
|
||||
- `pnpm atlassian jira-update ... [--dry-run]`
|
||||
- `pnpm atlassian jira-comment ... [--dry-run]`
|
||||
- `pnpm atlassian jira-transitions --issue ABC-123`
|
||||
- `pnpm atlassian jira-transition ... [--dry-run]`
|
||||
- `pnpm atlassian conf-search --query "..."`
|
||||
- `pnpm atlassian conf-get --page 12345`
|
||||
- `pnpm atlassian conf-create ... [--dry-run]`
|
||||
- `pnpm atlassian conf-update ... [--dry-run]`
|
||||
- `pnpm atlassian conf-comment ... [--dry-run]`
|
||||
- `pnpm atlassian conf-children --page 12345`
|
||||
- `pnpm atlassian raw --product jira|confluence --method GET|POST|PUT --path ...`
|
||||
|
||||
## Usage Examples
|
||||
|
||||
- `pnpm atlassian jira-transition --issue ENG-123 --transition 31 --dry-run`
|
||||
- `pnpm atlassian conf-create --space OPS --title "Runbook" --body-file page.storage.html --dry-run`
|
||||
- `pnpm atlassian raw --product jira --method GET --path "/rest/api/3/issue/ENG-123"`
|
||||
|
||||
## Safety Rules
|
||||
|
||||
- Prefer JSON output for machine consumption.
|
||||
- Use `--dry-run` on writes unless the user explicitly asks to commit the remote mutation.
|
||||
- Restrict `--body-file` to project files.
|
||||
- Use `raw` only for unsupported edge cases.
|
||||
- `DELETE` is intentionally unsupported in raw mode.
|
||||
|
||||
## Notes
|
||||
|
||||
- Atlassian Cloud is first-class in v1; Data Center support is future work.
|
||||
- The CLI contract is shared across all agent variants so the same usage pattern works everywhere.
|
||||
@@ -0,0 +1,99 @@
|
||||
---
|
||||
name: atlassian
|
||||
description: Interact with Atlassian Cloud Jira and Confluence through a portable task-oriented CLI for search, issue/page edits, comments, transitions, and bounded raw requests.
|
||||
---
|
||||
|
||||
# Atlassian (Pi)
|
||||
|
||||
Portable Atlassian workflows for pi using the shared TypeScript CLI in `scripts/`.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Node.js 20+
|
||||
- `pnpm`
|
||||
- Atlassian Cloud account access
|
||||
- `ATLASSIAN_BASE_URL`
|
||||
- `ATLASSIAN_EMAIL`
|
||||
- `ATLASSIAN_API_TOKEN`
|
||||
|
||||
The `ATLASSIAN_*` values may come from the shell environment or a `.env` file in the installed skill's `scripts/` directory.
|
||||
|
||||
## First-Time Setup
|
||||
|
||||
Global install:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.pi/agent/skills/atlassian
|
||||
cp -R skills/atlassian/pi/* ~/.pi/agent/skills/atlassian/
|
||||
cd ~/.pi/agent/skills/atlassian/scripts
|
||||
pnpm install
|
||||
```
|
||||
|
||||
Project-local install:
|
||||
|
||||
```bash
|
||||
mkdir -p .pi/skills/atlassian
|
||||
cp -R skills/atlassian/pi/* .pi/skills/atlassian/
|
||||
cd .pi/skills/atlassian/scripts
|
||||
pnpm install
|
||||
```
|
||||
|
||||
Pi can also load this repo through settings or package installs as documented in [docs/PI.md](../../../docs/PI.md).
|
||||
|
||||
If you installed this repo from a local checkout with `./scripts/install-pi-package.sh`, the runtime stays in the checkout mirror at `pi-package/skills/atlassian/scripts`.
|
||||
|
||||
## Prerequisite Check (MANDATORY)
|
||||
|
||||
Run inside the skill runtime directory that matches your install style:
|
||||
|
||||
- local checkout package install: `pi-package/skills/atlassian/scripts`
|
||||
- project-local copied install: `.pi/skills/atlassian/scripts`
|
||||
- global copied install: `~/.pi/agent/skills/atlassian/scripts`
|
||||
|
||||
```bash
|
||||
cd pi-package/skills/atlassian/scripts
|
||||
node -e "require.resolve('commander');require.resolve('dotenv');console.log('OK: runtime dependencies installed')"
|
||||
node -e 'require("dotenv").config({ path: ".env" }); const required = ["ATLASSIAN_BASE_URL", "ATLASSIAN_EMAIL", "ATLASSIAN_API_TOKEN"]; const missing = required.filter((key) => !(process.env[key] || "").trim()); if (missing.length) { console.error("Missing required Atlassian config: " + missing.join(", ")); process.exit(1); } console.log("OK: Atlassian config present")'
|
||||
pnpm atlassian health
|
||||
```
|
||||
|
||||
If any check fails, stop and return:
|
||||
|
||||
`Missing dependency/config: atlassian requires installed CLI dependencies and valid Atlassian Cloud credentials. Configure ATLASSIAN_* in the shell environment or scripts/.env, then retry.`
|
||||
|
||||
## Supported Commands
|
||||
|
||||
- `pnpm atlassian health`
|
||||
- `pnpm atlassian jira-search --jql "..."`
|
||||
- `pnpm atlassian jira-get --issue ABC-123`
|
||||
- `pnpm atlassian jira-create ... [--dry-run]`
|
||||
- `pnpm atlassian jira-update ... [--dry-run]`
|
||||
- `pnpm atlassian jira-comment ... [--dry-run]`
|
||||
- `pnpm atlassian jira-transitions --issue ABC-123`
|
||||
- `pnpm atlassian jira-transition ... [--dry-run]`
|
||||
- `pnpm atlassian conf-search --query "..."`
|
||||
- `pnpm atlassian conf-get --page 12345`
|
||||
- `pnpm atlassian conf-create ... [--dry-run]`
|
||||
- `pnpm atlassian conf-update ... [--dry-run]`
|
||||
- `pnpm atlassian conf-comment ... [--dry-run]`
|
||||
- `pnpm atlassian conf-children --page 12345`
|
||||
- `pnpm atlassian raw --product jira|confluence --method GET|POST|PUT --path ...`
|
||||
|
||||
## Usage Examples
|
||||
|
||||
- `pnpm atlassian jira-search --jql "project = ENG ORDER BY updated DESC" --max-results 10`
|
||||
- `pnpm atlassian conf-comment --page 12345 --body-file comment.storage.html --dry-run`
|
||||
- `pnpm atlassian raw --product jira --method GET --path "/rest/api/3/issue/ENG-123"`
|
||||
|
||||
## Safety Rules
|
||||
|
||||
- Default output is JSON; prefer that for agent workflows.
|
||||
- Use `--dry-run` before any mutating command unless the user clearly wants the write to happen immediately.
|
||||
- `raw` is for explicit edge cases only and does not allow `DELETE`.
|
||||
- `--body-file` must stay inside the current workspace.
|
||||
- Confluence write bodies should be storage-format inputs in v1.
|
||||
|
||||
## Notes
|
||||
|
||||
- Atlassian Cloud is the primary supported platform in v1.
|
||||
- Package installs use the repo's `pi-package/skills/atlassian/` mirror so the installed skill directory name matches `atlassian`.
|
||||
@@ -0,0 +1,97 @@
|
||||
{
|
||||
"$schema": "https://ai-coding-skills.dev/schemas/generated-manifest/v1.json",
|
||||
"generator": "scripts/generate-skills.mjs",
|
||||
"generatedRoot": "skills/atlassian/claude-code",
|
||||
"files": [
|
||||
{
|
||||
"path": "scripts/package.json",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "4030a6965cd5b29674beedd2b5b1bce08b9bd8a7304dfc22d68ca9c0cdd0f6a3"
|
||||
},
|
||||
{
|
||||
"path": "scripts/pnpm-lock.yaml",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "15556a6f53e68bb8d92d2710aae0836bc80af7f29be9d63aa1b87fcbd33732c6"
|
||||
},
|
||||
{
|
||||
"path": "scripts/src/adf.ts",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "c7c3b4a78ccd8fb5a8ab99c82e0eab67a0a0d656b3985c1f56817bda199ad20f"
|
||||
},
|
||||
{
|
||||
"path": "scripts/src/cli.ts",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "5c4f4db76817fa9dbdae0fd0c75be302248d4b87fc0a53f6bd3c90407a75ae98"
|
||||
},
|
||||
{
|
||||
"path": "scripts/src/config.ts",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "700dcdce96afab5294426e09f539135ae5432632370260190d6292071422eb3f"
|
||||
},
|
||||
{
|
||||
"path": "scripts/src/confluence.ts",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "709d5d61fdb14e37aa4eaa7175eb7f17f0ec661376c96071020fbc9574ddbb73"
|
||||
},
|
||||
{
|
||||
"path": "scripts/src/files.ts",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "16296eaa3ae41a4d7c694773036f9bb4bd2baa2db6a9c318078532b713678dba"
|
||||
},
|
||||
{
|
||||
"path": "scripts/src/health.ts",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "1db4b49e05b16a095b7e7ca31cdc4e22ebda19e20e05c40baaaac648eaec0d08"
|
||||
},
|
||||
{
|
||||
"path": "scripts/src/http.ts",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "66444b777d4d9b14d9793eb051c586eb811d2b36815b1018dd9d7517666c7eb2"
|
||||
},
|
||||
{
|
||||
"path": "scripts/src/jira.ts",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "485d8d618fe04eb1ce546c1694eadf15d867bc83c2a6f7df994688ab0335ea4f"
|
||||
},
|
||||
{
|
||||
"path": "scripts/src/output.ts",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "38e99818582a4962c09a83175634cba2bfead6acf33bd5f43cdca5caed7100a0"
|
||||
},
|
||||
{
|
||||
"path": "scripts/src/raw.ts",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "2309c96dd45a03509df204803de9ecf0b5ff82fd488730f55ac5dd6a23b81dd8"
|
||||
},
|
||||
{
|
||||
"path": "scripts/src/types.ts",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "9f92d27ab68604d5abfd0f5dc9552b96fed6d1f9fc7dc6eb30190d8b617628bf"
|
||||
},
|
||||
{
|
||||
"path": "scripts/tsconfig.json",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "3c2eb7ba5c95a16cada153de4787ca7a4bf179609bf3848e12ff15b1b7927a68"
|
||||
},
|
||||
{
|
||||
"path": "SKILL.md",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "96b660983b82a4060d5e3d91f916aa683f584a7b26f74c3145b3e23994030b71"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -3,6 +3,8 @@ name: atlassian
|
||||
description: Interact with Atlassian Cloud Jira and Confluence through a portable task-oriented CLI for search, issue/page edits, comments, transitions, and bounded raw requests.
|
||||
---
|
||||
|
||||
<!-- ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/atlassian/_source/claude-code/SKILL.md and run `pnpm run sync:pi`. -->
|
||||
|
||||
# Atlassian (Claude Code)
|
||||
|
||||
Portable Atlassian workflows for Claude Code using a shared TypeScript CLI.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "atlassian-skill-scripts",
|
||||
"name": "@ai-coding-skills/atlassian-claude-code",
|
||||
"version": "1.0.0",
|
||||
"description": "Shared runtime for the Atlassian skill",
|
||||
"type": "module",
|
||||
@@ -16,5 +16,6 @@
|
||||
"tsx": "^4.20.5",
|
||||
"typescript": "^5.9.2"
|
||||
},
|
||||
"packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34"
|
||||
"packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34",
|
||||
"private": true
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/atlassian/shared/scripts/ and run `pnpm run sync:pi`.
|
||||
const TEXT_NODE = "text";
|
||||
|
||||
function textNode(text: string) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/atlassian/shared/scripts/ and run `pnpm run sync:pi`.
|
||||
import process from "node:process";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/atlassian/shared/scripts/ and run `pnpm run sync:pi`.
|
||||
import path from "node:path";
|
||||
|
||||
import { config as loadDotEnv } from "dotenv";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/atlassian/shared/scripts/ and run `pnpm run sync:pi`.
|
||||
import { sendJsonRequest } from "./http.js";
|
||||
import type { AtlassianConfig, CommandOutput, FetchLike } from "./types.js";
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/atlassian/shared/scripts/ and run `pnpm run sync:pi`.
|
||||
import { readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/atlassian/shared/scripts/ and run `pnpm run sync:pi`.
|
||||
import { createJsonHeaders, createStatusError } from "./http.js";
|
||||
import type { AtlassianConfig, CommandOutput, FetchLike } from "./types.js";
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/atlassian/shared/scripts/ and run `pnpm run sync:pi`.
|
||||
import { createBasicAuthHeader } from "./config.js";
|
||||
import type { AtlassianConfig, FetchLike } from "./types.js";
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/atlassian/shared/scripts/ and run `pnpm run sync:pi`.
|
||||
import { markdownToAdf } from "./adf.js";
|
||||
import { sendJsonRequest } from "./http.js";
|
||||
import type { AtlassianConfig, CommandOutput, FetchLike, JiraIssueSummary } from "./types.js";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/atlassian/shared/scripts/ and run `pnpm run sync:pi`.
|
||||
import type { CommandOutput, OutputFormat, Writer } from "./types.js";
|
||||
|
||||
function renderText(payload: CommandOutput<unknown>) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/atlassian/shared/scripts/ and run `pnpm run sync:pi`.
|
||||
import { readWorkspaceFile } from "./files.js";
|
||||
import { sendJsonRequest } from "./http.js";
|
||||
import type { AtlassianConfig, CommandOutput, FetchLike } from "./types.js";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/atlassian/shared/scripts/ and run `pnpm run sync:pi`.
|
||||
export type AtlassianConfig = {
|
||||
baseUrl: string;
|
||||
jiraBaseUrl: string;
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
{
|
||||
"$schema": "https://ai-coding-skills.dev/schemas/generated-manifest/v1.json",
|
||||
"generator": "scripts/generate-skills.mjs",
|
||||
"generatedRoot": "skills/atlassian/codex",
|
||||
"files": [
|
||||
{
|
||||
"path": "scripts/package.json",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "2462c2f552c99460071fc60231c5d2d9eca5283bd045e6c3f845e60f351e7e73"
|
||||
},
|
||||
{
|
||||
"path": "scripts/pnpm-lock.yaml",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "15556a6f53e68bb8d92d2710aae0836bc80af7f29be9d63aa1b87fcbd33732c6"
|
||||
},
|
||||
{
|
||||
"path": "scripts/src/adf.ts",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "c7c3b4a78ccd8fb5a8ab99c82e0eab67a0a0d656b3985c1f56817bda199ad20f"
|
||||
},
|
||||
{
|
||||
"path": "scripts/src/cli.ts",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "5c4f4db76817fa9dbdae0fd0c75be302248d4b87fc0a53f6bd3c90407a75ae98"
|
||||
},
|
||||
{
|
||||
"path": "scripts/src/config.ts",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "700dcdce96afab5294426e09f539135ae5432632370260190d6292071422eb3f"
|
||||
},
|
||||
{
|
||||
"path": "scripts/src/confluence.ts",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "709d5d61fdb14e37aa4eaa7175eb7f17f0ec661376c96071020fbc9574ddbb73"
|
||||
},
|
||||
{
|
||||
"path": "scripts/src/files.ts",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "16296eaa3ae41a4d7c694773036f9bb4bd2baa2db6a9c318078532b713678dba"
|
||||
},
|
||||
{
|
||||
"path": "scripts/src/health.ts",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "1db4b49e05b16a095b7e7ca31cdc4e22ebda19e20e05c40baaaac648eaec0d08"
|
||||
},
|
||||
{
|
||||
"path": "scripts/src/http.ts",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "66444b777d4d9b14d9793eb051c586eb811d2b36815b1018dd9d7517666c7eb2"
|
||||
},
|
||||
{
|
||||
"path": "scripts/src/jira.ts",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "485d8d618fe04eb1ce546c1694eadf15d867bc83c2a6f7df994688ab0335ea4f"
|
||||
},
|
||||
{
|
||||
"path": "scripts/src/output.ts",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "38e99818582a4962c09a83175634cba2bfead6acf33bd5f43cdca5caed7100a0"
|
||||
},
|
||||
{
|
||||
"path": "scripts/src/raw.ts",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "2309c96dd45a03509df204803de9ecf0b5ff82fd488730f55ac5dd6a23b81dd8"
|
||||
},
|
||||
{
|
||||
"path": "scripts/src/types.ts",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "9f92d27ab68604d5abfd0f5dc9552b96fed6d1f9fc7dc6eb30190d8b617628bf"
|
||||
},
|
||||
{
|
||||
"path": "scripts/tsconfig.json",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "3c2eb7ba5c95a16cada153de4787ca7a4bf179609bf3848e12ff15b1b7927a68"
|
||||
},
|
||||
{
|
||||
"path": "SKILL.md",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "e951cc63a6a3abf6b050b15f7316ea3675dd6aa3bc54677faa76ef5e6f1f7c16"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -3,6 +3,8 @@ name: atlassian
|
||||
description: Interact with Atlassian Cloud Jira and Confluence through a portable task-oriented CLI for search, issue/page edits, comments, transitions, and bounded raw requests.
|
||||
---
|
||||
|
||||
<!-- ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/atlassian/_source/codex/SKILL.md and run `pnpm run sync:pi`. -->
|
||||
|
||||
# Atlassian (Codex)
|
||||
|
||||
Portable Atlassian workflows for Codex using a shared TypeScript CLI.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "atlassian-skill-scripts",
|
||||
"name": "@ai-coding-skills/atlassian-codex",
|
||||
"version": "1.0.0",
|
||||
"description": "Shared runtime for the Atlassian skill",
|
||||
"type": "module",
|
||||
@@ -16,5 +16,6 @@
|
||||
"tsx": "^4.20.5",
|
||||
"typescript": "^5.9.2"
|
||||
},
|
||||
"packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34"
|
||||
"packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34",
|
||||
"private": true
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/atlassian/shared/scripts/ and run `pnpm run sync:pi`.
|
||||
const TEXT_NODE = "text";
|
||||
|
||||
function textNode(text: string) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/atlassian/shared/scripts/ and run `pnpm run sync:pi`.
|
||||
import process from "node:process";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/atlassian/shared/scripts/ and run `pnpm run sync:pi`.
|
||||
import path from "node:path";
|
||||
|
||||
import { config as loadDotEnv } from "dotenv";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/atlassian/shared/scripts/ and run `pnpm run sync:pi`.
|
||||
import { sendJsonRequest } from "./http.js";
|
||||
import type { AtlassianConfig, CommandOutput, FetchLike } from "./types.js";
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/atlassian/shared/scripts/ and run `pnpm run sync:pi`.
|
||||
import { readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/atlassian/shared/scripts/ and run `pnpm run sync:pi`.
|
||||
import { createJsonHeaders, createStatusError } from "./http.js";
|
||||
import type { AtlassianConfig, CommandOutput, FetchLike } from "./types.js";
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/atlassian/shared/scripts/ and run `pnpm run sync:pi`.
|
||||
import { createBasicAuthHeader } from "./config.js";
|
||||
import type { AtlassianConfig, FetchLike } from "./types.js";
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/atlassian/shared/scripts/ and run `pnpm run sync:pi`.
|
||||
import { markdownToAdf } from "./adf.js";
|
||||
import { sendJsonRequest } from "./http.js";
|
||||
import type { AtlassianConfig, CommandOutput, FetchLike, JiraIssueSummary } from "./types.js";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/atlassian/shared/scripts/ and run `pnpm run sync:pi`.
|
||||
import type { CommandOutput, OutputFormat, Writer } from "./types.js";
|
||||
|
||||
function renderText(payload: CommandOutput<unknown>) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/atlassian/shared/scripts/ and run `pnpm run sync:pi`.
|
||||
import { readWorkspaceFile } from "./files.js";
|
||||
import { sendJsonRequest } from "./http.js";
|
||||
import type { AtlassianConfig, CommandOutput, FetchLike } from "./types.js";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/atlassian/shared/scripts/ and run `pnpm run sync:pi`.
|
||||
export type AtlassianConfig = {
|
||||
baseUrl: string;
|
||||
jiraBaseUrl: string;
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
{
|
||||
"$schema": "https://ai-coding-skills.dev/schemas/generated-manifest/v1.json",
|
||||
"generator": "scripts/generate-skills.mjs",
|
||||
"generatedRoot": "skills/atlassian/cursor",
|
||||
"files": [
|
||||
{
|
||||
"path": "scripts/package.json",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "a48e86c0cd54ac3c9972b1d2818ee62a086ec34f1f7e88adcfc66eccc0adf04c"
|
||||
},
|
||||
{
|
||||
"path": "scripts/pnpm-lock.yaml",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "15556a6f53e68bb8d92d2710aae0836bc80af7f29be9d63aa1b87fcbd33732c6"
|
||||
},
|
||||
{
|
||||
"path": "scripts/src/adf.ts",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "c7c3b4a78ccd8fb5a8ab99c82e0eab67a0a0d656b3985c1f56817bda199ad20f"
|
||||
},
|
||||
{
|
||||
"path": "scripts/src/cli.ts",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "5c4f4db76817fa9dbdae0fd0c75be302248d4b87fc0a53f6bd3c90407a75ae98"
|
||||
},
|
||||
{
|
||||
"path": "scripts/src/config.ts",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "700dcdce96afab5294426e09f539135ae5432632370260190d6292071422eb3f"
|
||||
},
|
||||
{
|
||||
"path": "scripts/src/confluence.ts",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "709d5d61fdb14e37aa4eaa7175eb7f17f0ec661376c96071020fbc9574ddbb73"
|
||||
},
|
||||
{
|
||||
"path": "scripts/src/files.ts",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "16296eaa3ae41a4d7c694773036f9bb4bd2baa2db6a9c318078532b713678dba"
|
||||
},
|
||||
{
|
||||
"path": "scripts/src/health.ts",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "1db4b49e05b16a095b7e7ca31cdc4e22ebda19e20e05c40baaaac648eaec0d08"
|
||||
},
|
||||
{
|
||||
"path": "scripts/src/http.ts",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "66444b777d4d9b14d9793eb051c586eb811d2b36815b1018dd9d7517666c7eb2"
|
||||
},
|
||||
{
|
||||
"path": "scripts/src/jira.ts",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "485d8d618fe04eb1ce546c1694eadf15d867bc83c2a6f7df994688ab0335ea4f"
|
||||
},
|
||||
{
|
||||
"path": "scripts/src/output.ts",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "38e99818582a4962c09a83175634cba2bfead6acf33bd5f43cdca5caed7100a0"
|
||||
},
|
||||
{
|
||||
"path": "scripts/src/raw.ts",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "2309c96dd45a03509df204803de9ecf0b5ff82fd488730f55ac5dd6a23b81dd8"
|
||||
},
|
||||
{
|
||||
"path": "scripts/src/types.ts",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "9f92d27ab68604d5abfd0f5dc9552b96fed6d1f9fc7dc6eb30190d8b617628bf"
|
||||
},
|
||||
{
|
||||
"path": "scripts/tsconfig.json",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "3c2eb7ba5c95a16cada153de4787ca7a4bf179609bf3848e12ff15b1b7927a68"
|
||||
},
|
||||
{
|
||||
"path": "SKILL.md",
|
||||
"kind": "file",
|
||||
"mode": "644",
|
||||
"sha256": "39cb974a328958272e0fefa21969b41fca010326dbff121be2062af3138f9a9b"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -3,6 +3,8 @@ name: atlassian
|
||||
description: Interact with Atlassian Cloud Jira and Confluence through a portable task-oriented CLI for search, issue/page edits, comments, transitions, and bounded raw requests.
|
||||
---
|
||||
|
||||
<!-- ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/atlassian/_source/cursor/SKILL.md and run `pnpm run sync:pi`. -->
|
||||
|
||||
# Atlassian (Cursor Agent CLI)
|
||||
|
||||
Portable Atlassian workflows for Cursor Agent CLI using a shared TypeScript CLI.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "atlassian-skill-scripts",
|
||||
"name": "@ai-coding-skills/atlassian-cursor",
|
||||
"version": "1.0.0",
|
||||
"description": "Shared runtime for the Atlassian skill",
|
||||
"type": "module",
|
||||
@@ -16,5 +16,6 @@
|
||||
"tsx": "^4.20.5",
|
||||
"typescript": "^5.9.2"
|
||||
},
|
||||
"packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34"
|
||||
"packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34",
|
||||
"private": true
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/atlassian/shared/scripts/ and run `pnpm run sync:pi`.
|
||||
const TEXT_NODE = "text";
|
||||
|
||||
function textNode(text: string) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user