minimal-vibe-coding-kit 0.5.0 → 0.5.1

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 (91) hide show
  1. package/.agents/skills/tutien/SKILL.md +36 -13
  2. package/.agents/skills/tutien/references/adaptive-response.md +68 -0
  3. package/.agents/skills/tutien/references/lore-sources.md +28 -0
  4. package/.agents/skills/tutien/references/privacy.md +13 -1
  5. package/.agents/skills/tutien/references/schema.md +32 -1
  6. package/.agents/skills/tutien/references/story-system.md +176 -0
  7. package/.agents/skills/tutien/references/vi-style-guide.md +89 -0
  8. package/.agents/skills/tutien/references/voice-and-mode.md +23 -8
  9. package/.agents/skills/tutien/scripts/catalog.mjs +17 -17
  10. package/.agents/skills/tutien/scripts/classify.mjs +64 -37
  11. package/.agents/skills/tutien/scripts/command.mjs +40 -21
  12. package/.agents/skills/tutien/scripts/project-profile.mjs +202 -0
  13. package/.agents/skills/tutien/scripts/render-report.mjs +128 -72
  14. package/.agents/skills/tutien/scripts/response-brief.mjs +103 -0
  15. package/.agents/skills/tutien/scripts/run-tutien.mjs +141 -63
  16. package/.agents/skills/tutien/scripts/story-ledger.mjs +241 -0
  17. package/.agents/skills/tutien/scripts/villains.mjs +14 -14
  18. package/.claude/skills/tutien/SKILL.md +36 -13
  19. package/.claude/skills/tutien/references/adaptive-response.md +68 -0
  20. package/.claude/skills/tutien/references/lore-sources.md +28 -0
  21. package/.claude/skills/tutien/references/privacy.md +13 -1
  22. package/.claude/skills/tutien/references/schema.md +32 -1
  23. package/.claude/skills/tutien/references/story-system.md +176 -0
  24. package/.claude/skills/tutien/references/vi-style-guide.md +89 -0
  25. package/.claude/skills/tutien/references/voice-and-mode.md +23 -8
  26. package/.claude/skills/tutien/scripts/catalog.mjs +17 -17
  27. package/.claude/skills/tutien/scripts/classify.mjs +64 -37
  28. package/.claude/skills/tutien/scripts/command.mjs +40 -21
  29. package/.claude/skills/tutien/scripts/project-profile.mjs +202 -0
  30. package/.claude/skills/tutien/scripts/render-report.mjs +128 -72
  31. package/.claude/skills/tutien/scripts/response-brief.mjs +103 -0
  32. package/.claude/skills/tutien/scripts/run-tutien.mjs +141 -63
  33. package/.claude/skills/tutien/scripts/story-ledger.mjs +241 -0
  34. package/.claude/skills/tutien/scripts/villains.mjs +14 -14
  35. package/.codex-plugin/plugin.json +1 -1
  36. package/.cursor/skills/tutien/SKILL.md +36 -13
  37. package/.cursor/skills/tutien/references/adaptive-response.md +68 -0
  38. package/.cursor/skills/tutien/references/lore-sources.md +28 -0
  39. package/.cursor/skills/tutien/references/privacy.md +13 -1
  40. package/.cursor/skills/tutien/references/schema.md +32 -1
  41. package/.cursor/skills/tutien/references/story-system.md +176 -0
  42. package/.cursor/skills/tutien/references/vi-style-guide.md +89 -0
  43. package/.cursor/skills/tutien/references/voice-and-mode.md +23 -8
  44. package/.cursor/skills/tutien/scripts/catalog.mjs +17 -17
  45. package/.cursor/skills/tutien/scripts/classify.mjs +64 -37
  46. package/.cursor/skills/tutien/scripts/command.mjs +40 -21
  47. package/.cursor/skills/tutien/scripts/project-profile.mjs +202 -0
  48. package/.cursor/skills/tutien/scripts/render-report.mjs +128 -72
  49. package/.cursor/skills/tutien/scripts/response-brief.mjs +103 -0
  50. package/.cursor/skills/tutien/scripts/run-tutien.mjs +141 -63
  51. package/.cursor/skills/tutien/scripts/story-ledger.mjs +241 -0
  52. package/.cursor/skills/tutien/scripts/villains.mjs +14 -14
  53. package/.grok/skills/tutien/SKILL.md +36 -13
  54. package/.grok/skills/tutien/references/adaptive-response.md +68 -0
  55. package/.grok/skills/tutien/references/lore-sources.md +28 -0
  56. package/.grok/skills/tutien/references/privacy.md +13 -1
  57. package/.grok/skills/tutien/references/schema.md +32 -1
  58. package/.grok/skills/tutien/references/story-system.md +176 -0
  59. package/.grok/skills/tutien/references/vi-style-guide.md +89 -0
  60. package/.grok/skills/tutien/references/voice-and-mode.md +23 -8
  61. package/.grok/skills/tutien/scripts/catalog.mjs +17 -17
  62. package/.grok/skills/tutien/scripts/classify.mjs +64 -37
  63. package/.grok/skills/tutien/scripts/command.mjs +40 -21
  64. package/.grok/skills/tutien/scripts/project-profile.mjs +202 -0
  65. package/.grok/skills/tutien/scripts/render-report.mjs +128 -72
  66. package/.grok/skills/tutien/scripts/response-brief.mjs +103 -0
  67. package/.grok/skills/tutien/scripts/run-tutien.mjs +141 -63
  68. package/.grok/skills/tutien/scripts/story-ledger.mjs +241 -0
  69. package/.grok/skills/tutien/scripts/villains.mjs +14 -14
  70. package/.vibekit/skills/tutien/SKILL.md +36 -13
  71. package/.vibekit/skills/tutien/references/adaptive-response.md +68 -0
  72. package/.vibekit/skills/tutien/references/lore-sources.md +28 -0
  73. package/.vibekit/skills/tutien/references/privacy.md +13 -1
  74. package/.vibekit/skills/tutien/references/schema.md +32 -1
  75. package/.vibekit/skills/tutien/references/story-system.md +176 -0
  76. package/.vibekit/skills/tutien/references/vi-style-guide.md +89 -0
  77. package/.vibekit/skills/tutien/references/voice-and-mode.md +23 -8
  78. package/.vibekit/skills/tutien/scripts/catalog.mjs +17 -17
  79. package/.vibekit/skills/tutien/scripts/classify.mjs +64 -37
  80. package/.vibekit/skills/tutien/scripts/command.mjs +40 -21
  81. package/.vibekit/skills/tutien/scripts/project-profile.mjs +202 -0
  82. package/.vibekit/skills/tutien/scripts/render-report.mjs +128 -72
  83. package/.vibekit/skills/tutien/scripts/response-brief.mjs +103 -0
  84. package/.vibekit/skills/tutien/scripts/run-tutien.mjs +141 -63
  85. package/.vibekit/skills/tutien/scripts/story-ledger.mjs +241 -0
  86. package/.vibekit/skills/tutien/scripts/villains.mjs +14 -14
  87. package/CHANGELOG.md +22 -1
  88. package/README.md +5 -3
  89. package/docs/README.vi.md +5 -3
  90. package/docs/README.zh-CN.md +5 -3
  91. package/package.json +2 -2
@@ -0,0 +1,103 @@
1
+ // Compact, aggregate-only handoff between the deterministic analyzer and the
2
+ // agent-authored response. It carries facts and constraints, never stock prose.
3
+
4
+ const localizeMissing = (reason, language) => {
5
+ if (language !== 'vi') return reason;
6
+ if (reason === 'token usage coverage below 60%; supply provider usage metadata to compute a realm') {
7
+ return 'Độ phủ số liệu token dưới 60%; cần bổ sung dữ liệu sử dụng từ nhà cung cấp để tính cảnh giới.';
8
+ }
9
+ return reason;
10
+ };
11
+
12
+ function localized(value, language) {
13
+ if (!value || typeof value !== 'object') return value ?? null;
14
+ return value[language] ?? value.en ?? value.vi ?? null;
15
+ }
16
+
17
+ function problemEvidence(problem) {
18
+ const evidence = problem.evidence ?? {};
19
+ if (problem.problemType === 'repeated-failure' || problem.problemType === 'too-many-prompts') return { repeatCount: evidence.repeatCount ?? 0 };
20
+ if (problem.problemType === 'conflicting-instructions') return { count: evidence.count ?? 0 };
21
+ if (problem.problemType === 'unrecovered-failure') return { count: evidence.count ?? 0 };
22
+ if (problem.problemType === 'work-without-proof') return { commits: evidence.commits ?? 0 };
23
+ return {};
24
+ }
25
+
26
+ export function buildResponseBrief(model, options = {}) {
27
+ const language = options.language === 'vi' ? 'vi' : 'en';
28
+ const profile = options.profile ?? {};
29
+ const classification = model.cultivation?.classification ?? null;
30
+ const progression = model.cultivation?.progression ?? null;
31
+ return {
32
+ schema: 'tutien-response-brief-v1',
33
+ semanticNamespace: model.experience.semanticNamespace,
34
+ language,
35
+ tone: model.tone,
36
+ policy: {
37
+ state: model.policyState,
38
+ suppressGamification: model.suppressGamification
39
+ },
40
+ project: {
41
+ id: profile.projectId ?? 'repo',
42
+ type: profile.projectType ?? null,
43
+ primaryLanguage: profile.primaryLanguage ?? null,
44
+ domains: profile.domains ?? [],
45
+ stack: profile.stack ?? [],
46
+ packageManager: profile.packageManager ?? null,
47
+ validationCommands: profile.validationCommands ?? [],
48
+ recommendedValidation: profile.recommendedValidation ?? null,
49
+ metadataSources: profile.metadataSources ?? [],
50
+ kitInstalled: profile.kitInstalled === true
51
+ },
52
+ evidence: {
53
+ coverage: model.coverage,
54
+ tokens: model.tokens,
55
+ realm: model.realm ? { name: model.realm.name, score: model.score } : null,
56
+ enoughEvidence: model.enoughEvidence,
57
+ missing: (model.missing ?? []).map((reason) => localizeMissing(reason, language)),
58
+ dimensions: model.dimensions
59
+ },
60
+ cultivation: classification ? {
61
+ faction: classification.faction?.id ?? null,
62
+ affiliation: classification.affiliation?.id ?? null,
63
+ paths: (classification.paths ?? []).map((item) => item.id),
64
+ progression: progression ? {
65
+ tuVi: progression.tuVi?.window ?? 0,
66
+ congDuc: progression.congDuc?.window ?? 0,
67
+ nghiepLuc: progression.nghiepLuc?.window ?? 0,
68
+ daoHanhWindows: progression.daoHanh?.windows ?? 0
69
+ } : null
70
+ } : null,
71
+ findings: (model.problems ?? []).map((problem) => ({
72
+ type: problem.problemType,
73
+ confidence: problem.confidence,
74
+ priority: problem.priority,
75
+ evidence: problemEvidence(problem),
76
+ counterTechnique: localized(problem.counterTechnique, language),
77
+ projectHelp: problem.projectHelp ?? null,
78
+ microQuest: localized(problem.microQuest, language),
79
+ victory: localized(problem.victory, language)
80
+ })),
81
+ nextPractice: model.ifThen ? localized(model.ifThen, language) : null,
82
+ story: options.storyContext ? {
83
+ evidenceKey: options.storyContext.evidenceKey,
84
+ language: options.storyContext.language,
85
+ style: options.storyContext.style,
86
+ focus: options.storyContext.focus,
87
+ canWriteChapter: options.storyContext.canWriteChapter
88
+ } : null,
89
+ artifacts: { evidenceLedger: options.ledgerPath ?? null },
90
+ composition: {
91
+ finalResponse: 'agent-authored',
92
+ evidenceLedgerIsNotFinalResponse: true,
93
+ deriveFrom: ['current-user-request', 'project-facts', 'approved-evidence', 'living-chronicle', 'recent-response-rhythm'],
94
+ preserve: model.composition.preserve,
95
+ vary: model.composition.vary,
96
+ prohibitions: ['stock-heading-sequence', 'stock-opening', 'stock-closing', 'raw-prompt-text', 'raw-event-ids'],
97
+ vietnameseEndingIntent: language === 'vi'
98
+ ? 'Kết cà khịa, luôn giáo huấn, không tâng bốc và kéo dài dư âm vai diễn.'
99
+ : null
100
+ }
101
+ };
102
+ }
103
+
@@ -5,12 +5,21 @@ import process from 'node:process';
5
5
  import crypto from 'node:crypto';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import { spawnSync } from 'node:child_process';
8
- import { parseInvocation, TUTIEN_EXPERIENCE } from './command.mjs';
8
+ import {
9
+ normalizeStoryFocus,
10
+ normalizeStoryLanguage,
11
+ normalizeStoryStyle,
12
+ parseInvocation,
13
+ TUTIEN_EXPERIENCE
14
+ } from './command.mjs';
9
15
  import { analyze } from './analyze-history.mjs';
10
16
  import { renderReport, resolveLanguage, renderClassificationMarkdown } from './render-report.mjs';
11
17
  import { buildSnapshot, snapshotsToPrune } from './snapshot.mjs';
12
18
  import { compareSnapshots } from './compare.mjs';
13
19
  import { classifyProject } from './classify.mjs';
20
+ import { buildStoryContext, storyPaths, validateStoryTree, writeStoryContext } from './story-ledger.mjs';
21
+ import { buildProjectProfile, inventoryProjectMetadata } from './project-profile.mjs';
22
+ import { buildResponseBrief } from './response-brief.mjs';
14
23
 
15
24
  // End-to-end executor for the advertised /tutien actions. All state lives
16
25
  // under the git-ignored .vibekit/reports/tutien/ of the current repo; the
@@ -41,6 +50,20 @@ function saveState(state) {
41
50
  fs.writeFileSync(stateFile, `${JSON.stringify(state, null, 2)}\n`);
42
51
  }
43
52
 
53
+ function resolveStoryPreferences(state, options, providedOptions = []) {
54
+ const provided = new Set(providedOptions);
55
+ const prior = state.storyPreferences ?? {};
56
+ const choose = (key, optionKey, fallback) => provided.has(key) ? options[optionKey] : (prior[optionKey] ?? options[optionKey] ?? fallback);
57
+ const preferences = {
58
+ story: choose('story', 'story', 'on') === 'off' ? 'off' : 'on',
59
+ storyLanguage: normalizeStoryLanguage(choose('story-language', 'storyLanguage', 'auto')),
60
+ storyStyle: normalizeStoryStyle(choose('story-style', 'storyStyle', 'auto')),
61
+ storyFocus: normalizeStoryFocus(choose('story-focus', 'storyFocus', 'balanced'))
62
+ };
63
+ state.storyPreferences = preferences;
64
+ return preferences;
65
+ }
66
+
44
67
  function classifySources(sources) {
45
68
  const spec = { jsonlFiles: [], transcriptFiles: [], gitRoot: null };
46
69
  for (const s of sources) {
@@ -51,61 +74,60 @@ function classifySources(sources) {
51
74
  return spec;
52
75
  }
53
76
 
54
- const scopeToken = (sources, range) => sha16(JSON.stringify({ sources: [...sources].sort(), range }));
77
+ const scopeToken = (sources, range, metadata = inventoryProjectMetadata(root)) => sha16(JSON.stringify({
78
+ sources: [...sources].sort(),
79
+ range,
80
+ metadata: metadata.map(({ path: file, size, mtimeMs }) => ({ path: file, size, mtimeMs }))
81
+ }));
55
82
 
56
- // Project profile for classification: repo metadata only (backbone.yml
57
- // fields plus a distinct-commit-author COUNT no names or emails are kept)
58
- // merged with explicit declarations from the invocation.
83
+ // Project profile for classification: a bounded set of known manifests plus a
84
+ // distinct-commit-author COUNT. Script bodies, names, emails, and file content
85
+ // are never returned or persisted.
59
86
  function repoProfile(options) {
60
- let description = null;
61
- let primaryLanguage = null;
62
- let projectType = null;
63
- try {
64
- const text = fs.readFileSync(path.join(root, 'backbone.yml'), 'utf8');
65
- const grab = (re) => {
66
- const m = text.match(re);
67
- return m ? m[1].trim().replace(/^["']|["']$/g, '') : null;
68
- };
69
- description = grab(/^\s*description:\s*(.+)$/m);
70
- primaryLanguage = grab(/^\s*primary_language:\s*(.+)$/m);
71
- projectType = grab(/^\s*type:\s*(.+)$/m);
72
- } catch {}
87
+ const profile = buildProjectProfile(root, options);
73
88
  // Author identifiers are read transiently (mailmap-collapsed) and reduced to
74
89
  // a distinct COUNT immediately; no email or name is retained or returned.
75
90
  let authorsCount = null;
76
91
  const res = spawnSync('git', ['log', '--use-mailmap', '--format=%aE'], { cwd: root, encoding: 'utf8' });
77
92
  if (res.status === 0) authorsCount = new Set(res.stdout.split('\n').filter(Boolean)).size;
78
- return {
79
- description,
80
- primaryLanguage,
81
- projectType,
82
- domains: options.domains ?? [],
83
- authorization: options.authorization ?? null,
84
- authorsCount,
85
- declared: { faction: options.faction, affiliation: options.affiliation, paths: options.paths }
86
- };
93
+ return { ...profile, authorsCount };
87
94
  }
88
95
 
89
96
  // Preview inspects no content: file existence and size only, plus the git
90
97
  // toplevel path. Nothing is parsed and nothing is written except the pending
91
98
  // approval in the state file.
92
- function previewLines(options) {
93
- const lines = ['tutien preview — the observatory is open, but no scroll has been read; nothing is analyzed until this exact scope is approved.'];
94
- if (options.sources.length === 0) lines.push('- (no sources; pass sources=git,/path/to/export.jsonl)');
99
+ function previewLines(options, language = 'en') {
100
+ const vi = language === 'vi';
101
+ const metadata = inventoryProjectMetadata(root);
102
+ const lines = [vi
103
+ ? 'Xem trước phạm vi tu tiên: đài quan sát đã mở nhưng chưa đọc cuộn thư nào. Chỉ phân tích sau khi phạm vi chính xác này được phê duyệt.'
104
+ : 'tutien preview — the observatory is open, but no scroll has been read; nothing is analyzed until this exact scope is approved.'];
105
+ if (options.sources.length === 0) lines.push(vi ? '- Chưa có nguồn. Hãy truyền `sources=git,/duong-dan/export.jsonl`.' : '- (no sources; pass sources=git,/path/to/export.jsonl)');
95
106
  for (const s of options.sources) {
96
107
  if (s === 'git') {
97
- lines.push(`- git: commit metadata of ${root} (read-only git log, current repository only)`);
108
+ lines.push(vi ? `- Git: chỉ đọc siêu dữ liệu bản ghi của kho hiện tại tại ${root}.` : `- git: commit metadata of ${root} (read-only git log, current repository only)`);
98
109
  } else {
99
110
  let size = null;
100
111
  try {
101
112
  size = fs.statSync(s).size;
102
113
  } catch {}
103
- lines.push(size == null ? `- file: ${s} (NOT FOUND)` : `- file: ${s} (${size} bytes)`);
114
+ lines.push(size == null
115
+ ? (vi ? `- Tệp: ${s} (không tìm thấy)` : `- file: ${s} (NOT FOUND)`)
116
+ : (vi ? `- Tệp: ${s} (${size} byte)` : `- file: ${s} (${size} bytes)`));
104
117
  }
105
118
  }
106
- lines.push(`- range: ${options.range}`);
107
- const token = scopeToken(options.sources, options.range);
108
- lines.push(`To approve exactly this scope: analyze approve=${token} sources=${options.sources.join(',')}`);
119
+ if (metadata.length) {
120
+ lines.push(vi
121
+ ? `- Metadata dự án: ${metadata.map((item) => `${item.path} (${item.size} byte)`).join(', ')}; chỉ đọc nội dung sau khi phê duyệt.`
122
+ : `- project metadata: ${metadata.map((item) => `${item.path} (${item.size} bytes)`).join(', ')}; content is read only after approval.`);
123
+ } else {
124
+ lines.push(vi ? '- Metadata dự án: không tìm thấy manifest đã biết.' : '- project metadata: no known manifest found.');
125
+ }
126
+ lines.push(vi ? `- Phạm vi thời gian: ${options.range}` : `- range: ${options.range}`);
127
+ const token = scopeToken(options.sources, options.range, metadata);
128
+ lines.push(vi
129
+ ? `Để phê duyệt đúng phạm vi này: \`analyze approve=${token} sources=${options.sources.join(',')}\``
130
+ : `To approve exactly this scope: analyze approve=${token} sources=${options.sources.join(',')}`);
109
131
  return { lines, token };
110
132
  }
111
133
 
@@ -118,33 +140,50 @@ const EXPLAIN = {
118
140
  realm: 'Weighted dimension score (delivery 30%, validation 25%, clarity 15%, recovery 15%, safety 10%, automation 5%) mapped to the realm ladder; requires >= 60% known-token coverage, otherwise "Chưa đủ thiên cơ".'
119
141
  };
120
142
 
143
+ const EXPLAIN_VI = {
144
+ reportedTotal: 'Tổng số token hợp lệ do nhà cung cấp báo cáo. Bản ghi luồng tích lũy có cùng `requestId` chỉ được tính một lần theo giá trị cuối; các đoạn độc lập được cộng riêng. Bản ghi không có tổng dương hợp lệ sẽ không được tính.',
145
+ estimatedTotal: 'Tổng số token hợp lệ được ước lượng cục bộ; không bao giờ gộp vào `reportedTotal`.',
146
+ unknownTurns: 'Số lượt trả lời của trợ lý không có dữ liệu sử dụng hoặc không có tổng hợp lệ. Tuyên bố độ chính xác mà thiếu số liệu vẫn được tính là chưa rõ.',
147
+ retryLoops: 'Chuỗi từ ba lời yêu cầu giống hoặc gần giống nhau trong cùng một nhiệm vụ, xen giữa bởi phản hồi của trợ lý hay công cụ; đây là các dấu hiệu có chấm mức tin cậy.',
148
+ conflicts: 'Một chỉ thị dạng “không bao giờ” hoặc “không làm X”, sau đó trong cùng nhiệm vụ xuất hiện lời yêu cầu làm X; cặp mã sự kiện có độ tin cậy 0,6.',
149
+ realm: 'Điểm có trọng số từ sáu chiều được ánh xạ vào thang cảnh giới. Cần ít nhất 60% độ phủ token đã biết; nếu thiếu sẽ trả về “Chưa đủ thiên cơ”.'
150
+ };
151
+
121
152
  export function run(argsString = '') {
122
- const { action, explicitAction, options } = parseInvocation(argsString);
153
+ const { action, explicitAction, options, providedOptions } = parseInvocation(argsString);
123
154
  const state = loadState();
155
+ const storyPreferences = resolveStoryPreferences(state, options, providedOptions);
156
+ const uiLanguage = resolveLanguage(options.language, { invocationText: argsString });
157
+ const say = (vi, en) => uiLanguage === 'vi' ? vi : en;
124
158
  const refuse = (msg) => ({ code: 2, out: [msg] });
125
159
 
126
160
  if (action === 'off') {
127
161
  state.mode = 'off';
128
162
  delete state.pendingApproval;
129
163
  saveState(state);
130
- return { code: 0, out: ['Tutien mode is off. Normal kit writing style restored.'] };
164
+ return { code: 0, out: [say('Chế độ tu tiên đã tắt. Văn phong bình thường của bộ công cụ đã được khôi phục.', 'Tutien mode is off. Normal kit writing style restored.')] };
131
165
  }
132
166
  if (action === 'on') {
133
167
  state.mode = 'on';
134
168
  saveState(state);
135
- return { code: 0, out: ['Tutien mode is on — the mountain gate is open. Run preview to see the exact scope before any analysis.'] };
169
+ return { code: 0, out: [say('Chế độ tu tiên đã bật. Sơn môn đã mở; hãy chạy `preview` để xem phạm vi chính xác trước khi phân tích.', 'Tutien mode is on — the mountain gate is open. Run preview to see the exact scope before any analysis.')] };
136
170
  }
137
171
  if (action === 'status') {
138
172
  const snaps = fs.existsSync(snapDir) ? fs.readdirSync(snapDir).length : 0;
173
+ const storyStatus = validateStoryTree(root);
139
174
  return {
140
175
  code: 0,
141
176
  out: [
142
- `mode: ${state.mode ?? 'off'}`,
143
- `experience: ${TUTIEN_EXPERIENCE.kind}`,
144
- `voice: ${TUTIEN_EXPERIENCE.narrativeStyle}`,
145
- `semantic namespace: ${TUTIEN_EXPERIENCE.semanticNamespace}`,
146
- `pending approval: ${state.pendingApproval ?? 'none'}`,
147
- `snapshots: ${snaps}`
177
+ say(`chế độ: ${(state.mode ?? 'off') === 'on' ? 'bật' : 'tắt'}`, `mode: ${state.mode ?? 'off'}`),
178
+ say(`trải nghiệm: \`${TUTIEN_EXPERIENCE.kind}\``, `experience: ${TUTIEN_EXPERIENCE.kind}`),
179
+ say(`giọng văn: \`${TUTIEN_EXPERIENCE.narrativeStyle}\``, `voice: ${TUTIEN_EXPERIENCE.narrativeStyle}`),
180
+ say(`không gian ngữ nghĩa: \`${TUTIEN_EXPERIENCE.semanticNamespace}\``, `semantic namespace: ${TUTIEN_EXPERIENCE.semanticNamespace}`),
181
+ say(`phê duyệt đang chờ: ${state.pendingApproval ?? 'không có'}`, `pending approval: ${state.pendingApproval ?? 'none'}`),
182
+ say(`ảnh chụp tổng hợp: ${snaps}`, `snapshots: ${snaps}`),
183
+ say(`trường thiên: ${storyPreferences.story === 'on' ? 'bật' : 'tắt'} (${storyPreferences.storyLanguage}, ${storyPreferences.storyStyle}, ${storyPreferences.storyFocus})`, `story: ${storyPreferences.story} (${storyPreferences.storyLanguage}, ${storyPreferences.storyStyle}, ${storyPreferences.storyFocus})`),
184
+ say(`số chương: ${storyStatus.chapters}`, `story chapters: ${storyStatus.chapters}`),
185
+ say(`tổng cương: ${path.relative(root, storyPaths(root).plot)}`, `story plot: ${path.relative(root, storyPaths(root).plot)}`),
186
+ say(`sổ chương: ${storyStatus.ok ? 'hợp lệ' : `cần xem lại (${storyStatus.errors.length} lỗi)`}`, `story ledger: ${storyStatus.ok ? 'valid' : `needs attention (${storyStatus.errors.length})`}`)
148
187
  ]
149
188
  };
150
189
  }
@@ -152,21 +191,24 @@ export function run(argsString = '') {
152
191
  // Report actions. A bare `/tutien` re-activates the mode; an explicit
153
192
  // report action while off is refused (off-mode suppression).
154
193
  if ((state.mode ?? 'off') !== 'on') {
155
- if (explicitAction) return refuse("tutien mode is off; run 'on' (or bare /tutien) first.");
194
+ if (explicitAction) return refuse(say('Chế độ tu tiên đang tắt. Hãy chạy `on` hoặc gọi `/tutien` trước.', "tutien mode is off; run 'on' (or bare /tutien) first."));
156
195
  state.mode = 'on';
157
196
  }
158
197
 
159
198
  if (action === 'preview') {
160
- const { lines, token } = previewLines(options);
199
+ const { lines, token } = previewLines(options, uiLanguage);
200
+ lines.push(say(
201
+ `- Trường thiên: ${storyPreferences.story === 'on' ? 'bật' : 'tắt'}; ngôn ngữ ${storyPreferences.storyLanguage}, phong cách ${storyPreferences.storyStyle}, trọng tâm ${storyPreferences.storyFocus}.`,
202
+ `- living chronicle: ${storyPreferences.story} (language=${storyPreferences.storyLanguage}, style=${storyPreferences.storyStyle}, focus=${storyPreferences.storyFocus})`
203
+ ));
161
204
  state.pendingApproval = token;
162
205
  saveState(state);
163
206
  return { code: 0, out: lines };
164
207
  }
165
208
 
166
209
  if (action === 'classify') {
167
- // classify is a metadata-only action (no history content): it reads
168
- // backbone.yml fields and a transient distinct-author count of the current
169
- // repo. It states that scope before producing the classification.
210
+ // classify is a metadata-only action (no history content): it reads only
211
+ // the known manifests advertised below and a transient author count.
170
212
  let classification;
171
213
  try {
172
214
  classification = classifyProject(repoProfile(options));
@@ -175,37 +217,38 @@ export function run(argsString = '') {
175
217
  }
176
218
  saveState(state);
177
219
  const language = resolveLanguage(options.language, { invocationText: argsString });
220
+ const sources = inventoryProjectMetadata(root).map((item) => item.path).join(', ') || 'none';
178
221
  const scope = language === 'vi'
179
- ? 'Phạm vi classify: chỉ đọc metadata (backbone.yml + số lượng tác giả commit của repo hiện tại) — không đọc nội dung lịch sử, không tính tiến cảnh (tiến cảnh cần `analyze`).'
180
- : 'classify scope: metadata only (backbone.yml + a distinct commit-author count of the current repo) — no history content is read, and no progression is computed (progression needs `analyze`).';
222
+ ? `Phạm vi classify: chỉ đọc manifest đã biết (${sources}) số lượng tác giả Git; không đọc nội dung lịch sử, không tính tiến cảnh (tiến cảnh cần \`analyze\`).`
223
+ : `classify scope: known manifests only (${sources}) plus a distinct Git-author count; no history content is read, and no progression is computed (progression needs \`analyze\`).`;
181
224
  return { code: 0, out: [scope, '', renderClassificationMarkdown(classification, language)] };
182
225
  }
183
226
 
184
227
  if (action === 'explain') {
185
228
  if (options.metric && EXPLAIN[options.metric]) {
186
- return { code: 0, out: [`A note from the Dao ledger — ${options.metric}: ${EXPLAIN[options.metric]}`] };
229
+ return { code: 0, out: [say(`Ghi chú từ đạo ký về \`${options.metric}\`: ${EXPLAIN_VI[options.metric]}`, `A note from the Dao ledger — ${options.metric}: ${EXPLAIN[options.metric]}`)] };
187
230
  }
188
- return { code: 0, out: [`The Dao ledger can explain: ${Object.keys(EXPLAIN).join(', ')} (pass metric=<name>).`] };
231
+ return { code: 0, out: [say(`Đạo ký có thể giải thích: ${Object.keys(EXPLAIN).map((key) => `\`${key}\``).join(', ')}. Hãy truyền \`metric=<tên>\`.`, `The Dao ledger can explain: ${Object.keys(EXPLAIN).join(', ')} (pass metric=<name>).`)] };
189
232
  }
190
233
 
191
234
  if (action === 'compare') {
192
235
  const files = fs.existsSync(snapDir) ? fs.readdirSync(snapDir).filter((f) => f.endsWith('.json')).sort() : [];
193
- if (files.length < 2) return refuse('need at least two snapshots to compare; run analyze snapshot=true first.');
236
+ if (files.length < 2) return refuse(say('Cần ít nhất hai ảnh chụp tổng hợp để so sánh. Hãy chạy `analyze snapshot=true` trước.', 'need at least two snapshots to compare; run analyze snapshot=true first.'));
194
237
  const load = (f) => JSON.parse(fs.readFileSync(path.join(snapDir, f), 'utf8'));
195
238
  const result = compareSnapshots(load(files[files.length - 2]), load(files[files.length - 1]));
196
239
  return {
197
240
  code: 0,
198
- out: ['Two cultivation records rest beneath the same lamp; the figures below show only what changed.', JSON.stringify(result, null, 2)]
241
+ out: [say('Hai bản tu luyện cùng đặt dưới một ngọn đèn; số liệu dưới đây chỉ cho thấy phần đã thay đổi.', 'Two cultivation records rest beneath the same lamp; the figures below show only what changed.'), JSON.stringify(result, null, 2)]
199
242
  };
200
243
  }
201
244
 
202
245
  if (action === 'analyze') {
203
- const token = scopeToken(options.sources, options.range);
246
+ const token = scopeToken(options.sources, options.range, inventoryProjectMetadata(root));
204
247
  if (!options.approve) {
205
- return refuse('analysis requires explicit approval: run preview, then pass the approve=<token> it printed.');
248
+ return refuse(say('Phân tích cần được phê duyệt rõ ràng. Hãy chạy `preview`, rồi truyền lại `approve=<token>` đã nhận.', 'analysis requires explicit approval: run preview, then pass the approve=<token> it printed.'));
206
249
  }
207
250
  if (options.approve !== token || state.pendingApproval !== token) {
208
- return refuse('approval token does not match the previewed scope; run preview again and approve that exact scope.');
251
+ return refuse(say('Token phê duyệt không khớp phạm vi đã xem trước. Hãy chạy lại `preview` và phê duyệt đúng phạm vi đó.', 'approval token does not match the previewed scope; run preview again and approve that exact scope.'));
209
252
  }
210
253
  delete state.pendingApproval;
211
254
 
@@ -249,19 +292,54 @@ export function run(argsString = '') {
249
292
  });
250
293
  const file = path.join(snapDir, `${createdAt.replace(/[:.]/g, '-')}.json`);
251
294
  fs.writeFileSync(file, `${JSON.stringify(snap, null, 2)}\n`);
252
- out.push(`snapshot written: ${path.relative(root, file)}`);
295
+ out.push(say(`Đã ghi ảnh chụp tổng hợp: ${path.relative(root, file)}`, `snapshot written: ${path.relative(root, file)}`));
253
296
  const prune = snapshotsToPrune(fs.readdirSync(snapDir).filter((f) => f.endsWith('.json')), 20);
254
297
  if (prune.length) {
255
- out.push(`retention: remove old snapshots with: trash ${prune.map((f) => path.relative(root, path.join(snapDir, f))).join(' ')}`);
298
+ out.push(say(`Lưu giữ: có thể chuyển các ảnh chụp cũ vào thùng rác bằng lệnh \`trash ${prune.map((f) => path.relative(root, path.join(snapDir, f))).join(' ')}\`.`, `retention: remove old snapshots with: trash ${prune.map((f) => path.relative(root, path.join(snapDir, f))).join(' ')}`));
299
+ }
300
+ }
301
+ let storyContext = null;
302
+ if (storyPreferences.story === 'on') {
303
+ if (model.suppressGamification) {
304
+ out.push(say(`Trường thiên tạm dừng: trạng thái chính sách \`${model.policyState}\` không cho phép viết cốt truyện.`, `living chronicle paused: policy state ${model.policyState} does not permit lore.`));
305
+ } else {
306
+ storyContext = buildStoryContext(model, {
307
+ profile,
308
+ range: options.range,
309
+ language: storyPreferences.storyLanguage,
310
+ invocationText: argsString,
311
+ conversationLanguage: language,
312
+ style: storyPreferences.storyStyle,
313
+ focus: storyPreferences.storyFocus
314
+ });
315
+ const contextFile = writeStoryContext(root, storyContext);
316
+ out.push(say(`Đã ghi ngữ cảnh trường thiên: ${path.relative(root, contextFile)}; bằng chứng \`${storyContext.evidenceKey}\`.`, `story context written: ${path.relative(root, contextFile)} (evidence ${storyContext.evidenceKey})`));
256
317
  }
257
318
  }
258
319
  fs.mkdirSync(reportDir, { recursive: true });
259
- fs.writeFileSync(path.join(reportDir, 'latest.md'), `${markdown}\n`);
320
+ const ledgerFile = path.join(reportDir, 'latest.md');
321
+ const briefFile = path.join(reportDir, 'latest-brief.json');
322
+ fs.writeFileSync(ledgerFile, `${markdown}\n`);
323
+ const brief = buildResponseBrief(model, {
324
+ language,
325
+ profile,
326
+ storyContext,
327
+ ledgerPath: path.relative(root, ledgerFile)
328
+ });
329
+ fs.writeFileSync(briefFile, `${JSON.stringify(brief, null, 2)}\n`);
330
+ out.push(say(
331
+ `Đã ghi đạo ký chứng cứ: ${path.relative(root, ledgerFile)}.`,
332
+ `evidence ledger written: ${path.relative(root, ledgerFile)}.`
333
+ ));
334
+ out.push(say(
335
+ `Gói sáng tác đã sẵn sàng: ${path.relative(root, briefFile)}; hãy dùng dữ kiện này để viết phản hồi linh hoạt theo dự án, không sao chép bố cục đạo ký.`,
336
+ `response brief ready: ${path.relative(root, briefFile)}; compose adaptively from it instead of copying the ledger structure.`
337
+ ));
260
338
  saveState(state);
261
- return { code: 0, out: [...out, markdown] };
339
+ return { code: 0, out: options.output === 'ledger' ? [...out, markdown] : out };
262
340
  }
263
341
 
264
- return refuse(`unknown action "${action}"`);
342
+ return refuse(say(`Không nhận ra hành động "${action}".`, `unknown action "${action}"`));
265
343
  }
266
344
 
267
345
  const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);