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)