tink-harness 1.2.1 → 1.2.2

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 (51) hide show
  1. package/.claude-plugin/plugin.json +2 -2
  2. package/CHANGELOG.md +103 -75
  3. package/README.ko.md +130 -122
  4. package/README.md +96 -92
  5. package/VERSIONING.md +2 -2
  6. package/bin/install.js +318 -257
  7. package/commands/cast.md +179 -172
  8. package/docs/context-change-review.ko.md +14 -0
  9. package/docs/context-change-review.md +14 -0
  10. package/docs/external-context-policy.ko.md +15 -0
  11. package/docs/external-context-policy.md +15 -0
  12. package/docs/graph-contracts-and-guards.md +61 -0
  13. package/docs/harness-lifecycle-signals.ko.md +23 -0
  14. package/docs/harness-lifecycle-signals.md +23 -0
  15. package/docs/hooks.md +49 -0
  16. package/docs/memory-decision-layers.ko.md +14 -0
  17. package/docs/memory-decision-layers.md +14 -0
  18. package/docs/memory.md +31 -0
  19. package/docs/phase-5-update-confidence.ko.md +99 -0
  20. package/docs/phase-5-update-confidence.md +97 -0
  21. package/docs/planned-work-units.ko.md +77 -0
  22. package/docs/planned-work-units.md +77 -0
  23. package/docs/pr/2026-06-07-phase-5-6-follow-up.ko.md +35 -0
  24. package/docs/pr/2026-06-07-v1.2.0-improvements.html +450 -0
  25. package/docs/pr/2026-06-08-planned-work-units.ko.md +27 -0
  26. package/docs/pr/2026-06-08-v1.2.2.ko.md +27 -0
  27. package/docs/repo-signals.ko.md +104 -0
  28. package/docs/repo-signals.md +95 -77
  29. package/docs/research.md +16 -0
  30. package/docs/tink-idea-implementation-plan.ko.md +201 -0
  31. package/docs/update-diagnosis.ko.md +16 -0
  32. package/docs/update-diagnosis.md +16 -0
  33. package/docs/update-troubleshooting.ko.md +113 -0
  34. package/docs/update-troubleshooting.md +100 -0
  35. package/docs/update-verification-recipe.ko.md +118 -0
  36. package/docs/update-verification-recipe.md +119 -0
  37. package/docs/verification-evidence-details.ko.md +14 -0
  38. package/docs/verification-evidence-details.md +14 -0
  39. package/docs/work-state.ko.md +94 -0
  40. package/docs/work-state.md +92 -0
  41. package/package.json +2 -4
  42. package/templates/claude/commands/tink/cast.md +179 -172
  43. package/templates/codex/skills/tink-cast/SKILL.md +14 -13
  44. package/templates/codex/skills/tink-core/RULES.md +163 -112
  45. package/templates/tink/memory/approved/README.md +5 -0
  46. package/templates/tink/memory/candidate/README.md +5 -0
  47. package/templates/tink/memory/evidence/README.md +5 -0
  48. package/templates/tink/memory/rejected/README.md +5 -0
  49. package/templates/tink/schemas/harness-lifecycle.schema.json +44 -0
  50. package/templates/tink/schemas/mcp-policy.schema.json +65 -0
  51. package/templates/tink/schemas/verification.schema.json +154 -141
package/bin/install.js CHANGED
@@ -11,13 +11,20 @@ const __dirname = path.dirname(__filename);
11
11
  const root = path.resolve(__dirname, '..');
12
12
  const args = process.argv.slice(2);
13
13
  const command = args[0] || 'install';
14
- const isUpdate = command === 'update';
15
- const dryRun = args.includes('--dry-run');
16
- const force = args.includes('--force');
17
- const yes = args.includes('--yes') || args.includes('-y');
18
- const interactive = process.stdin.isTTY && process.stdout.isTTY && !yes && !dryRun;
19
- const source = 'https://github.com/dotoricode/tink-harness.git';
20
- const validSurfaces = new Set(['claude', 'codex']);
14
+ const isUpdate = command === 'update';
15
+ const dryRun = args.includes('--dry-run');
16
+ const force = args.includes('--force');
17
+ const yes = args.includes('--yes') || args.includes('-y');
18
+ const interactive = process.stdin.isTTY && process.stdout.isTTY && !yes && !dryRun;
19
+ const source = 'https://github.com/dotoricode/tink-harness.git';
20
+ const validSurfaces = new Set(['claude', 'codex']);
21
+ const operationLog = {
22
+ written: [],
23
+ updated: [],
24
+ preserved: [],
25
+ removedLegacy: [],
26
+ keptUnknown: []
27
+ };
21
28
 
22
29
  const COPY = {
23
30
  en: {
@@ -67,124 +74,124 @@ const COPY = {
67
74
  }
68
75
  };
69
76
 
70
- const COMPONENTS = {
77
+ const COMPONENTS = {
71
78
  en: [
72
- { value: 'commands', label: 'Claude Code commands', hint: '/tink:setup, /tink:cast, /tink:verify, /tink:list, /tink:frog, /tink:weave, /tink:update' },
79
+ { value: 'commands', label: 'Claude Code commands', hint: '/tink:setup, /tink:cast, /tink:verify, /tink:list, /tink:frog, /tink:weave, /tink:update' },
73
80
  { value: 'skill', label: 'Tink skill', hint: 'Tink operating rules for Claude Code' },
74
81
  { value: 'harnesses', label: 'Built-in harnesses', hint: 'Reusable task templates' },
75
82
  { value: 'memory', label: 'Memory templates', hint: 'Approved mistakes/preferences/lessons files' },
76
83
  { value: 'hook', label: 'Hook recommendation (optional)', hint: 'Registers a safe UserPromptSubmit hook when selected. Off by default.' }
77
84
  ],
78
85
  ko: [
79
- { value: 'commands', label: 'Claude Code 명령', hint: '/tink:setup, /tink:cast, /tink:verify, /tink:list, /tink:frog, /tink:weave, /tink:update' },
86
+ { value: 'commands', label: 'Claude Code 명령', hint: '/tink:setup, /tink:cast, /tink:verify, /tink:list, /tink:frog, /tink:weave, /tink:update' },
80
87
  { value: 'skill', label: 'Tink skill', hint: 'Claude Code가 읽는 Tink 작업 원칙' },
81
88
  { value: 'harnesses', label: '기본 harness', hint: '재사용 작업 템플릿' },
82
89
  { value: 'memory', label: 'Memory 템플릿', hint: '승인된 실수/선호/교훈 파일' },
83
90
  { value: 'hook', label: 'Hook 추천 (선택)', hint: '선택하면 안전한 UserPromptSubmit hook으로 등록합니다. 기본 off.' }
84
91
  ],
85
92
  zh: [
86
- { value: 'commands', label: 'Claude Code 命令', hint: '/tink:setup, /tink:cast, /tink:verify, /tink:list, /tink:frog, /tink:weave, /tink:update' },
93
+ { value: 'commands', label: 'Claude Code 命令', hint: '/tink:setup, /tink:cast, /tink:verify, /tink:list, /tink:frog, /tink:weave, /tink:update' },
87
94
  { value: 'skill', label: 'Tink skill', hint: 'Claude Code 读取的 Tink 工作规则' },
88
95
  { value: 'harnesses', label: '内置 harness', hint: '可复用任务模板' },
89
96
  { value: 'memory', label: 'Memory 模板', hint: '经批准的错误/偏好/经验文件' },
90
97
  { value: 'hook', label: 'Hook 推荐(可选)', hint: '选择后注册安全的 UserPromptSubmit hook。默认关闭。' }
91
98
  ]
92
- };
93
-
94
- const SURFACE_OPTIONS = {
95
- en: [
96
- { value: 'claude', label: 'Claude Code', hint: 'Install /tink:* commands, Claude skill, and optional hook support' },
97
- { value: 'codex', label: 'Codex', hint: 'Install $tink:* skills into CODEX_HOME or ~/.codex' }
98
- ],
99
- ko: [
100
- { value: 'claude', label: 'Claude Code', hint: '/tink:* 명령, Claude skill, 선택 hook 지원 설치' },
101
- { value: 'codex', label: 'Codex', hint: '$tink:* skills를 CODEX_HOME 또는 ~/.codex에 설치' }
102
- ],
103
- zh: [
104
- { value: 'claude', label: 'Claude Code', hint: 'Install /tink:* commands, Claude skill, and optional hook support' },
105
- { value: 'codex', label: 'Codex', hint: 'Install $tink:* skills into CODEX_HOME or ~/.codex' }
106
- ]
107
- };
108
-
109
- function argValue(name) {
110
- const prefix = `${name}=`;
111
- const found = args.find((arg) => arg.startsWith(prefix));
112
- return found ? found.slice(prefix.length) : undefined;
113
- }
114
-
115
- function usage() {
116
- console.log(`Tink installer for Claude Code and Codex\n\nUsage:\n npx tink-harness@latest [install] [--scope=repo|global] [--global] [--lang=en|ko|zh] [--yes] [--with-hook] [--dry-run] [--force]\n npx tink-harness@latest update [--scope=repo|global] [--global] [--lang=en|ko|zh] [--yes] [--dry-run] [--force]\n\nCommands:\n install Install Tink.\n update Update Tink to the latest templates. Keeps user-modified files.\n\nDefault interactive flow:\n 1. Select language\n 2. Show TINK wizard\n 3. Select Claude Code, Codex, or both\n 4. Select components\n 5. Select repo/global installation scope\n 6. Select git tracking policy for project state\n\nScopes:\n repo Install shared .tink files into the current project.\n global Install shared .tink files into your home directory.\n`);
117
- }
118
-
119
- function normalizeSurfaces(surfaces) {
120
- const values = [...new Set(surfaces)];
121
- if (values.some((value) => !validSurfaces.has(value))) {
122
- console.error(`Invalid install surface: ${values.find((value) => !validSurfaces.has(value))}`);
123
- usage();
124
- process.exit(1);
125
- }
126
- if (values.includes('claude') && values.includes('codex')) return 'all';
127
- return values[0] || 'claude';
128
- }
129
-
130
- function resolveDefaultSurfaces() {
131
- if (argValue('--agent')) {
132
- console.error('--agent is no longer supported. Run the interactive installer and select Claude Code, Codex, or both during setup.');
133
- usage();
134
- process.exit(1);
135
- }
136
-
137
- const envValue = process.env.TINK_INSTALL_SURFACES;
138
- if (!envValue) return 'claude';
139
- if (envValue.trim().toLowerCase() === 'all') return 'all';
140
- return normalizeSurfaces(envValue.split(',').map((value) => value.trim().toLowerCase()).filter(Boolean));
141
- }
142
-
143
- function includesClaude(agent) {
144
- return agent === 'claude' || agent === 'all';
145
- }
146
-
147
- function includesCodex(agent) {
148
- return agent === 'codex' || agent === 'all';
149
- }
150
-
151
- function codexHome() {
152
- return process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
153
- }
154
-
155
- function componentOptionsFor(agent, language) {
156
- const options = COMPONENTS[language].filter((item) => {
157
- if (item.value === 'commands') return includesClaude(agent);
158
- if (item.value === 'hook') return includesClaude(agent);
159
- return true;
160
- });
161
- return options.map((item) => {
162
- if (item.value !== 'skill') return item;
163
- if (agent === 'codex') {
164
- return {
165
- value: 'skill',
166
- label: language === 'ko' ? 'Codex Tink skills' : language === 'zh' ? 'Codex Tink skills' : 'Codex Tink skills',
167
- hint: language === 'ko'
168
- ? 'Codex가 $tink로 읽는 Tink 작업 원칙'
169
- : language === 'zh'
170
- ? 'Codex 通过 $tink 读取的 Tink 工作规则'
171
- : 'Tink operating rules for Codex through $tink:*'
172
- };
173
- }
174
- if (agent === 'all') {
175
- return {
176
- value: 'skill',
177
- label: language === 'ko' ? 'Tink skills' : language === 'zh' ? 'Tink skills' : 'Tink skills',
178
- hint: language === 'ko'
179
- ? 'Claude Code와 Codex가 읽는 Tink 작업 원칙'
180
- : language === 'zh'
181
- ? 'Claude Code 和 Codex 读取的 Tink 工作规则'
182
- : 'Tink operating rules for Claude Code and Codex'
183
- };
184
- }
185
- return item;
186
- });
187
- }
99
+ };
100
+
101
+ const SURFACE_OPTIONS = {
102
+ en: [
103
+ { value: 'claude', label: 'Claude Code', hint: 'Install /tink:* commands, Claude skill, and optional hook support' },
104
+ { value: 'codex', label: 'Codex', hint: 'Install $tink:* skills into CODEX_HOME or ~/.codex' }
105
+ ],
106
+ ko: [
107
+ { value: 'claude', label: 'Claude Code', hint: '/tink:* 명령, Claude skill, 선택 hook 지원 설치' },
108
+ { value: 'codex', label: 'Codex', hint: '$tink:* skills를 CODEX_HOME 또는 ~/.codex에 설치' }
109
+ ],
110
+ zh: [
111
+ { value: 'claude', label: 'Claude Code', hint: 'Install /tink:* commands, Claude skill, and optional hook support' },
112
+ { value: 'codex', label: 'Codex', hint: 'Install $tink:* skills into CODEX_HOME or ~/.codex' }
113
+ ]
114
+ };
115
+
116
+ function argValue(name) {
117
+ const prefix = `${name}=`;
118
+ const found = args.find((arg) => arg.startsWith(prefix));
119
+ return found ? found.slice(prefix.length) : undefined;
120
+ }
121
+
122
+ function usage() {
123
+ console.log(`Tink installer for Claude Code and Codex\n\nUsage:\n npx tink-harness@latest [install] [--scope=repo|global] [--global] [--lang=en|ko|zh] [--yes] [--with-hook] [--dry-run] [--force]\n npx tink-harness@latest update [--scope=repo|global] [--global] [--lang=en|ko|zh] [--yes] [--dry-run] [--force]\n\nCommands:\n install Install Tink.\n update Update Tink to the latest templates. Keeps user-modified files.\n\nDefault interactive flow:\n 1. Select language\n 2. Show TINK wizard\n 3. Select Claude Code, Codex, or both\n 4. Select components\n 5. Select repo/global installation scope\n 6. Select git tracking policy for project state\n\nScopes:\n repo Install shared .tink files into the current project.\n global Install shared .tink files into your home directory.\n`);
124
+ }
125
+
126
+ function normalizeSurfaces(surfaces) {
127
+ const values = [...new Set(surfaces)];
128
+ if (values.some((value) => !validSurfaces.has(value))) {
129
+ console.error(`Invalid install surface: ${values.find((value) => !validSurfaces.has(value))}`);
130
+ usage();
131
+ process.exit(1);
132
+ }
133
+ if (values.includes('claude') && values.includes('codex')) return 'all';
134
+ return values[0] || 'claude';
135
+ }
136
+
137
+ function resolveDefaultSurfaces() {
138
+ if (argValue('--agent')) {
139
+ console.error('--agent is no longer supported. Run the interactive installer and select Claude Code, Codex, or both during setup.');
140
+ usage();
141
+ process.exit(1);
142
+ }
143
+
144
+ const envValue = process.env.TINK_INSTALL_SURFACES;
145
+ if (!envValue) return 'claude';
146
+ if (envValue.trim().toLowerCase() === 'all') return 'all';
147
+ return normalizeSurfaces(envValue.split(',').map((value) => value.trim().toLowerCase()).filter(Boolean));
148
+ }
149
+
150
+ function includesClaude(agent) {
151
+ return agent === 'claude' || agent === 'all';
152
+ }
153
+
154
+ function includesCodex(agent) {
155
+ return agent === 'codex' || agent === 'all';
156
+ }
157
+
158
+ function codexHome() {
159
+ return process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
160
+ }
161
+
162
+ function componentOptionsFor(agent, language) {
163
+ const options = COMPONENTS[language].filter((item) => {
164
+ if (item.value === 'commands') return includesClaude(agent);
165
+ if (item.value === 'hook') return includesClaude(agent);
166
+ return true;
167
+ });
168
+ return options.map((item) => {
169
+ if (item.value !== 'skill') return item;
170
+ if (agent === 'codex') {
171
+ return {
172
+ value: 'skill',
173
+ label: language === 'ko' ? 'Codex Tink skills' : language === 'zh' ? 'Codex Tink skills' : 'Codex Tink skills',
174
+ hint: language === 'ko'
175
+ ? 'Codex가 $tink로 읽는 Tink 작업 원칙'
176
+ : language === 'zh'
177
+ ? 'Codex 通过 $tink 读取的 Tink 工作规则'
178
+ : 'Tink operating rules for Codex through $tink:*'
179
+ };
180
+ }
181
+ if (agent === 'all') {
182
+ return {
183
+ value: 'skill',
184
+ label: language === 'ko' ? 'Tink skills' : language === 'zh' ? 'Tink skills' : 'Tink skills',
185
+ hint: language === 'ko'
186
+ ? 'Claude Code와 Codex가 읽는 Tink 작업 원칙'
187
+ : language === 'zh'
188
+ ? 'Claude Code 和 Codex 读取的 Tink 工作规则'
189
+ : 'Tink operating rules for Claude Code and Codex'
190
+ };
191
+ }
192
+ return item;
193
+ });
194
+ }
188
195
 
189
196
  function colorLine(line, color) {
190
197
  if (!process.stdout.isTTY && !interactive) return line;
@@ -235,13 +242,24 @@ function displayPath(base, filePath) {
235
242
  return path.relative(base, filePath) || '.';
236
243
  }
237
244
 
238
- function isAlwaysUpdatePath(src) {
239
- const rel = path.relative(root, src).replace(/\\/g, '/');
240
- return rel.startsWith('templates/claude/commands/') ||
241
- rel.startsWith('templates/claude/skills/') ||
242
- rel.startsWith('templates/codex/skills/') ||
243
- rel.startsWith('templates/tink/maintenance/');
244
- }
245
+ function recordOperation(kind, base, filePath) {
246
+ operationLog[kind].push(displayPath(base, filePath).replace(/\\/g, '/'));
247
+ }
248
+
249
+ function shortList(items, emptyText = '- none') {
250
+ if (!items.length) return emptyText;
251
+ const shown = items.slice(0, 8).map((item) => `- ${item}`);
252
+ if (items.length > shown.length) shown.push(`- ...and ${items.length - shown.length} more`);
253
+ return shown.join('\n');
254
+ }
255
+
256
+ function isAlwaysUpdatePath(src) {
257
+ const rel = path.relative(root, src).replace(/\\/g, '/');
258
+ return rel.startsWith('templates/claude/commands/') ||
259
+ rel.startsWith('templates/claude/skills/') ||
260
+ rel.startsWith('templates/codex/skills/') ||
261
+ rel.startsWith('templates/tink/maintenance/');
262
+ }
245
263
 
246
264
  function writeFileFromTemplate(src, dest, base) {
247
265
  const exists = fs.existsSync(dest);
@@ -254,6 +272,7 @@ function writeFileFromTemplate(src, dest, base) {
254
272
  }
255
273
  if (!isAlwaysUpdatePath(src)) {
256
274
  log.message(`keep user-modified ${displayPath(base, dest)}`);
275
+ recordOperation('preserved', base, dest);
257
276
  return;
258
277
  }
259
278
  // commands/skills/maintenance: always update to new version
@@ -263,6 +282,7 @@ function writeFileFromTemplate(src, dest, base) {
263
282
  }
264
283
  }
265
284
  log.message(`${dryRun ? 'would write' : (exists ? 'update' : 'write')} ${displayPath(base, dest)}`);
285
+ recordOperation(exists ? 'updated' : 'written', base, dest);
266
286
  if (!dryRun) {
267
287
  fs.mkdirSync(path.dirname(dest), { recursive: true });
268
288
  fs.copyFileSync(src, dest);
@@ -282,7 +302,7 @@ function copyDir(src, dest, base) {
282
302
  }
283
303
  }
284
304
 
285
- function copyTinkCommands(templateRoot, target) {
305
+ function copyTinkCommands(templateRoot, target) {
286
306
  const commandSrc = path.join(templateRoot, 'claude/commands/tink');
287
307
  const commandDest = path.join(target, '.claude/commands/tink');
288
308
  const flatCommandDest = path.join(target, '.claude/commands');
@@ -294,6 +314,7 @@ function copyTinkCommands(templateRoot, target) {
294
314
  const legacy = path.join(flatCommandDest, name);
295
315
  if (fs.existsSync(legacy)) {
296
316
  log.message(`${dryRun ? 'would remove legacy' : 'remove legacy'} ${displayPath(target, legacy)}`);
317
+ recordOperation('removedLegacy', target, legacy);
297
318
  if (!dryRun) fs.rmSync(legacy, { force: true });
298
319
  }
299
320
  }
@@ -301,6 +322,7 @@ function copyTinkCommands(templateRoot, target) {
301
322
  const legacy = path.join(commandDest, name);
302
323
  if (fs.existsSync(legacy)) {
303
324
  log.message(`${dryRun ? 'would remove legacy' : 'remove legacy'} ${displayPath(target, legacy)}`);
325
+ recordOperation('removedLegacy', target, legacy);
304
326
  if (!dryRun) fs.rmSync(legacy, { force: true });
305
327
  }
306
328
  }
@@ -308,12 +330,14 @@ function copyTinkCommands(templateRoot, target) {
308
330
  const legacy = path.join(flatCommandDest, name);
309
331
  if (fs.existsSync(legacy)) {
310
332
  log.message(`${dryRun ? 'would remove legacy Tiny' : 'remove legacy Tiny'} ${displayPath(target, legacy)}`);
333
+ recordOperation('removedLegacy', target, legacy);
311
334
  if (!dryRun) fs.rmSync(legacy, { force: true });
312
335
  }
313
336
  }
314
337
  for (const legacyDir of legacyDirs) {
315
338
  if (fs.existsSync(legacyDir)) {
316
339
  log.message(`${dryRun ? 'would remove legacy Tiny' : 'remove legacy Tiny'} ${displayPath(target, legacyDir)}`);
340
+ recordOperation('removedLegacy', target, legacyDir);
317
341
  if (!dryRun) fs.rmSync(legacyDir, { recursive: true, force: true });
318
342
  }
319
343
  }
@@ -321,29 +345,31 @@ function copyTinkCommands(templateRoot, target) {
321
345
  if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
322
346
  writeFileFromTemplate(path.join(commandSrc, entry.name), path.join(commandDest, entry.name), target);
323
347
  }
324
- }
325
-
326
- function removeLegacyCodexSkill(codexTarget) {
327
- const legacyDir = path.join(codexTarget, 'skills/tink');
328
- const legacySkill = path.join(legacyDir, 'SKILL.md');
329
- if (!fs.existsSync(legacyDir)) return;
330
-
331
- let shouldRemove = false;
332
- if (fs.existsSync(legacySkill)) {
333
- const text = fs.readFileSync(legacySkill, 'utf8');
334
- shouldRemove = text.includes('name: tink') && text.includes('Tink');
335
- }
336
-
337
- if (!shouldRemove) {
338
- log.message(`keep unknown ${displayPath(codexTarget, legacyDir)}`);
339
- return;
340
- }
341
-
342
- log.message(`${dryRun ? 'would remove legacy' : 'remove legacy'} ${displayPath(codexTarget, legacyDir)}`);
343
- if (!dryRun) fs.rmSync(legacyDir, { recursive: true, force: true });
344
- }
345
-
346
- function readJsonFile(filePath, fallback) {
348
+ }
349
+
350
+ function removeLegacyCodexSkill(codexTarget) {
351
+ const legacyDir = path.join(codexTarget, 'skills/tink');
352
+ const legacySkill = path.join(legacyDir, 'SKILL.md');
353
+ if (!fs.existsSync(legacyDir)) return;
354
+
355
+ let shouldRemove = false;
356
+ if (fs.existsSync(legacySkill)) {
357
+ const text = fs.readFileSync(legacySkill, 'utf8');
358
+ shouldRemove = text.includes('name: tink') && text.includes('Tink');
359
+ }
360
+
361
+ if (!shouldRemove) {
362
+ log.message(`keep unknown ${displayPath(codexTarget, legacyDir)}`);
363
+ recordOperation('keptUnknown', codexTarget, legacyDir);
364
+ return;
365
+ }
366
+
367
+ log.message(`${dryRun ? 'would remove legacy' : 'remove legacy'} ${displayPath(codexTarget, legacyDir)}`);
368
+ recordOperation('removedLegacy', codexTarget, legacyDir);
369
+ if (!dryRun) fs.rmSync(legacyDir, { recursive: true, force: true });
370
+ }
371
+
372
+ function readJsonFile(filePath, fallback) {
347
373
  if (!fs.existsSync(filePath)) return fallback;
348
374
  try {
349
375
  return JSON.parse(fs.readFileSync(filePath, 'utf8'));
@@ -381,42 +407,42 @@ function registerClaudeHook(target, scope, base) {
381
407
  writeJsonFile(settingsPath, settings, base);
382
408
  }
383
409
 
384
- function copySelected(scope, components, agent) {
385
- const repoTarget = process.cwd();
386
- const globalTarget = os.homedir();
387
- const codexTarget = codexHome();
388
- const target = scope === 'global' ? globalTarget : repoTarget;
389
- const templateRoot = path.join(root, 'templates');
390
-
391
- if (includesClaude(agent) && components.includes('commands')) {
392
- copyTinkCommands(templateRoot, target);
393
- }
394
- if (components.includes('skill')) {
395
- if (includesClaude(agent)) {
396
- copyDir(path.join(templateRoot, 'claude/skills'), path.join(target, '.claude/skills'), target);
397
- }
398
- if (includesCodex(agent)) {
399
- removeLegacyCodexSkill(codexTarget);
400
- copyDir(path.join(templateRoot, 'codex/skills'), path.join(codexTarget, 'skills'), codexTarget);
401
- }
402
- }
403
- if (components.includes('harnesses')) {
404
- copyDir(path.join(templateRoot, 'tink/harnesses'), path.join(target, '.tink/harnesses'), target);
405
- copyDir(path.join(templateRoot, 'tink/rules'), path.join(target, '.tink/rules'), target);
406
- copyDir(path.join(templateRoot, 'tink/schemas'), path.join(target, '.tink/schemas'), target);
407
- copyDir(path.join(templateRoot, 'tink/maintenance'), path.join(target, '.tink/maintenance'), target);
408
- writeFileFromTemplate(path.join(templateRoot, 'tink/config.json'), path.join(target, '.tink/config.json'), target);
409
- }
410
+ function copySelected(scope, components, agent) {
411
+ const repoTarget = process.cwd();
412
+ const globalTarget = os.homedir();
413
+ const codexTarget = codexHome();
414
+ const target = scope === 'global' ? globalTarget : repoTarget;
415
+ const templateRoot = path.join(root, 'templates');
416
+
417
+ if (includesClaude(agent) && components.includes('commands')) {
418
+ copyTinkCommands(templateRoot, target);
419
+ }
420
+ if (components.includes('skill')) {
421
+ if (includesClaude(agent)) {
422
+ copyDir(path.join(templateRoot, 'claude/skills'), path.join(target, '.claude/skills'), target);
423
+ }
424
+ if (includesCodex(agent)) {
425
+ removeLegacyCodexSkill(codexTarget);
426
+ copyDir(path.join(templateRoot, 'codex/skills'), path.join(codexTarget, 'skills'), codexTarget);
427
+ }
428
+ }
429
+ if (components.includes('harnesses')) {
430
+ copyDir(path.join(templateRoot, 'tink/harnesses'), path.join(target, '.tink/harnesses'), target);
431
+ copyDir(path.join(templateRoot, 'tink/rules'), path.join(target, '.tink/rules'), target);
432
+ copyDir(path.join(templateRoot, 'tink/schemas'), path.join(target, '.tink/schemas'), target);
433
+ copyDir(path.join(templateRoot, 'tink/maintenance'), path.join(target, '.tink/maintenance'), target);
434
+ writeFileFromTemplate(path.join(templateRoot, 'tink/config.json'), path.join(target, '.tink/config.json'), target);
435
+ }
410
436
  if (components.includes('memory')) {
411
437
  copyDir(path.join(templateRoot, 'tink/memory'), path.join(target, '.tink/memory'), target);
412
438
  }
413
- if (includesClaude(agent) && components.includes('hook')) {
414
- copyDir(path.join(templateRoot, 'tink/hooks'), path.join(target, '.tink/hooks'), target);
415
- registerClaudeHook(target, scope, target);
416
- }
417
-
418
- return { repoTarget, globalTarget, codexTarget, installTarget: target };
419
- }
439
+ if (includesClaude(agent) && components.includes('hook')) {
440
+ copyDir(path.join(templateRoot, 'tink/hooks'), path.join(target, '.tink/hooks'), target);
441
+ registerClaudeHook(target, scope, target);
442
+ }
443
+
444
+ return { repoTarget, globalTarget, codexTarget, installTarget: target };
445
+ }
420
446
 
421
447
  function updateGitignore(target, policy) {
422
448
  if (policy === 'all') {
@@ -441,31 +467,64 @@ function updateGitignore(target, policy) {
441
467
  }
442
468
  }
443
469
 
444
- function patchConfig(target, scope, hookScope, language) {
470
+ function patchConfig(target, scope, hookScope, language) {
445
471
  const configPath = path.join(target, '.tink/config.json');
446
472
  if (!fs.existsSync(configPath) || dryRun) return;
473
+ const rel = displayPath(target, configPath).replace(/\\/g, '/');
474
+ if (isUpdate && !force && operationLog.preserved.includes(rel)) return;
447
475
  const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
448
476
  config.install_scope = scope;
449
477
  config.hook_scope = hookScope;
450
478
  config.language = language;
451
479
  fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
452
- }
453
-
454
- function nextStepFor(agent) {
455
- if (agent === 'codex') {
456
- return 'Next: open Codex and use $tink:cast <task> to start. Use $tink:setup only to review or change settings.';
457
- }
458
- if (agent === 'all') {
459
- return 'Next: in Claude Code run /tink:cast <task>, or in Codex use $tink:cast <task>.';
460
- }
461
- return 'Next: open Claude Code and run /tink:cast <task> to start. Run /tink:setup only to review or change settings.';
462
- }
463
-
464
- function doneLineFor(agent) {
465
- if (agent === 'codex') return '\nDone. Open Codex and use $tink:cast <task> to start.';
466
- if (agent === 'all') return '\nDone. Use /tink:cast <task> in Claude Code or $tink:cast <task> in Codex to start.';
467
- return '\nDone. Open Claude Code and run /tink:cast <task> to start.';
468
- }
480
+ }
481
+
482
+ function nextStepFor(agent) {
483
+ if (agent === 'codex') {
484
+ return 'Next: open Codex and use $tink:cast <task> to start. Use $tink:setup only to review or change settings.';
485
+ }
486
+ if (agent === 'all') {
487
+ return 'Next: in Claude Code run /tink:cast <task>, or in Codex use $tink:cast <task>.';
488
+ }
489
+ return 'Next: open Claude Code and run /tink:cast <task> to start. Run /tink:setup only to review or change settings.';
490
+ }
491
+
492
+ function doneLineFor(agent) {
493
+ if (agent === 'codex') return '\nDone. Open Codex and use $tink:cast <task> to start.';
494
+ if (agent === 'all') return '\nDone. Use /tink:cast <task> in Claude Code or $tink:cast <task> in Codex to start.';
495
+ return '\nDone. Open Claude Code and run /tink:cast <task> to start.';
496
+ }
497
+
498
+ function updateResultSummary(agent, targets) {
499
+ const locations = [
500
+ includesClaude(agent) ? `Claude Code commands: ${path.join(targets.installTarget, '.claude/commands/tink')}` : null,
501
+ includesClaude(agent) ? `Claude Code skill: ${path.join(targets.installTarget, '.claude/skills/tink')}` : null,
502
+ includesCodex(agent) ? `Codex skills: ${path.join(targets.codexTarget, 'skills')}` : null,
503
+ `Tink shared files: ${path.join(targets.installTarget, '.tink')}`
504
+ ].filter(Boolean);
505
+
506
+ return [
507
+ 'Update Result Summary',
508
+ `Surfaces: ${agent}`,
509
+ `Install target: ${targets.installTarget}`,
510
+ includesCodex(agent) ? `Codex target: ${targets.codexTarget}` : null,
511
+ '',
512
+ 'Updated or added:',
513
+ shortList([...operationLog.updated, ...operationLog.written]),
514
+ '',
515
+ 'Preserved user-modified files:',
516
+ shortList(operationLog.preserved),
517
+ '',
518
+ 'Removed legacy paths:',
519
+ shortList(operationLog.removedLegacy),
520
+ operationLog.keptUnknown.length ? ['', 'Kept unknown legacy-looking paths:', shortList(operationLog.keptUnknown)] : null,
521
+ '',
522
+ 'Installed locations:',
523
+ locations.map((item) => `- ${item}`).join('\n'),
524
+ '',
525
+ `Next: ${nextStepFor(agent).replace(/^Next: /, '')}`
526
+ ].flat().filter((line) => line !== null).join('\n');
527
+ }
469
528
 
470
529
  function detectLanguage() {
471
530
  const envLang = (process.env.LANG || process.env.LANGUAGE || process.env.LC_ALL || '').toLowerCase();
@@ -474,27 +533,27 @@ function detectLanguage() {
474
533
  return 'en';
475
534
  }
476
535
 
477
- async function resolveChoices() {
478
- let agent = resolveDefaultSurfaces();
479
- let scope = args.includes('--global') ? 'global' : (argValue('--scope') || undefined);
480
- let language = argValue('--lang') || argValue('--language') || detectLanguage();
481
- if (scope && !['repo', 'global'].includes(scope)) {
482
- console.error(`Invalid scope: ${scope}`);
483
- usage();
536
+ async function resolveChoices() {
537
+ let agent = resolveDefaultSurfaces();
538
+ let scope = args.includes('--global') ? 'global' : (argValue('--scope') || undefined);
539
+ let language = argValue('--lang') || argValue('--language') || detectLanguage();
540
+ if (scope && !['repo', 'global'].includes(scope)) {
541
+ console.error(`Invalid scope: ${scope}`);
542
+ usage();
484
543
  process.exit(1);
485
- }
486
- if (!['en', 'ko', 'zh'].includes(language)) language = 'en';
487
-
488
- let components = componentOptionsFor(agent, language).map((item) => item.value).filter((value) => value !== 'hook');
489
- if (includesClaude(agent) && args.includes('--with-hook')) components.push('hook');
490
- let gitPolicy = 'harnesses';
491
- let hookScope = 'off';
492
-
493
- if (!interactive) {
494
- scope = scope || 'repo';
495
- if (components.includes('hook')) hookScope = scope;
496
- return { agent, scope, components, gitPolicy, hookScope, language };
497
- }
544
+ }
545
+ if (!['en', 'ko', 'zh'].includes(language)) language = 'en';
546
+
547
+ let components = componentOptionsFor(agent, language).map((item) => item.value).filter((value) => value !== 'hook');
548
+ if (includesClaude(agent) && args.includes('--with-hook')) components.push('hook');
549
+ let gitPolicy = 'harnesses';
550
+ let hookScope = 'off';
551
+
552
+ if (!interactive) {
553
+ scope = scope || 'repo';
554
+ if (components.includes('hook')) hookScope = scope;
555
+ return { agent, scope, components, gitPolicy, hookScope, language };
556
+ }
498
557
 
499
558
  language = handleCancel(await select({
500
559
  message: COPY[language].language,
@@ -525,23 +584,23 @@ async function resolveChoices() {
525
584
  language === 'ko' ? '항목 설명' : language === 'zh' ? '项目说明' : 'Component notes'
526
585
  );
527
586
 
528
- agent = normalizeSurfaces(handleCancel(await multiselect({
529
- message: language === 'ko'
530
- ? '설치할 agent surface를 선택하세요 (space로 토글)'
531
- : 'Select agent surfaces to install (space to toggle)',
532
- options: SURFACE_OPTIONS[language],
533
- initialValues: agent === 'all' ? ['claude', 'codex'] : [agent],
534
- required: true
535
- })));
536
- components = componentOptionsFor(agent, language).map((item) => item.value).filter((value) => value !== 'hook');
537
- if (includesClaude(agent) && args.includes('--with-hook')) components.push('hook');
538
-
539
- components = handleCancel(await multiselect({
540
- message: copy.components,
541
- options: componentOptionsFor(agent, language),
542
- initialValues: components,
543
- required: true
544
- }));
587
+ agent = normalizeSurfaces(handleCancel(await multiselect({
588
+ message: language === 'ko'
589
+ ? '설치할 agent surface를 선택하세요 (space로 토글)'
590
+ : 'Select agent surfaces to install (space to toggle)',
591
+ options: SURFACE_OPTIONS[language],
592
+ initialValues: agent === 'all' ? ['claude', 'codex'] : [agent],
593
+ required: true
594
+ })));
595
+ components = componentOptionsFor(agent, language).map((item) => item.value).filter((value) => value !== 'hook');
596
+ if (includesClaude(agent) && args.includes('--with-hook')) components.push('hook');
597
+
598
+ components = handleCancel(await multiselect({
599
+ message: copy.components,
600
+ options: componentOptionsFor(agent, language),
601
+ initialValues: components,
602
+ required: true
603
+ }));
545
604
 
546
605
  scope = handleCancel(await select({
547
606
  message: copy.scope,
@@ -593,8 +652,8 @@ async function resolveChoices() {
593
652
  }));
594
653
  }
595
654
 
596
- if (includesClaude(agent) && components.includes('hook')) {
597
- hookScope = scope;
655
+ if (includesClaude(agent) && components.includes('hook')) {
656
+ hookScope = scope;
598
657
  note(
599
658
  language === 'ko'
600
659
  ? `Hook 추천을 ${scope} 범위의 Claude Code UserPromptSubmit에 등록합니다. 추가 범위 질문은 하지 않습니다. 작업 실행/저장 없이 일반 프롬프트에서만 Tink 사용을 추천합니다.`
@@ -605,8 +664,8 @@ async function resolveChoices() {
605
664
  );
606
665
  }
607
666
 
608
- return { agent, scope, components, gitPolicy, hookScope, language };
609
- }
667
+ return { agent, scope, components, gitPolicy, hookScope, language };
668
+ }
610
669
 
611
670
  async function main() {
612
671
  if (command === 'help' || args.includes('--help')) {
@@ -620,18 +679,18 @@ async function main() {
620
679
  process.exit(1);
621
680
  }
622
681
 
623
- const { agent, scope, components, gitPolicy, hookScope, language } = await resolveChoices();
624
-
625
- if (!interactive) {
626
- console.log(`Installing Tink for ${agent === 'claude' ? 'Claude Code' : agent === 'codex' ? 'Codex' : 'Claude Code and Codex'}`);
627
- console.log(`Source: ${source}`);
628
- console.log(`surfaces ${agent}`);
629
- console.log(`language ${language}`);
630
- console.log(`scope ${scope}`);
631
- console.log(`components ${components.join(', ')}`);
632
- }
633
-
634
- const targets = copySelected(scope, components, agent);
682
+ const { agent, scope, components, gitPolicy, hookScope, language } = await resolveChoices();
683
+
684
+ if (!interactive) {
685
+ console.log(`${isUpdate ? 'Updating' : 'Installing'} Tink for ${agent === 'claude' ? 'Claude Code' : agent === 'codex' ? 'Codex' : 'Claude Code and Codex'}`);
686
+ console.log(`Source: ${source}`);
687
+ console.log(`surfaces ${agent}`);
688
+ console.log(`language ${language}`);
689
+ console.log(`scope ${scope}`);
690
+ console.log(`components ${components.join(', ')}`);
691
+ }
692
+
693
+ const targets = copySelected(scope, components, agent);
635
694
 
636
695
  if (scope === 'repo' && components.some((item) => ['harnesses', 'memory', 'hook'].includes(item))) {
637
696
  updateGitignore(targets.repoTarget, gitPolicy);
@@ -640,25 +699,27 @@ async function main() {
640
699
  }
641
700
  patchConfig(targets.installTarget, scope, hookScope, language);
642
701
 
643
- const summary = [
644
- `Language: ${language}`,
645
- `Surfaces: ${agent}`,
646
- `Scope: ${scope}`,
647
- `Install target: ${targets.installTarget}`,
648
- includesCodex(agent) ? `Codex target: ${targets.codexTarget}` : null,
649
- `Components: ${components.join(', ')}`,
650
- `Hook scope: ${hookScope}`,
651
- nextStepFor(agent)
652
- ].filter(Boolean).join('\n');
702
+ const summary = [
703
+ `Language: ${language}`,
704
+ `Surfaces: ${agent}`,
705
+ `Scope: ${scope}`,
706
+ `Install target: ${targets.installTarget}`,
707
+ includesCodex(agent) ? `Codex target: ${targets.codexTarget}` : null,
708
+ `Components: ${components.join(', ')}`,
709
+ `Hook scope: ${hookScope}`,
710
+ nextStepFor(agent)
711
+ ].filter(Boolean).join('\n');
653
712
 
654
713
  if (interactive) {
655
714
  note(summary, COPY[language].installed);
715
+ if (isUpdate) note(updateResultSummary(agent, targets), 'Update Result Summary');
656
716
  outro(COPY[language].done);
657
- } else {
658
- console.log(`\n${summary}`);
659
- console.log(doneLineFor(agent));
660
- }
661
- }
717
+ } else {
718
+ console.log(`\n${summary}`);
719
+ if (isUpdate) console.log(`\n${updateResultSummary(agent, targets)}`);
720
+ console.log(doneLineFor(agent));
721
+ }
722
+ }
662
723
 
663
724
  main().catch((error) => {
664
725
  console.error(error instanceof Error ? error.message : String(error));