nono-skills 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +127 -0
  3. package/bin/cli.js +23 -0
  4. package/package.json +46 -0
  5. package/plugin/.codex-plugin/plugin.json +26 -0
  6. package/plugin/skills/api-design/SKILL.md +40 -0
  7. package/plugin/skills/api-design/agents/openai.yaml +4 -0
  8. package/plugin/skills/architecture-review/SKILL.md +40 -0
  9. package/plugin/skills/architecture-review/agents/openai.yaml +4 -0
  10. package/plugin/skills/brainstorm/SKILL.md +41 -0
  11. package/plugin/skills/brainstorm/agents/openai.yaml +4 -0
  12. package/plugin/skills/database-design/SKILL.md +40 -0
  13. package/plugin/skills/database-design/agents/openai.yaml +4 -0
  14. package/plugin/skills/debug/SKILL.md +40 -0
  15. package/plugin/skills/debug/agents/openai.yaml +4 -0
  16. package/plugin/skills/estimate/SKILL.md +40 -0
  17. package/plugin/skills/estimate/agents/openai.yaml +4 -0
  18. package/plugin/skills/fix-findings/SKILL.md +40 -0
  19. package/plugin/skills/fix-findings/agents/openai.yaml +4 -0
  20. package/plugin/skills/implement/SKILL.md +40 -0
  21. package/plugin/skills/implement/agents/openai.yaml +4 -0
  22. package/plugin/skills/migration/SKILL.md +40 -0
  23. package/plugin/skills/migration/agents/openai.yaml +4 -0
  24. package/plugin/skills/plan/SKILL.md +40 -0
  25. package/plugin/skills/plan/agents/openai.yaml +4 -0
  26. package/plugin/skills/refactor/SKILL.md +40 -0
  27. package/plugin/skills/refactor/agents/openai.yaml +4 -0
  28. package/plugin/skills/release-readiness/SKILL.md +39 -0
  29. package/plugin/skills/release-readiness/agents/openai.yaml +4 -0
  30. package/plugin/skills/review/SKILL.md +39 -0
  31. package/plugin/skills/review/agents/openai.yaml +4 -0
  32. package/plugin/skills/security-review/SKILL.md +39 -0
  33. package/plugin/skills/security-review/agents/openai.yaml +4 -0
  34. package/plugin/skills/test/SKILL.md +40 -0
  35. package/plugin/skills/test/agents/openai.yaml +4 -0
  36. package/scripts/validate.mjs +29 -0
  37. package/src/cli.js +85 -0
  38. package/src/codex.js +13 -0
  39. package/src/commands.js +98 -0
  40. package/src/doctor.js +40 -0
  41. package/src/fs-safe.js +38 -0
  42. package/src/plugin-install.js +135 -0
  43. package/src/plugin-state.js +82 -0
  44. package/src/project-init.js +53 -0
  45. package/src/uninstall.js +50 -0
  46. package/templates/AGENTS.md +48 -0
  47. package/templates/docs/agent/decision-log.md +15 -0
  48. package/templates/docs/agent/findings.md +13 -0
  49. package/templates/docs/agent/handoff.md +20 -0
  50. package/templates/docs/agent/plan.md +14 -0
  51. package/templates/docs/agent/spec.md +20 -0
@@ -0,0 +1,135 @@
1
+ import { cp, mkdir, readFile, rename, rm, stat } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ import { listFiles } from './fs-safe.js';
5
+ import {
6
+ createOwnershipManifest, readMarketplace, upsertMarketplaceEntry,
7
+ verifyOwnership, writeJsonAtomic,
8
+ } from './plugin-state.js';
9
+
10
+ const PLUGIN_NAME = 'engineering';
11
+ const MARKETPLACE_ENTRY = {
12
+ name: PLUGIN_NAME,
13
+ source: { source: 'local', path: './plugins/engineering' },
14
+ policy: { installation: 'AVAILABLE', authentication: 'ON_INSTALL' },
15
+ category: 'Developer Tools',
16
+ };
17
+
18
+ async function exists(file) {
19
+ try { await stat(file); return true; }
20
+ catch (error) { if (error.code === 'ENOENT') return false; throw error; }
21
+ }
22
+
23
+ async function readJson(file) {
24
+ return JSON.parse(await readFile(file, 'utf8'));
25
+ }
26
+
27
+ function withCachebuster(version, cachebuster) {
28
+ return `${version.split('+')[0]}+codex.${cachebuster}`;
29
+ }
30
+
31
+ async function transact(context, requireExisting) {
32
+ const { home, packageRoot, runCodex, packageVersion } = context;
33
+ const cachebuster = context.clock();
34
+ const preflight = await runCodex(['--version']);
35
+ if (preflight.code !== 0) throw new Error(`Codex CLI is unavailable: ${preflight.stderr || 'version check failed'}`);
36
+
37
+ const pluginsRoot = path.join(home, 'plugins');
38
+ const destination = path.join(pluginsRoot, PLUGIN_NAME);
39
+ const statePath = path.join(destination, '.installer-state.json');
40
+ const marketplacePath = path.join(home, '.agents', 'plugins', 'marketplace.json');
41
+ const oldMarketplace = await readMarketplace(marketplacePath);
42
+ const destinationExists = await exists(destination);
43
+ if (requireExisting && !destinationExists) throw new Error('Engineering plugin is not installed');
44
+
45
+ let oldState;
46
+ if (destinationExists) {
47
+ if (!await exists(statePath)) throw new Error('Existing engineering plugin is not owned by this installer');
48
+ oldState = await readJson(statePath);
49
+ const ownership = await verifyOwnership(oldState, destination);
50
+ if (!ownership.valid) throw new Error(`Existing engineering plugin is not owned safely; changed files: ${ownership.mismatches.join(', ')}`);
51
+ }
52
+
53
+ await mkdir(pluginsRoot, { recursive: true });
54
+ const stage = path.join(pluginsRoot, `.engineering-stage-${process.pid}-${Date.now()}`);
55
+ const backup = path.join(pluginsRoot, `.engineering-backup-${process.pid}-${Date.now()}`);
56
+ let movedExisting = false;
57
+ let installedStage = false;
58
+ let registrationSucceeded = false;
59
+ let registrationMarketplace = oldMarketplace?.name ?? 'personal';
60
+ try {
61
+ await cp(path.join(packageRoot, 'plugin'), stage, { recursive: true, errorOnExist: true });
62
+ const pluginJsonPath = path.join(stage, '.codex-plugin', 'plugin.json');
63
+ const plugin = await readJson(pluginJsonPath);
64
+ plugin.version = withCachebuster(plugin.version, cachebuster);
65
+ await writeJsonAtomic(pluginJsonPath, plugin);
66
+ const files = await listFiles(stage);
67
+ const manifest = await createOwnershipManifest({
68
+ packageVersion,
69
+ pluginVersion: plugin.version,
70
+ marketplaceName: oldMarketplace?.name ?? 'personal',
71
+ root: stage,
72
+ files,
73
+ });
74
+ await writeJsonAtomic(path.join(stage, '.installer-state.json'), manifest);
75
+
76
+ if (oldState && JSON.stringify(oldState.files) === JSON.stringify(manifest.files) && oldState.packageVersion === manifest.packageVersion) {
77
+ const marketplaceResult = upsertMarketplaceEntry(oldMarketplace, MARKETPLACE_ENTRY);
78
+ if (marketplaceResult.change !== 'unchanged') await writeJsonAtomic(marketplacePath, marketplaceResult.marketplace);
79
+ const marketplaceName = marketplaceResult.marketplace.name;
80
+ registrationMarketplace = marketplaceName;
81
+ const registration = await runCodex(['plugin', 'add', `${PLUGIN_NAME}@${marketplaceName}`]);
82
+ if (registration.code !== 0) throw new Error(registration.stderr || 'Codex plugin registration failed');
83
+ registrationSucceeded = true;
84
+ const verification = await runCodex(['plugin', 'list']);
85
+ if (verification.code !== 0 || !verification.stdout.includes(`${PLUGIN_NAME}@`)) {
86
+ throw new Error(verification.stderr || 'Codex plugin verification failed');
87
+ }
88
+ await rm(stage, { recursive: true, force: true });
89
+ return { status: marketplaceResult.change === 'unchanged' ? 'unchanged' : 'repaired', pluginVersion: plugin.version };
90
+ }
91
+
92
+ if (destinationExists) {
93
+ await rename(destination, backup);
94
+ movedExisting = true;
95
+ }
96
+ await rename(stage, destination);
97
+ installedStage = true;
98
+
99
+ const marketplaceResult = upsertMarketplaceEntry(oldMarketplace, MARKETPLACE_ENTRY);
100
+ await writeJsonAtomic(marketplacePath, marketplaceResult.marketplace);
101
+ const marketplaceName = marketplaceResult.marketplace.name;
102
+ registrationMarketplace = marketplaceName;
103
+ const registration = await runCodex(['plugin', 'add', `${PLUGIN_NAME}@${marketplaceName}`]);
104
+ if (registration.code !== 0) throw new Error(registration.stderr || 'Codex plugin registration failed');
105
+ registrationSucceeded = true;
106
+ const verification = await runCodex(['plugin', 'list']);
107
+ if (verification.code !== 0 || !verification.stdout.includes(`${PLUGIN_NAME}@`)) {
108
+ throw new Error(verification.stderr || 'Codex plugin verification failed');
109
+ }
110
+ await rm(backup, { recursive: true, force: true });
111
+ return { status: destinationExists ? 'updated' : 'installed', pluginVersion: plugin.version };
112
+ } catch (error) {
113
+ await rm(stage, { recursive: true, force: true });
114
+ if (installedStage) await rm(destination, { recursive: true, force: true });
115
+ if (movedExisting && await exists(backup)) await rename(backup, destination);
116
+ if (oldMarketplace === null) await rm(marketplacePath, { force: true });
117
+ else await writeJsonAtomic(marketplacePath, oldMarketplace);
118
+ if (registrationSucceeded) {
119
+ const previousEntry = oldMarketplace?.plugins?.find((plugin) => plugin.name === PLUGIN_NAME);
120
+ const rollback = previousEntry && oldState
121
+ ? await runCodex(['plugin', 'add', `${PLUGIN_NAME}@${oldMarketplace.name}`])
122
+ : await runCodex(['plugin', 'remove', `${PLUGIN_NAME}@${registrationMarketplace}`]);
123
+ if (rollback.code !== 0) throw new Error(`${error.message}; Codex rollback failed: ${rollback.stderr || 'unknown error'}`);
124
+ }
125
+ throw error;
126
+ }
127
+ }
128
+
129
+ export function installPlugin(context) {
130
+ return transact(context, false);
131
+ }
132
+
133
+ export function updatePlugin(context) {
134
+ return transact(context, true);
135
+ }
@@ -0,0 +1,82 @@
1
+ import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ import { listFiles, sha256File } from './fs-safe.js';
5
+
6
+ export async function readMarketplace(file) {
7
+ try {
8
+ return JSON.parse(await readFile(file, 'utf8'));
9
+ } catch (error) {
10
+ if (error.code === 'ENOENT') return null;
11
+ throw error;
12
+ }
13
+ }
14
+
15
+ export function upsertMarketplaceEntry(input, entry) {
16
+ const marketplace = structuredClone(input ?? {
17
+ name: 'personal', interface: { displayName: 'Personal' }, plugins: [],
18
+ });
19
+ marketplace.plugins ??= [];
20
+ const index = marketplace.plugins.findIndex((plugin) => plugin.name === entry.name);
21
+ if (index === -1) {
22
+ marketplace.plugins.push(structuredClone(entry));
23
+ return { marketplace, change: 'created' };
24
+ }
25
+ const existing = marketplace.plugins[index];
26
+ if (existing.source?.source !== 'local' || existing.source?.path !== entry.source.path) {
27
+ throw new Error(`Marketplace entry ${entry.name} is not owned by this installer`);
28
+ }
29
+ if (JSON.stringify(existing) === JSON.stringify(entry)) return { marketplace, change: 'unchanged' };
30
+ marketplace.plugins[index] = structuredClone(entry);
31
+ return { marketplace, change: 'updated' };
32
+ }
33
+
34
+ export function removeMarketplaceEntry(input, pluginName) {
35
+ const marketplace = structuredClone(input);
36
+ const before = marketplace.plugins?.length ?? 0;
37
+ marketplace.plugins = (marketplace.plugins ?? []).filter((plugin) => plugin.name !== pluginName);
38
+ return { marketplace, removed: marketplace.plugins.length !== before };
39
+ }
40
+
41
+ export async function writeJsonAtomic(file, value) {
42
+ await mkdir(path.dirname(file), { recursive: true });
43
+ const temporary = `${file}.tmp-${process.pid}-${Date.now()}`;
44
+ try {
45
+ await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
46
+ await rename(temporary, file);
47
+ } catch (error) {
48
+ await rm(temporary, { force: true });
49
+ throw error;
50
+ }
51
+ }
52
+
53
+ export async function createOwnershipManifest({ packageVersion, pluginVersion, marketplaceName, root, files }) {
54
+ const checksums = {};
55
+ for (const relative of [...files].sort()) checksums[relative] = await sha256File(path.join(root, relative));
56
+ return {
57
+ schemaVersion: 1,
58
+ packageVersion,
59
+ pluginVersion,
60
+ marketplaceName,
61
+ files: checksums,
62
+ };
63
+ }
64
+
65
+ export async function verifyOwnership(manifest, root) {
66
+ const mismatches = [];
67
+ for (const [relative, expected] of Object.entries(manifest.files ?? {}).sort(([a], [b]) => a.localeCompare(b))) {
68
+ try {
69
+ if (await sha256File(path.join(root, relative)) !== expected) mismatches.push(relative);
70
+ } catch (error) {
71
+ if (error.code === 'ENOENT') mismatches.push(relative);
72
+ else throw error;
73
+ }
74
+ }
75
+ const expectedFiles = new Set(Object.keys(manifest.files ?? {}));
76
+ const actualFiles = (await listFiles(root)).filter((relative) => relative !== '.installer-state.json');
77
+ for (const relative of actualFiles) {
78
+ if (!expectedFiles.has(relative)) mismatches.push(relative);
79
+ }
80
+ mismatches.sort();
81
+ return { valid: mismatches.length === 0, mismatches };
82
+ }
@@ -0,0 +1,53 @@
1
+ import { copyFile, mkdir, stat } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ import { filesEqual, listFiles } from './fs-safe.js';
5
+
6
+ async function exists(file) {
7
+ try {
8
+ await stat(file);
9
+ return true;
10
+ } catch (error) {
11
+ if (error.code === 'ENOENT') return false;
12
+ throw error;
13
+ }
14
+ }
15
+
16
+ export async function planProjectInit({ templateRoot, targetRoot, force = false, dryRun = false, clock = () => new Date().toISOString().replaceAll(/[-:.]/g, '') }) {
17
+ const files = await listFiles(templateRoot);
18
+ const actions = [];
19
+ const stamp = clock();
20
+ for (const relative of files) {
21
+ const source = path.join(templateRoot, relative);
22
+ const destination = path.join(targetRoot, relative);
23
+ let type = 'create';
24
+ let backup;
25
+ if (await exists(destination)) {
26
+ if (await filesEqual(source, destination)) type = 'skip';
27
+ else if (force) {
28
+ type = 'replace';
29
+ backup = path.join(targetRoot, '.codex-engineering-skills-backup', stamp, relative);
30
+ } else type = 'conflict';
31
+ }
32
+ actions.push({ type, source, destination, relative, backup, dryRun });
33
+ }
34
+ return actions;
35
+ }
36
+
37
+ export async function applyProjectInit(actions) {
38
+ const results = [];
39
+ for (const action of actions) {
40
+ if (action.dryRun || action.type === 'skip' || action.type === 'conflict') {
41
+ results.push({ ...action, applied: false });
42
+ continue;
43
+ }
44
+ await mkdir(path.dirname(action.destination), { recursive: true });
45
+ if (action.type === 'replace') {
46
+ await mkdir(path.dirname(action.backup), { recursive: true });
47
+ await copyFile(action.destination, action.backup);
48
+ }
49
+ await copyFile(action.source, action.destination);
50
+ results.push({ ...action, applied: true });
51
+ }
52
+ return results;
53
+ }
@@ -0,0 +1,50 @@
1
+ import { readFile, rm, stat } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ import { sha256File } from './fs-safe.js';
5
+ import { readMarketplace, removeMarketplaceEntry, verifyOwnership, writeJsonAtomic } from './plugin-state.js';
6
+
7
+ async function exists(file) {
8
+ try { await stat(file); return true; }
9
+ catch (error) { if (error.code === 'ENOENT') return false; throw error; }
10
+ }
11
+
12
+ export async function uninstallPlugin({ home, runCodex }) {
13
+ const pluginRoot = path.join(home, 'plugins', 'engineering');
14
+ const statePath = path.join(pluginRoot, '.installer-state.json');
15
+ if (!await exists(statePath)) throw new Error('Engineering plugin is not owned by this installer');
16
+ const state = JSON.parse(await readFile(statePath, 'utf8'));
17
+ const ownership = await verifyOwnership(state, pluginRoot);
18
+ if (!ownership.valid) throw new Error(`Engineering plugin ownership check failed: ${ownership.mismatches.join(', ')}`);
19
+
20
+ const marketplacePath = path.join(home, '.agents', 'plugins', 'marketplace.json');
21
+ const marketplace = await readMarketplace(marketplacePath);
22
+ const entry = marketplace?.plugins?.find((plugin) => plugin.name === 'engineering');
23
+ if (!entry || entry.source?.source !== 'local' || entry.source?.path !== './plugins/engineering') {
24
+ throw new Error('Engineering marketplace entry is not owned by this installer');
25
+ }
26
+
27
+ const removal = await runCodex(['plugin', 'remove', `engineering@${state.marketplaceName}`]);
28
+ if (removal.code !== 0) throw new Error(removal.stderr || 'Codex plugin removal failed');
29
+ const next = removeMarketplaceEntry(marketplace, 'engineering');
30
+ await writeJsonAtomic(marketplacePath, next.marketplace);
31
+ await rm(pluginRoot, { recursive: true });
32
+ return { status: 'uninstalled', projectArtifactsPreserved: true };
33
+ }
34
+
35
+ export async function purgeProject({ targetRoot, recordedChecksums }) {
36
+ const removed = [];
37
+ const preserved = [];
38
+ for (const [relative, expected] of Object.entries(recordedChecksums).sort(([a], [b]) => a.localeCompare(b))) {
39
+ const file = path.join(targetRoot, relative);
40
+ try {
41
+ if (await sha256File(file) === expected) {
42
+ await rm(file);
43
+ removed.push(relative);
44
+ } else preserved.push(relative);
45
+ } catch (error) {
46
+ if (error.code !== 'ENOENT') throw error;
47
+ }
48
+ }
49
+ return { removed, preserved };
50
+ }
@@ -0,0 +1,48 @@
1
+ # Repository Guidance
2
+
3
+ Keep this file specific to the repository. Replace placeholders with commands and conventions that are true here; remove sections that do not apply.
4
+
5
+ ## Repository facts
6
+
7
+ - Setup:
8
+ - Lint:
9
+ - Typecheck:
10
+ - Test:
11
+ - Build:
12
+ - Important directories:
13
+
14
+ ## Durable rules
15
+
16
+ - Treat executable code, configuration, tests, migrations, and observed behavior as stronger evidence than stale prose.
17
+ - Preserve unrelated behavior and backward compatibility unless the requirement changes it.
18
+ - Keep external writes, commits, pushes, merges, releases, deployments, and production mutations behind explicit user authorization.
19
+ - Verify changed behavior with the strongest safe repository checks available. Report what ran and what remains unverified.
20
+
21
+ ## Task artifacts
22
+
23
+ Use existing files under `docs/agent/` only when they help the current task:
24
+
25
+ - `spec.md`: goal, scope, constraints, acceptance criteria, and unresolved product choices
26
+ - `plan.md`: current execution map, status, dependencies, and verification targets
27
+ - `decision-log.md`: material decisions, evidence, alternatives, and consequences
28
+ - `findings.md`: review findings and their lifecycle
29
+ - `handoff.md`: resumable state when work remains or ownership changes
30
+
31
+ Do not create missing workflow artifacts unless the user requests durable artifacts or runs `npx nono-skills init`. Without them, put the same material information in the final response.
32
+
33
+ Append a decision only when it changes a contract, resolves meaningful ambiguity, accepts risk or a costly tradeoff, changes the plan materially, or records an assumption future work must preserve. Supersede accepted entries instead of rewriting history. Do not log routine edits or shell commands.
34
+
35
+ ## Definition of done
36
+
37
+ - The requested outcome and acceptance criteria are satisfied.
38
+ - Relevant targeted checks pass, followed by broader checks in proportion to risk.
39
+ - Compatibility, migration, security, and operational effects are addressed when applicable.
40
+ - Remaining risks, blockers, and unverified assumptions are explicit.
41
+
42
+ ## Project conventions
43
+
44
+ Add architecture boundaries, naming rules, generated-file policy, release procedures, ownership, and recurring review guidance here only after they become repository facts.
45
+
46
+ ## Skills
47
+
48
+ Let Codex select the smallest applicable set, or invoke one explicitly as `$engineering:<skill>` such as `$engineering:plan` or `$engineering:review`. Skills provide task-specific process; this file provides repository-specific truth.
@@ -0,0 +1,15 @@
1
+ # Decision Log
2
+
3
+ Append material decisions. Never silently rewrite an accepted decision; supersede it with a new entry.
4
+
5
+ ## YYYY-MM-DD HH:MM — Decision title
6
+
7
+ - Status: proposed | accepted | superseded | rejected
8
+ - Context:
9
+ - Decision:
10
+ - Alternatives:
11
+ - Evidence and rationale:
12
+ - Consequences:
13
+ - Follow-up:
14
+ - Related:
15
+
@@ -0,0 +1,13 @@
1
+ # Findings
2
+
3
+ ## Finding ID — Short title
4
+
5
+ - Severity: critical | high | medium | low
6
+ - State: open | accepted | fixed | verified | wont-fix | not-reproducible
7
+ - Evidence:
8
+ - Impact:
9
+ - Reproduction or reasoning:
10
+ - Remediation direction:
11
+ - Verification:
12
+ - Owner or follow-up:
13
+
@@ -0,0 +1,20 @@
1
+ # Agent Handoff
2
+
3
+ ## Objective
4
+
5
+ ## Current status
6
+
7
+ ## Completed
8
+
9
+ ## In progress
10
+
11
+ ## Remaining
12
+
13
+ ## Decisions and assumptions
14
+
15
+ ## Blockers and human input
16
+
17
+ ## Verification performed
18
+
19
+ ## Resume from here
20
+
@@ -0,0 +1,14 @@
1
+ # Working Plan
2
+
3
+ Status values: `pending`, `in-progress`, `blocked`, `done`.
4
+
5
+ | Status | Work item | Verification | Notes |
6
+ |---|---|---|---|
7
+ | pending | | | |
8
+
9
+ ## Risks and dependencies
10
+
11
+ ## Human decisions needed
12
+
13
+ ## Latest checkpoint
14
+
@@ -0,0 +1,20 @@
1
+ # Working Spec
2
+
3
+ ## Goal
4
+
5
+ ## Users and outcome
6
+
7
+ ## In scope
8
+
9
+ ## Out of scope
10
+
11
+ ## Constraints
12
+
13
+ ## Acceptance criteria
14
+
15
+ - [ ]
16
+
17
+ ## Open questions
18
+
19
+ ## Evidence and references
20
+