@abarcenas/ai-workflow-template 1.45.0 → 1.46.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 (47) hide show
  1. package/.agents/config/README.md +43 -0
  2. package/.agents/config/domain.md +37 -0
  3. package/.agents/instructions/learned-knowledge.instructions.md +110 -0
  4. package/.agents/instructions/memory-bank.instructions.md +11 -0
  5. package/.agents/skills/domain-modeling/ADR-FORMAT.md +47 -0
  6. package/.agents/skills/domain-modeling/GLOSSARY-FORMAT.md +60 -0
  7. package/.agents/skills/domain-modeling/SKILL.md +74 -0
  8. package/.agents/skills/grill-me/SKILL.md +7 -0
  9. package/.agents/skills/grill-with-docs/SKILL.md +7 -0
  10. package/.agents/skills/grilling/SKILL.md +28 -0
  11. package/.agents/skills/prototype/LOGIC.md +67 -0
  12. package/.agents/skills/prototype/SKILL.md +26 -0
  13. package/.agents/skills/prototype/UI.md +112 -0
  14. package/.github/workflows/npm-publish.yml +10 -0
  15. package/.github/workflows/pr-test.yml +31 -0
  16. package/.opencode/agents/orchestrator/orchestrator.agent.md +68 -3
  17. package/.opencode/agents/scope.agent.md +299 -0
  18. package/AGENTS.md +42 -3
  19. package/README.md +5 -2
  20. package/package.json +1 -1
  21. package/scripts/bump-version.test.js +238 -0
  22. package/scripts/mcp/playwright-mcp-launcher.js +55 -29
  23. package/scripts/mcp/playwright-mcp-launcher.test.js +217 -0
  24. package/scripts/mcp-memory-server.js +93 -76
  25. package/scripts/mcp-memory-server.test.js +398 -0
  26. package/scripts/memory-cli.test.js +297 -0
  27. package/scripts/memory-index.js +119 -31
  28. package/scripts/memory-index.test.js +1173 -0
  29. package/scripts/normalize-memory.js +26 -1
  30. package/scripts/normalize-memory.test.js +753 -0
  31. package/scripts/setup/discover.test.js +124 -2
  32. package/scripts/setup/hooks.test.js +188 -1
  33. package/scripts/setup/husky-init.test.js +311 -0
  34. package/scripts/setup/index.js +35 -5
  35. package/scripts/setup/index.test.js +279 -0
  36. package/scripts/setup/knowledgebase.test.js +100 -0
  37. package/scripts/setup/skills-phase.js +568 -0
  38. package/scripts/setup/skills-phase.test.js +1274 -0
  39. package/scripts/setup/sync-phase.js +6 -1
  40. package/scripts/setup/sync-phase.test.js +180 -2
  41. package/scripts/setup/ui.test.js +579 -0
  42. package/scripts/setup/utils.test.js +586 -0
  43. package/scripts/sync-unit.test.js +1342 -0
  44. package/scripts/sync.js +540 -246
  45. package/scripts/sync.test.js +283 -0
  46. package/scripts/vocab-sync.test.js +88 -0
  47. package/skills-lock.json +90 -0
@@ -11,9 +11,10 @@
11
11
  // 6. Phase 3 — Prepare (configure consumer package.json prepare script)
12
12
  // 7. Phase 4 — Husky Init (generate .husky/_/ shims, unless --skip-hooks)
13
13
  // 8. Phase 5 — Sync (spawn sync.js + normalize-memory.js, unless --skip-sync)
14
- // 9. Phase 6 — Knowledgebase (register with centralized knowledgebase, unless --skip-knowledgebase)
15
- // 10. Render summary table via ui.summary()
16
- // 11. Determine exit code and return it
14
+ // 9. Phase 6 — Skills (refresh pinned skills from upstream, gated with --skip-sync)
15
+ // 10. Phase 7 — Knowledgebase (register with centralized knowledgebase, unless --skip-knowledgebase)
16
+ // 11. Render summary table via ui.summary()
17
+ // 12. Determine exit code and return it
17
18
  //
18
19
  // Each phase is wrapped in try/catch. Only discovery failure is fatal.
19
20
  // All other phase errors are logged and the pipeline continues (REQ-04).
@@ -39,6 +40,7 @@ import { installHooks } from './hooks.js'
39
40
  import { handlePrepare } from './prepare.js'
40
41
  import { initHusky } from './husky-init.js'
41
42
  import { runSyncPhase } from './sync-phase.js'
43
+ import { runSkillsPhase } from './skills-phase.js'
42
44
  import { registerKnowledgebase } from './knowledgebase.js'
43
45
 
44
46
  // ── Package version (lazy, cached) ───────────────────────────────────────────
@@ -343,10 +345,38 @@ export async function main(argv) {
343
345
  }
344
346
 
345
347
  // ═════════════════════════════════════════════════════════════════════════
346
- // Phase 6 — Knowledgebase (unless --skip-knowledgebase)
348
+ // Phase 6 — Skills (pinned upstream refresh; gated with the sync phase)
349
+ // ═════════════════════════════════════════════════════════════════════════
350
+ if (!flags.skipSync) {
351
+ section('Phase 6: Skills')
352
+
353
+ try {
354
+ const skillsResult = await runSkillsPhase({ ...context, flags })
355
+ context.skillsResult = skillsResult
356
+ phases.push({
357
+ phase: 'skills',
358
+ status: actionStatus(skillsResult.action),
359
+ message: skillsResult.message,
360
+ })
361
+ } catch (err) {
362
+ // Never fatal — the pinned skills shipped in the package are already
363
+ // on disk, so a phase error only degrades to a warning.
364
+ stepWarn(`Skills phase skipped: ${err.message}`)
365
+ phases.push({
366
+ phase: 'skills',
367
+ status: 'warn',
368
+ message: `Skipped \u2014 ${err.message}`,
369
+ })
370
+ }
371
+ } else if (!flags.quiet) {
372
+ stepWarn('Skills: phase skipped (--skip-sync)')
373
+ }
374
+
375
+ // ═════════════════════════════════════════════════════════════════════════
376
+ // Phase 7 — Knowledgebase (unless --skip-knowledgebase)
347
377
  // ═════════════════════════════════════════════════════════════════════════
348
378
  if (!flags.skipKnowledgebase) {
349
- section('Phase 6: Knowledgebase')
379
+ section('Phase 7: Knowledgebase')
350
380
 
351
381
  try {
352
382
  const kbResult = await registerKnowledgebase(context)
@@ -29,6 +29,9 @@
29
29
  // ═════════════════════════════════════════════════════════════════════════════
30
30
 
31
31
  import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'
32
+ import { mkdtempSync, writeFileSync, rmSync } from 'fs'
33
+ import { join } from 'path'
34
+ import { tmpdir } from 'os'
32
35
 
33
36
  // ═════════════════════════════════════════════════════════════════════════════
34
37
  // Module mocks — all external dependencies are fully mocked so tests are
@@ -76,6 +79,10 @@ vi.mock('./knowledgebase.js', () => ({
76
79
  registerKnowledgebase: vi.fn(),
77
80
  }))
78
81
 
82
+ vi.mock('./skills-phase.js', () => ({
83
+ runSkillsPhase: vi.fn(),
84
+ }))
85
+
79
86
  // ═════════════════════════════════════════════════════════════════════════════
80
87
  // Imports — these resolve to the mocked modules above
81
88
  // ═════════════════════════════════════════════════════════════════════════════
@@ -98,6 +105,7 @@ import { handlePrepare } from './prepare.js'
98
105
  import { initHusky } from './husky-init.js'
99
106
  import { runSyncPhase } from './sync-phase.js'
100
107
  import { registerKnowledgebase } from './knowledgebase.js'
108
+ import { runSkillsPhase } from './skills-phase.js'
101
109
  import { EXIT_CODES } from './constants.js'
102
110
 
103
111
  // ═════════════════════════════════════════════════════════════════════════════
@@ -203,6 +211,10 @@ describe('main() — setup orchestrator', () => {
203
211
  chunks_indexed: 5,
204
212
  chunks_skipped: 0,
205
213
  })
214
+ runSkillsPhase.mockResolvedValue({
215
+ action: 'completed',
216
+ message: 'Pinned skills refreshed',
217
+ })
206
218
 
207
219
  // Spy on console.log for --version tests
208
220
  logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
@@ -739,4 +751,271 @@ describe('main() — setup orchestrator', () => {
739
751
  expect(initHusky).toHaveBeenCalledTimes(1)
740
752
  expect(registerKnowledgebase).toHaveBeenCalledTimes(1)
741
753
  })
754
+ // ── 24. Hooks aggregation branches ──────────────────────────────────────
755
+
756
+ it('mixed hook actions are summarised by count', async () => {
757
+ installHooks.mockResolvedValue([
758
+ { hook: 'pre-commit', action: 'created', message: 'a' },
759
+ { hook: 'post-merge', action: 'merged', message: 'b' },
760
+ { hook: 'post-commit', action: 'overwritten', message: 'c' },
761
+ ])
762
+
763
+ const code = await main([])
764
+
765
+ expect(code).toBe(EXIT_CODES.SUCCESS)
766
+ expect(summary).toHaveBeenCalledWith(
767
+ expect.arrayContaining([
768
+ expect.objectContaining({
769
+ phase: 'hooks',
770
+ status: 'success',
771
+ message: '1 created, 1 merged, 1 overwritten',
772
+ }),
773
+ ]),
774
+ )
775
+ })
776
+
777
+ it('a skipped hook marks the hooks phase as an error', async () => {
778
+ installHooks.mockResolvedValue([
779
+ { hook: 'pre-commit', action: 'skipped', message: 'denied' },
780
+ { hook: 'post-merge', action: 'created', message: 'ok' },
781
+ { hook: 'post-commit', action: 'created', message: 'ok' },
782
+ ])
783
+
784
+ const code = await main([])
785
+
786
+ expect(code).toBe(EXIT_CODES.FATAL)
787
+ expect(summary).toHaveBeenCalledWith(
788
+ expect.arrayContaining([
789
+ expect.objectContaining({
790
+ phase: 'hooks',
791
+ status: 'error',
792
+ message: 'One or more hooks failed',
793
+ }),
794
+ ]),
795
+ )
796
+ })
797
+
798
+ it('all-overwritten hooks report "All hooks installed"', async () => {
799
+ installHooks.mockResolvedValue([
800
+ { hook: 'pre-commit', action: 'overwritten', message: 'a' },
801
+ { hook: 'post-merge', action: 'overwritten', message: 'b' },
802
+ { hook: 'post-commit', action: 'overwritten', message: 'c' },
803
+ ])
804
+
805
+ const code = await main([])
806
+
807
+ expect(code).toBe(EXIT_CODES.SUCCESS)
808
+ expect(summary).toHaveBeenCalledWith(
809
+ expect.arrayContaining([
810
+ expect.objectContaining({
811
+ phase: 'hooks',
812
+ status: 'success',
813
+ message: 'All hooks installed',
814
+ }),
815
+ ]),
816
+ )
817
+ })
818
+
819
+ it('hook dry-run results report "Dry-run complete"', async () => {
820
+ parseCliArgs.mockReturnValue({ ...defaultFlags, dryRun: true })
821
+ installHooks.mockResolvedValue([
822
+ { hook: 'pre-commit', action: 'dry-run', message: 'a' },
823
+ { hook: 'post-merge', action: 'dry-run', message: 'b' },
824
+ { hook: 'post-commit', action: 'dry-run', message: 'c' },
825
+ ])
826
+
827
+ const code = await main(['--dry-run'])
828
+
829
+ expect(code).toBe(EXIT_CODES.SUCCESS)
830
+ expect(summary).toHaveBeenCalledWith(
831
+ expect.arrayContaining([
832
+ expect.objectContaining({
833
+ phase: 'hooks',
834
+ status: 'success',
835
+ message: 'Dry-run complete (no files modified)',
836
+ }),
837
+ ]),
838
+ )
839
+ })
840
+
841
+ // ── 25. actionStatus mapping ────────────────────────────────────────────
842
+
843
+ it('maps skipped and partial actions to warnings', async () => {
844
+ handlePrepare.mockResolvedValue({ action: 'skipped', message: 'prepare skipped' })
845
+ runSyncPhase.mockResolvedValue({
846
+ action: 'partial',
847
+ message: 'sync partially completed',
848
+ results: [],
849
+ })
850
+
851
+ const code = await main([])
852
+
853
+ expect(code).toBe(EXIT_CODES.WARNINGS)
854
+ expect(summary).toHaveBeenCalledWith(
855
+ expect.arrayContaining([
856
+ expect.objectContaining({ phase: 'prepare', status: 'warn' }),
857
+ expect.objectContaining({ phase: 'sync', status: 'warn' }),
858
+ ]),
859
+ )
860
+ })
861
+
862
+ it('maps a failed knowledgebase action to an error status', async () => {
863
+ registerKnowledgebase.mockResolvedValue({
864
+ action: 'failed',
865
+ message: 'sync failed',
866
+ })
867
+
868
+ const code = await main([])
869
+
870
+ expect(code).toBe(EXIT_CODES.FATAL)
871
+ expect(summary).toHaveBeenCalledWith(
872
+ expect.arrayContaining([
873
+ expect.objectContaining({ phase: 'knowledgebase', status: 'error' }),
874
+ ]),
875
+ )
876
+ })
877
+
878
+ it('treats an unknown phase action as a warning', async () => {
879
+ handlePrepare.mockResolvedValue({ action: 'mystery-action', message: 'hm' })
880
+
881
+ const code = await main([])
882
+
883
+ expect(code).toBe(EXIT_CODES.WARNINGS)
884
+ expect(summary).toHaveBeenCalledWith(
885
+ expect.arrayContaining([
886
+ expect.objectContaining({ phase: 'prepare', status: 'warn', message: 'hm' }),
887
+ ]),
888
+ )
889
+ })
890
+
891
+ // ── 26. Skills / knowledgebase failure paths ────────────────────────────
892
+
893
+ it('skills phase failure degrades to a warning and continues', async () => {
894
+ runSkillsPhase.mockRejectedValue(new Error('network down'))
895
+
896
+ const code = await main([])
897
+
898
+ expect(code).toBe(EXIT_CODES.WARNINGS)
899
+ expect(stepWarn).toHaveBeenCalledWith(
900
+ expect.stringContaining('Skills phase skipped: network down'),
901
+ )
902
+ expect(summary).toHaveBeenCalledWith(
903
+ expect.arrayContaining([
904
+ expect.objectContaining({
905
+ phase: 'skills',
906
+ status: 'warn',
907
+ message: 'Skipped — network down',
908
+ }),
909
+ ]),
910
+ )
911
+ // Later phases still ran
912
+ expect(registerKnowledgebase).toHaveBeenCalledTimes(1)
913
+ })
914
+
915
+ it('knowledgebase phase failure degrades to a warning and continues', async () => {
916
+ registerKnowledgebase.mockRejectedValue(new Error('pg unreachable'))
917
+
918
+ const code = await main([])
919
+
920
+ expect(code).toBe(EXIT_CODES.WARNINGS)
921
+ expect(stepWarn).toHaveBeenCalledWith(
922
+ expect.stringContaining('Knowledgebase registration skipped: pg unreachable'),
923
+ )
924
+ expect(summary).toHaveBeenCalledWith(
925
+ expect.arrayContaining([
926
+ expect.objectContaining({
927
+ phase: 'knowledgebase',
928
+ status: 'warn',
929
+ message: 'Skipped — pg unreachable',
930
+ }),
931
+ ]),
932
+ )
933
+ })
934
+
935
+ it('--skip-knowledgebase does not call the knowledgebase phase', async () => {
936
+ parseCliArgs.mockReturnValue({ ...defaultFlags, skipKnowledgebase: true })
937
+
938
+ const code = await main(['--skip-knowledgebase'])
939
+
940
+ expect(code).toBe(EXIT_CODES.SUCCESS)
941
+ expect(registerKnowledgebase).not.toHaveBeenCalled()
942
+ })
943
+
944
+ it('--skip-sync also skips the skills phase', async () => {
945
+ parseCliArgs.mockReturnValue({ ...defaultFlags, skipSync: true })
946
+
947
+ const code = await main(['--skip-sync'])
948
+
949
+ expect(code).toBe(EXIT_CODES.SUCCESS)
950
+ expect(runSyncPhase).not.toHaveBeenCalled()
951
+ expect(runSkillsPhase).not.toHaveBeenCalled()
952
+ expect(stepWarn).toHaveBeenCalledWith(
953
+ 'Skills: phase skipped (--skip-sync)',
954
+ )
955
+ })
956
+
957
+ it('--skip-sync in quiet mode omits the skills skip notice', async () => {
958
+ parseCliArgs.mockReturnValue({
959
+ ...defaultFlags,
960
+ skipSync: true,
961
+ quiet: true,
962
+ })
963
+
964
+ await main(['--skip-sync', '--quiet'])
965
+
966
+ expect(runSkillsPhase).not.toHaveBeenCalled()
967
+ expect(stepWarn).not.toHaveBeenCalledWith(
968
+ 'Skills: phase skipped (--skip-sync)',
969
+ )
970
+ })
971
+
972
+ // ── 27. Quiet fatal path ────────────────────────────────────────────────
973
+
974
+ it('discovery failure in quiet mode suppresses the summary', async () => {
975
+ parseCliArgs.mockReturnValue({ ...defaultFlags, quiet: true })
976
+ discover.mockRejectedValue(new Error('fatal discovery'))
977
+
978
+ const code = await main(['--quiet'])
979
+
980
+ expect(code).toBe(EXIT_CODES.FATAL)
981
+ expect(stepError).toHaveBeenCalledWith(
982
+ expect.stringContaining('fatal discovery'),
983
+ )
984
+ expect(summary).not.toHaveBeenCalled()
985
+ })
986
+
987
+ // ── 28. --version fallbacks (fresh module instances) ────────────────────
988
+
989
+ it('--version falls back to 0.0.0 when package.json cannot be read', async () => {
990
+ vi.resetModules()
991
+ resolvePackageRoot.mockImplementation(() => {
992
+ throw new Error('package root unavailable')
993
+ })
994
+ parseCliArgs.mockReturnValue({ ...defaultFlags, version: true })
995
+
996
+ const fresh = await import('./index.js')
997
+ const code = await fresh.main(['--version'])
998
+
999
+ expect(code).toBe(EXIT_CODES.SUCCESS)
1000
+ expect(logSpy).toHaveBeenCalledWith('0.0.0')
1001
+ })
1002
+
1003
+ it('--version falls back to 0.0.0 when the version field is missing', async () => {
1004
+ const dir = mkdtempSync(join(tmpdir(), 'index-test-'))
1005
+ writeFileSync(join(dir, 'package.json'), JSON.stringify({ name: 'no-version' }))
1006
+ vi.resetModules()
1007
+ resolvePackageRoot.mockImplementation(() => dir)
1008
+ parseCliArgs.mockReturnValue({ ...defaultFlags, version: true })
1009
+
1010
+ try {
1011
+ const fresh = await import('./index.js')
1012
+ const code = await fresh.main(['--version'])
1013
+
1014
+ expect(code).toBe(EXIT_CODES.SUCCESS)
1015
+ expect(logSpy).toHaveBeenCalledWith('0.0.0')
1016
+ } finally {
1017
+ rmSync(dir, { recursive: true, force: true })
1018
+ }
1019
+ })
1020
+
742
1021
  })
@@ -35,6 +35,7 @@ vi.mock('./utils.js', () => ({
35
35
 
36
36
  import { spawn } from 'child_process'
37
37
  import { registerKnowledgebase } from './knowledgebase.js'
38
+ import { logWarn } from './utils.js'
38
39
 
39
40
  // ── Test helpers ──────────────────────────────────────────────────────────
40
41
 
@@ -250,3 +251,102 @@ describe('registerKnowledgebase', () => {
250
251
  expect(spawn).toHaveBeenCalledTimes(1)
251
252
  })
252
253
  })
254
+
255
+
256
+ // ── Execution / option branches ────────────────────────────────────────────
257
+
258
+ describe('registerKnowledgebase — execution branches', () => {
259
+ beforeEach(() => {
260
+ vi.clearAllMocks()
261
+ process.env.DATABASE_URL = 'postgres://localhost:5432/knowledgebase'
262
+ })
263
+
264
+ afterEach(() => {
265
+ delete process.env.DATABASE_URL
266
+ })
267
+
268
+ it('spawns the CLI with the project id and inherits stdio in verbose mode', async () => {
269
+ vi.mocked(spawn).mockImplementation(() =>
270
+ createMockChild({ exitCode: 0 }),
271
+ )
272
+
273
+ const result = await registerKnowledgebase(makeContext({ verbose: true }))
274
+
275
+ // No chunk-count line in the output → both counters default to zero
276
+ expect(result).toMatchObject({
277
+ action: 'indexed',
278
+ chunks_indexed: 0,
279
+ chunks_skipped: 0,
280
+ })
281
+ expect(result.message).toBe(
282
+ 'Registered "test-consumer" — 0 chunks indexed, 0 skipped',
283
+ )
284
+
285
+ const call = vi.mocked(spawn).mock.calls[0]
286
+ expect(call[1]).toEqual([
287
+ '/fake/package/root/scripts/knowledgebase-cli.js',
288
+ 'sync',
289
+ '--project',
290
+ 'test-consumer',
291
+ ])
292
+ expect(call[2].stdio).toBe('inherit')
293
+ expect(call[2].env.INIT_CWD).toBe('/fake/consumer')
294
+ })
295
+
296
+ it('falls back to process.cwd() when consumerRoot is missing', async () => {
297
+ vi.mocked(spawn).mockImplementation(() =>
298
+ createMockChild({
299
+ exitCode: 0,
300
+ stdoutData: 'Indexed 1 new, updated 0, skipped 0 chunks from project "test-consumer"',
301
+ }),
302
+ )
303
+
304
+ const ctx = makeContext()
305
+ delete ctx.consumerRoot
306
+
307
+ const result = await registerKnowledgebase(ctx)
308
+
309
+ expect(result.action).toBe('indexed')
310
+ expect(vi.mocked(spawn).mock.calls[0][2].cwd).toBe(process.cwd())
311
+ })
312
+
313
+ it('treats a null exit code (signal) as -1', async () => {
314
+ vi.mocked(spawn).mockImplementation(() =>
315
+ createMockChild({ exitCode: null }),
316
+ )
317
+
318
+ const result = await registerKnowledgebase(makeContext())
319
+
320
+ expect(result.action).toBe('failed')
321
+ expect(result.message).toContain('exit code -1')
322
+ })
323
+
324
+ it('gracefully skips when spawning throws synchronously', async () => {
325
+ vi.mocked(spawn).mockImplementation(() => {
326
+ throw new Error('ENOENT: knowledgebase-cli.js is missing')
327
+ })
328
+
329
+ const result = await registerKnowledgebase(makeContext())
330
+
331
+ expect(result.action).toBe('skipped')
332
+ expect(result.message).toContain('knowledgebase-cli.js not available')
333
+ expect(result.message).toContain('ENOENT')
334
+ expect(logWarn).toHaveBeenCalledWith(
335
+ expect.stringContaining('Skipping Phase 6'),
336
+ )
337
+ })
338
+
339
+ it('defaults flags and verbose when the context omits them', async () => {
340
+ vi.mocked(spawn).mockImplementation(() =>
341
+ createMockChild({ exitCode: 0 }),
342
+ )
343
+
344
+ const result = await registerKnowledgebase({
345
+ consumerRoot: '/fake/consumer',
346
+ consumerPackageJson: { name: 'test-consumer' },
347
+ })
348
+
349
+ expect(result.action).toBe('indexed')
350
+ expect(vi.mocked(spawn).mock.calls[0][2].stdio).toBe('pipe')
351
+ })
352
+ })