@abarcenas/ai-workflow-template 1.33.0 → 1.34.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.
|
@@ -66,3 +66,30 @@
|
|
|
66
66
|
- **coder**: When fixing child process output capture, consider both stdout and stderr channels independently — they serve different purposes (results vs diagnostics). `stdio: 'inherit'` is clean for real-time streaming but means stdout is no longer capturable programmatically
|
|
67
67
|
- **unit-tester**: When testing child process spawn behavior, mock `child_process` spawn module rather than using real subprocesses — this avoids side effects and keeps tests fast. Use `vi.mock` with factory functions to control `stdio` arg and `env` values
|
|
68
68
|
- **tracker**: Record dead-code discoveries alongside implementation details — the "existing but unused infrastructure" finding is a recurring pattern worth tracking across sessions
|
|
69
|
+
|
|
70
|
+
## Session: 2026-07-24 — Fix Hook Script References for Consumer Projects
|
|
71
|
+
|
|
72
|
+
**Pipeline:** researcher → implementer → coder (5 parallel tasks T1–T5) → tracker
|
|
73
|
+
**Coverage:** 7/7 unit tests passing (2 existing + 5 new), 52/52 integration assertions, 13/13 MCP references verified
|
|
74
|
+
**TDD Iterations:** 0 (feature pipeline, not TDD)
|
|
75
|
+
|
|
76
|
+
**New knowledge:**
|
|
77
|
+
- **Hash-based manifest namespace pattern** — `__scripts__/<relPath>` extends the proven `.agents/` (bare path) and `__opencode__/` namespace pattern. The 3-case sync logic (new → copy, untouched → overwrite, locally modified → skip with warning) applies identically across all sync sections. No manifest version schema change needed for adding new namespace keys.
|
|
78
|
+
- **Co-location constraint for imported dependencies** — `memory-cli.js` imports `'./memory-index.js'` and `mcp-memory-server.js` also imports `'./memory-index.js'`. Both the consumer-facing script AND its co-located dependency must be in the same sync array. This is an architectural constraint: if you sync one, you must sync all of its relative-import dependencies.
|
|
79
|
+
- **`ensureParentDirectory()` handles nested paths automatically** — `mcp/playwright-mcp-launcher.js` lives in a subdirectory, and the existing `ensureParentDirectory()` helper (which calls `mkdirSync(dirname(filePath), { recursive: true })`) creates it without any special handling.
|
|
80
|
+
- **`spawnSync` for testing modules with `process.exit()`** — `sync.js` calls `process.exit()` and cannot be imported as a module. The proven pattern: use `child_process.spawnSync` to invoke it as a child process, with `INIT_CWD` pointing to an isolated temp directory (created via `mkdtempSync`), and clean up in `afterAll`. This is the same pattern used by `sync-phase.js` in production.
|
|
81
|
+
- **Two Playwright MCP patterns coexist by design** — The template's own root `opencode.json` uses `npx @playwright/mcp@latest` (modern, no file dependency), while `opencode.mcp.example.json` uses the launcher script wrapper with env var configuration (`HEADLESS`, `SLOW_MO`, `VIEWPORT`). Both serve different audiences: the template itself vs. consumer projects that may need launcher configuration.
|
|
82
|
+
- **Marker comment for managed-file detection** — The `HOOK_MARKER` (`# Managed by @abarcenas/ai-workflow-template setup`) is the first line of hook files. This enables `discover.js` `isManaged` detection by reading the first line. The marker must be on line 1 for detection to work — the shebang (`#!/bin/sh`) moves to line 2, which is safe because husky sources hook files rather than executing them directly.
|
|
83
|
+
- **Relative paths in hooks become valid after script sync — no hook content changes needed** — The key architectural insight: `constants.js` `TEMPLATE_HOOKS` content uses `node scripts/memory-cli.js update` which resolves correctly from the consumer root once `scripts/` exists there. Zero modifications to `constants.js`, `hooks.js`, `.husky/post-merge`, or `.husky/pre-commit` are needed.
|
|
84
|
+
- **Content integrity via SHA-256 manifest tracking** — Every synced file is hashed with SHA-256. The manifest tracks `__scripts__/<relPath>` → hash. This enables idempotent re-runs (hash match → no warning), local modification detection (on-disk hash ≠ manifest hash → skip with warning), and force overwrite (`--force` ignores local modifications).
|
|
85
|
+
- **Cross-file MCP reference auditing is essential** — 13 reference points across 7 files needed verification (`opencode.mcp.example.json`, `opencode.mcp.json`, `opencode.json` root, `scripts/mcp-memory-server.js`, `scripts/sync.js`, `scripts/sync.test.js`, `.agents-sync-manifest.json`, `docs/playwright-mcp-configuration.md`). Every reference from MCP configs, sync arrays, test assertions, manifest entries, and documentation must be cross-checked to ensure path resolution after sync changes — this is easy to miss.
|
|
86
|
+
- **`--force` flag consistency across all sync sections** — The `--force` flag semantics are identical for `.agents/`, `.opencode/`, `__scripts__/`, and root files: it bypasses local modification protection and overwrites. This consistency should be preserved for any future sync sections.
|
|
87
|
+
|
|
88
|
+
**Agent tuning notes:**
|
|
89
|
+
- **researcher**: Must conduct a 6-tier script inventory with dependency chain analysis when determining sync scope. The co-location constraint (`memory-cli.js` imports `'./memory-index.js'`) was discovered by tracing `import` chains — always follow relative imports to find all files that must be co-located. Also check MCP config files (`opencode.mcp.json`, `opencode.json`) for script path references that create additional sync requirements.
|
|
90
|
+
- **implementer (planning)**: When parallel batches share a dependency (Phase 2 and 3 both depend on Phase 1), verify that batch-independent tasks truly have no shared-file dependencies. T2 (test code), T3 (integration verification), T4 (audit), and T5 (documentation) all depended on T1 but were fully independent of each other — this enabled parallel execution without conflicts. The plan's explicit dependency table with "Can run in parallel?" and "Depends on" columns is essential for coder batch scheduling.
|
|
91
|
+
- **coder (T1 — sync.js core logic)**: When adding a new sync section that mirrors an existing one (`.opencode/` block), copy-paste with careful variable renaming is correct and efficient. The 3-case hash logic is proven and consistent. Only 3 things need to change: (1) the tracked key namespace (`__scripts__/` instead of `__opencode__/`), (2) source/target directory paths, (3) the log prefix in verbose messages. The accumulator pattern (`scriptsCopied`/`scriptsSkipped` feeding into global `copied`/`skipped`) should also be mirrored.
|
|
92
|
+
- **coder (T2 — unit tests)**: Test infrastructure for `process.exit()` modules is reusable across all sync test files — create a `createTempDir()` helper with `mkdtempSync`, a `runSync(tempDir, args)` helper using `spawnSync`, and cleanup in `afterAll`. Assertion patterns to reuse: file existence (`expect(existsSync(path)).toBe(true)`), content match (`readFileSync(path) === sourceContent`), mtime invariance for skip tests, manifest key presence, and SHA-256 hash format validation (`/^[a-f0-9]{64}$/`).
|
|
93
|
+
- **coder (T3 — integration verification)**: Integration verification in a simulated consumer project requires a comprehensive test harness. The pattern: create temp consumer dir with minimal `package.json`, run sync with `INIT_CWD`, then assert on filesystem state and script execution. Content integrity via SHA-256 comparison is more reliable than byte-by-byte comparison. Test groups should cover: first-run sync, idempotent re-run, locally modified preservation, `--force` overwrite, `--dry-run`, hook path resolution, post-merge hook path, syntax validation for all synced scripts, and script execution from consumer root.
|
|
94
|
+
- **coder (T4 — MCP audit)**: MCP reference auditing requires structured cross-referencing. Create a table with columns: File, Lines, Reference text, and Status. Track every reference point across ALL files, not just obvious ones (documentation references are the ones most often missed). Note architectural nuances like the two Playwright patterns (npx vs launcher script) — these are design decisions worth documenting, not bugs.
|
|
95
|
+
- **tracker**: The feature-specific documentation (`docs/hook-script-references/tracker.md`) should include a "Total Results" summary section at the end with unit tests, integration assertions, MCP references, and files modified. This gives readers an executive-summary view without reading the full multi-step pipeline.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abarcenas/ai-workflow-template",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.34.0",
|
|
4
4
|
"description": "A comprehensive template repository for AI-assisted development workflows using GitHub Copilot. This template provides a structured approach to leveraging AI assistance through pre-configured skills, prompts, agents, and coding standards.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
package/scripts/sync.js
CHANGED
|
@@ -171,6 +171,61 @@ if (existsSync(sourceOpenCodeDir)) {
|
|
|
171
171
|
}
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
+
/* ── Sync scripts/ directory ──────────────────────────────── */
|
|
175
|
+
|
|
176
|
+
const scriptsToSync = [
|
|
177
|
+
'memory-cli.js',
|
|
178
|
+
'memory-index.js',
|
|
179
|
+
'bump-version.js',
|
|
180
|
+
'validate-memory-schema.js',
|
|
181
|
+
'mcp-memory-server.js',
|
|
182
|
+
'mcp/playwright-mcp-launcher.js',
|
|
183
|
+
]
|
|
184
|
+
|
|
185
|
+
const sourceScriptsDir = resolve(packageRoot, 'scripts')
|
|
186
|
+
const targetScriptsDir = resolve(consumerRoot, 'scripts')
|
|
187
|
+
let scriptsCopied = 0
|
|
188
|
+
let scriptsSkipped = 0
|
|
189
|
+
|
|
190
|
+
for (const scriptRelPath of scriptsToSync) {
|
|
191
|
+
const sourceFile = resolve(sourceScriptsDir, scriptRelPath)
|
|
192
|
+
const targetFile = resolve(targetScriptsDir, scriptRelPath)
|
|
193
|
+
|
|
194
|
+
if (!existsSync(sourceFile)) {
|
|
195
|
+
continue
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const sourceHash = hashFile(sourceFile)
|
|
199
|
+
const existingHash = existsSync(targetFile) ? hashFile(targetFile) : null
|
|
200
|
+
const trackedKey = `__scripts__/${scriptRelPath}`
|
|
201
|
+
const recordedHash = manifest.files[trackedKey] ?? null
|
|
202
|
+
|
|
203
|
+
if (!existsSync(targetFile)) {
|
|
204
|
+
if (isVerbose) console.error(` … syncing: scripts/${scriptRelPath}`)
|
|
205
|
+
syncFile(sourceFile, targetFile)
|
|
206
|
+
manifest.files[trackedKey] = sourceHash
|
|
207
|
+
scriptsCopied += 1
|
|
208
|
+
copied += 1
|
|
209
|
+
added += 1
|
|
210
|
+
continue
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const isUntouched = recordedHash !== null && existingHash === recordedHash
|
|
214
|
+
|
|
215
|
+
if (forceOverwrite || isUntouched) {
|
|
216
|
+
if (isVerbose) console.error(` … syncing: scripts/${scriptRelPath}`)
|
|
217
|
+
syncFile(sourceFile, targetFile)
|
|
218
|
+
manifest.files[trackedKey] = sourceHash
|
|
219
|
+
scriptsCopied += 1
|
|
220
|
+
copied += 1
|
|
221
|
+
continue
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
scriptsSkipped += 1
|
|
225
|
+
skipped += 1
|
|
226
|
+
console.warn(`[ai-workflow-template] Skipped locally modified script file: scripts/${scriptRelPath}`)
|
|
227
|
+
}
|
|
228
|
+
|
|
174
229
|
for (const rootFile of rootFiles) {
|
|
175
230
|
const sourceFile = resolve(packageRoot, rootFile)
|
|
176
231
|
if (!existsSync(sourceFile)) {
|
package/scripts/sync.test.js
CHANGED
|
@@ -1,4 +1,40 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
1
|
+
import { describe, expect, it, afterAll } from 'vitest'
|
|
2
|
+
import { spawnSync } from 'child_process'
|
|
3
|
+
import {
|
|
4
|
+
copyFileSync,
|
|
5
|
+
existsSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
rmSync,
|
|
10
|
+
mkdtempSync,
|
|
11
|
+
statSync,
|
|
12
|
+
} from 'fs'
|
|
13
|
+
import { join, dirname } from 'path'
|
|
14
|
+
import { tmpdir } from 'os'
|
|
15
|
+
|
|
16
|
+
const projectRoot = process.cwd()
|
|
17
|
+
const tempDirs = []
|
|
18
|
+
|
|
19
|
+
function createTempDir() {
|
|
20
|
+
const tmpDir = mkdtempSync(join(tmpdir(), 'sync-test-'))
|
|
21
|
+
tempDirs.push(tmpDir)
|
|
22
|
+
return tmpDir
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function runSync(tempDir, args = []) {
|
|
26
|
+
return spawnSync('node', ['scripts/sync.js', ...args], {
|
|
27
|
+
cwd: projectRoot,
|
|
28
|
+
env: { ...process.env, INIT_CWD: tempDir },
|
|
29
|
+
encoding: 'utf8',
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
afterAll(() => {
|
|
34
|
+
for (const dir of tempDirs) {
|
|
35
|
+
rmSync(dir, { recursive: true, force: true })
|
|
36
|
+
}
|
|
37
|
+
})
|
|
2
38
|
|
|
3
39
|
describe('sync utility functions', () => {
|
|
4
40
|
it('should validate basic assertions work', () => {
|
|
@@ -10,3 +46,143 @@ describe('sync utility functions', () => {
|
|
|
10
46
|
expect(result).toBe('ai workflow template')
|
|
11
47
|
})
|
|
12
48
|
})
|
|
49
|
+
|
|
50
|
+
describe('__scripts__ sync behavior', () => {
|
|
51
|
+
const scriptsToSync = [
|
|
52
|
+
'memory-cli.js',
|
|
53
|
+
'memory-index.js',
|
|
54
|
+
'bump-version.js',
|
|
55
|
+
'validate-memory-schema.js',
|
|
56
|
+
'mcp-memory-server.js',
|
|
57
|
+
'mcp/playwright-mcp-launcher.js',
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
it('should copy scripts when they do not exist in consumer project', () => {
|
|
61
|
+
const tempDir = createTempDir()
|
|
62
|
+
|
|
63
|
+
// Create a manifest with __scripts__ entries (initial tracking)
|
|
64
|
+
const manifest = {
|
|
65
|
+
version: 1,
|
|
66
|
+
files: Object.fromEntries(
|
|
67
|
+
scriptsToSync.map((s) => [`__scripts__/${s}`, null]),
|
|
68
|
+
),
|
|
69
|
+
}
|
|
70
|
+
writeFileSync(
|
|
71
|
+
join(tempDir, '.agents-sync-manifest.json'),
|
|
72
|
+
JSON.stringify(manifest, null, 2),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
const result = runSync(tempDir)
|
|
76
|
+
expect(result.status).toBe(0)
|
|
77
|
+
|
|
78
|
+
// Verify all 6 script files were created in the consumer's scripts/ dir
|
|
79
|
+
for (const script of scriptsToSync) {
|
|
80
|
+
const targetFile = join(tempDir, 'scripts', script)
|
|
81
|
+
expect(
|
|
82
|
+
existsSync(targetFile),
|
|
83
|
+
`Expected scripts/${script} to exist`,
|
|
84
|
+
).toBe(true)
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('should skip scripts that exist in target without manifest tracking', () => {
|
|
89
|
+
const tempDir = createTempDir()
|
|
90
|
+
const sourceScriptsDir = join(projectRoot, 'scripts')
|
|
91
|
+
|
|
92
|
+
// Pre-populate target with files matching source content (same hash)
|
|
93
|
+
// but create a manifest that has NO __scripts__ entries
|
|
94
|
+
for (const script of scriptsToSync) {
|
|
95
|
+
const sourceFile = join(sourceScriptsDir, script)
|
|
96
|
+
const targetFile = join(tempDir, 'scripts', script)
|
|
97
|
+
mkdirSync(dirname(targetFile), { recursive: true })
|
|
98
|
+
copyFileSync(sourceFile, targetFile)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Empty manifest — no __scripts__ keys → isUntouched = false for all
|
|
102
|
+
writeFileSync(
|
|
103
|
+
join(tempDir, '.agents-sync-manifest.json'),
|
|
104
|
+
JSON.stringify({ version: 1, files: {} }),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
// Record mtimes before sync
|
|
108
|
+
const mtimesBefore = {}
|
|
109
|
+
for (const script of scriptsToSync) {
|
|
110
|
+
mtimesBefore[script] = statSync(
|
|
111
|
+
join(tempDir, 'scripts', script),
|
|
112
|
+
).mtimeMs
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const result = runSync(tempDir)
|
|
116
|
+
expect(result.status).toBe(0)
|
|
117
|
+
|
|
118
|
+
// Verify mtimes are unchanged (files were NOT overwritten)
|
|
119
|
+
for (const script of scriptsToSync) {
|
|
120
|
+
expect(statSync(join(tempDir, 'scripts', script)).mtimeMs).toBe(
|
|
121
|
+
mtimesBefore[script],
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('should force overwrite all scripts with --force', () => {
|
|
127
|
+
const tempDir = createTempDir()
|
|
128
|
+
const sourceScriptsDir = join(projectRoot, 'scripts')
|
|
129
|
+
|
|
130
|
+
// Pre-populate target with dummy content (different from source)
|
|
131
|
+
for (const script of scriptsToSync) {
|
|
132
|
+
const targetFile = join(tempDir, 'scripts', script)
|
|
133
|
+
mkdirSync(dirname(targetFile), { recursive: true })
|
|
134
|
+
writeFileSync(targetFile, `// modified ${script}`)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const result = runSync(tempDir, ['--force'])
|
|
138
|
+
expect(result.status).toBe(0)
|
|
139
|
+
|
|
140
|
+
// Verify all 6 files were overwritten with source content
|
|
141
|
+
for (const script of scriptsToSync) {
|
|
142
|
+
const targetFile = join(tempDir, 'scripts', script)
|
|
143
|
+
const sourceFile = join(sourceScriptsDir, script)
|
|
144
|
+
expect(existsSync(targetFile)).toBe(true)
|
|
145
|
+
expect(readFileSync(targetFile, 'utf8')).toBe(
|
|
146
|
+
readFileSync(sourceFile, 'utf8'),
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('should show scripts in --dry-run output but not write files', () => {
|
|
152
|
+
const tempDir = createTempDir()
|
|
153
|
+
|
|
154
|
+
const result = runSync(tempDir, ['--dry-run'])
|
|
155
|
+
expect(result.status).toBe(0)
|
|
156
|
+
expect(result.stdout).toContain('dry-run')
|
|
157
|
+
|
|
158
|
+
// Verify no script files were written to disk
|
|
159
|
+
for (const script of scriptsToSync) {
|
|
160
|
+
expect(existsSync(join(tempDir, 'scripts', script))).toBe(false)
|
|
161
|
+
}
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('should track new manifest entries after sync', () => {
|
|
165
|
+
const tempDir = createTempDir()
|
|
166
|
+
|
|
167
|
+
// Create empty manifest
|
|
168
|
+
writeFileSync(
|
|
169
|
+
join(tempDir, '.agents-sync-manifest.json'),
|
|
170
|
+
JSON.stringify({ version: 1, files: {} }),
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
const result = runSync(tempDir)
|
|
174
|
+
expect(result.status).toBe(0)
|
|
175
|
+
|
|
176
|
+
// Read back the updated manifest
|
|
177
|
+
const updatedManifest = JSON.parse(
|
|
178
|
+
readFileSync(join(tempDir, '.agents-sync-manifest.json'), 'utf8'),
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
// Verify all 6 __scripts__ entries are tracked with valid SHA-256 hashes
|
|
182
|
+
for (const script of scriptsToSync) {
|
|
183
|
+
const key = `__scripts__/${script}`
|
|
184
|
+
expect(updatedManifest.files[key]).toBeDefined()
|
|
185
|
+
expect(updatedManifest.files[key]).toMatch(/^[a-f0-9]{64}$/)
|
|
186
|
+
}
|
|
187
|
+
})
|
|
188
|
+
})
|