diff --git a/adapters/agy/README.md b/adapters/agy/README.md new file mode 100644 index 0000000..90288aa --- /dev/null +++ b/adapters/agy/README.md @@ -0,0 +1,9 @@ +# AGY adapter + +Generated AGY/Antigravity adapters project AgentFlow SDLC skills into skill-compatible local adapter surfaces. + +Rules: + +- Use Windows-safe generated files, not symlinks. +- The AGY-facing skill must run deterministic CLI validators and interpret JSON results. +- `.agy` or AGY plugin folders are generated adapter surfaces only. diff --git a/adapters/claude-code/README.md b/adapters/claude-code/README.md new file mode 100644 index 0000000..956dde6 --- /dev/null +++ b/adapters/claude-code/README.md @@ -0,0 +1,9 @@ +# Claude Code adapter + +Generated Claude Code adapters project AgentFlow SDLC skills into `.claude/skills/`. + +Rules: + +- Canonical source remains under `skills/` and product docs/schemas. +- Generated adapters instruct Claude to use deterministic `agentflow-sdlc sdlc ...` validators. +- `.claude` settings/skills are generated or harness-owned local artifacts, not source of truth. diff --git a/adapters/codex/README.md b/adapters/codex/README.md new file mode 100644 index 0000000..6495102 --- /dev/null +++ b/adapters/codex/README.md @@ -0,0 +1,9 @@ +# Codex adapter + +Generated Codex adapters project AgentFlow SDLC skills into Codex-compatible skill/plugin surfaces. + +Rules: + +- Keep plugin metadata generated from canonical product payload. +- Do not install duplicate modes for one harness: choose skills-only, native-plugin, or cli-managed. +- Codex adapters delegate validation to `agentflow-sdlc` CLI commands. diff --git a/adapters/pi/README.md b/adapters/pi/README.md new file mode 100644 index 0000000..b52e5c6 --- /dev/null +++ b/adapters/pi/README.md @@ -0,0 +1,9 @@ +# Pi adapter + +Generated Pi adapters project AgentFlow SDLC skills into `.pi/skills/` or future Pi package surfaces. + +Rules: + +- `.pi` is not canonical product source. +- Pi adapter copies are generated from harness-neutral `skills/` sources. +- Pi workflows must record launcher/executor/transport/delegation boundary like every other harness. diff --git a/bin/cli.mjs b/bin/cli.mjs index 9b98eac..cc2df32 100644 --- a/bin/cli.mjs +++ b/bin/cli.mjs @@ -111,13 +111,8 @@ function handleSdlc(rest, targetDir) { return runScript('scripts/validate-sdlc-skill.mjs', pass, targetDir) if (subcommand === 'validate-agent') return runScript('scripts/validate-sdlc-agent.mjs', pass, targetDir) - if (subcommand === 'audit') return runScript('scripts/validate-sdlc-config.mjs', pass, targetDir) - if (subcommand === 'migrate') { - process.stdout.write( - 'SDLC migration is preview-first. Initial v1 migrator validates config and reports no writes.\n', - ) - return runScript('scripts/validate-sdlc-config.mjs', pass, targetDir) - } + if (subcommand === 'audit') return runScript('scripts/sdlc-audit.mjs', pass, targetDir) + if (subcommand === 'migrate') return runScript('scripts/sdlc-migrate.mjs', pass, targetDir) process.stderr.write(`Usage: agentflow-sdlc sdlc validate [--target ] [--json] agentflow-sdlc sdlc validate-issue --path [--json] diff --git a/lib/framework-files.mjs b/lib/framework-files.mjs index b4362ef..2cb97f2 100644 --- a/lib/framework-files.mjs +++ b/lib/framework-files.mjs @@ -232,6 +232,11 @@ export const FRAMEWORK_FILES = [ 'skills/sdlc-audit/templates/audit-report.md', 'skills/sdlc-audit/checklists/workflow-compliance.md', 'skills/sdlc-audit/evals/audit-findings-cases.md', + 'adapters/claude-code/README.md', + 'adapters/agy/README.md', + 'adapters/codex/README.md', + 'adapters/pi/README.md', + 'manifests/product-payload.json', 'agents/templates/role-pass.md', 'agents/templates/pr-manifest.md', 'agents/templates/workflow-status-comment.md', @@ -253,6 +258,9 @@ export const FRAMEWORK_FILES = [ 'scripts/validate-sdlc-release.mjs', 'scripts/validate-sdlc-skill.mjs', 'scripts/validate-sdlc-agent.mjs', + 'scripts/sdlc-audit.mjs', + 'scripts/sdlc-migrate.mjs', + 'scripts/sdlc-sandbox-smoke.mjs', 'scripts/validate-extension-packs.mjs', 'scripts/extension-pack.mjs', 'scripts/resolve-role-route.mjs', diff --git a/lib/sdlc-state.mjs b/lib/sdlc-state.mjs index bb20070..6a7eb43 100644 --- a/lib/sdlc-state.mjs +++ b/lib/sdlc-state.mjs @@ -137,7 +137,7 @@ export function validateNoForbiddenEvidenceText(text = '') { if (/BEGIN (RSA|OPENSSH|PRIVATE) KEY|api[_-]?key\s*=|secret\s*=|token\s*=/i.test(text)) { findings.push(finding('blocker', 'evidence.secrets', 'possible secret in durable evidence')) } - if (/raw transcript|tool input|full log|prompt:/i.test(text) && text.length > 1000) { + if (/(^|\n)(User|Assistant|Tool|System):/i.test(text) && text.length > 2000) { findings.push( finding('high', 'evidence.raw-log', 'possible raw prompt/transcript/log in durable evidence'), ) diff --git a/manifests/product-payload.json b/manifests/product-payload.json new file mode 100644 index 0000000..9d43244 --- /dev/null +++ b/manifests/product-payload.json @@ -0,0 +1,40 @@ +{ + "version": 1, + "entries": [ + { + "source": "docs/sdlc-definition.md", + "target": "docs/sdlc-definition.md", + "ownership": "managed" + }, + { + "source": "docs/sdlc-packaging.md", + "target": "docs/sdlc-packaging.md", + "ownership": "managed" + }, + { + "source": "defaults/sdlc.config.json", + "target": "sdlc.config.json", + "ownership": "seed-once" + }, + { + "source": "schemas/sdlc-config.schema.json", + "target": "schemas/sdlc-config.schema.json", + "ownership": "managed" + }, + { + "source": "skills/sdlc-definition/SKILL.md", + "target": "skills/sdlc-definition/SKILL.md", + "ownership": "managed" + }, + { + "source": "skills/sdlc-migration/SKILL.md", + "target": "skills/sdlc-migration/SKILL.md", + "ownership": "managed" + }, + { + "source": "skills/sdlc-audit/SKILL.md", + "target": "skills/sdlc-audit/SKILL.md", + "ownership": "managed" + } + ] +} diff --git a/scripts/sdlc-audit.mjs b/scripts/sdlc-audit.mjs new file mode 100644 index 0000000..87b0692 --- /dev/null +++ b/scripts/sdlc-audit.mjs @@ -0,0 +1,80 @@ +#!/usr/bin/env node +import { existsSync, readFileSync } from 'node:fs' +import { join } from 'node:path' +import { + loadSdlcConfig, + validateSdlcConfigShape, + validateNoForbiddenEvidenceText, + finding, + report, +} from '../lib/sdlc-state.mjs' + +const args = process.argv.slice(2) +const json = args.includes('--json') +const target = args.includes('--target') ? args[args.indexOf('--target') + 1] : process.cwd() +const findings = [] + +const config = loadSdlcConfig(target) +findings.push(...validateSdlcConfigShape(config).findings) + +for (const dir of ['.pi', '.claude', '.agy', '.codex']) { + if (existsSync(join(target, dir))) { + findings.push( + finding( + 'info', + 'harness.generated-surface', + `${dir} present; ensure no canonical product source is stored there`, + { source: dir }, + ), + ) + } +} + +for (const skill of ['sdlc-definition', 'sdlc-migration', 'sdlc-audit']) { + const skillPath = join(target, 'skills', skill, 'SKILL.md') + if (!existsSync(skillPath)) + findings.push(finding('high', 'skill.missing', `missing ${skill} skill`, { source: skillPath })) +} + +for (const path of ['docs/sdlc-definition.md', 'schemas/sdlc-config.schema.json']) { + if (!existsSync(join(target, path))) + findings.push(finding('blocker', 'sdlc.required-file', `missing ${path}`, { source: path })) +} +if ( + !existsSync(join(target, 'sdlc.config.json')) && + !existsSync(join(target, 'defaults/sdlc.config.json')) +) { + findings.push( + finding( + 'blocker', + 'sdlc.required-file', + 'missing sdlc.config.json or defaults/sdlc.config.json', + { + source: 'sdlc.config.json', + }, + ), + ) +} + +for (const path of ['docs/sdlc-definition.md', 'docs/cockpit-concepts-and-rules.md']) { + const full = join(target, path) + if (existsSync(full)) + findings.push( + ...validateNoForbiddenEvidenceText(readFileSync(full, 'utf8')).findings.map((item) => ({ + ...item, + source: path, + })), + ) +} + +const result = report(findings, 'full') +if (json) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`) +else { + process.stdout.write('[sdlc-audit]\n') + for (const item of result.findings) + process.stdout.write( + ` ${item.severity.toUpperCase()} ${item.code}: ${item.message}${item.source ? ` (${item.source})` : ''}\n`, + ) + process.stdout.write(`Result: ${result.ok ? 'READY' : 'FAILED'}\n`) +} +process.exit(result.ok ? 0 : 1) diff --git a/scripts/sdlc-migrate.mjs b/scripts/sdlc-migrate.mjs new file mode 100644 index 0000000..8e83cf3 --- /dev/null +++ b/scripts/sdlc-migrate.mjs @@ -0,0 +1,37 @@ +#!/usr/bin/env node +import { existsSync } from 'node:fs' +import { join } from 'node:path' +import { loadSdlcConfig, validateSdlcConfigShape } from '../lib/sdlc-state.mjs' + +const args = process.argv.slice(2) +const json = args.includes('--json') +const target = args.includes('--target') ? args[args.indexOf('--target') + 1] : process.cwd() +const apply = args.includes('--apply') || args.includes('--write') +const changes = [] +for (const path of [ + 'docs/sdlc-definition.md', + 'sdlc.config.json', + 'schemas/sdlc-config.schema.json', +]) { + if (!existsSync(join(target, path))) + changes.push({ action: 'install', path, reason: 'required SDLC product file missing' }) +} +const configReport = existsSync(join(target, 'sdlc.config.json')) + ? validateSdlcConfigShape(loadSdlcConfig(target)) + : { ok: false, findings: [] } +const result = { + ok: !apply && changes.length >= 0, + mode: apply ? 'blocked-preview-first' : 'dry-run', + message: apply + ? 'Migration apply is gated; run init/sync for file installation and review this dry-run first.' + : 'Preview migration plan; no writes performed.', + changes, + validation: configReport, +} +if (json) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`) +else { + process.stdout.write(`[sdlc-migrate] ${result.mode}\n${result.message}\n`) + for (const change of changes) + process.stdout.write(` - ${change.action} ${change.path}: ${change.reason}\n`) +} +process.exit(apply ? 1 : 0) diff --git a/scripts/sdlc-sandbox-smoke.mjs b/scripts/sdlc-sandbox-smoke.mjs new file mode 100644 index 0000000..346cda0 --- /dev/null +++ b/scripts/sdlc-sandbox-smoke.mjs @@ -0,0 +1,31 @@ +#!/usr/bin/env node +import { execFileSync } from 'node:child_process' +import { existsSync, mkdtempSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join, resolve } from 'node:path' + +const repoRoot = resolve(process.cwd()) +const cli = join(repoRoot, 'bin', 'cli.mjs') +const tmp = mkdtempSync(join(tmpdir(), 'agentflow-sdlc-smoke-')) +function run(args) { + return execFileSync(process.execPath, [cli, ...args, '--target', tmp], { encoding: 'utf8' }) +} +execFileSync('git', ['init'], { cwd: tmp, stdio: 'ignore' }) +run(['init']) +run(['sdlc', 'validate', '--json']) +run(['sdlc', 'audit', '--json']) +run(['sdlc', 'migrate', '--json']) +run(['skills', 'sync', '--harness', 'all', '--apply']) +run(['skills', 'status', '--harness', 'all', '--json']) +const expected = [ + 'docs/sdlc-definition.md', + 'sdlc.config.json', + 'schemas/sdlc-config.schema.json', + '.claude/skills/sdlc-definition/SKILL.md', + '.pi/skills/sdlc-audit/SKILL.md', + '.agents/skills/sdlc-migration/SKILL.md', +] +const missing = expected.filter((item) => !existsSync(join(tmp, item))) +const result = { ok: missing.length === 0, tmp, missing } +process.stdout.write(`${JSON.stringify(result, null, 2)}\n`) +process.exit(result.ok ? 0 : 1)