@gon1942/ai-dev-external-framework 0.0.0-stage → 0.8.1

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 (34) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +39 -2
  3. package/bin/ai-dev.js +21 -0
  4. package/lib/init.js +31 -0
  5. package/lib/install.js +46 -0
  6. package/lib/status.js +16 -0
  7. package/package.json +34 -4
  8. package/scripts/external-install.py +101 -0
  9. package/scripts/publish-check.cjs +8 -0
  10. package/scripts/verification.py +40 -0
  11. package/scripts/workflow.py +165 -0
  12. package/templates/.ai-dev/spec-adapter.json +5 -0
  13. package/templates/.ai-dev/verification-profiles.json +37 -0
  14. package/templates/AGENTS.md +23 -0
  15. package/templates/ARCHITECTURE.md +3 -0
  16. package/templates/docs/DESIGN.md +3 -0
  17. package/templates/docs/FRONTEND.md +3 -0
  18. package/templates/docs/PLANS.md +23 -0
  19. package/templates/docs/PRODUCT_SENSE.md +3 -0
  20. package/templates/docs/QUALITY_SCORE.md +3 -0
  21. package/templates/docs/RELIABILITY.md +3 -0
  22. package/templates/docs/SECURITY.md +3 -0
  23. package/templates/docs/ai-workflow/EXTERNAL-SKILLS.md +13 -0
  24. package/templates/docs/ai-workflow/POLICY.md +8 -0
  25. package/templates/docs/design-docs/core-beliefs.md +3 -0
  26. package/templates/docs/design-docs/index.md +3 -0
  27. package/templates/docs/exec-plans/active/.gitkeep +0 -0
  28. package/templates/docs/exec-plans/completed/.gitkeep +0 -0
  29. package/templates/docs/exec-plans/tech-debt-tracker.md +4 -0
  30. package/templates/docs/generated/db-schema.md +3 -0
  31. package/templates/docs/product-specs/index.md +3 -0
  32. package/templates/docs/references/design-system-reference-llms.txt +1 -0
  33. package/templates/docs/references/nixpacks-llms.txt +1 -0
  34. package/templates/docs/references/uv-llms.txt +1 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,40 @@
1
- # Temporary Holding Version
1
+ # External AI Development Framework v0.8.1
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Single npm source tree for OpenCode / Codex / Claude Code. Installs **original upstream** Matt Pocock, Superpowers and Ponytail skills, project docs, session hooks and workflow tracking. No custom replacement Analyze/Plan/Implement/Verify/Review skills.
4
+
5
+ ## Publish (requires your npm account)
6
+
7
+ ```bash
8
+ npm test
9
+ npm run publish:check
10
+ npm login
11
+ npm publish --access public
12
+ ```
13
+
14
+ Check npm package availability and account permission before publishing; this project is not published automatically.
15
+
16
+ ## Install from npm after publication
17
+
18
+ ```bash
19
+ npx --yes @gon1942/ai-dev-external-framework@0.8.1 init my-project
20
+ cd my-project
21
+ opencode # or codex / claude
22
+ ```
23
+
24
+ `init` creates a missing project directory, installs docs / hooks and **downloads and installs original upstream skills** in one operation. Requires Node.js >=20, Python 3, Git and internet access. A failed download is surfaced as a nonzero exit; retry is safe for unchanged files. For an existing project use `init .`; differing project-specific files are preserved, managed-file conflicts are blocked.
25
+
26
+ Preview without changing files (also skips network fetch):
27
+
28
+ ```bash
29
+ npx --yes @gon1942/ai-dev-external-framework@0.8.1 init my-project --dry-run
30
+ ```
31
+
32
+ For offline installation of vendored **unchanged original** checkouts:
33
+
34
+ ```bash
35
+ npx --yes @gon1942/ai-dev-external-framework@0.8.1 init my-project --source-dir /path/to/three-repos
36
+ ```
37
+
38
+ Source directory must contain `mattpocock/`, `superpowers/`, `ponytail/`. Pin exact vendor git revisions via `--pins pins.json`. Without pins, upstream HEAD is fetched, which is not reproducible across installations. Skills are installed under `.agents/skills`, `.opencode/skills`, `.claude/skills`.
39
+
40
+ `ai-dev setup`, `ai-dev install-skills`, `ai-dev status`, `ai-dev doctor`, `ai-dev work` remain available. `init` is the one-command convenience entrypoint. CRG is not installed. Actual host-specific hook, subagent and Ponytail plugin execution requires validation in each tool. The CLI does not publish to npm and does not modify user approval decisions.
package/bin/ai-dev.js ADDED
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ const path=require('node:path');const {spawnSync}=require('node:child_process');const fs=require('node:fs');const {setup,installSkills}=require('../lib/install');
4
+ const {init}=require('../lib/init');const {sessionStatus,formatStatus}=require('../lib/status');
5
+ const [command,...argv]=process.argv.slice(2);const get=(k,d)=>{const i=argv.indexOf(k);return i<0?d:argv[i+1]};const has=(k)=>argv.includes(k);const project=path.resolve(get('--project',process.cwd()));
6
+ try{
7
+ if(!command||['help','--help'].includes(command)){console.log(`ai-dev v0.8.1\n init <PATH> [--agents all|opencode|codex|claude] [--dry-run] [--source-dir PATH] [--pins FILE]\n setup [--project PATH] [--agents all|opencode|codex|claude] [--dry-run]\n install-skills [--project PATH] [--agents all|...] [--source-dir PATH] [--dry-run] [--update] [--pins FILE]\n status [--project PATH] [--json]\n work <workflow.py arguments>\n doctor [--project PATH]\n\nNote: setup installs native hooks + docs; install-skills fetches unmodified upstream skills separately.`);process.exit(0)}
8
+ if(command==='init'){init(argv);process.exit(0)}
9
+ if(!fs.existsSync(project)||!fs.statSync(project).isDirectory())throw Error('Project directory must exist: '+project);
10
+ if(command==='setup'){for(const action of setup(project,{dry:has('--dry-run'),agents:get('--agents','all')}))console.log(action);console.log('Native hooks may need host trust approval; see README.');}
11
+ else if(command==='install-skills')installSkills(project,{agents:get('--agents','all'),sourceDir:get('--source-dir',undefined),dry:has('--dry-run'),update:has('--update'),pins:get('--pins',undefined)});
12
+ else if(command==='status'){const s=sessionStatus(project);console.log(has('--json')?JSON.stringify(s,null,2):formatStatus(s));}
13
+ else if(command==='doctor'){
14
+ const paths=['AGENTS.md','scripts/workflow.py','.ai-dev/session-status.cjs','.claude/settings.json','.codex/hooks.json','.opencode/plugins/ai-dev-session.js'];let fail=0;
15
+ for(const p of paths){const good=fs.existsSync(path.join(project,p));console.log((good?'PASS ':'MISSING ')+p);if(!good)fail++}
16
+ for(const [name,dir] of Object.entries({codex:'.agents/skills',opencode:'.opencode/skills',claude:'.claude/skills'})){const full=path.join(project,dir);console.log((fs.existsSync(full)?'FOUND ':'NOT INSTALLED ')+`${name} upstream skills`)}
17
+ if(fail)process.exitCode=1;
18
+ }
19
+ else if(command==='work'){const rest=argv.filter((_,i)=>!(argv[i-1]==='--project'||argv[i]==='--project'));const r=spawnSync('python3',[path.join(project,'scripts','workflow.py'),...rest],{stdio:'inherit',cwd:project});process.exitCode=r.status??2;}
20
+ else throw Error('Unknown command: '+command);
21
+ }catch(e){console.error('ERROR:',e.message);process.exitCode=2}
package/lib/init.js ADDED
@@ -0,0 +1,31 @@
1
+ 'use strict';
2
+ const fs=require('node:fs');
3
+ const path=require('node:path');
4
+ const {setup,installSkills}=require('./install');
5
+ function init(argv){
6
+ const value=(key)=>{const i=argv.indexOf(key);if(i<0)return undefined;if(!argv[i+1]||argv[i+1].startsWith('--'))throw Error(`Missing value for ${key}`);return argv[i+1]};
7
+ const positional=argv.filter((x,i)=>!x.startsWith('--')&&!['--agents','--pins','--source-dir'].includes(argv[i-1]));
8
+ const target=positional[0];
9
+ if(!target||positional.length!==1)throw Error('Usage: ai-dev init <project-directory> [--agents all] [--dry-run]');
10
+ const project=path.resolve(target),dry=argv.includes('--dry-run');
11
+ const agents=value('--agents')||'all',sourceDir=value('--source-dir'),pins=value('--pins');
12
+ if(!['all','opencode','codex','claude'].includes(agents))throw Error('Unknown agents option: '+agents);
13
+ if(fs.existsSync(project)&&!fs.statSync(project).isDirectory())throw Error('Target exists and is not a directory: '+project);
14
+ if(!fs.existsSync(project)){
15
+ console.log(`${dry?'WOULD CREATE':'CREATE'} project ${project}`);
16
+ if(!dry)fs.mkdirSync(project,{recursive:true});
17
+ }
18
+ // All setup actions are planned before any upstream fetch. Existing project files are never overwritten.
19
+ // A failed upstream fetch leaves installed docs intact, so retrying `init` is possible.
20
+ if(!dry||fs.existsSync(project))for(const action of setup(project,{dry,agents}))console.log(action);
21
+ if(dry&&!fs.existsSync(project)){
22
+ console.log('Dry-run: new project does not yet exist; setup and upstream skills would be installed at '+project);
23
+ console.log('WOULD INSTALL original upstream skills (fetch required on real install)');return;
24
+ }
25
+ if(dry){console.log('Dry-run does not download upstream repositories.');return;}
26
+ console.log('Installing original upstream skills...');
27
+ installSkills(project,{agents,sourceDir,pins});
28
+ console.log('INIT COMPLETE '+project);
29
+ console.log('Run `opencode`, `codex`, or `claude` inside the project after checking the installation.');
30
+ }
31
+ module.exports={init};
package/lib/install.js ADDED
@@ -0,0 +1,46 @@
1
+ 'use strict';
2
+ const fs=require('node:fs');const path=require('node:path');const {spawnSync}=require('node:child_process');
3
+ const ROOT=path.resolve(__dirname,'..');
4
+ function walk(dir){const out=[];for(const e of fs.readdirSync(dir,{withFileTypes:true})){const p=path.join(dir,e.name);if(e.isDirectory())out.push(...walk(p));else out.push(p)}return out;}
5
+ function put(file,data,dry,mode='managed'){
6
+ if(fs.existsSync(file)){
7
+ const old=fs.readFileSync(file);if(old.equals(Buffer.isBuffer(data)?data:Buffer.from(data)))return 'UNCHANGED';
8
+ if(mode==='managed')throw Error(`Existing file differs: ${file} (refusing overwrite)`);return 'PRESERVED';
9
+ }
10
+ if(!dry){fs.mkdirSync(path.dirname(file),{recursive:true});fs.writeFileSync(file,data)}return dry?'WOULD_CREATE':'CREATED';
11
+ }
12
+ function mergeHooks(file, entry, dry){let obj={};if(fs.existsSync(file)){obj=JSON.parse(fs.readFileSync(file,'utf8'));if(!obj||Array.isArray(obj)||typeof obj!=='object')throw Error(`Invalid JSON: ${file}`)}
13
+ obj.hooks??={};obj.hooks.SessionStart??=[];const sig=JSON.stringify(entry);if(obj.hooks.SessionStart.some(x=>JSON.stringify(x)===sig))return 'UNCHANGED';obj.hooks.SessionStart.push(entry);
14
+ if(!dry){fs.mkdirSync(path.dirname(file),{recursive:true});fs.writeFileSync(file,JSON.stringify(obj,null,2)+'\n')}return dry?'WOULD_MERGE':'MERGED';
15
+ }
16
+ function setup(project,{dry=false,agents='all'}={}){
17
+ const selected=agents==='all'?['opencode','codex','claude']:agents.split(',');for(const a of selected)if(!['opencode','codex','claude'].includes(a))throw Error(`Unknown agent ${a}`);
18
+ const actions=[];const source=path.join(ROOT,'templates');for(const f of walk(source)){
19
+ const rel=path.relative(source,f);if(rel==='AGENTS.md'&&fs.existsSync(path.join(project,rel))){actions.push(`PRESERVED ${rel} (manually merge the workflow guidance)`);continue}
20
+ const target=path.join(project,rel); actions.push(`${put(target,fs.readFileSync(f),dry,'project')} ${rel}`);
21
+ }
22
+ const py=path.join(ROOT,'scripts','workflow.py');
23
+ // Installed copies are self-contained and never overwritten without explicit user review.
24
+ actions.push(`${put(path.join(project,'scripts','workflow.py'),fs.readFileSync(py),dry)} scripts/workflow.py`);
25
+ const entryScript=path.join(project,'.ai-dev','session-status.cjs');
26
+ // The installed hook imports the exact same status engine used by `ai-dev status`.
27
+ const statusEngine=path.join(project,'.ai-dev','status-engine.cjs');
28
+ actions.push(`${put(statusEngine,fs.readFileSync(path.join(ROOT,'lib','status.js')),dry)} .ai-dev/status-engine.cjs`);
29
+ const portable=`const path=require('node:path');\nconst {sessionStatus,formatStatus}=require('./status-engine.cjs');\nprocess.stdout.write(formatStatus(sessionStatus(path.resolve(__dirname,'..')))+'\\n');\n`;
30
+ actions.push(`${put(entryScript,portable,dry)} .ai-dev/session-status.cjs`);
31
+ // hook command cwd is tool-defined; use absolute path resolved at setup, with robust JSON/shell quoting.
32
+ const cmd=`node ${JSON.stringify(entryScript)}`;
33
+ if(selected.includes('claude')){const target=path.join(project,'.claude','settings.json');actions.push(`${mergeHooks(target,{hooks:[{type:'command',command:cmd,timeout:15}]},dry)} .claude/settings.json (SessionStart)`)}
34
+ if(selected.includes('codex')){const target=path.join(project,'.codex','hooks.json');actions.push(`${mergeHooks(target,{hooks:[{type:'command',command:cmd,timeout:15}]},dry)} .codex/hooks.json (SessionStart)`)}
35
+ if(selected.includes('opencode')){
36
+ const target=path.join(project,'.opencode','plugins','ai-dev-session.js');
37
+ const script=`import { execFileSync } from 'node:child_process';\nexport const AiDevSession = async ({client}) => {\n const seen=new Set();\n return {event:async ({event})=>{\n if(event.type!=='session.created')return;\n const id=event.properties?.info?.id || event.properties?.sessionID; if(!id||seen.has(id))return;seen.add(id);\n try{ const status=execFileSync('node',[${JSON.stringify(entryScript)}],{encoding:'utf8',timeout:15000});\n await client.tui.showToast({body:{title:'AI work status',message:status.slice(0,900),variant:'info',duration:8000}});\n }catch(e){console.warn('[ai-dev] session status unavailable:',String(e));}\n }};\n};\n`;
38
+ actions.push(`${put(target,script,dry)} .opencode/plugins/ai-dev-session.js`);
39
+ }
40
+ return actions;
41
+ }
42
+ function installSkills(project,options={}){
43
+ const args=[path.join(ROOT,'scripts','external-install.py'),'--project',project,'--agents',options.agents||'all'];if(options.sourceDir)args.push('--source-dir',options.sourceDir);if(options.dry)args.push('--dry-run');if(options.update)args.push('--update');if(options.pins)args.push('--pins',options.pins);
44
+ const r=spawnSync('python3',args,{stdio:'inherit'});if(r.status!==0)throw Error(`Original upstream skill install failed (exit ${r.status})`);
45
+ }
46
+ module.exports={setup,installSkills};
package/lib/status.js ADDED
@@ -0,0 +1,16 @@
1
+ 'use strict';
2
+ const fs=require('node:fs'); const path=require('node:path');
3
+ function sessionStatus(root){
4
+ const base=path.join(root,'docs','exec-plans','active');let items=[],warnings=[];
5
+ if(fs.existsSync(base))for(const ent of fs.readdirSync(base,{withFileTypes:true}).filter(x=>x.isDirectory()).sort((a,b)=>a.name.localeCompare(b.name))){
6
+ const file=path.join(base,ent.name,'status.json');if(!fs.existsSync(file))continue;
7
+ try {const s=JSON.parse(fs.readFileSync(file,'utf8')); const hasPlan=['spec.md','plan.md','ac-tests.json'].every(x=>fs.existsSync(path.join(base,ent.name,x)));let phase='PLAN_DRAFT';
8
+ if(s.approved===true||Boolean(s.approved_by)){phase=s.review?.verdict==='PASS'?'REVERIFY_OR_COMPLETION_CHECK':'IMPLEMENT_OR_VERIFY';}
9
+ else if(hasPlan){const ac=JSON.parse(fs.readFileSync(path.join(base,ent.name,'ac-tests.json'),'utf8'));if(Object.keys(ac).length)phase='PLAN_APPROVAL_REQUIRED';}
10
+ items.push({id:ent.name,title:s.title||'',phase,path:`docs/exec-plans/active/${ent.name}`});
11
+ }catch(e){warnings.push(`${ent.name}: ${e.message}`)}
12
+ }
13
+ return {project:root,active_count:items.length,tasks:items,warnings};
14
+ }
15
+ function formatStatus(s){const lines=['=== AI WORK SESSION STATUS ===',`Project: ${s.project}`,`Active tasks: ${s.active_count}`]; if(!s.tasks.length)lines.push('No active tasks. Do not auto-create work at session start.');for(const t of s.tasks)lines.push(`${t.id} | ${t.phase} | ${t.title}\n ${t.path}`);for(const w of s.warnings)lines.push(`WARNING: ${w}`);lines.push('Read existing task files before starting new work. Show this summary to the user.');return lines.join('\n');}
16
+ module.exports={sessionStatus,formatStatus};
package/package.json CHANGED
@@ -1,6 +1,36 @@
1
1
  {
2
2
  "name": "@gon1942/ai-dev-external-framework",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.8.1",
4
+ "private": false,
5
+ "description": "Cross-agent external skills installer, session status hooks and task tracking for OpenCode, Codex and Claude Code",
6
+ "bin": {
7
+ "ai-dev": "bin/ai-dev.js"
8
+ },
9
+ "engines": {
10
+ "node": ">=20"
11
+ },
12
+ "scripts": {
13
+ "test": "node --test test/*.test.js && python3 -m unittest discover -s test -p \"test_*.py\"",
14
+ "prepack": "npm test",
15
+ "publish:check": "node scripts/publish-check.cjs && npm pack --dry-run --json"
16
+ },
17
+ "license": "MIT",
18
+ "files": [
19
+ "bin/",
20
+ "lib/",
21
+ "scripts/",
22
+ "templates/",
23
+ "adapters/",
24
+ "external-skills/",
25
+ "README.md",
26
+ "LICENSE"
27
+ ],
28
+ "keywords": [
29
+ "agent-skills",
30
+ "opencode",
31
+ "codex",
32
+ "claude-code",
33
+ "workflow",
34
+ "task-graph"
35
+ ]
36
+ }
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env python3
2
+ """Install unmodified upstream agent skills for Codex, OpenCode and Claude Code."""
3
+ import argparse, json, shutil, subprocess, tempfile, pathlib, hashlib, datetime, sys
4
+
5
+ ROOT=pathlib.Path(__file__).resolve().parent.parent
6
+ CATALOG={
7
+ 'mattpocock':('https://github.com/mattpocock/skills.git', ['grill-with-docs','to-spec','to-tickets','implement','implement-spec','code-review','retro','diagnosing-bugs']),
8
+ 'superpowers':('https://github.com/obra/superpowers.git', ['brainstorming','writing-plans','test-driven-development','systematic-debugging','verification-before-completion','requesting-code-review','receiving-code-review','using-git-worktrees','subagent-driven-development']),
9
+ 'ponytail':('https://github.com/DietrichGebert/ponytail.git', ['ponytail']),
10
+ }
11
+
12
+ def run(args):
13
+ p=subprocess.run(args,text=True,capture_output=True)
14
+ if p.returncode: raise RuntimeError(f"{' '.join(args)}\n{p.stderr.strip()}")
15
+ return p.stdout.strip()
16
+
17
+ def sha(b): return hashlib.sha256(b).hexdigest()
18
+
19
+ def main():
20
+ ap=argparse.ArgumentParser(description=__doc__)
21
+ ap.add_argument('--project',required=True)
22
+ ap.add_argument('--agents',choices=['all','codex','opencode','claude'],default='all')
23
+ ap.add_argument('--source-dir',help='Optional offline folder containing mattpocock/, superpowers/ and ponytail/ checkouts')
24
+ ap.add_argument('--dry-run',action='store_true')
25
+ ap.add_argument('--pins',help='JSON mapping vendor names to exact commit SHA; required for reproducible team installs')
26
+ ap.add_argument('--update',action='store_true',help='Only replace files owned by a prior install when upstream changes; never overwrite foreign modifications')
27
+ args=ap.parse_args()
28
+ project=pathlib.Path(args.project).expanduser().resolve()
29
+ if not project.is_dir(): ap.error('project directory must exist')
30
+ pins=json.loads(pathlib.Path(args.pins).read_text()) if args.pins else {}
31
+ if pins and (set(pins)!=set(CATALOG) or any(not isinstance(v,str) or len(v)!=40 or any(c not in '0123456789abcdefABCDEF' for c in v) for v in pins.values())):
32
+ raise RuntimeError('pins must map every vendor to a 40-character git SHA')
33
+ agents=['codex','opencode','claude'] if args.agents=='all' else [args.agents]
34
+ # OpenCode gets its own .opencode/skills; no implicit dependency on Codex
35
+ dests={'codex':project/'.agents/skills','opencode':project/'.opencode/skills','claude':project/'.claude/skills'}
36
+ manifest_path=project/'.external-skills-lock.json'
37
+ old=json.loads(manifest_path.read_text()) if manifest_path.exists() else {'files':{}}
38
+ new={'generated_at':datetime.datetime.now(datetime.timezone.utc).isoformat(),'sources':{},'files':dict(old.get('files',{}))}
39
+ with tempfile.TemporaryDirectory() as td:
40
+ src={}
41
+ for vendor,(url,skills) in CATALOG.items():
42
+ if args.source_dir:
43
+ repo=pathlib.Path(args.source_dir).expanduser().resolve()/vendor
44
+ if not repo.is_dir(): raise RuntimeError(f'Missing offline source: {repo}')
45
+ else:
46
+ repo=pathlib.Path(td)/vendor
47
+ print(f'FETCH {url}',flush=True)
48
+ run(['git','clone',url,str(repo)]) if vendor in pins else run(['git','clone','--depth','1',url,str(repo)])
49
+ if vendor in pins:run(['git','-C',str(repo),'checkout','--detach',pins[vendor]])
50
+ src[vendor]=repo
51
+ try: revision=run(['git','-C',str(repo),'rev-parse','HEAD'])
52
+ except RuntimeError: revision='local-unversioned'
53
+ if vendor in pins and revision.lower()!=pins[vendor].lower():raise RuntimeError(f'Pin mismatch for {vendor}: {revision} != {pins[vendor]}')
54
+ new['sources'][vendor]={'url':url,'revision':revision}
55
+ changes=[]
56
+ for vendor,(_,names) in CATALOG.items():
57
+ repo=src[vendor]
58
+ for name in names:
59
+ matches=sorted(x for x in (repo/'skills').rglob('SKILL.md') if x.parent.name==name)
60
+ if len(matches)!=1:
61
+ raise RuntimeError(f'Missing or ambiguous REQUIRED skill {vendor}/{name}: expected 1 exact source, found {len(matches)}')
62
+ source=matches[0].parent
63
+ if not (source/'SKILL.md').is_file() or not (source/'SKILL.md').read_text(encoding='utf-8').strip():
64
+ raise RuntimeError(f'Empty REQUIRED skill {vendor}/{name}')
65
+ for agent in agents:
66
+ # Namespace avoids collisions; upstream SKILL.md internal name remains unmodified
67
+ dest=dests[agent]/name
68
+ if dest.exists():
69
+ # Compare entire directory contents before accepting or replacing
70
+ existing={str(x.relative_to(dest)):sha(x.read_bytes()) for x in dest.rglob('*') if x.is_file()}
71
+ incoming={str(x.relative_to(source)):sha(x.read_bytes()) for x in source.rglob('*') if x.is_file()}
72
+ if existing==incoming:
73
+ print(f'SKIP IDENTICAL {agent}:{name}'); continue
74
+ previous={k[len(str(dest.relative_to(project)))+1:]:v for k,v in old['files'].items() if k.startswith(str(dest.relative_to(project))+'/')}
75
+ if not(args.update and previous and previous==existing):
76
+ raise RuntimeError(f'Collision: {dest}. Refusing to overwrite; backup/remove manually or use --update for unchanged managed files.')
77
+ changes.append((source,dest,agent,vendor,name))
78
+ for source,dest,agent,vendor,name in changes:
79
+ print(f'{"WOULD INSTALL" if args.dry_run else "INSTALL"} {agent}: {vendor}/{name} -> {dest}')
80
+ if args.dry_run: continue
81
+ if dest.exists(): shutil.rmtree(dest)
82
+ shutil.copytree(source,dest,symlinks=False)
83
+ if not args.dry_run:
84
+ for vendor,(_,names) in CATALOG.items():
85
+ repo=src[vendor]
86
+ for name in names:
87
+ matches=[x for x in (repo/'skills').rglob('SKILL.md') if x.parent.name==name]
88
+ if len(matches)!=1: continue
89
+ for agent in agents:
90
+ dest=dests[agent]/name
91
+ if dest.exists():
92
+ for f in dest.rglob('*'):
93
+ if f.is_file(): new['files'][str(f.relative_to(project))]=sha(f.read_bytes())
94
+ manifest_path.write_text(json.dumps(new,indent=2,ensure_ascii=False)+'\n')
95
+ else: print('Dry-run complete: no files changed')
96
+ print('NOTE: This installs skill files only. Ponytail skill is installed, but its native plugin, hooks, mode-switching and other runtime capabilities are NOT installed. See README. Other upstream plugins and runtimes are not installed.')
97
+
98
+ if __name__=='__main__':
99
+ try: main()
100
+ except (RuntimeError,OSError) as e:
101
+ print(f'ERROR: {e}',file=sys.stderr);sys.exit(2)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ const fs=require('node:fs');const path=require('node:path');
4
+ const pkg=require('../package.json');
5
+ if(pkg.private===true) throw Error('package must be public');
6
+ if(!pkg.name||!pkg.bin?.['ai-dev']||!fs.existsSync(path.join(__dirname,'..',pkg.bin['ai-dev'])))throw Error('bin missing');
7
+ for(const p of ['scripts/external-install.py','scripts/workflow.py','templates/AGENTS.md','lib/install.js','lib/status.js'])if(!fs.existsSync(path.join(__dirname,'..',p)))throw Error('missing '+p);
8
+ console.log('PUBLISH_CHECK_PASS',pkg.name,pkg.version);
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env python3
2
+ """Declarative, project-owned verification profiles; never execute undocumented commands."""
3
+ import argparse, json, pathlib, fnmatch, subprocess, datetime, hashlib, sys
4
+
5
+ def load(project):
6
+ f=project/'.ai-dev/verification-profiles.json'
7
+ if not f.exists():raise ValueError(f'Missing verification profiles: {f}')
8
+ cfg=json.loads(f.read_text())
9
+ if not isinstance(cfg.get('profiles'),dict):raise ValueError('profiles object required')
10
+ for name,profile in cfg['profiles'].items():
11
+ if not isinstance(profile.get('patterns'),list) or not isinstance(profile.get('checks'),dict):raise ValueError('invalid profile '+name)
12
+ for check,cmd in profile['checks'].items():
13
+ if not isinstance(cmd,list) or not cmd or not all(isinstance(s,str) and s for s in cmd):raise ValueError('checks must be argv lists')
14
+ return cfg
15
+
16
+ def decide(cfg,files):
17
+ found={}
18
+ for name,profile in cfg['profiles'].items():
19
+ matching=[f for f in files if any(fnmatch.fnmatch(f,pat) for pat in profile['patterns'])]
20
+ if matching:found[name]={'files':matching,'checks':profile['checks']}
21
+ return found
22
+
23
+ def main():
24
+ ap=argparse.ArgumentParser();ap.add_argument('--project',default='.');ap.add_argument('--files',nargs='+',required=True);ap.add_argument('--run',action='store_true');args=ap.parse_args()
25
+ project=pathlib.Path(args.project).resolve();cfg=load(project);found=decide(cfg,args.files)
26
+ if not found:print(json.dumps({'profiles':{},'warning':'NO_MATCHING_PROFILE: manual verification plan required'},indent=2));return 2
27
+ if not args.run:print(json.dumps({'profiles':found,'note':'Preview only: choose appropriate AC mappings before execution'},indent=2));return 0
28
+ logs=project/'docs/generated/verification';logs.mkdir(parents=True,exist_ok=True)
29
+ results=[]
30
+ for name,data in found.items():
31
+ for check,cmd in data['checks'].items():
32
+ r=subprocess.run(cmd,cwd=project,text=True,stdout=subprocess.PIPE,stderr=subprocess.STDOUT)
33
+ key=datetime.datetime.now(datetime.timezone.utc).strftime('%Y%m%dT%H%M%S%fZ')
34
+ f=logs/(key+'-'+name+'-'+check+'.log');f.write_text(r.stdout)
35
+ results.append({'profile':name,'check':check,'command':cmd,'exit_code':r.returncode,'log':str(f.relative_to(project)),'sha256':hashlib.sha256(f.read_bytes()).hexdigest()})
36
+ print(json.dumps({'results':results},indent=2))
37
+ return 1 if any(x['exit_code'] for x in results) else 0
38
+ if __name__=='__main__':
39
+ try:sys.exit(main())
40
+ except (ValueError,OSError,json.JSONDecodeError) as e:print('BLOCK:',e,file=sys.stderr);sys.exit(2)
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/env python3
2
+ """Shared file-based gates for externally installed AI coding skills."""
3
+ import argparse, pathlib, json, datetime, hashlib, subprocess, sys, shutil, re
4
+ ROOT=pathlib.Path(__file__).resolve().parent.parent
5
+ BASE=ROOT/'docs'/'exec-plans'
6
+ def now():return datetime.datetime.now(datetime.timezone.utc).isoformat()
7
+ def get(task):
8
+ if not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9_-]{1,79}',task):raise ValueError('invalid task ID')
9
+ for state in ['active','completed']:
10
+ p=BASE/state/task
11
+ if p.is_dir():return p
12
+ raise ValueError(f'unknown task: {task}')
13
+ def meta(p):return json.loads((p/'status.json').read_text())
14
+ def save(p,data):(p/'status.json').write_text(json.dumps(data,indent=2,ensure_ascii=False)+'\n')
15
+ def digest():
16
+ h=hashlib.sha256()
17
+ skip={'.git','.agents','.opencode','.claude','node_modules','.venv','venv','.pytest_cache','__pycache__','.external-skills-lock.json'}
18
+ for f in sorted(ROOT.rglob('*')):
19
+ if not f.is_file() or any(c in skip for c in f.relative_to(ROOT).parts):continue
20
+ rel=f.relative_to(ROOT).as_posix()
21
+ if rel.startswith('docs/exec-plans/') or rel.startswith('docs/generated/') or rel.startswith('docs/ai-workflow/') or rel.startswith('scripts/'):continue
22
+ h.update(rel.encode());h.update(hashlib.sha256(f.read_bytes()).digest())
23
+ return h.hexdigest()
24
+ def acmap(p):
25
+ try:
26
+ data=json.loads((p/'ac-tests.json').read_text());assert isinstance(data,dict) and data
27
+ for k,v in data.items():assert re.fullmatch(r'AC-[0-9]+',k) and isinstance(v,list) and bool(v) and all(isinstance(x,str) and x.strip() for x in v)
28
+ return data
29
+ except (ValueError,AssertionError,OSError) as e:raise ValueError('ac-tests.json must contain AC IDs mapped to nonempty command descriptions') from e
30
+ def proof(p):
31
+ records=[json.loads(line) for line in (p/'evidence.jsonl').read_text().splitlines()] if (p/'evidence.jsonl').exists() else []
32
+ mapping=acmap(p);d=digest();issues=[]
33
+ for ac in mapping:
34
+ if not any(x.get('ac')==ac and x.get('exit_code')==0 and x.get('source_digest')==d and x.get('log_file') and (p/x['log_file']).is_file() and x.get('command') in [v.split() for v in mapping[ac]] and not x.get('source_changed_during_test',False) for x in records):issues.append(ac+': missing current successful recorded execution')
35
+ return issues
36
+ def gate(p,stage):
37
+ s=meta(p)
38
+ if stage=='implement':
39
+ if not (s.get('approved') is True or bool(s.get('approved_by'))):raise ValueError('plan is not human-approved')
40
+ if not (p/'spec.md').read_text().strip() or not (p/'plan.md').read_text().strip():raise ValueError('missing spec/plan')
41
+ acmap(p)
42
+ elif stage in ['review','complete']:
43
+ gate(p,'implement')
44
+ issues=proof(p)
45
+ if issues:raise ValueError('; '.join(issues))
46
+ if stage=='complete':
47
+ r=s.get('review',{})
48
+ if r.get('verdict')!='PASS' or r.get('source_digest')!=digest():raise ValueError('missing PASS review for current source')
49
+ else:raise ValueError('unknown stage')
50
+ def main():
51
+ parser=argparse.ArgumentParser();sub=parser.add_subparsers(dest='cmd',required=True)
52
+ sub.add_parser('status');sub.add_parser('next');se=sub.add_parser('session');se.add_argument('--json',action='store_true');se.add_argument('--all',action='store_true',help='Include completed tasks (default active only)');n=sub.add_parser('new');n.add_argument('--id',required=True);n.add_argument('--title',required=True)
53
+ for action in ['approve','gate','run','review','complete']:
54
+ a=sub.add_parser(action);a.add_argument('--id',required=True)
55
+ if action in ['approve','complete']:a.add_argument('--user-confirmed',action='store_true',help='Agent attests explicit user approval in current conversation; not identity authentication')
56
+ if action=='gate':a.add_argument('--stage',choices=['implement','review','complete'],required=True)
57
+ if action=='run':a.add_argument('--ac',required=True);a.add_argument('command',nargs=argparse.REMAINDER)
58
+ if action=='review':a.add_argument('--verdict',choices=['PASS','FAIL'],required=True);a.add_argument('--notes-file',required=True)
59
+ args=parser.parse_args()
60
+ if args.cmd=='session':
61
+ # Read-only snapshot for a fresh agent session; do not infer approvals from chat.
62
+ tasks=[];problems=[]
63
+ states=['active','completed'] if args.all else ['active']
64
+ for state in states:
65
+ for sf in sorted((BASE/state).glob('*/status.json')):
66
+ q=sf.parent
67
+ try:
68
+ d=meta(q);phase='COMPLETED' if state=='completed' else 'PLAN_APPROVAL_REQUIRED';issue=''
69
+ if state=='active':
70
+ if not d.get('approved') and not d.get('approved_by'):
71
+ # A newly created task may have no meaningful plan yet.
72
+ phase='PLAN_DRAFT' if not (q/'ac-tests.json').is_file() or (q/'ac-tests.json').read_text().strip()=='{}' else 'PLAN_APPROVAL_REQUIRED'
73
+ else:
74
+ try:gate(q,'review');phase='INDEPENDENT_REVIEW_REQUIRED'
75
+ except (ValueError,OSError,KeyError,json.JSONDecodeError) as e:phase='IMPLEMENT_OR_VERIFY';issue=str(e)
76
+ if d.get('review',{}).get('verdict')=='PASS':
77
+ try:gate(q,'complete');phase='COMPLETION_APPROVAL_REQUIRED';issue=''
78
+ except (ValueError,OSError,KeyError,json.JSONDecodeError) as e:phase='REVERIFY_OR_REVIEW';issue=str(e)
79
+ tasks.append({'id':q.name,'title':d.get('title',''), 'phase':phase,'path':str(q.relative_to(ROOT)),'created_at':d.get('created_at',''),'issue':issue})
80
+ except (ValueError,OSError,KeyError,json.JSONDecodeError) as e:
81
+ problems.append({'path':str(q.relative_to(ROOT)),'error':str(e)})
82
+ order={'REVERIFY_OR_REVIEW':0,'IMPLEMENT_OR_VERIFY':1,'INDEPENDENT_REVIEW_REQUIRED':2,'PLAN_DRAFT':3,'PLAN_APPROVAL_REQUIRED':4,'COMPLETION_APPROVAL_REQUIRED':5,'COMPLETED':6}
83
+ tasks.sort(key=lambda d:(order.get(d['phase'],99), d['id']))
84
+ payload={'project':str(ROOT),'active_count':sum(t['phase']!='COMPLETED' for t in tasks),'tasks':tasks,'warnings':problems}
85
+ if args.json:print(json.dumps(payload,indent=2,ensure_ascii=False));return
86
+ print('=== AI WORK SESSION STATUS ===')
87
+ print('Project:',ROOT)
88
+ print('Active tasks:',payload['active_count'])
89
+ if not tasks:print('No tracked active tasks. For a new user request, create a task; do not create one merely because a session started.')
90
+ for t in tasks:
91
+ print(f"- {t['id']} | {t['phase']} | {t['title']}")
92
+ print(' files:',t['path'])
93
+ if t['issue']:print(' attention:',t['issue'])
94
+ for err in problems:print('WARNING:',err['path'],err['error'])
95
+ print('Next: Read relevant active task files; report current status before new coding. Do not infer user approval.')
96
+ return
97
+ if args.cmd=='next':
98
+ tasks=[]
99
+ for state in ['active','completed']:
100
+ for sf in sorted((BASE/state).glob('*/status.json')):
101
+ q=sf.parent;d=meta(q);phase='COMPLETED' if state=='completed' else 'PLAN_APPROVAL_REQUIRED'
102
+ if state=='active' and d.get('approved') is True:
103
+ try:gate(q,'review');phase='INDEPENDENT_REVIEW_REQUIRED'
104
+ except (ValueError,OSError):phase='IMPLEMENT_OR_VERIFY'
105
+ if d.get('review',{}).get('verdict')=='PASS':
106
+ try:gate(q,'complete');phase='COMPLETION_APPROVAL_REQUIRED'
107
+ except (ValueError,OSError):phase='REVERIFY_OR_REVIEW'
108
+ tasks.append({'id':q.name,'title':d.get('title'),'phase':phase,'directory':str(q.relative_to(ROOT))})
109
+ print(json.dumps({'tasks':tasks},indent=2,ensure_ascii=False));return
110
+ if args.cmd=='status':
111
+ for state in ['active','completed']:
112
+ for p in sorted((BASE/state).glob('*/status.json')):
113
+ d=json.loads(p.read_text());print(state,p.parent.name,d.get('title'), 'approved='+str(d.get('approved') is True or bool(d.get('approved_by'))),'review='+str(d.get('review',{}).get('verdict')))
114
+ return
115
+ if args.cmd=='new':
116
+ task=args.id
117
+ if not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9_-]{1,79}',task):raise ValueError('invalid task ID')
118
+ if any((BASE/st/task).exists() for st in ['active','completed']):raise ValueError('task exists')
119
+ p=BASE/'active'/task;p.mkdir(parents=True)
120
+ (p/'analysis.md').write_text('# Analysis\n\n');(p/'spec.md').write_text('# Spec / Acceptance Criteria\n\n');(p/'plan.md').write_text('# Execution plan / Task graph\n\n');(p/'ac-tests.json').write_text('{}\n')
121
+ save(p,{'id':task,'title':args.title,'created_at':now(),'approved':False,'review':{}});print('CREATED',p);return
122
+ p=get(args.id);s=meta(p)
123
+ if args.cmd=='approve':
124
+ if not args.user_confirmed:raise ValueError('explicit user approval required; agent must pass --user-confirmed only after user authorizes plan')
125
+ if p.parent.name!='active':raise ValueError('completed task')
126
+ if not (p/'spec.md').read_text().strip() or not (p/'plan.md').read_text().strip():raise ValueError('missing spec/plan')
127
+ acmap(p)
128
+ adapter_file=ROOT/'.ai-dev/spec-adapter.json'
129
+ if adapter_file.exists():
130
+ adapter=json.loads(adapter_file.read_text())
131
+ if adapter.get('mode')=='openspec':
132
+ change=s.get('openspec_change_id')
133
+ if not change or not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9_.-]{0,100}',change) or not (ROOT/adapter.get('source','openspec/changes')/change).is_dir():raise ValueError('valid openspec_change_id pointing to existing change directory required')
134
+ s.pop('approved_by',None);s['approved']=True;s['approved_at']=now();save(p,s);print('APPROVED',p.name);return
135
+ if args.cmd=='gate':gate(p,args.stage);print('PASS',args.stage,p.name);return
136
+ if args.cmd=='run':
137
+ gate(p,'implement');mapping=acmap(p)
138
+ if args.ac not in mapping:raise ValueError('unknown AC')
139
+ cmd=args.command
140
+ if cmd and cmd[0]=='--':cmd=cmd[1:]
141
+ if not cmd:raise ValueError('command required after --')
142
+ if cmd not in [v.split() for v in mapping[args.ac]]:raise ValueError('command is not declared for this AC in ac-tests.json')
143
+ if cmd[0].startswith('-'):raise ValueError('command must begin with executable')
144
+ before=digest();proc=subprocess.run(cmd,cwd=ROOT,text=True,stdout=subprocess.PIPE,stderr=subprocess.STDOUT)
145
+ after=digest();logs=p/'logs';logs.mkdir(exist_ok=True)
146
+ tag=datetime.datetime.now(datetime.timezone.utc).strftime('%Y%m%dT%H%M%S%fZ');name='logs/'+tag+'-'+args.ac+'.log'
147
+ (p/name).write_text('$ '+' '.join(cmd)+'\n'+proc.stdout)
148
+ row={'timestamp':now(),'ac':args.ac,'command':cmd,'exit_code':proc.returncode,'source_digest':after,'source_changed_during_test':before!=after,'log_file':name}
149
+ if before!=after:row['exit_code']=999
150
+ with (p/'evidence.jsonl').open('a') as f:f.write(json.dumps(row,ensure_ascii=False)+'\n')
151
+ print('RECORDED',args.ac,'exit',proc.returncode,'source_changed',before!=after,'log',p/name)
152
+ sys.exit(1 if before!=after else proc.returncode)
153
+ if args.cmd=='review':
154
+ gate(p,'review');notes=pathlib.Path(args.notes_file).resolve()
155
+ if not notes.is_file() or not notes.read_text().strip():raise ValueError('review notes required')
156
+ reviewdir=p/'reviews';reviewdir.mkdir(exist_ok=True);dest=reviewdir/('review-'+now().replace(':','-')+'.md');shutil.copyfile(notes,dest)
157
+ s['review']={'verdict':args.verdict,'at':now(),'notes':str(dest.relative_to(p)),'source_digest':digest()};save(p,s);print('REVIEW',args.verdict);return
158
+ if args.cmd=='complete':
159
+ if not args.user_confirmed:raise ValueError('explicit user completion approval required; agent must pass --user-confirmed only after user authorizes completion')
160
+ gate(p,'complete')
161
+ if p.parent.name!='active':raise ValueError('already completed')
162
+ s.pop('completed_by',None);s['completed_at']=now();save(p,s);dst=BASE/'completed'/p.name;p.rename(dst);print('COMPLETED',dst)
163
+ if __name__=='__main__':
164
+ try:main()
165
+ except (ValueError,OSError,json.JSONDecodeError) as e:print('BLOCK:',e,file=sys.stderr);sys.exit(2)
@@ -0,0 +1,5 @@
1
+ {
2
+ "mode": "markdown",
3
+ "source": "docs/product-specs",
4
+ "note": "Set mode=openspec and source=openspec/changes only if repository uses OpenSpec. Do not duplicate spec content."
5
+ }
@@ -0,0 +1,37 @@
1
+ {
2
+ "profiles": {
3
+ "web": {
4
+ "patterns": [
5
+ "src/**/*.tsx",
6
+ "src/**/*.ts",
7
+ "web/**",
8
+ "src/*.ts",
9
+ "src/*.tsx"
10
+ ],
11
+ "checks": {
12
+ "lint": [
13
+ "npm",
14
+ "run",
15
+ "lint"
16
+ ],
17
+ "test": [
18
+ "npm",
19
+ "test"
20
+ ]
21
+ }
22
+ },
23
+ "python": {
24
+ "patterns": [
25
+ "**/*.py",
26
+ "*.py"
27
+ ],
28
+ "checks": {
29
+ "unit": [
30
+ "python3",
31
+ "-m",
32
+ "pytest"
33
+ ]
34
+ }
35
+ }
36
+ }
37
+ }
@@ -0,0 +1,23 @@
1
+ # Shared AI development workflow — OpenCode / Codex / Claude Code
2
+
3
+ **Use upstream installed skills** (Matt Pocock and Superpowers), not home-grown substitutes. CRG is independently managed. Apply this file in all three agents; actual skill discovery/activation must be verified in each host.
4
+
5
+ ## On every new user development request / resumed session
6
+ 1. **At the start of EVERY NEW AGENT SESSION, before planning or coding**, run `ai-dev status` from the project root. Display the active task summary to the user (ID, phase, next action), even if the user has not asked for a new task. Read applicable task files and resume rather than duplicating. This is required for OpenCode, Codex, and Claude Code. If invocation is unavailable, say the session check was not performed; never claim it ran.
7
+ 2. Read `docs/PLANS.md`, `docs/ai-workflow/POLICY.md`, and the applicable task files. Invoke upstream skills through the host's skill tool; merely reading SKILL.md is not equivalent to invoking a skill.
8
+ 3. If a new substantive request, **agent** creates task using `ai-dev work new --id TASK-... --title '...'`. Agent writes analysis.md, spec.md, plan.md, ac-tests.json using original upstream skills.
9
+ 4. Summarize plan and explicitly ask the human for approval. **Do not run approve before human approval.** An explicit natural-language approval (e.g. '계획 승인', '진행해') in response to the presented plan allows the **agent** to run `ai-dev work approve --id TASK-... --user-confirmed`. No person needs to type CLI commands.
10
+ 5. Before editing source, **agent** runs `ai-dev work gate --id TASK-... --stage implement`. Use upstream TDD/implementation skills. Never bypass a failed gate.
11
+ 6. **Agent** runs tests through `workflow.py run --id TASK-... --ac AC-.. -- <command>`, reviews real logs, and checks `workflow.py gate --id TASK-... --stage review`. Failed/insufficient coverage: fix and rerun.
12
+ 7. Invoke original `code-review` / reviewer skill in an independent context where possible, evaluate diff+spec+evidence; write substantial notes and **agent** records `workflow.py review --id TASK-... --verdict PASS|FAIL --notes-file <path>`. A recorded verdict is not proof of reviewer independence.
13
+ 8. When `workflow.py gate --id TASK-... --stage complete` passes, report `READY_FOR_REVIEW` and ask human to confirm completion. Only after explicit human confirmation may **agent** call `workflow.py complete --id TASK-... --user-confirmed`. No self-approval.
14
+ 9. For all stages, keep user interaction **natural-language only**; CLI is internal agent tooling. On session changes, any of the three agents resumes the same project files.
15
+
16
+ ## External skill mapping
17
+ Analyze: `grill-with-docs`, `systematic-debugging`, CRG if installed. Plan: `to-spec`, `to-tickets`, `writing-plans`. Implement: `implement`, `test-driven-development`, optional `implement-spec` only with compatible host runtime. Verify: `verification-before-completion`. Review: `code-review`, `requesting-code-review`.
18
+
19
+ ## Boundaries
20
+ No automatic commit, push, merge, or PR; never invent test/review evidence. An ambiguous 'yes' not clearly responding to a specific approval question is not consent. Agent runs no approval command on inference, and never claims person identity is verified. The CLI gates are structural; AC semantic adequacy and independent-review authenticity require critical inspection. If original skills aren't available, report that; don't claim successful invocation.
21
+
22
+ ## Native session status hooks
23
+ `ai-dev setup` installs read-only SessionStart hooks (OpenCode plugin, Codex and Claude hook configs). Agents must report the injected work-state to user once per new session and read the work files before edits. Session status is a snapshot, not proof of current test evidence. Do not create a task just because a session starts.
@@ -0,0 +1,3 @@
1
+ # Architecture
2
+
3
+ Document actual module boundaries, dependencies and important interfaces here. Keep this file project-specific.
@@ -0,0 +1,3 @@
1
+ # Design principles
2
+
3
+ Project-specific UI/UX and design constraints.
@@ -0,0 +1,3 @@
1
+ # Frontend conventions
2
+
3
+ Document chosen frontend stack and interaction testing.
@@ -0,0 +1,23 @@
1
+ # Work tracking — CLI commands are **for agents**, not users
2
+
3
+ Agents start each session with `python3 scripts/workflow.py next`, then use the **original externally installed skills**. All three hosts share `docs/exec-plans/active/TASK-ID/`.
4
+
5
+ | Stage | Agent operation | Artifact / control |
6
+ |---|---|---|
7
+ | Analyze | `workflow.py new --id TASK-001 --title '...'` then upstream diagnostic skills | `analysis.md` |
8
+ | Plan | upstream `to-spec`, `to-tickets`, `writing-plans` | `spec.md`, `plan.md`, `ac-tests.json` |
9
+ | Approval | **ask user in chat**; only after explicit user plan approval: `workflow.py approve --id TASK-001 --user-confirmed` | timestamp/flag only; not user authentication |
10
+ | Implement | `workflow.py gate --id TASK-001 --stage implement`; upstream TDD skills | code, tests, optional RED record |
11
+ | Verify | `workflow.py run --id TASK-001 --ac AC-01 -- python3 -m pytest tests/test_feature.py` | timestamped execution log, exit code, source digest |
12
+ | Review | `workflow.py gate --id TASK-001 --stage review`, independent original `code-review`; `workflow.py review --id TASK-001 --verdict PASS --notes-file review.md` | substantive reviewer notes; independence not automatically verified |
13
+ | Completion | `workflow.py gate --id TASK-001 --stage complete`; **ask user in chat**; only after explicit completion approval run `workflow.py complete --id TASK-001 --user-confirmed` | moves task folder to `completed/` |
14
+
15
+ Natural language interaction with user is sufficient; the agent must issue internal commands without asking user to copy commands. `--user-confirmed` is an attestation by the agent, **not** proof of human identity. Never silently auto-approve. On review FAIL, return to implementation/verification and rerun tests; a stale review cannot complete.
16
+
17
+ `ac-tests.json` maps each AC ID to nonempty descriptions of test commands. Gate checks successful recorded command executions and current source digest, **not** semantic adequacy of test-to-AC mapping. Do not use a no-op or unrelated test as evidence.
18
+
19
+ ## New session — required first action
20
+
21
+ All agents run `python3 scripts/workflow.py session` on a new session and summarize active tasks in chat **before proceeding with any work**. It is read-only: session start alone never creates, approves, reviews, or completes tasks. `--json` supports machine parsing and `--all` includes completed plans. The user does not need to type the command. Native host startup execution is not assumed: use the supplied launcher for guaranteed terminal start-time printing; host prompt compliance still needs host-level testing.
22
+
23
+ When OpenSpec is active, do not duplicate its change spec under product-specs; store only task execution plan and evidence.
@@ -0,0 +1,3 @@
1
+ # Product sense
2
+
3
+ Define audience, user value, and non-goals.
@@ -0,0 +1,3 @@
1
+ # Quality criteria
2
+
3
+ Document lint, typecheck, unit, integration, and E2E expectations.
@@ -0,0 +1,3 @@
1
+ # Reliability
2
+
3
+ Document SLAs, alerting, failure handling, backups and recovery.
@@ -0,0 +1,3 @@
1
+ # Security
2
+
3
+ Document security requirements and prohibit secrets in logs.
@@ -0,0 +1,13 @@
1
+ # Upstream skills (installed on demand)
2
+
3
+ Matt Pocock: https://github.com/mattpocock/skills
4
+ Superpowers: https://github.com/obra/superpowers
5
+ Ponytail: https://github.com/DietrichGebert/ponytail
6
+
7
+ Analyze: grill-with-docs, diagnosing-bugs, systematic-debugging, brainstorming.
8
+ Plan: to-spec, to-tickets, writing-plans.
9
+ Implement: implement, implement-spec, test-driven-development, using-git-worktrees, subagent-driven-development, ponytail (minimal implementation guidance).
10
+ Verify: verification-before-completion.
11
+ Review: code-review, requesting-code-review, receiving-code-review, retro.
12
+
13
+ **Important:** Original skills can require extra capabilities/plugins/issues/subagents. Only the skill directories are installed by this package. Ponytail native plugin activation, mode switching and lifecycle hooks are NOT installed; for complete Ponytail functionality install its official host plugin following README instructions. Treat unsupported subagent and background functionality as unavailable, and use sequential fallback. CRG is intentionally not installed or modified by this package.
@@ -0,0 +1,8 @@
1
+ # Governance
2
+ - Original third-party skills are installed unmodified. External skill instructions never override repository approval rules.
3
+ - Human approval required before implementation and completion; no autonomous commit/push/merge.
4
+ - Verify actual exit status and retain evidence. Do not mark AC pass without a substantive coverage review.
5
+ - Avoid sensitive data in commands and output: verification logs are stored on disk.
6
+ - `workflow.py` is a deterministic state/file/evidence controller, not an AI skill or a substitute for actual testing.
7
+
8
+ - User operates in natural language; internal CLI is agent-invoked. Explicit human approval required for plan and final completion; `--user-confirmed` is a self-attestation, not authentication.
@@ -0,0 +1,3 @@
1
+ # Core beliefs
2
+
3
+ Evidence precedes completion claims. Favor minimal, reversible changes.
@@ -0,0 +1,3 @@
1
+ # Design documentation
2
+
3
+ Link architectural decisions and significant designs.
File without changes
File without changes
@@ -0,0 +1,4 @@
1
+ # Technical debt
2
+
3
+ | Item | Impact | Owner | Planned resolution |
4
+ | --- | --- | --- | --- |
@@ -0,0 +1,3 @@
1
+ # Generated schema (placeholder)
2
+
3
+ No schema extracted yet.
@@ -0,0 +1,3 @@
1
+ # Product specifications
2
+
3
+ One canonical spec lives inside each active/completed task folder; link notable specs here.
@@ -0,0 +1 @@
1
+ Placeholder: add trusted design-system reference here.
@@ -0,0 +1 @@
1
+ Placeholder: add trusted Nixpacks reference here.
@@ -0,0 +1 @@
1
+ Placeholder: add trusted uv reference here.