@produck/agent-toolkit 0.2.0 → 0.3.3

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 (32) hide show
  1. package/README.md +53 -9
  2. package/bin/agent-toolkit.mjs +76 -654
  3. package/bin/build-publish-assets.mjs +49 -0
  4. package/bin/command/enforce-node-baseline/help.txt +19 -0
  5. package/bin/command/enforce-node-baseline/index.mjs +155 -0
  6. package/{templates/help/main.txt → bin/command/main/help.txt} +7 -0
  7. package/bin/command/main/index.mjs +11 -0
  8. package/bin/command/preflight/help.txt +9 -0
  9. package/bin/command/preflight/index.mjs +147 -0
  10. package/bin/command/run-capture/index.mjs +100 -0
  11. package/bin/command/shared/args.mjs +45 -0
  12. package/bin/command/shared/text-resource.mjs +19 -0
  13. package/bin/command/summarize-log/index.mjs +64 -0
  14. package/bin/command/sync-coverage-script/help.txt +22 -0
  15. package/bin/command/sync-coverage-script/index.mjs +275 -0
  16. package/bin/command/sync-husky-hooks/help.txt +22 -0
  17. package/bin/command/sync-husky-hooks/index.mjs +267 -0
  18. package/bin/command/sync-instructions/index.mjs +272 -0
  19. package/bin/command/validate-commit-msg/help.txt +9 -0
  20. package/bin/command/validate-commit-msg/index.mjs +183 -0
  21. package/package.json +5 -3
  22. package/publish-assets/instructions/produck/00-produck-base.instructions.md +37 -39
  23. package/publish-assets/instructions/produck/10-produck-node.instructions.md +130 -26
  24. package/publish-assets/instructions/produck/15-produck-workspace.instructions.md +59 -27
  25. package/publish-assets/instructions/produck/20-produck-commit.instructions.md +24 -8
  26. package/publish-assets/instructions/produck/tooling-version-baseline.json +32 -0
  27. package/templates/help/preflight.txt +0 -3
  28. package/templates/help/validate-commit-msg.txt +0 -7
  29. /package/{templates/help/run-capture.txt → bin/command/run-capture/help.txt} +0 -0
  30. /package/{templates/help/summarize-log.txt → bin/command/summarize-log/help.txt} +0 -0
  31. /package/{templates/help/sync-instructions.txt → bin/command/sync-instructions/help.txt} +0 -0
  32. /package/{templates → bin/command/sync-instructions}/user-space-bootstrap.md +0 -0
@@ -0,0 +1,272 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ import { getSingle, hasFlag } from '../shared/args.mjs';
6
+ import { loadTextResource, printTextResource } from '../shared/text-resource.mjs';
7
+
8
+ const COMMAND_DIR = path.dirname(fileURLToPath(import.meta.url));
9
+ const PACKAGE_ROOT = path.resolve(COMMAND_DIR, '../../..');
10
+ const PUBLISH_ASSETS_ROOT = path.resolve(PACKAGE_ROOT, 'publish-assets');
11
+ const PUBLISH_INSTRUCTIONS_ROOT = path.resolve(PUBLISH_ASSETS_ROOT, 'instructions');
12
+ const PUBLISH_NAMESPACE_ROOT = path.resolve(PUBLISH_INSTRUCTIONS_ROOT, 'produck');
13
+ const MANAGED_MARKER = '<!-- managed-by: @produck/agent-toolkit -->';
14
+ const DEFAULT_NAMESPACE_OUT_DIR = '.github/instructions/produck';
15
+ const USER_SPACE_ENTRYPOINT = '.github/copilot-instructions.md';
16
+ const HELP_FILE = path.resolve(COMMAND_DIR, 'help.txt');
17
+ const USER_SPACE_BOOTSTRAP_FILE = path.resolve(COMMAND_DIR, 'user-space-bootstrap.md');
18
+
19
+ export function printSyncInstructionsHelp() {
20
+ printTextResource(HELP_FILE);
21
+ }
22
+
23
+ function loadDefaultInstructionsTemplate() {
24
+ if (fs.existsSync(PUBLISH_NAMESPACE_ROOT)) {
25
+ const names = fs
26
+ .readdirSync(PUBLISH_NAMESPACE_ROOT)
27
+ .filter((name) => name.endsWith('.instructions.md'))
28
+ .sort((a, b) => a.localeCompare(b));
29
+ const entries = names.map((name) => {
30
+ const abs = path.resolve(PUBLISH_NAMESPACE_ROOT, name);
31
+ let text = fs.readFileSync(abs, 'utf8');
32
+ if (!text.endsWith('\n')) {
33
+ text = `${text}\n`;
34
+ }
35
+ return {
36
+ fileName: name,
37
+ content: text,
38
+ sourcePath: abs,
39
+ };
40
+ });
41
+ return {
42
+ type: 'dir',
43
+ sourcePath: PUBLISH_NAMESPACE_ROOT,
44
+ entries,
45
+ };
46
+ }
47
+
48
+ console.error('No built-in instruction assets found.');
49
+ console.error('Run prepack/publish to generate publish-assets, or pass --source explicitly.');
50
+ process.exit(2);
51
+ }
52
+
53
+ function readInstructionEntriesFromDirectory(sourceDir) {
54
+ const names = fs
55
+ .readdirSync(sourceDir)
56
+ .filter((name) => name.endsWith('.instructions.md'))
57
+ .sort((a, b) => a.localeCompare(b));
58
+ return names.map((name) => {
59
+ const sourcePath = path.resolve(sourceDir, name);
60
+ let content = fs.readFileSync(sourcePath, 'utf8');
61
+ if (!content.endsWith('\n')) {
62
+ content = `${content}\n`;
63
+ }
64
+ return {
65
+ fileName: name,
66
+ content,
67
+ sourcePath,
68
+ };
69
+ });
70
+ }
71
+
72
+ function isManagedFile(filePath) {
73
+ const content = fs.readFileSync(filePath, 'utf8');
74
+ return content.includes(MANAGED_MARKER);
75
+ }
76
+
77
+ function buildUserSpaceBootstrapContent(namespaceDirPath, cwd) {
78
+ const namespaceDisplayPath = path.relative(cwd, namespaceDirPath).replace(/\\/g, '/');
79
+ let content = loadTextResource(USER_SPACE_BOOTSTRAP_FILE);
80
+ content = content.replace(/\{\{NAMESPACE_GLOB\}\}/g, `${namespaceDisplayPath}/*.instructions.md`);
81
+ if (!content.endsWith('\n')) {
82
+ content = `${content}\n`;
83
+ }
84
+ return content;
85
+ }
86
+
87
+ export function runSyncInstructions(options) {
88
+ const cwd = path.resolve(getSingle(options, '--cwd', process.cwd()));
89
+ const outArg = getSingle(options, '--out', DEFAULT_NAMESPACE_OUT_DIR);
90
+ const sourceArg = getSingle(options, '--source', '');
91
+ const force = hasFlag(options, '--force');
92
+ const dryRun = hasFlag(options, '--dry-run');
93
+ const prune = hasFlag(options, '--prune');
94
+
95
+ if (!fs.existsSync(cwd)) {
96
+ console.error(`CWD does not exist: ${cwd}`);
97
+ process.exit(2);
98
+ }
99
+
100
+ const defaults = loadDefaultInstructionsTemplate();
101
+ let sourceType = defaults.type;
102
+ let sourceResolved = defaults.sourcePath;
103
+ let entries = defaults.entries;
104
+
105
+ if (sourceArg) {
106
+ const sourcePath = path.resolve(cwd, sourceArg);
107
+ if (!fs.existsSync(sourcePath)) {
108
+ console.error(`Source path does not exist: ${sourcePath}`);
109
+ process.exit(2);
110
+ }
111
+ const stat = fs.statSync(sourcePath);
112
+ if (stat.isDirectory()) {
113
+ sourceType = 'dir';
114
+ sourceResolved = sourcePath;
115
+ entries = readInstructionEntriesFromDirectory(sourcePath);
116
+ if (entries.length === 0) {
117
+ console.error(`No .instructions.md files in source directory: ${sourcePath}`);
118
+ process.exit(2);
119
+ }
120
+ } else {
121
+ sourceType = 'file';
122
+ sourceResolved = sourcePath;
123
+ let content = fs.readFileSync(sourcePath, 'utf8');
124
+ if (!content.endsWith('\n')) {
125
+ content = `${content}\n`;
126
+ }
127
+ entries = [
128
+ {
129
+ fileName: path.basename(sourcePath),
130
+ content,
131
+ sourcePath,
132
+ },
133
+ ];
134
+ }
135
+ }
136
+
137
+ const outPath = path.resolve(cwd, outArg);
138
+ const outLooksLikeFile = outArg.endsWith('.md');
139
+
140
+ if (outLooksLikeFile && entries.length > 1) {
141
+ console.error('Target --out is a file path but source has multiple instruction files.');
142
+ console.error('Use an output directory for multi-file sync.');
143
+ process.exit(2);
144
+ }
145
+
146
+ if (outLooksLikeFile) {
147
+ const entry = entries[0];
148
+ const exists = fs.existsSync(outPath);
149
+ if (exists && !force) {
150
+ const current = fs.readFileSync(outPath, 'utf8');
151
+ if (current !== entry.content) {
152
+ console.error(`Target already exists: ${outPath}`);
153
+ console.error('Use --force to overwrite.');
154
+ process.exit(2);
155
+ }
156
+ }
157
+
158
+ const report = {
159
+ mode: 'single-file',
160
+ cwd,
161
+ sourceType,
162
+ source: sourceResolved,
163
+ outPath,
164
+ exists,
165
+ overwritten: exists && force,
166
+ dryRun,
167
+ prune: false,
168
+ initializedUserSpaceEntry: false,
169
+ userSpaceEntryPath: null,
170
+ };
171
+
172
+ if (dryRun) {
173
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
174
+ process.exit(0);
175
+ }
176
+
177
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
178
+ fs.writeFileSync(outPath, entry.content, 'utf8');
179
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
180
+ return;
181
+ }
182
+
183
+ const outDir = outPath;
184
+ const planned = entries.map((entry) => {
185
+ return {
186
+ fileName: entry.fileName,
187
+ sourcePath: entry.sourcePath,
188
+ targetPath: path.resolve(outDir, entry.fileName),
189
+ content: entry.content,
190
+ };
191
+ });
192
+
193
+ const targetSet = new Set(planned.map((item) => item.targetPath));
194
+ const unchanged = [];
195
+ for (const item of planned) {
196
+ if (fs.existsSync(item.targetPath)) {
197
+ const current = fs.readFileSync(item.targetPath, 'utf8');
198
+ if (current === item.content) {
199
+ unchanged.push(item.targetPath);
200
+ }
201
+ }
202
+ }
203
+
204
+ const toWrite = planned.filter((item) => !unchanged.includes(item.targetPath));
205
+ const conflicts = toWrite.filter((item) => fs.existsSync(item.targetPath));
206
+ if (conflicts.length > 0 && !force) {
207
+ console.error('Some target files already exist and would change:');
208
+ for (const item of conflicts) {
209
+ console.error(`- ${item.targetPath}`);
210
+ }
211
+ console.error('Use --force to overwrite.');
212
+ process.exit(2);
213
+ }
214
+
215
+ const pruneDeletes = [];
216
+ if (prune && fs.existsSync(outDir)) {
217
+ const existing = fs
218
+ .readdirSync(outDir)
219
+ .filter((name) => name.endsWith('.instructions.md'))
220
+ .map((name) => path.resolve(outDir, name));
221
+ for (const existingPath of existing) {
222
+ if (!targetSet.has(existingPath) && isManagedFile(existingPath)) {
223
+ pruneDeletes.push(existingPath);
224
+ }
225
+ }
226
+ }
227
+
228
+ const userSpaceEntryPath = path.resolve(cwd, USER_SPACE_ENTRYPOINT);
229
+ const shouldInitUserSpaceEntry = !fs.existsSync(userSpaceEntryPath);
230
+
231
+ const report = {
232
+ mode: 'directory',
233
+ cwd,
234
+ sourceType,
235
+ source: sourceResolved,
236
+ outDir,
237
+ dryRun,
238
+ force,
239
+ prune,
240
+ initializedUserSpaceEntry: shouldInitUserSpaceEntry,
241
+ userSpaceEntryPath,
242
+ files: planned.map((item) => ({
243
+ fileName: item.fileName,
244
+ sourcePath: item.sourcePath,
245
+ targetPath: item.targetPath,
246
+ unchanged: unchanged.includes(item.targetPath),
247
+ })),
248
+ deleteFiles: pruneDeletes,
249
+ };
250
+
251
+ if (dryRun) {
252
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
253
+ process.exit(0);
254
+ }
255
+
256
+ fs.mkdirSync(outDir, { recursive: true });
257
+ for (const item of toWrite) {
258
+ fs.writeFileSync(item.targetPath, item.content, 'utf8');
259
+ }
260
+
261
+ if (shouldInitUserSpaceEntry) {
262
+ fs.mkdirSync(path.dirname(userSpaceEntryPath), { recursive: true });
263
+ const userSpaceBootstrap = buildUserSpaceBootstrapContent(outDir, cwd);
264
+ fs.writeFileSync(userSpaceEntryPath, userSpaceBootstrap, 'utf8');
265
+ }
266
+
267
+ for (const filePath of pruneDeletes) {
268
+ fs.unlinkSync(filePath);
269
+ }
270
+
271
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
272
+ }
@@ -0,0 +1,9 @@
1
+ Usage:
2
+ agent-toolkit validate-commit-msg --file <message-file>
3
+
4
+ Rules:
5
+ - In monorepo mode, a section header is required before tagged lines
6
+ - Non-empty lines must be either a section header (for example workspace: or @scope/pkg:) or start with [TAG]
7
+ - If section headers are used, each section header must be followed by at least one tagged line
8
+ - No empty lines are allowed
9
+ - Optional target form: [TAG] <target>: <summary>
@@ -0,0 +1,183 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ import { getSingle } from '../shared/args.mjs';
6
+ import { printTextResource } from '../shared/text-resource.mjs';
7
+
8
+ const ALLOWED_TAGS = ['INIT', 'ADD', 'REMOVE', 'FIX', 'REFACTOR', 'UPGRADE', 'PUBLISH'];
9
+ const ALLOWED_TARGETS = ['docs', 'test', 'ci', 'deps', 'api', 'schema', 'infra', 'fmt'];
10
+ const SECTION_HEADER_RE = /^(?:@[\w.-]+\/)?[\w.-]+:$/;
11
+ const COMMAND_DIR = path.dirname(fileURLToPath(import.meta.url));
12
+ const HELP_FILE = path.resolve(COMMAND_DIR, 'help.txt');
13
+ const ROOT_PACKAGE_FILE = path.resolve(COMMAND_DIR, '../../../../../package.json');
14
+
15
+ export function printValidateCommitMsgHelp() {
16
+ printTextResource(HELP_FILE);
17
+ }
18
+
19
+ function validateCommitLine(line, lineNo) {
20
+ if (line.trim() === '') {
21
+ return `Line ${lineNo}: empty line is not allowed`;
22
+ }
23
+
24
+ const head = line.match(/^\[([A-Z]+)\]\s+/);
25
+ if (!head) {
26
+ return `Line ${lineNo}: must start with [TAG] followed by a space`;
27
+ }
28
+
29
+ const tag = head[1];
30
+ if (!ALLOWED_TAGS.includes(tag)) {
31
+ return `Line ${lineNo}: tag [${tag}] is not allowed`;
32
+ }
33
+
34
+ const rest = line.slice(head[0].length);
35
+ if (rest.trim() === '') {
36
+ return `Line ${lineNo}: summary is required after tag`;
37
+ }
38
+
39
+ const targetMatch = rest.match(/^<([^>]+)>:\s+(.+)$/);
40
+ if (targetMatch) {
41
+ const target = targetMatch[1];
42
+ const summary = targetMatch[2];
43
+ if (!ALLOWED_TARGETS.includes(target)) {
44
+ return `Line ${lineNo}: target <${target}> is not allowed`;
45
+ }
46
+ if (summary.trim() === '') {
47
+ return `Line ${lineNo}: summary is required after target`;
48
+ }
49
+ }
50
+
51
+ return null;
52
+ }
53
+
54
+ function isSectionHeaderLine(line) {
55
+ return SECTION_HEADER_RE.test(line.trim());
56
+ }
57
+
58
+ function isMonorepoRoot() {
59
+ if (!fs.existsSync(ROOT_PACKAGE_FILE)) {
60
+ return false;
61
+ }
62
+
63
+ try {
64
+ const rootPackage = JSON.parse(fs.readFileSync(ROOT_PACKAGE_FILE, 'utf8'));
65
+ return Array.isArray(rootPackage.workspaces) && rootPackage.workspaces.length > 0;
66
+ } catch {
67
+ return false;
68
+ }
69
+ }
70
+
71
+ function validateSectionFormat(lines) {
72
+ const errors = [];
73
+ let currentSection = '';
74
+ let currentSectionLineNo = 0;
75
+ let currentSectionHasTaggedLine = false;
76
+
77
+ for (let i = 0; i < lines.length; i += 1) {
78
+ const lineNo = i + 1;
79
+ const line = lines[i];
80
+
81
+ if (line.trim() === '') {
82
+ errors.push(`Line ${lineNo}: empty line is not allowed`);
83
+ continue;
84
+ }
85
+
86
+ if (isSectionHeaderLine(line)) {
87
+ if (currentSection && !currentSectionHasTaggedLine) {
88
+ errors.push(
89
+ `Line ${currentSectionLineNo}: section header "${currentSection}" must be followed by at least one tagged line`,
90
+ );
91
+ }
92
+
93
+ currentSection = line.trim();
94
+ currentSectionLineNo = lineNo;
95
+ currentSectionHasTaggedLine = false;
96
+ continue;
97
+ }
98
+
99
+ if (!currentSection) {
100
+ errors.push(
101
+ `Line ${lineNo}: section header is required before tagged lines when package/workspace sections are used`,
102
+ );
103
+ continue;
104
+ }
105
+
106
+ const err = validateCommitLine(line, lineNo);
107
+ if (err) {
108
+ errors.push(err);
109
+ continue;
110
+ }
111
+
112
+ currentSectionHasTaggedLine = true;
113
+ }
114
+
115
+ if (currentSection && !currentSectionHasTaggedLine) {
116
+ errors.push(
117
+ `Line ${currentSectionLineNo}: section header "${currentSection}" must be followed by at least one tagged line`,
118
+ );
119
+ }
120
+
121
+ return errors;
122
+ }
123
+
124
+ export function runValidateCommitMsg(options) {
125
+ const file = getSingle(options, '--file', '');
126
+ if (!file) {
127
+ printValidateCommitMsgHelp();
128
+ process.exit(2);
129
+ }
130
+
131
+ const filePath = path.resolve(file);
132
+ if (!fs.existsSync(filePath)) {
133
+ console.error(`Message file not found: ${filePath}`);
134
+ process.exit(2);
135
+ }
136
+
137
+ const raw = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
138
+ const lines = raw.endsWith('\n') ? raw.slice(0, -1).split('\n') : raw.split('\n');
139
+
140
+ if (lines.length === 0 || (lines.length === 1 && lines[0].trim() === '')) {
141
+ console.error('Commit message is empty');
142
+ process.exit(2);
143
+ }
144
+
145
+ const mustUseSectionHeaders = isMonorepoRoot();
146
+ const hasSectionHeaders = lines.some((line) => isSectionHeaderLine(line));
147
+
148
+ // [PUBLISH] is generated by lerna and is always a repo-wide tag.
149
+ // In independent mode lerna appends package/version lines after the tag.
150
+ // Neither section headers nor a summary are required for this special tag.
151
+ const isPublishOnlyMessage = /^\[PUBLISH\](\s+.*)?$/.test(lines[0].trim());
152
+
153
+ if (isPublishOnlyMessage) {
154
+ console.log('Commit message validation passed');
155
+ return;
156
+ }
157
+
158
+ if (mustUseSectionHeaders && !hasSectionHeaders) {
159
+ console.error('Commit message validation failed:');
160
+ console.error('- Line 1: section header is required before tagged lines in monorepo mode');
161
+ process.exit(1);
162
+ }
163
+
164
+ const errors = hasSectionHeaders ? validateSectionFormat(lines) : [];
165
+ if (!hasSectionHeaders) {
166
+ for (let i = 0; i < lines.length; i += 1) {
167
+ const err = validateCommitLine(lines[i], i + 1);
168
+ if (err) {
169
+ errors.push(err);
170
+ }
171
+ }
172
+ }
173
+
174
+ if (errors.length > 0) {
175
+ console.error('Commit message validation failed:');
176
+ for (const err of errors) {
177
+ console.error(`- ${err}`);
178
+ }
179
+ process.exit(1);
180
+ }
181
+
182
+ console.log('Commit message validation passed');
183
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@produck/agent-toolkit",
3
- "version": "0.2.0",
3
+ "version": "0.3.3",
4
4
  "description": "Central CLI toolkit for organization AI execution workflows",
5
5
  "type": "module",
6
6
  "repository": {
@@ -13,12 +13,14 @@
13
13
  },
14
14
  "scripts": {
15
15
  "prepack": "node ./bin/build-publish-assets.mjs",
16
+ "coverage": "npm exec --yes -- c8 --reporter=lcov --reporter=html --reporter=text-summary node --test test/index.mjs",
17
+ "coverage:check": "npm exec --yes -- c8 --check-coverage --lines 100 --functions 100 --branches 100 --statements 100 node --test test/index.mjs",
18
+ "test": "node --test test/index.mjs",
16
19
  "verify": "node ./bin/agent-toolkit.mjs --help && node ./bin/agent-toolkit.mjs preflight --cwd . --require package.json",
17
20
  "pack:check": "npm pack --dry-run"
18
21
  },
19
22
  "files": [
20
23
  "bin",
21
- "templates",
22
24
  "publish-assets"
23
25
  ],
24
26
  "publishConfig": {
@@ -28,5 +30,5 @@
28
30
  "node": ">=18.0.0"
29
31
  },
30
32
  "license": "MIT",
31
- "gitHead": "8fbe9d26ada86a17e4f0444c2342e3904ba0f7f2"
33
+ "gitHead": "45bc825ac6c290ca99b58881ee3b052bd0c2265e"
32
34
  }
@@ -79,11 +79,7 @@ indent_style = space
79
79
  indent_size = 2
80
80
  trim_trailing_whitespace = true
81
81
 
82
- [*.yml]
83
- indent_style = space
84
- indent_size = 2
85
-
86
- [*.yaml]
82
+ [*.{yml,yaml}]
87
83
  indent_style = space
88
84
  indent_size = 2
89
85
 
@@ -118,8 +114,11 @@ max_line_length = 80
118
114
  be present in repository lint configuration.
119
115
  - Apply minimal patching only: keep existing repository/framework structure and
120
116
  add the smallest necessary changes.
121
- - Repository-specific overrides are allowed, but should layer on top of
122
- `@produck/eslint-rules` instead of bypassing it.
117
+ - Repository-specific overrides are optional and should be added only when
118
+ behavior intentionally differs from shared presets.
119
+ - In ESLint flat config, "layer on top" means local override items must appear
120
+ after shared preset items in exported order.
121
+ - No-op overrides that repeat inherited values should be avoided.
123
122
 
124
123
  ## Language conventions
125
124
 
@@ -142,24 +141,16 @@ max_line_length = 80
142
141
  - If details are needed, use additional tagged lines.
143
142
  - Do not keep summary as an untagged standalone line.
144
143
  - Recommended local validation:
145
- `npm exec --package=@produck/agent-toolkit@latest agent-toolkit
146
- validate-commit-msg --file <message-file>`.
147
- - Use uppercase tags from this whitelist: `[INIT]`, `[ADD]`, `[REMOVE]`,
148
- `[FIX]`, `[REFACTOR]`, `[UPGRADE]`.
149
- - Legacy tag mapping for migration is `[ADDED]` -> `[ADD]`, `[REMOVED]` ->
150
- `[REMOVE]`, and `[FIXED]` -> `[FIX]`.
151
- - To express content domain, summary may use target syntax: `[TAG] <target>:
152
- <summary>`.
153
- - Allowed targets are `docs`, `test`, `ci`, `deps`, `api`, `schema`, and
154
- `infra`.
155
- - If target syntax is used, target must be wrapped in angle brackets and must be
156
- from the allowed target list.
144
+ `npm exec -- agent-toolkit validate-commit-msg --file <message-file>`.
145
+ - Commit precheck policy follows
146
+ `.github/distribution/produck/20-produck-commit.instructions.md`.
147
+ - Canonical source for commit tag/target whitelists, legacy mapping, and
148
+ target syntax is
149
+ `.github/distribution/produck/20-produck-commit.instructions.md`.
150
+ - Do not redefine commit tag or target whitelists in other instruction files.
157
151
  - For non-monorepo repositories, use `[TAG] summary` directly (no
158
152
  package/workspace section headers).
159
153
  - Bracketed commit summaries should be in English
160
- - `[UPGRADE] deps` is allowed for pure dependency upgrades; if IFF artifacts or
161
- IPC-related artifacts/calls are updated, the summary must name those updates
162
- explicitly.
163
154
  - PR title format is repository-defined; no organization-level title format
164
155
  restriction
165
156
  - In PR descriptions, summarize what changed, why it changed, how it was
@@ -189,14 +180,10 @@ Recommended three-step flow:
189
180
 
190
181
  Recommended local tools:
191
182
 
192
- - `npm exec --package=@produck/agent-toolkit@latest agent-toolkit preflight
193
- --cwd . --require package.json --ensure-dir logs`
194
- - `npm exec --package=@produck/agent-toolkit@latest agent-toolkit run-capture
195
- --out logs/run.log --cmd "<command>"`
196
- - `npm exec --package=@produck/agent-toolkit@latest agent-toolkit summarize-log
197
- --file logs/run.log --last 120`
198
- - `npm exec --package=@produck/agent-toolkit@latest agent-toolkit summarize-log
199
- --file logs/run.log --match "FAIL|ERROR"`
183
+ - `npm exec -- agent-toolkit preflight --cwd . --require package.json --ensure-dir logs`
184
+ - `npm exec -- agent-toolkit run-capture --out logs/run.log --cmd "<command>"`
185
+ - `npm exec -- agent-toolkit summarize-log --file logs/run.log --last 120`
186
+ - `npm exec -- agent-toolkit summarize-log --file logs/run.log --match "FAIL|ERROR"`
200
187
 
201
188
  Guardrails:
202
189
 
@@ -247,21 +234,31 @@ When CI enforcement is deferred, use manual sync per repository:
247
234
  `.github/instructions/produck/`.
248
235
  2. Keep repository-specific exceptions in `.github/copilot-instructions.md`.
249
236
  3. Validate critical policies manually in each update cycle.
237
+ 4. After instruction sync, validate duplicated policy sections remain
238
+ consistent across instruction files, especially commit tag and target
239
+ whitelists.
250
240
 
251
241
  Recommended command:
252
242
 
253
- - `npm exec --package=@produck/agent-toolkit@latest -- agent-toolkit sync-instructions --cwd . --source <path-to-org>/.github/distribution/produck --force --prune`
243
+ - `npm exec -- agent-toolkit sync-instructions --cwd . --source <path-to-org>/.github/distribution/produck --force --prune`
254
244
 
255
- If the package has been published, `--source` can be omitted to use built-in
256
- assets from `@produck/agent-toolkit`.
245
+ If the installed package includes bundled assets, `--source` can be omitted to
246
+ use those built-in assets.
257
247
 
258
248
  This keeps instruction entrypoints aligned without requiring submodule or
259
249
  automatic PR rollout.
260
250
 
261
251
  ### Central package execution policy
262
252
 
263
- When bridge mechanism uses a central npm package, default execution strategy is
264
- `@latest` to deliver new capabilities quickly.
253
+ When bridge mechanism uses a central npm package, the package is installed
254
+ locally in downstream repositories at a fixed version managed by the
255
+ organization baseline.
256
+
257
+ - Local install and pinned version are deployed by
258
+ `agent-toolkit sync-husky-hooks`.
259
+ - Invocation uses the locally installed copy:
260
+ `npm exec -- <bin> ...`.
261
+ - Do not use `npm exec --package=<pkg>@latest` for routine invocations.
265
262
 
266
263
  Local implementation reference in this repository:
267
264
 
@@ -270,12 +267,12 @@ Local implementation reference in this repository:
270
267
  - This local path is the implementation source, not an automatic runtime mount
271
268
  for other repositories.
272
269
 
273
- Required safeguards for `@latest`:
270
+ Required safeguards for central tooling:
274
271
 
275
272
  - Print resolved package version before running high-impact commands.
276
273
  - For high-risk operations, run dry-run/preview first, then execute.
277
274
  - Keep an emergency fallback path to a pinned version for incident mitigation.
278
- - Prefer `npm exec --package=<pkg>@latest <bin> ...` for predictable invocation.
275
+ - Prefer `npm exec -- <bin> ...` for predictable invocation.
279
276
 
280
277
  Version observability (required before high-impact operations):
281
278
 
@@ -307,7 +304,8 @@ Use the following template text in organization AI instructions:
307
304
  - For large output tasks, use two phases: capture full output, then analyze.
308
305
  - Avoid fragile shell pipeline post-processing for long-output commands.
309
306
  - Central package policy:
310
- - Default to `<pkg>@latest` for organization tooling commands.
307
+ - Central package is installed locally at a fixed organization-baseline version.
308
+ - Invoke via `npm exec -- <bin> ...` to use the locally installed copy.
311
309
  - Print resolved package version before high-impact execution.
312
310
  - Use dry-run first for risky operations; keep rollback path to pinned version.
313
311
  - Commit message policy:
@@ -316,7 +314,7 @@ Use the following template text in organization AI instructions:
316
314
  - Use only allowed tags: `[INIT]`, `[ADD]`, `[REMOVE]`, `[FIX]`,
317
315
  `[REFACTOR]`, `[UPGRADE]`.
318
316
  - Optional target syntax is `[TAG] <target>: <summary>` with target in:
319
- `docs`, `test`, `ci`, `deps`, `api`, `schema`, `infra`.
317
+ `docs`, `test`, `ci`, `deps`, `api`, `schema`, `infra`, `fmt`.
320
318
  - Do not assume scripts from organization `.github` repository exist in target
321
319
  repositories.
322
320
  - If a repository provides stricter rules, repository rules override