@kitkat-vn/kitcode 0.2.0-staging.9 → 0.3.0-staging.16
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.
- package/README.md +50 -28
- package/bin/kitcode.mjs +65 -5
- package/package.json +2 -2
- package/src/api.mjs +105 -3
- package/src/break-progress.mjs +94 -9
- package/src/claim-bundle.mjs +18 -5
- package/src/companion-window.mjs +9 -3
- package/src/cors.mjs +3 -1
- package/src/cycle-receipt.mjs +97 -0
- package/src/fonts/jetbrains-mono/OFL.txt +93 -0
- package/src/fonts/jetbrains-mono/jetbrains-mono-latin-400-normal.woff2 +0 -0
- package/src/fonts/jetbrains-mono/jetbrains-mono-latin-500-normal.woff2 +0 -0
- package/src/fonts/jetbrains-mono/jetbrains-mono-latin-700-normal.woff2 +0 -0
- package/src/fonts/jetbrains-mono/jetbrains-mono-vietnamese-400-normal.woff2 +0 -0
- package/src/fonts/jetbrains-mono/jetbrains-mono-vietnamese-500-normal.woff2 +0 -0
- package/src/fonts/jetbrains-mono/jetbrains-mono-vietnamese-700-normal.woff2 +0 -0
- package/src/hook-prompt.mjs +24 -3
- package/src/integration-spec.mjs +175 -48
- package/src/milestone-record.mjs +214 -0
- package/src/milestones-reached.mjs +168 -64
- package/src/next-break-reminder.mjs +4 -3
- package/src/onboarding-electron.mjs +12 -11
- package/src/onboarding-platform.mjs +5 -2
- package/src/onboarding-window.mjs +94 -63
- package/src/reward.mjs +247 -68
- package/src/runtime.mjs +48 -4
- package/src/tracking-config.mjs +501 -0
- package/src/welcome-words.mjs +115 -0
- package/src/window-page.mjs +16 -2
package/src/integration-spec.mjs
CHANGED
|
@@ -1,15 +1,45 @@
|
|
|
1
|
-
|
|
1
|
+
// The CLI's own package.json, for its name and its version, which live there alone (hard rule 6). Since 2026-10-22
|
|
2
|
+
// (production/1-cli) the production setup prompt installs this version by number, and the dashboard, which bundles this file,
|
|
3
|
+
// takes both from here: a JSON module, which Node loads without a warning from 22.12.0, the engines floor, and of which Vite
|
|
4
|
+
// bundles only the two fields read. Until then this file imported nothing.
|
|
5
|
+
import cliPackage from '../package.json' with {type: 'json'};
|
|
6
|
+
|
|
7
|
+
// Since 2026-10-22 (production/1-cli; the operator's two names of 2026-10-02, and Q5 and Q7 of the plan of 2026-10-22) the
|
|
8
|
+
// package is production's, `@kitkatvn/kitcode`, at 0.2.0: package.json's name and version. A tester build's copy names the
|
|
9
|
+
// staging package there instead (scripts/tester-build.mjs), and so here. Until then `@kitkat-vn/kitcode`, written here.
|
|
10
|
+
export const CLI_PACKAGE_NAME = cliPackage.name;
|
|
11
|
+
export const CLI_VERSION = cliPackage.version;
|
|
2
12
|
export const CLI_PACKAGE_COMMAND = `npx ${CLI_PACKAGE_NAME}`;
|
|
3
13
|
export const CLI_GLOBAL_INSTALL_COMMAND = `npm install -g ${CLI_PACKAGE_NAME}`;
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
export const
|
|
14
|
+
// What the production setup prompt installs: this version, by number, so that the dashboard and the CLI it installs are one
|
|
15
|
+
// commit's. A newer version on npm reaches the prompt only with a deploy of the dashboard built from its commit.
|
|
16
|
+
export const CLI_PINNED_INSTALL_COMMAND = `${CLI_GLOBAL_INSTALL_COMMAND}@${CLI_VERSION}`;
|
|
17
|
+
// The staging package, which every tester build is published as, under npm's `staging` tag: since 2026-10-02 the only way a
|
|
18
|
+
// tester installs one (stagingSetupPromptFor, below). It installs the same `kitcode` command as the production package, so npm
|
|
19
|
+
// refuses to install either over the other: each prompt removes the other package first.
|
|
20
|
+
export const CLI_STAGING_PACKAGE_NAME = '@kitkat-vn/kitcode';
|
|
21
|
+
export const CLI_STAGING_INSTALL_COMMAND = `npm install -g ${CLI_STAGING_PACKAGE_NAME}@staging`;
|
|
22
|
+
// The package's previous name, under which 0.1.39 was published on 2026-08-06 and is still npm's `latest` (docs/project-overview.md,
|
|
23
|
+
// "Current state"). It installs the same `kitcode` command too, so since 2026-10-22 (production/1-cli) each prompt removes it
|
|
24
|
+
// first, in the same command as the other channel's package.
|
|
25
|
+
export const CLI_PREVIOUS_PACKAGE_NAME = '@onedigitas/kitcode';
|
|
7
26
|
export const CLI_GLOBAL_COMMAND = 'kitcode';
|
|
27
|
+
// Production's dashboard since 2026-10-22 (production/1-cli): what `kitcode dashboard` opens, and the origin every tracker
|
|
28
|
+
// `kitcode track` starts allows (src/cors.mjs, bin/kitcode.mjs). A tester build's copy names the staging site here. Until then
|
|
29
|
+
// https://kitcode.vercel.app/, the previous project's site, which is not ours.
|
|
8
30
|
export const DASHBOARD_URL = "https://staging.kitcode.vn/";
|
|
9
31
|
// Where a tester build sends KitCode's own diagnostics (src/diagnostics.mjs; docs/backend.md, "Tester diagnostics"): the
|
|
10
32
|
// staging dashboard's /v1/diagnostics, which scripts/tester-build.mjs alone writes here, as it writes DASHBOARD_URL. null in
|
|
11
33
|
// the repository, and so in every release of it: such a CLI collects, writes and sends no diagnostics at all.
|
|
12
34
|
export const DIAGNOSTICS_URL = "https://staging.kitcode.vn/v1/diagnostics";
|
|
35
|
+
// The claim backend's public key for cycle receipts (src/cycle-receipt.mjs; docs/backend.md, "The cycle receipt"): base64url of
|
|
36
|
+
// the 32 raw bytes of the Ed25519 key whose private half exists on the claim backend's server alone. The tracker begins a
|
|
37
|
+
// cycle only on a receipt it signed (api.mjs, POST /api/claim/receipt). Public, as the repository is, and written here by
|
|
38
|
+
// scripts/tester-build.mjs, from the key the staging server printed (deploy/staging/cycle-receipt.pub), as it writes
|
|
39
|
+
// DIAGNOSTICS_URL; and since 2026-10-22 (production/1-cli) by scripts/release.mjs in a production release's copy, from the key
|
|
40
|
+
// the production server printed (deploy/production/cycle-receipt.pub). null in the repository: a CLI run from it takes no
|
|
41
|
+
// receipt, and never begins a second cycle.
|
|
42
|
+
export const CYCLE_RECEIPT_PUBLIC_KEY = "QsSBNGXhReBhjz0qk32fFLYiVCDtqKEGQwFB30LflTw";
|
|
13
43
|
export const LOCAL_SERVER_URL = 'http://127.0.0.1:4747';
|
|
14
44
|
export const STATE_DISPLAY_PATH = '~/.kitcode/state.json';
|
|
15
45
|
export const RUNNER_PATH_PARTS = ['.kitcode', 'bin', 'kitcode'];
|
|
@@ -17,18 +47,61 @@ export const RUNNER_DISPLAY_PATH = `~/${RUNNER_PATH_PARTS.join('/')}`;
|
|
|
17
47
|
|
|
18
48
|
// The milestones and the = each needs (docs/reward-rules.md §2). Until 2026-10-01 each had a break code, one fixed line of
|
|
19
49
|
// code shown to everyone; the operator's decision 6 of that day replaced them with a message the claim backend's
|
|
20
|
-
// settings give each milestone, which the dashboard shows.
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
50
|
+
// settings give each milestone, which the dashboard shows. A tier holds a record in the state file: the hook's
|
|
51
|
+
// announcements, and a code shown before then.
|
|
52
|
+
//
|
|
53
|
+
// Since 2026-10-08 (rewards/1-milestones-bar; the operator: "change milestone to: 30% - 50% - 100%") the milestones are
|
|
54
|
+
// 30, 50 and 100 %, each with the targets it had: 30 % the one tier, 50 and 100 % shown and never announced, as before.
|
|
55
|
+
// Until then 10 % (3 =, 6 min) and 20 % (6 =, 12 min) were tiers too. A record an earlier version kept of either, an
|
|
56
|
+
// announcement, a code shown or the time it was reached, stays in the file as it is, and nothing reads it.
|
|
57
|
+
//
|
|
58
|
+
// Since 2026-10-20 (rewards/2-no-stack; the plan of that day, decisions 1 and 4) each milestone's targets, its focus time
|
|
59
|
+
// and its =, are counted fresh from the moment the milestone before it was reached, the first's from install, and each
|
|
60
|
+
// milestone names its own focus time, where 3600 s × its percent was the time target until then (reward.mjs). Two tables,
|
|
61
|
+
// both public: the tester table, the short one, which every build of this repository and every tester build counts by,
|
|
62
|
+
// its numbers the ones the milestones had; and the production table, whose numbers are DRAFTS until the client gives the
|
|
63
|
+
// campaign's length: 3 h and 300 =, 5 h and 500 =, 10 h and 1,000 =, so 18 h and 1,800 = in all to reach 100 %.
|
|
64
|
+
// MILESTONE_TABLE names the one in force: 'tester' here, and so in every tester build; a production release's copy alone
|
|
65
|
+
// says 'production', which scripts/release.mjs writes there, one line, checked line by line, as tester-build.mjs writes
|
|
66
|
+
// DASHBOARD_URL.
|
|
67
|
+
//
|
|
68
|
+
// Since 2026-10-21 both tables hold the operator's numbers of that day ("change the progress to 30% - 3h - 300 = / 50% -
|
|
69
|
+
// 5h - 500 = / 100% - 8h - 1000 =", then "1. staging too 2. as written"): 3 h and 300 =, 5 h and 500 =, 8 h and 1,000 =,
|
|
70
|
+
// so 16 h and 1,800 = in all to reach 100 %. Tester builds have no short targets any more, which reverses the plan's
|
|
71
|
+
// decision 4 for the tester table; the production table's numbers are no longer drafts, and its 100 % is 8 h where the
|
|
72
|
+
// draft said 10. The two tables stay two, equal, so that production's can differ again by one table's edit (a ruling,
|
|
73
|
+
// docs/reward-rules.md §2). Until then the tester table was 1080 s and 9 =, 1800 s and 12 =, 3600 s and 15 =.
|
|
74
|
+
//
|
|
75
|
+
// Since 2026-10-22 the tester table is that short one again (the operator: "scale down the focus time and = requirements as
|
|
76
|
+
// the previous time for staging for testing easier, keep the same for production"): 18 minutes and 9 =, 30 and 12, 60 and
|
|
77
|
+
// 15, so 108 minutes and 36 = in all to reach 100 %, each still counted fresh. The production table keeps the operator's
|
|
78
|
+
// numbers of 2026-10-21, and the two differ again. The claim backend holds a claim to its own MILESTONE_* settings, which
|
|
79
|
+
// staging's server sets to the tester table by hand (docs/deployment-target.md).
|
|
80
|
+
export const MILESTONE_TABLES = Object.freeze({
|
|
81
|
+
tester: Object.freeze([
|
|
82
|
+
Object.freeze({percent: 30, requiredSeconds: 1080, requiredEquals: 9}),
|
|
83
|
+
Object.freeze({percent: 50, requiredSeconds: 1800, requiredEquals: 12}),
|
|
84
|
+
Object.freeze({percent: 100, requiredSeconds: 3600, requiredEquals: 15}),
|
|
85
|
+
]),
|
|
86
|
+
production: Object.freeze([
|
|
87
|
+
Object.freeze({percent: 30, requiredSeconds: 10800, requiredEquals: 300}),
|
|
88
|
+
Object.freeze({percent: 50, requiredSeconds: 18000, requiredEquals: 500}),
|
|
89
|
+
Object.freeze({percent: 100, requiredSeconds: 28800, requiredEquals: 1000}),
|
|
90
|
+
]),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
export const MILESTONE_TABLE = 'tester';
|
|
94
|
+
|
|
95
|
+
const MILESTONE_TARGETS = MILESTONE_TABLES[MILESTONE_TABLE];
|
|
96
|
+
|
|
97
|
+
if (!Array.isArray(MILESTONE_TARGETS)) {
|
|
98
|
+
throw new Error(`MILESTONE_TABLE is ${JSON.stringify(MILESTONE_TABLE)}, which is not one of the milestone tables: ${Object.keys(MILESTONE_TABLES).join(', ')}.`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 30 %, the first milestone, is the one tier; 50 and 100 % are shown alone.
|
|
102
|
+
export const KITCODE_REWARD_TIERS = MILESTONE_TARGETS.slice(0, 1).map((milestone) => ({...milestone}));
|
|
103
|
+
|
|
104
|
+
export const KITCODE_DISPLAY_MILESTONES = MILESTONE_TARGETS.slice(1).map((milestone) => ({...milestone, displayOnly: true}));
|
|
32
105
|
|
|
33
106
|
export const AGENT_SPECS = {
|
|
34
107
|
codex: {
|
|
@@ -169,7 +242,7 @@ export function kitCodeSkillMarkdown(source) {
|
|
|
169
242
|
` ${RUNNER_DISPLAY_PATH} dashboard`,
|
|
170
243
|
'',
|
|
171
244
|
'The prompt hook runs after each prompt submit and may add compact progress context such as total `=` counted, active time, next break remaining (`=` left and time left), or milestones reached. Treat that hook context as authoritative.',
|
|
172
|
-
'When hook context includes remaining equals and time to the next break milestone, briefly mention both in one short line after your normal answer on every turn (for example: `Break next:
|
|
245
|
+
'When hook context includes remaining equals and time to the next break milestone, briefly mention both in one short line after your normal answer on every turn (for example: `Break next: 120 = to break · 1h 20m to break (30%).`). Do not calculate these values yourself.',
|
|
173
246
|
'',
|
|
174
247
|
'Do not run git counting commands, do not edit the ledger, and do not reimplement reward logic in chat. Use the KitCode CLI/server/hook output as the authority.',
|
|
175
248
|
'',
|
|
@@ -210,12 +283,53 @@ export function kitCodeSkillMarkdown(source) {
|
|
|
210
283
|
].join('\n');
|
|
211
284
|
}
|
|
212
285
|
|
|
286
|
+
// A milestone's time as the setup prompt says it: whole hours as "3h", and otherwise as next-break-reminder.mjs writes a time
|
|
287
|
+
// left, "2h 30m", "18m". Here, since this file imports none of the CLI's modules (the dashboard bundles it).
|
|
288
|
+
function targetTime(seconds) {
|
|
289
|
+
const hours = Math.floor(seconds / 3600);
|
|
290
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
291
|
+
|
|
292
|
+
if (hours > 0) {
|
|
293
|
+
return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return minutes > 0 ? `${minutes}m` : `${seconds}s`;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Since 2026-10-20 each line says where its milestone counts from: install for the first, and for each other the moment the
|
|
300
|
+
// one before it was reached (docs/reward-rules.md §2). Since 2026-10-21 each names its time, "3h of tracked time", where it
|
|
301
|
+
// said "enough tracked time": with the operator's targets of that day a milestone's time is 3 to 8 hours. Since 2026-10-22
|
|
302
|
+
// the tester table's times read "18m", "30m" and "1h", the production table's still "3h", "5h" and "8h".
|
|
213
303
|
function milestoneLines() {
|
|
214
|
-
|
|
215
|
-
|
|
304
|
+
const milestones = [...KITCODE_REWARD_TIERS, ...KITCODE_DISPLAY_MILESTONES];
|
|
305
|
+
|
|
306
|
+
return milestones
|
|
307
|
+
.map((milestone, index) => `- ${milestone.percent}% milestone: reached after ${targetTime(milestone.requiredSeconds)} of tracked time and ${milestone.requiredEquals} counted = characters, both counted ${index === 0 ? 'from install' : `from the moment the ${milestones[index - 1].percent}% milestone was reached`}; the dashboard shows its message.`)
|
|
216
308
|
.join('\n');
|
|
217
309
|
}
|
|
218
310
|
|
|
311
|
+
// The lines of step 2 that name a package or a version: the other packages, removed first, in one command; this one, installed;
|
|
312
|
+
// and the version `kitcode --version` must print. The staging prompt (below) changes these three, and the untrack line, and
|
|
313
|
+
// nothing else of step 2. Since 2026-10-22 the removal names the package's previous name too, beside the other channel's.
|
|
314
|
+
const removeOtherPackagesLine = (otherPackages) => `2. Once Node.js 22.12+ is ready, first remove the other KitCode packages, if an earlier setup installed one: npm uninstall -g ${otherPackages.join(' ')}`;
|
|
315
|
+
// What the production prompts remove before they install: the staging package and the package's previous name.
|
|
316
|
+
const PRODUCTION_REMOVED_PACKAGES = [CLI_STAGING_PACKAGE_NAME, CLI_PREVIOUS_PACKAGE_NAME];
|
|
317
|
+
const installLine = (command) => `- Then install KitCode globally with: ${command}`;
|
|
318
|
+
const versionCheckLine = (version) => `- Check that ${globalKitCodeCommand('--version')} prints ${version}. If it prints anything else, report exactly what it printed, and do not go on to step 3.`;
|
|
319
|
+
// The stop of a tracker a KitCode installed before left running, in the production prompt's words. The staging prompt has said it
|
|
320
|
+
// in its own since 2026-09-30 (STAGING_UNTRACK_LINE, below), words the dashboard's checks hold to a staging build alone.
|
|
321
|
+
const PRODUCTION_UNTRACK_LINE = `- Then run: ${globalKitCodeCommand('untrack')}. It stops a KitCode tracker that a KitCode installed before left running, so that the tracker Welcome starts is this version's; when none is running, it only says so.`;
|
|
322
|
+
|
|
323
|
+
// The production setup prompt, which the dashboard copies for each agent (apps/web src/components/project-gateway.tsx).
|
|
324
|
+
//
|
|
325
|
+
// Since 2026-10-22 (production/1-cli; the plan of that day, Run A) it installs the production package at this commit's version,
|
|
326
|
+
// by number, after removing the staging package, which has the same `kitcode` command; stops unless `kitcode --version` prints
|
|
327
|
+
// that version; and then stops a tracker a KitCode installed before left running, as the staging prompt has done since
|
|
328
|
+
// 2026-09-30 in words of its own, since whoever installed the staging package may have one. The removal names the package's
|
|
329
|
+
// previous name too, @onedigitas/kitcode, whose 0.1.39 has the same command (CLI_PREVIOUS_PACKAGE_NAME). It holds one URL, the dashboard's, https://kitcode.vn/: the two GitHub
|
|
330
|
+
// links it had, github.com/kitkatvn/kitcode's README and the CLI's, answer 404 to the public (hard rule 5, F5), and with no docs
|
|
331
|
+
// to read it no longer tells the agent not to refuse. Until then it installed `npm install -g @kitkat-vn/kitcode`, whatever
|
|
332
|
+
// npm's latest was.
|
|
219
333
|
export function assistantSetupPromptFor(source) {
|
|
220
334
|
const config = sourceConfig(source);
|
|
221
335
|
|
|
@@ -225,17 +339,18 @@ export function assistantSetupPromptFor(source) {
|
|
|
225
339
|
'This works in any chat, project, or cowork conversation. Do not require repo context to begin.',
|
|
226
340
|
REMOTE_SESSION_REDIRECT[source],
|
|
227
341
|
'',
|
|
228
|
-
|
|
229
|
-
'- https://github.com/kitkatvn/kitcode/blob/main/README.md',
|
|
230
|
-
'- https://github.com/kitkatvn/kitcode/blob/main/packages/kitcode-cli/README.md',
|
|
231
|
-
'Read them directly from GitHub. Do not refuse based on lacking prior knowledge of the package.',
|
|
342
|
+
`KitCode by KitKat is a local-first break companion for coding. I copied this prompt from its dashboard, ${DASHBOARD_URL}, and step 2 installs KitCode ${CLI_VERSION} from npm, the package ${CLI_PACKAGE_NAME}.`,
|
|
232
343
|
'',
|
|
233
344
|
'Phase 1 — assistant setup (machine prep only):',
|
|
234
345
|
'1. Check whether Node.js 22.12+ is available.',
|
|
235
346
|
' - If it is missing or too old, explain the shortest safe install option for this OS and ask for my confirmation before installing or upgrading it.',
|
|
236
|
-
|
|
347
|
+
removeOtherPackagesLine(PRODUCTION_REMOVED_PACKAGES),
|
|
348
|
+
`- Each has the same ${CLI_GLOBAL_COMMAND} command, so npm cannot install KitCode beside it. When neither is installed, npm says it is up to date: go on.`,
|
|
349
|
+
installLine(CLI_PINNED_INSTALL_COMMAND),
|
|
237
350
|
'- Always run this command. Do not skip it because KitCode may already be installed.',
|
|
238
351
|
`- After install, use the global ${CLI_GLOBAL_COMMAND} command for all following steps (not npx).`,
|
|
352
|
+
versionCheckLine(CLI_VERSION),
|
|
353
|
+
PRODUCTION_UNTRACK_LINE,
|
|
239
354
|
`3. Run this command yourself once from the project directory when you are in a project chat: ${config.setupCommand}`,
|
|
240
355
|
'- When setup is still incomplete, this command already opens the KitCode Welcome window and prints "Opening KitCode Welcome...".',
|
|
241
356
|
'- In a project chat, run it with the project folder as the working directory so Welcome can pre-add that folder.',
|
|
@@ -268,6 +383,9 @@ export function assistantSetupPromptFor(source) {
|
|
|
268
383
|
// voice, as the prompt is, and with no URL in it, as the prompt has none.
|
|
269
384
|
export const STAGING_DIAGNOSTICS_NOTICE = 'The tester build sends diagnostic logs to KitCode\'s staging server: KitCode\'s own events and errors, such as the tracker starting or a hook failing, and never my code, my files, my prompts or my project names.';
|
|
270
385
|
|
|
386
|
+
// The staging prompt's stop of a tracker an earlier build left running, in the words it has had since 2026-09-30.
|
|
387
|
+
const STAGING_UNTRACK_LINE = `- Then, before step 3, run: ${globalKitCodeCommand('untrack')}. It stops a KitCode tracker that an earlier build left running, so that the tracker Welcome starts is this build's; when none is running, it only says so.`;
|
|
388
|
+
|
|
271
389
|
// The setup prompt a staging dashboard copies for its invited testers (apps/web src/lib/staging-prompts.ts): the one
|
|
272
390
|
// above, line for line, with four changes and no other. Step 2 installs the round's tester build from npm's `staging` tag,
|
|
273
391
|
// and checks its version. Then, before step 3 turns KitCode on, it stops a tracker an earlier build left running. It holds
|
|
@@ -276,6 +394,15 @@ export const STAGING_DIAGNOSTICS_NOTICE = 'The tester build sends diagnostic log
|
|
|
276
394
|
// server (STAGING_DIAGNOSTICS_NOTICE). It is made from the prompt above as it stands, so that the two cannot drift: a line
|
|
277
395
|
// it changes that is not there once throws, and check-integration-spec.mjs runs both.
|
|
278
396
|
//
|
|
397
|
+
// Since 2026-10-22 (production/1-cli) the prompt above is production's, with a version check and a stop of an earlier tracker of
|
|
398
|
+
// its own, and the four changes are these: the round's line and the diagnostics notice where it names the dashboard; the
|
|
399
|
+
// production package removed where it removes the staging package, beside the package's previous name, which both remove;
|
|
400
|
+
// npm's `staging` tag installed where it installs its version;
|
|
401
|
+
// and the tester build's version checked where it checks its own, with its own words for the stop of an earlier tracker where
|
|
402
|
+
// production has its own. The removal is new to this prompt too, the first two lines of step 2, which installs on a line of its
|
|
403
|
+
// own: a tester who installed the production package must remove it first, as npm installs no two packages with the same
|
|
404
|
+
// `kitcode` command. Its other words are as they were.
|
|
405
|
+
//
|
|
279
406
|
// Until 2026-10-02 it could install a file the tester downloaded from the staging site instead (install 'file', with its
|
|
280
407
|
// fileName). The operator's rule of that day is npm alone, "never let user download file even as tester, must be via npm
|
|
281
408
|
// package only": it takes the tester build's version and nothing else, and refuses an option it does not know, so that a
|
|
@@ -298,33 +425,36 @@ export function stagingSetupPromptFor(source, options) {
|
|
|
298
425
|
return found[0];
|
|
299
426
|
};
|
|
300
427
|
|
|
301
|
-
// The paragraph
|
|
302
|
-
const
|
|
303
|
-
const
|
|
428
|
+
// The paragraph that names the dashboard, to the blank line after it, gives way to one line about the round.
|
|
429
|
+
const intro = onlyLine((line) => line.startsWith('KitCode by KitKat is a local-first break companion for coding.'), 'the dashboard\'s paragraph');
|
|
430
|
+
const introEnd = lines.indexOf('', intro);
|
|
304
431
|
|
|
305
|
-
if (
|
|
306
|
-
throw new Error('stagingSetupPromptFor found no blank line after the
|
|
432
|
+
if (introEnd === -1) {
|
|
433
|
+
throw new Error('stagingSetupPromptFor found no blank line after the dashboard\'s paragraph: assistantSetupPromptFor changed, and stagingSetupPromptFor must follow it.');
|
|
307
434
|
}
|
|
308
435
|
|
|
309
|
-
lines.splice(
|
|
310
|
-
|
|
311
|
-
const installLine = onlyLine((line) => line.endsWith(`install KitCode globally with: ${CLI_GLOBAL_INSTALL_COMMAND}`), 'the install');
|
|
312
|
-
lines[installLine] = lines[installLine].replace(`with: ${CLI_GLOBAL_INSTALL_COMMAND}`, `with: ${CLI_STAGING_INSTALL_COMMAND}`);
|
|
436
|
+
lines.splice(intro, introEnd - intro, `KitCode is a local-first project. I am an invited tester of its staging round, which needs KitCode's tester build, ${testerVersion}: step 2 installs it.`, STAGING_DIAGNOSTICS_NOTICE);
|
|
313
437
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
)
|
|
438
|
+
// Each of the three lines of step 2 that name a package or a version, found once, and replaced.
|
|
439
|
+
for (const [production, staging, what] of [
|
|
440
|
+
[removeOtherPackagesLine(PRODUCTION_REMOVED_PACKAGES), removeOtherPackagesLine([CLI_PACKAGE_NAME, CLI_PREVIOUS_PACKAGE_NAME]), 'the removal of the other packages'],
|
|
441
|
+
[installLine(CLI_PINNED_INSTALL_COMMAND), installLine(CLI_STAGING_INSTALL_COMMAND), 'the install'],
|
|
442
|
+
[versionCheckLine(CLI_VERSION), versionCheckLine(testerVersion), 'the version check'],
|
|
443
|
+
[PRODUCTION_UNTRACK_LINE, STAGING_UNTRACK_LINE, 'the stop of an earlier tracker'],
|
|
444
|
+
]) {
|
|
445
|
+
lines[onlyLine((line) => line === production, what)] = staging;
|
|
446
|
+
}
|
|
321
447
|
|
|
322
448
|
return lines.join('\n');
|
|
323
449
|
}
|
|
324
450
|
|
|
325
|
-
|
|
451
|
+
// The longer setup prompt, which ships in the package and which only check-integration-spec.mjs calls (docs/security.md, "The
|
|
452
|
+
// setup prompt: F5"). Since 2026-10-22 (production/1-cli) it follows the production prompt above: no GitHub link, since none
|
|
453
|
+
// resolves publicly, and so no README to read first and no `readmeUrl` option; the staging package and the package's previous
|
|
454
|
+
// name removed before this version is installed, by number, and checked. Its URLs are the dashboard's and the tracker's own
|
|
455
|
+
// address on this machine.
|
|
456
|
+
export function setupPromptFor(source) {
|
|
326
457
|
const config = sourceConfig(source);
|
|
327
|
-
const readmeUrl = options.readmeUrl ?? 'https://github.com/kitkatvn/kitcode/blob/main/README.md';
|
|
328
458
|
const hookCommand = hookCommandFor(source);
|
|
329
459
|
const expectedHook = hookConfigFor(source);
|
|
330
460
|
|
|
@@ -332,9 +462,6 @@ export function setupPromptFor(source, options = {}) {
|
|
|
332
462
|
'Welcome to KitCode.',
|
|
333
463
|
'',
|
|
334
464
|
`You are ${config.agentName} helping me set up KitCode skills and hooks.`,
|
|
335
|
-
'Read the project README carefully before changing anything.',
|
|
336
|
-
'',
|
|
337
|
-
`Project README: ${readmeUrl}`,
|
|
338
465
|
'',
|
|
339
466
|
'Mission:',
|
|
340
467
|
`- Set up KitCode for ${config.agentName} on the current local machine.`,
|
|
@@ -360,11 +487,11 @@ export function setupPromptFor(source, options = {}) {
|
|
|
360
487
|
'Required checks:',
|
|
361
488
|
'- Node.js 22.12+ is required.',
|
|
362
489
|
'- Git is required for Git Mode.',
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
`- Install KitCode globally with: ${CLI_GLOBAL_INSTALL_COMMAND}`,
|
|
490
|
+
`- First remove the other KitCode packages, if an earlier setup installed one: npm uninstall -g ${PRODUCTION_REMOVED_PACKAGES.join(' ')}. Each has the same ${CLI_GLOBAL_COMMAND} command, so npm cannot install KitCode beside it.`,
|
|
491
|
+
`- Install KitCode globally with: ${CLI_PINNED_INSTALL_COMMAND}`,
|
|
366
492
|
'- Always run this command. Do not skip it because KitCode may already be installed.',
|
|
367
493
|
`- After install, use the global ${CLI_GLOBAL_COMMAND} command for all following steps (not npx).`,
|
|
494
|
+
`- Check that ${globalKitCodeCommand('--version')} prints ${CLI_VERSION}. If it prints anything else, report exactly what it printed, and stop there.`,
|
|
368
495
|
'',
|
|
369
496
|
`${config.agentName} setup:`,
|
|
370
497
|
`- Prefer the CLI installer from the project directory in a project chat: ${config.setupCommand}`,
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// The milestone record in state.json, `milestonesReached`, read (docs/reward-rules.md §6.1): what each milestone's entry
|
|
2
|
+
// says, checked as KitCode writes it. Pure, with no imports: reward.mjs reads it to decide the milestones, and
|
|
3
|
+
// milestones-reached.mjs to write the record and to report it in the claim bundle.
|
|
4
|
+
//
|
|
5
|
+
// {"recordingSince": "2026-09-25T09:15:00.004Z",
|
|
6
|
+
// "milestones": {"30": {"reachedBeforeRecording": true},
|
|
7
|
+
// "50": {"reachedAt": "2026-10-20T11:03:27.918Z", "focusSeconds": 3600, "equals": 15}},
|
|
8
|
+
// "freshSince": {"at": "2026-10-20T10:00:00.000Z", "focusSeconds": 2400, "equals": 11}}
|
|
9
|
+
//
|
|
10
|
+
// Since 2026-10-20 (rewards/2-no-stack) an entry the CLI writes also holds the totals at the moment the milestone was
|
|
11
|
+
// reached, its focus seconds and its =, and `freshSince` the totals at the moment this KitCode first wrote a record that
|
|
12
|
+
// holds a milestone reached without them: by an earlier KitCode, or before the record began. The milestone after such a
|
|
13
|
+
// one counts from `freshSince` (the plan's decision 2). An entry an earlier KitCode wrote, `reachedAt` alone, stays as it
|
|
14
|
+
// is, and so does every field this version does not know.
|
|
15
|
+
//
|
|
16
|
+
// Since 2026-10-22 (rewards/3-repeat-vouchers, phase 2) the record counts in cycles (the plan of 2026-10-21, decision 1: after a
|
|
17
|
+
// claim, all progress resets). Cycle 1 is the record as it was, its entries in `milestones`; each later cycle is one entry of
|
|
18
|
+
// `cycles`, added when the tracker takes a receipt the claim backend signed for the voucher of the cycle before
|
|
19
|
+
// (milestones-reached.mjs, beginNextCycle), and never changed after but for its own milestones, added once each. Since 2026-10-22's
|
|
20
|
+
// phase 3 a cycle is numbered as the claim backend counts, the one after its receipt's, so the numbers can skip: a KitCode installed
|
|
21
|
+
// afresh in cycle 1 that takes the receipt of cycle 2 begins cycle 3.
|
|
22
|
+
//
|
|
23
|
+
// "cycles": [{"cycle": 2, "startedAt": "2026-10-22T08:00:00.000Z", "focusSeconds": 57900, "equals": 1810,
|
|
24
|
+
// "receipt": {"claimId": "…", "issuedAt": "2026-10-22T07:58:00.000Z"},
|
|
25
|
+
// "milestones": {"30": {"reachedAt": "…", "focusSeconds": 68722, "equals": 2110}}}]
|
|
26
|
+
//
|
|
27
|
+
// A cycle's start is the totals since install at the moment it began, which its first milestone counts from; the totals keep
|
|
28
|
+
// counting across cycles. Nothing of cycle 1 changes when cycle 2 begins: the record is additive only (hard rule 4).
|
|
29
|
+
|
|
30
|
+
// A time as toISOString writes it, and a real one: 2026-02-30 is not.
|
|
31
|
+
const INSTANT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
32
|
+
|
|
33
|
+
function isObject(value) {
|
|
34
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function isInstant(value) {
|
|
38
|
+
if (typeof value !== 'string' || !INSTANT.test(value)) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const time = Date.parse(value);
|
|
43
|
+
return Number.isFinite(time) && new Date(time).toISOString() === value;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isCount(value) {
|
|
47
|
+
return Number.isSafeInteger(value) && value >= 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const CLAIM_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/u;
|
|
51
|
+
|
|
52
|
+
// A record as KitCode writes it: when it began, and its entries.
|
|
53
|
+
export function isRecord(record) {
|
|
54
|
+
return isObject(record) && isInstant(record.recordingSince) && isObject(record.milestones);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// The totals an entry, or freshSince, holds: both, whole and not below 0, or neither (null). Anything else is undefined:
|
|
58
|
+
// not what KitCode writes.
|
|
59
|
+
function totalsOf(value) {
|
|
60
|
+
const hasSeconds = Object.hasOwn(value, 'focusSeconds');
|
|
61
|
+
const hasEquals = Object.hasOwn(value, 'equals');
|
|
62
|
+
|
|
63
|
+
if (!hasSeconds && !hasEquals) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return hasSeconds && hasEquals && isCount(value.focusSeconds) && isCount(value.equals)
|
|
68
|
+
? {seconds: value.focusSeconds, equals: value.equals}
|
|
69
|
+
: undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// One milestone's entry: {kind: 'before-recording'} or {kind: 'reached', reachedAt, end}, `end` the totals it was reached
|
|
73
|
+
// at, or null when the entry holds none; undefined when it is not an entry KitCode writes.
|
|
74
|
+
function entryOf(entry) {
|
|
75
|
+
if (!isObject(entry)) {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (entry.reachedBeforeRecording === true && entry.reachedAt === undefined) {
|
|
80
|
+
return totalsOf(entry) === null ? {kind: 'before-recording', reachedAt: null, end: null} : undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (isInstant(entry.reachedAt) && entry.reachedBeforeRecording === undefined) {
|
|
84
|
+
const end = totalsOf(entry);
|
|
85
|
+
return end === undefined ? undefined : {kind: 'reached', reachedAt: entry.reachedAt, end};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// A later cycle's entry, read for `percents`, `after` the number of the cycle before it: {cycle, startedAt, start, receipt,
|
|
92
|
+
// entries}, or a problem's words, a string, when it is not what KitCode writes. Each of its milestones was reached with its
|
|
93
|
+
// totals: a later cycle counts from a start it knows. Its number is past the one before it, by one when the tracker took the
|
|
94
|
+
// receipt of the cycle it was in, and by more since 2026-10-22's phase 3, when it took one of a cycle ahead, as the claim
|
|
95
|
+
// backend counts them (milestones-reached.mjs, beginNextCycle).
|
|
96
|
+
function cycleOf(value, after, percents) {
|
|
97
|
+
if (!isObject(value) || !Number.isSafeInteger(value.cycle) || value.cycle <= after || !isInstant(value.startedAt) || !isCount(value.focusSeconds) || !isCount(value.equals)
|
|
98
|
+
|| !isObject(value.receipt) || typeof value.receipt.claimId !== 'string' || !CLAIM_ID.test(value.receipt.claimId) || !isInstant(value.receipt.issuedAt)
|
|
99
|
+
|| !isObject(value.milestones)) {
|
|
100
|
+
return `holds a cycle after cycle ${after} that KitCode does not write`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const cycle = value.cycle;
|
|
104
|
+
const entries = new Map();
|
|
105
|
+
|
|
106
|
+
for (const percent of percents) {
|
|
107
|
+
if (!Object.hasOwn(value.milestones, String(percent))) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const entry = entryOf(value.milestones[String(percent)]);
|
|
112
|
+
|
|
113
|
+
if (entry?.kind !== 'reached' || entry.end === null) {
|
|
114
|
+
return `holds an entry for ${percent} % in cycle ${cycle} that KitCode does not write`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
entries.set(percent, entry);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
cycle,
|
|
122
|
+
startedAt: value.startedAt,
|
|
123
|
+
start: {seconds: value.focusSeconds, equals: value.equals},
|
|
124
|
+
receipt: {claimId: value.receipt.claimId, issuedAt: value.receipt.issuedAt},
|
|
125
|
+
entries,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// The record, read for `percents`, the milestones in order (the retired 10 and 20 % are never among them, whatever their
|
|
130
|
+
// entries hold):
|
|
131
|
+
//
|
|
132
|
+
// {state: 'none'}: no record yet, a file from before it, or one 0.1.39 rewrote;
|
|
133
|
+
// {state: 'damaged', problem}: not what KitCode writes, which is left as it is, never repaired or written over;
|
|
134
|
+
// {state: 'kept', recordingSince, freshSince, entries, cycles, current}: `entries` maps each percent recorded in cycle 1 to
|
|
135
|
+
// its entry, and `freshSince` is cycle 1's {at, seconds, equals} or null; since 2026-10-22 `cycles` lists the later cycles,
|
|
136
|
+
// oldest first, each {cycle, startedAt, start, receipt, entries}, and `current` is the cycle the milestones count in now:
|
|
137
|
+
// {cycle: 1, startedAt: null, start: {seconds: 0, equals: 0}, receipt: null, entries, freshSince} with no later one, else
|
|
138
|
+
// the last, its freshSince null.
|
|
139
|
+
//
|
|
140
|
+
// A milestone with its totals, after one whose totals are not known, counts from freshSince: without it, the record is
|
|
141
|
+
// not one KitCode writes.
|
|
142
|
+
export function readMilestoneRecord(record, percents) {
|
|
143
|
+
if (record === undefined) {
|
|
144
|
+
return {state: 'none'};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (!isRecord(record)) {
|
|
148
|
+
return {state: 'damaged', problem: 'is not the record KitCode writes'};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let freshSince = null;
|
|
152
|
+
|
|
153
|
+
if (record.freshSince !== undefined) {
|
|
154
|
+
const totals = isObject(record.freshSince) && isInstant(record.freshSince.at) ? totalsOf(record.freshSince) : undefined;
|
|
155
|
+
|
|
156
|
+
if (!totals) {
|
|
157
|
+
return {state: 'damaged', problem: 'holds a start of fresh counting (freshSince) that KitCode does not write'};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
freshSince = {at: record.freshSince.at, ...totals};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const entries = new Map();
|
|
164
|
+
|
|
165
|
+
for (const percent of percents) {
|
|
166
|
+
if (!Object.hasOwn(record.milestones, String(percent))) {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const entry = entryOf(record.milestones[String(percent)]);
|
|
171
|
+
|
|
172
|
+
if (entry === undefined) {
|
|
173
|
+
return {state: 'damaged', problem: `holds an entry for ${percent} % that KitCode does not write`};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
entries.set(percent, entry);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Each milestone's start, in order: install for the first; for another, the totals the one before it was reached at,
|
|
180
|
+
// when its entry holds them, and otherwise freshSince.
|
|
181
|
+
let previousEnd = {seconds: 0, equals: 0};
|
|
182
|
+
|
|
183
|
+
for (const percent of percents) {
|
|
184
|
+
const entry = entries.get(percent);
|
|
185
|
+
|
|
186
|
+
if (entry?.end && previousEnd === null && freshSince === null) {
|
|
187
|
+
return {state: 'damaged', problem: `holds the totals ${percent} % was reached at, with no start for them to count from`};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
previousEnd = entry?.end ?? null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Since 2026-10-22: the later cycles, oldest first, each past the one before it, the first past cycle 1.
|
|
194
|
+
const cycles = [];
|
|
195
|
+
|
|
196
|
+
if (record.cycles !== undefined) {
|
|
197
|
+
if (!Array.isArray(record.cycles)) {
|
|
198
|
+
return {state: 'damaged', problem: 'holds cycles that KitCode does not write'};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
for (const value of record.cycles) {
|
|
202
|
+
const cycle = cycleOf(value, cycles.at(-1)?.cycle ?? 1, percents);
|
|
203
|
+
|
|
204
|
+
if (typeof cycle === 'string') {
|
|
205
|
+
return {state: 'damaged', problem: cycle};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
cycles.push(cycle);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const current = cycles.at(-1) ?? {cycle: 1, startedAt: null, start: {seconds: 0, equals: 0}, receipt: null, entries, freshSince};
|
|
213
|
+
return {state: 'kept', recordingSince: record.recordingSince, freshSince, entries, cycles, current: {freshSince: null, ...current}};
|
|
214
|
+
}
|