devmethod-ai 0.3.0 → 0.4.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 (52) hide show
  1. package/.agents/skills/devmethod-architecture/SKILL.md +14 -0
  2. package/.agents/skills/devmethod-correct-course/SKILL.md +12 -0
  3. package/.agents/skills/devmethod-design/SKILL.md +14 -0
  4. package/.agents/skills/devmethod-explore/SKILL.md +12 -0
  5. package/.agents/skills/devmethod-frame/SKILL.md +12 -0
  6. package/.agents/skills/devmethod-handoff/SKILL.md +14 -0
  7. package/.agents/skills/devmethod-implement/SKILL.md +14 -0
  8. package/.agents/skills/devmethod-integrate/SKILL.md +14 -0
  9. package/.agents/skills/devmethod-next/SKILL.md +14 -0
  10. package/.agents/skills/devmethod-plan/SKILL.md +14 -0
  11. package/.agents/skills/devmethod-ready/SKILL.md +14 -0
  12. package/.agents/skills/devmethod-review/SKILL.md +18 -0
  13. package/.agents/skills/devmethod-status/SKILL.md +12 -0
  14. package/.agents/skills/devmethod-verify/SKILL.md +14 -0
  15. package/.agents/skills/project-foundation/SKILL.md +1 -1
  16. package/.agents/skills/project-foundation/assets/START_HERE.md +1 -1
  17. package/.agents/skills/project-foundation/references/operating-commands.md +19 -15
  18. package/.agents/skills/scoped-delivery/assets/REVIEW.md +2 -2
  19. package/.agents/skills/scoped-delivery/references/review-format.md +26 -0
  20. package/COMPATIBILITY.md +5 -3
  21. package/README.md +41 -28
  22. package/START_HERE.md +1 -1
  23. package/dist/cli.js +3 -1
  24. package/dist/commands.js +20 -0
  25. package/dist/doctor.js +4 -2
  26. package/dist/init.js +3 -2
  27. package/dist/review-browser.js +1 -1
  28. package/dist/review-model.js +1 -1
  29. package/docs/ADR-009-visible-workflow-commands.md +11 -0
  30. package/docs/COMMANDS-VALIDATION.md +13 -0
  31. package/docs/COMMANDS.md +36 -0
  32. package/docs/RELEASE-0.3.1.md +15 -0
  33. package/docs/RELEASE-0.4.0.md +15 -0
  34. package/docs/REVIEW-GUIDE.md +70 -0
  35. package/docs/REVIEWS.md +4 -22
  36. package/docs/images/devmethod-delivery.svg +1 -1
  37. package/docs/images/devmethod-flow.svg +1 -1
  38. package/docs/images/review-correction.jpg +0 -0
  39. package/docs/images/review-coverage.jpg +0 -0
  40. package/docs/media/review-extension/README.md +21 -0
  41. package/docs/media/review-extension/scenes.json +70 -0
  42. package/docs/media/visual-chain/README.md +8 -2
  43. package/docs/media/visual-chain/devmethod-du-besoin-au-produit.fr.srt +65 -1
  44. package/docs/media/visual-chain/video-preview.jpg +0 -0
  45. package/docs/missions/review-media-0.3.1.md +11 -0
  46. package/docs/missions/workflow-0.3.md +3 -1
  47. package/examples/review/README.md +14 -0
  48. package/examples/review/REVIEW.md +98 -0
  49. package/examples/review/review-demo.html +1351 -0
  50. package/package.json +1 -1
  51. package/scripts/media/review-extension/extend.py +100 -0
  52. package/scripts/package-smoke.mjs +12 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devmethod-ai",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "From idea to delivery with your AI coding agents",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -0,0 +1,100 @@
1
+ """Insert real review screenshots into the existing 0.3.0 film; never rebuild its story.
2
+ Requires ffmpeg, ffprobe, macOS say/Thomas and Pillow. Run from repository root.
3
+ """
4
+ import json, math, pathlib, subprocess, re, hashlib
5
+ from PIL import Image, ImageDraw, ImageFont
6
+ ROOT=pathlib.Path.cwd(); TEMP=pathlib.Path('/private/tmp/devmethod-review-film'); TEMP.mkdir(exist_ok=True)
7
+ MEDIA=ROOT/'docs/media/visual-chain'; EXT=ROOT/'docs/media/review-extension'
8
+ MOVIE='docs/media/visual-chain/devmethod-du-besoin-au-produit-4k.fr.mp4'
9
+ CUT=171.52
10
+
11
+ def run(args):
12
+ subprocess.run(args,check=True,stdout=subprocess.DEVNULL,stderr=open(TEMP/'encode.log','a'))
13
+ def duration(file):
14
+ return float(subprocess.check_output(['ffprobe','-v','error','-show_entries','format=duration','-of','csv=p=0',str(file)]))
15
+ def font(size,serif=False,mono=False):
16
+ return ImageFont.truetype('/System/Library/Fonts/Menlo.ttc' if mono else '/System/Library/Fonts/Supplemental/'+('Georgia.ttf' if serif else 'Arial.ttf'),size)
17
+ def wrap(draw,text,f,width):
18
+ lines=[]; line=''
19
+ for word in text.split():
20
+ candidate=(line+' '+word).strip()
21
+ if draw.textlength(candidate,font=f)>width and line: lines.append(line);line=word
22
+ else:line=candidate
23
+ return lines+[line]
24
+ def frame(i,s):
25
+ im=Image.new('RGB',(3840,2160),'#0e1d21'); d=ImageDraw.Draw(im)
26
+ d.text((160,76),'DevMethod / Review',font=font(40),fill='#b0c6bf')
27
+ d.text((3020,76),f'COMPLÉMENT {i+1:02} / 06',font=font(32),fill='#b0c6bf')
28
+ d.text((160,210),s['title'],font=font(110,True),fill='#f6f0e5')
29
+ d.rounded_rectangle((160,440,1220,1850),radius=28,fill='#183137',outline='#45625f',width=2)
30
+ y=510
31
+ d.text((210,y),'DEPUIS LE PACKAGE NPM',font=font(30),fill='#e9ae82'); y+=95
32
+ for line in s['command'].split('\n'):
33
+ d.text((210,y),line,font=font(34,mono=True),fill='#f1b98e');y+=58
34
+ y+=90
35
+ for line in wrap(d,s['explanation'],font(49),950):
36
+ d.text((210,y),line,font=font(49),fill='#ecf1e8');y+=68
37
+ y+=75
38
+ for line in wrap(d,s['note'],font(33),950):
39
+ d.text((210,y),line,font=font(33),fill='#b5d0c2');y+=48
40
+ shot=Image.open(EXT/'captures'/s['capture']).convert('RGB');scale=min(2360/shot.width,1470/shot.height);shot=shot.resize((round(shot.width*scale),round(shot.height*scale)),Image.Resampling.LANCZOS)
41
+ # Screenshot pixels are preserved apart from proportional display scaling.
42
+ im.paste(shot,(1320+(2360-shot.width)//2,460+(1470-shot.height)//2))
43
+ d.text((160,2040),'Captures réelles de l’interface · Données de démonstration fictives · Montage commenté',font=font(33),fill='#b0c6bf')
44
+ im.save(TEMP/f'frame-{i}.png')
45
+
46
+ scenes=[
47
+ dict(title='La review devient consultable',capture='findings.jpg',command='npx devmethod-ai@0.3.1 '+chr(92)+'\n review --demo '+chr(92)+'\n --output review.html --open',explanation='Une interface locale pour relier constats, preuves et corrections.',note='HTML autonome hors ligne. La commande présente des résultats enregistrés ; elle ne réalise pas la review.',voice='Dev Method ajoute maintenant une interface de review. Voici sa démonstration fictive, distincte des résultats réels du pilote Lisière. Cette commande ouvre un rapport local.'),
48
+ dict(title='Retrouver le constat utile',capture='filtered.jpg',command='Constats → Rechercher\n« silencieux »',explanation='Rechercher, filtrer et sélectionner sans confondre gravité et confiance.',note='Un constat confirmé et un risque à vérifier restent distincts. Les compteurs décrivent toute la review.',voice='La recherche isole un constat. Les filtres distinguent domaine, gravité, confiance et résolution. Un risque à vérifier reste une hypothèse, pas un défaut confirmé.'),
49
+ dict(title='Comprendre, puis corriger',capture='correction.jpg',command='Preuve → Correction\n→ Vérification attendue',explanation='Du scénario observé à la correction et à sa nouvelle vérification.',note='Le ticket complète le constat. Fermer le panneau ne résout jamais le problème.',voice='Chaque constat relie scénario, preuve, impact et correction. La vérification attendue et le ticket restent accessibles. Masquer un résultat ne le marque pas comme résolu.'),
50
+ dict(title='Voir la couverture réelle',capture='coverage.jpg',command='Couverture\nRéussi · Échec · Non exécuté',explanation='Les contrôles ont leur propre statut, indépendant du nombre de constats.',note='Une zone non inspectée ne devient pas verte. La révision et les limites restent visibles.',voice='La couverture sépare les contrôles réussis, en échec et non exécutés. L’absence de constat ne prouve pas que tout a été vérifié.'),
51
+ dict(title='Vérifier les références',capture='sources.jpg',command='Sources\nVersion → Provenance → Usage',explanation='Une référence connue n’est pas automatiquement une source consultée.',note='La démonstration signale sa référence non vérifiée. Une vraie review consigne sa consultation et ses limites.',voice='Les sources indiquent technologie, version, provenance et usage. Ici, la référence est clairement non vérifiée : cette démonstration ne prétend pas l’avoir consultée.'),
52
+ dict(title='Exporter et reprendre',capture='export.jpg',command='Exporter le rapport\nHTML · Markdown · JSON',explanation='Une source structurée commune pour l’interface et les rapports.',note='Conserver la révision, les preuves et les limites. Le HTML exporté conserve aussi l’état de consultation.',voice='Exportez la même review en HTML, Markdown ou JSON. Les résultats partagent une source commune. Le rapport conserve la révision, les preuves et les limites pour la reprise.'),
53
+ ]
54
+ source=TEMP/'original-0.3.0.mp4'
55
+ if not source.exists(): source.write_bytes(subprocess.check_output(['git','show',f'v0.3.0:{MOVIE}']))
56
+ # Existing footage and narration are retained. Only the six-second ending is moved.
57
+ enc=['-c:v','libx264','-profile:v','high','-level:v','5.1','-pix_fmt','yuv420p','-r','25','-threads','4','-preset','veryfast','-crf','20','-c:a','aac','-ar','48000','-ac','1','-b:a','192k','-video_track_timescale','12800']
58
+ for i,s in enumerate(scenes):
59
+ frame(i,s)
60
+ (TEMP/f'voice-{i}.txt').write_text(s['voice'])
61
+ run(['say','-v','Thomas','-r','165','-f',str(TEMP/f'voice-{i}.txt'),'-o',str(TEMP/f'voice-{i}.aiff')])
62
+ s['duration']=math.ceil((duration(TEMP/f'voice-{i}.aiff')+0.8)*25)/25
63
+ clip=TEMP/f'clip-{i}.mp4'
64
+ fingerprint=hashlib.sha256((TEMP/f'frame-{i}.png').read_bytes()+(TEMP/f'voice-{i}.aiff').read_bytes()+json.dumps(enc).encode()).hexdigest()
65
+ cache=TEMP/f'clip-{i}.sha256'
66
+ if not clip.exists() or not cache.exists() or cache.read_text()!=fingerprint:
67
+ run(['ffmpeg','-y','-loop','1','-framerate','25','-i',str(TEMP/f'frame-{i}.png'),'-i',str(TEMP/f'voice-{i}.aiff'),'-vf',f"fade=t=in:st=0:d=0.3,fade=t=out:st={s['duration']-0.3}:d=0.3",'-af','apad','-t',str(s['duration']),*enc,str(clip)])
68
+ cache.write_text(fingerprint)
69
+ print('Review scene encoded',i+1,flush=True)
70
+ # Encode only the splice tracks once, leaving the original scene design and timing intact.
71
+ if not (TEMP/'before.mp4').exists():
72
+ run(['ffmpeg','-y','-i',str(source),'-t',str(CUT),'-map','0:v:0','-map','0:a:0',*enc,str(TEMP/'before.mp4')])
73
+ if not (TEMP/'after.mp4').exists():
74
+ run(['ffmpeg','-y','-ss',str(CUT),'-i',str(source),'-map','0:v:0','-map','0:a:0',*enc,str(TEMP/'after.mp4')])
75
+ clips=[TEMP/'before.mp4']+[TEMP/f'clip-{i}.mp4' for i in range(len(scenes))]+[TEMP/'after.mp4']
76
+ (TEMP/'concat.txt').write_text('\n'.join(f"file '{p}'" for p in clips))
77
+ # Subtitles follow measured segment durations, not estimated spoken word timing.
78
+ def seconds(st):
79
+ h,m,sec=st.replace(',','.').split(':');return int(h)*3600+int(m)*60+float(sec)
80
+ def stamp(sec):
81
+ n=round(sec*1000);return f'{n//3600000:02}:{n//60000%60:02}:{n//1000%60:02},{n%1000:03}'
82
+ original_srt=subprocess.check_output(['git','show','v0.3.0:docs/media/visual-chain/devmethod-du-besoin-au-produit.fr.srt']).decode()
83
+ added=sum(duration(p) for p in clips[1:-1]); prefix=duration(clips[0]); items=[]
84
+ for block in original_srt.strip().split('\n\n'):
85
+ lines=block.splitlines();a,b=map(seconds,lines[1].split(' --> ')); shift=added+(prefix-CUT) if a>=171.5 else 0
86
+ items.append((a+shift,b+shift,'\n'.join(lines[2:])))
87
+ cursor=prefix
88
+ for i,s in enumerate(scenes):
89
+ s['start']=cursor
90
+ parts=re.split(r'(?<=[.!?])\s+',s['voice']); spoken=duration(TEMP/f'voice-{i}.aiff');t=cursor
91
+ for part in parts:
92
+ length=spoken*len(part)/sum(map(len,parts));items.append((t,t+length,part));t+=length
93
+ cursor+=duration(TEMP/f'clip-{i}.mp4')
94
+ items.sort(); srt=MEDIA/'devmethod-du-besoin-au-produit.fr.srt'
95
+ srt.write_text('\n\n'.join(f'{i+1}\n{stamp(a)} --> {stamp(b)}\n{text}' for i,(a,b,text) in enumerate(items))+'\n')
96
+ run(['ffmpeg','-y','-f','concat','-safe','0','-i',str(TEMP/'concat.txt'),'-i',str(srt),'-map','0:v:0','-map','0:a:0','-map','1:0','-c:v','copy','-c:a','copy','-c:s','mov_text','-metadata:s:a:0','language=fra','-metadata:s:s:0','language=fra','-movflags','+faststart',str(ROOT/MOVIE)])
97
+ manifest=dict(baseTag='v0.3.0',baseSHA256=hashlib.sha256(source.read_bytes()).hexdigest(),insertionSeconds=CUT,baseDuration=duration(source),duration=duration(ROOT/MOVIE),method='Original footage and narration retained; review screenshot chapter inserted before original ending. Re-encoded splice tracks; no original scene regenerated.',scenes=scenes)
98
+ (EXT/'scenes.json').write_text(json.dumps(manifest,ensure_ascii=False,indent=2)+'\n')
99
+ run(['ffmpeg','-y','-ss',str(prefix+3),'-i',str(ROOT/MOVIE),'-frames:v','1','-vf','scale=1920:-1',str(MEDIA/'video-preview.jpg')])
100
+ print('Final duration',manifest['duration'],flush=True)
@@ -15,7 +15,7 @@ try {
15
15
  run('tar', ['-xzf', '-'], root, 0, fs.readFileSync(path.resolve(archive)));
16
16
  const pkg = path.join(root, 'package'); const cli = path.join(pkg, 'dist/cli.js');
17
17
  const call = (args, expected = 0) => run(process.execPath, [cli, ...args], root, expected);
18
- assert.equal(JSON.parse(fs.readFileSync(path.join(pkg, 'package.json'))).version, '0.3.0');
18
+ assert.equal(JSON.parse(fs.readFileSync(path.join(pkg, 'package.json'))).version, '0.4.0');
19
19
  run(process.execPath, ['scripts/check-docs.mjs'], pkg);
20
20
  assert.match(call(['--help']), /Markdown PLAN\/tickets and legacy missions/);
21
21
  for (const resource of ['project-foundation/references/exploration.md', 'project-foundation/references/delivery-planning.md', 'project-foundation/assets/EXISTANT.md', 'project-foundation/assets/OPPORTUNITES.md', 'project-foundation/assets/CADRAGE.md', 'project-foundation/assets/REGLES.md', 'scoped-delivery/assets/PLAN.md', 'scoped-delivery/assets/TICKET.md', 'scoped-delivery/assets/REPRISE.md', 'scoped-delivery/assets/MISSION.md', 'scoped-delivery/assets/REVIEW.md', 'scoped-delivery/references/review-workflow.md']) {
@@ -37,6 +37,12 @@ try {
37
37
  const project = path.join(root, host);
38
38
  call(['init', '--tool', host, '--dest', project]);
39
39
  assert.equal(JSON.parse(call(['doctor', '--dest', project, '--json'])).status, 'ok');
40
+ const skillRoot = { codex: '.agents/skills', claude: '.claude/skills', cursor: '.cursor/skills' }[host];
41
+ const entries = fs.readdirSync(path.join(project, skillRoot)).filter(name => name.startsWith('devmethod-'));
42
+ assert.equal(entries.length, 14);
43
+ assert.ok(entries.includes('devmethod-review'));
44
+ for (const name of entries) assert.ok(fs.statSync(path.join(project, skillRoot, name, 'SKILL.md')).size > 0);
45
+ assert.ok(fs.statSync(path.join(project, skillRoot, 'scoped-delivery/references/review-format.md')).size > 0);
40
46
  const profile = path.join(project, 'PROJECT_PROFILE.md'); fs.appendFileSync(profile, '\nFictional local customization.\n');
41
47
  const before = fs.readFileSync(profile);
42
48
  const preview = JSON.parse(call(['update-preview', '--dest', project, '--json']));
@@ -67,11 +73,13 @@ try {
67
73
  fs.appendFileSync(customized, '\nLocal workflow policy.\n'); fs.appendFileSync(profile, '\nFilled project context.\n');
68
74
  const before = [fs.readFileSync(customized), fs.readFileSync(profile)];
69
75
  const preview = JSON.parse(call(['update-preview', '--dest', adopted, '--json']));
70
- assert.equal(preview.entries.find(e => e.path === '.agents/skills/project-foundation/SKILL.md').classification, 'conflict');
76
+ const skillPath = '.agents/skills/project-foundation/SKILL.md';
77
+ const upstreamChanged = !fs.readFileSync(path.join(legacy, skillPath)).equals(fs.readFileSync(path.join(pkg, skillPath)));
78
+ assert.equal(preview.entries.find(e => e.path === skillPath).classification, upstreamChanged ? 'conflict' : 'customized');
71
79
  assert.equal(preview.provenance, JSON.parse(fs.readFileSync(path.join(adopted, 'kit-manifest.json'))).provenance ? 'recorded' : 'unknown');
72
80
  call(['init', '--tool', 'codex', '--dest', adopted], 2);
73
81
  assert.deepEqual([fs.readFileSync(customized), fs.readFileSync(profile)], before);
74
- console.log('Actual legacy tarball: local/upstream conflict detected and filled profile/custom skill preserved.');
82
+ console.log(`Actual legacy tarball: ${upstreamChanged ? 'local/upstream conflict' : 'unchanged upstream with local customization'} detected; filled profile/custom skill preserved.`);
75
83
  }
76
- console.log('Packed 0.3.0: three host installs, subset, customization preservation, mission/context/staleness/planning and documentation links passed. No native host execution.');
84
+ console.log('Packed 0.4.0: three host installs, subset, customization preservation, mission/context/staleness/planning and documentation links passed. No native host execution.');
77
85
  } finally { fs.rmSync(root, { recursive: true, force: true }); }