@kitkat-vn/kitcode 0.2.0-staging.9 → 0.3.0-staging.15
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 +45 -28
- package/bin/kitcode.mjs +9 -1
- 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 +7 -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 +15 -3
- package/src/integration-spec.mjs +175 -48
- package/src/milestone-record.mjs +214 -0
- package/src/milestones-reached.mjs +163 -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 +214 -63
- package/src/runtime.mjs +48 -4
- package/src/welcome-words.mjs +115 -0
- package/src/window-page.mjs +16 -2
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
// A cycle receipt (docs/backend.md, "The cycle receipt"; the plan of 2026-10-21, phase 2): what the claim backend signs once it
|
|
4
|
+
// has issued a voucher, and the dashboard hands to this tracker, so that the tracker begins the next cycle, whose milestones
|
|
5
|
+
// count fresh from that moment (decision 1). Its words are the server's: the install id the claim's bundle carried, the cycle
|
|
6
|
+
// the server counted for it, the claim's id and when its voucher was issued, signed with an Ed25519 key that exists on the
|
|
7
|
+
// claim backend's server alone. This file only reads one, with the matching public key, which a tester build carries
|
|
8
|
+
// (integration-spec.mjs, CYCLE_RECEIPT_PUBLIC_KEY). Pure: it reads no file, keeps nothing, and asks nothing of the network;
|
|
9
|
+
// whether the receipt is this install's, and of the cycle it is in, is the caller's to judge, against the record
|
|
10
|
+
// (milestones-reached.mjs, beginNextCycle).
|
|
11
|
+
//
|
|
12
|
+
// <payload>.<signature>
|
|
13
|
+
//
|
|
14
|
+
// `payload` is base64url (no padding) of the UTF-8 JSON {"v":1,"installId":…,"cycle":…,"claimId":…,"issuedAt":…}, those five
|
|
15
|
+
// fields alone; `signature`, base64url of the 64-byte Ed25519 signature of the ASCII text `kitcode-cycle-receipt-1.` followed
|
|
16
|
+
// by `payload` as it stands. The signature is checked over the bytes as they came, before anything in them is read: nothing is
|
|
17
|
+
// put in a canonical form, and no field the server did not sign is read. The text names what it is, so that the key signs
|
|
18
|
+
// nothing else that could pass for one. The server's side is apps/api/src/cycle-receipts.ts.
|
|
19
|
+
|
|
20
|
+
export const CYCLE_RECEIPT_CONTEXT = 'kitcode-cycle-receipt-1.';
|
|
21
|
+
|
|
22
|
+
// The longest text read: a receipt is about 300 characters.
|
|
23
|
+
export const MAX_CYCLE_RECEIPT_LENGTH = 1024;
|
|
24
|
+
|
|
25
|
+
const RECEIPT_TEXT = /^([A-Za-z0-9_-]{16,900})\.([A-Za-z0-9_-]{86})$/u;
|
|
26
|
+
const PUBLIC_KEY_TEXT = /^[A-Za-z0-9_-]{43}$/u;
|
|
27
|
+
const INSTALL_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
|
|
28
|
+
const CLAIM_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/u;
|
|
29
|
+
const INSTANT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u;
|
|
30
|
+
const PAYLOAD_KEYS = ['v', 'installId', 'cycle', 'claimId', 'issuedAt'];
|
|
31
|
+
|
|
32
|
+
function isInstant(value) {
|
|
33
|
+
if (typeof value !== 'string' || !INSTANT.test(value)) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const time = Date.parse(value);
|
|
38
|
+
return Number.isFinite(time) && new Date(time).toISOString() === value;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// The public key a build carries: base64url of its 32 raw bytes, the JWK's `x`. Null when it is not one.
|
|
42
|
+
export function cycleReceiptKey(text) {
|
|
43
|
+
if (typeof text !== 'string' || !PUBLIC_KEY_TEXT.test(text)) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const key = crypto.createPublicKey({key: {kty: 'OKP', crv: 'Ed25519', x: text}, format: 'jwk'});
|
|
49
|
+
return key.asymmetricKeyType === 'ed25519' && key.export({format: 'jwk'}).x === text ? key : null;
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Reads `text` with `publicKey` (a KeyObject, cycleReceiptKey's): {ok: true, receipt: {installId, cycle, claimId, issuedAt}},
|
|
56
|
+
// or {ok: false, problem}: 'unreadable', not the shape of a receipt, or its signed words not a receipt's; 'signature', not
|
|
57
|
+
// signed with the key's pair.
|
|
58
|
+
export function readCycleReceipt(text, publicKey) {
|
|
59
|
+
const parts = typeof text === 'string' && text.length <= MAX_CYCLE_RECEIPT_LENGTH ? RECEIPT_TEXT.exec(text) : null;
|
|
60
|
+
|
|
61
|
+
if (parts === null) {
|
|
62
|
+
return {ok: false, problem: 'unreadable'};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const [, payload, signature] = parts;
|
|
66
|
+
let signed;
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
signed = crypto.verify(null, Buffer.from(`${CYCLE_RECEIPT_CONTEXT}${payload}`, 'ascii'), publicKey, Buffer.from(signature, 'base64url'));
|
|
70
|
+
} catch {
|
|
71
|
+
signed = false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!signed) {
|
|
75
|
+
return {ok: false, problem: 'signature'};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let words;
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
words = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
|
|
82
|
+
} catch {
|
|
83
|
+
return {ok: false, problem: 'unreadable'};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const shaped = words !== null && typeof words === 'object' && !Array.isArray(words)
|
|
87
|
+
&& Object.keys(words).length === PAYLOAD_KEYS.length && PAYLOAD_KEYS.every((key) => Object.hasOwn(words, key))
|
|
88
|
+
&& words.v === 1
|
|
89
|
+
&& typeof words.installId === 'string' && INSTALL_ID.test(words.installId)
|
|
90
|
+
&& Number.isSafeInteger(words.cycle) && words.cycle >= 1
|
|
91
|
+
&& typeof words.claimId === 'string' && CLAIM_ID.test(words.claimId)
|
|
92
|
+
&& isInstant(words.issuedAt);
|
|
93
|
+
|
|
94
|
+
return shaped
|
|
95
|
+
? {ok: true, receipt: {installId: words.installId, cycle: words.cycle, claimId: words.claimId, issuedAt: words.issuedAt}}
|
|
96
|
+
: {ok: false, problem: 'unreadable'};
|
|
97
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono)
|
|
2
|
+
|
|
3
|
+
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
|
4
|
+
|
|
5
|
+
This license is copied below, and is also available with a FAQ at: https://scripts.sil.org/OFL
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
-----------------------------------------------------------
|
|
9
|
+
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
|
10
|
+
-----------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
PREAMBLE
|
|
13
|
+
The goals of the Open Font License (OFL) are to stimulate worldwide
|
|
14
|
+
development of collaborative font projects, to support the font creation
|
|
15
|
+
efforts of academic and linguistic communities, and to provide a free and
|
|
16
|
+
open framework in which fonts may be shared and improved in partnership
|
|
17
|
+
with others.
|
|
18
|
+
|
|
19
|
+
The OFL allows the licensed fonts to be used, studied, modified and
|
|
20
|
+
redistributed freely as long as they are not sold by themselves. The
|
|
21
|
+
fonts, including any derivative works, can be bundled, embedded,
|
|
22
|
+
redistributed and/or sold with any software provided that any reserved
|
|
23
|
+
names are not used by derivative works. The fonts and derivatives,
|
|
24
|
+
however, cannot be released under any other type of license. The
|
|
25
|
+
requirement for fonts to remain under this license does not apply
|
|
26
|
+
to any document created using the fonts or their derivatives.
|
|
27
|
+
|
|
28
|
+
DEFINITIONS
|
|
29
|
+
"Font Software" refers to the set of files released by the Copyright
|
|
30
|
+
Holder(s) under this license and clearly marked as such. This may
|
|
31
|
+
include source files, build scripts and documentation.
|
|
32
|
+
|
|
33
|
+
"Reserved Font Name" refers to any names specified as such after the
|
|
34
|
+
copyright statement(s).
|
|
35
|
+
|
|
36
|
+
"Original Version" refers to the collection of Font Software components as
|
|
37
|
+
distributed by the Copyright Holder(s).
|
|
38
|
+
|
|
39
|
+
"Modified Version" refers to any derivative made by adding to, deleting,
|
|
40
|
+
or substituting -- in part or in whole -- any of the components of the
|
|
41
|
+
Original Version, by changing formats or by porting the Font Software to a
|
|
42
|
+
new environment.
|
|
43
|
+
|
|
44
|
+
"Author" refers to any designer, engineer, programmer, technical
|
|
45
|
+
writer or other person who contributed to the Font Software.
|
|
46
|
+
|
|
47
|
+
PERMISSION & CONDITIONS
|
|
48
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
49
|
+
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
|
50
|
+
redistribute, and sell modified and unmodified copies of the Font
|
|
51
|
+
Software, subject to the following conditions:
|
|
52
|
+
|
|
53
|
+
1) Neither the Font Software nor any of its individual components,
|
|
54
|
+
in Original or Modified Versions, may be sold by itself.
|
|
55
|
+
|
|
56
|
+
2) Original or Modified Versions of the Font Software may be bundled,
|
|
57
|
+
redistributed and/or sold with any software, provided that each copy
|
|
58
|
+
contains the above copyright notice and this license. These can be
|
|
59
|
+
included either as stand-alone text files, human-readable headers or
|
|
60
|
+
in the appropriate machine-readable metadata fields within text or
|
|
61
|
+
binary files as long as those fields can be easily viewed by the user.
|
|
62
|
+
|
|
63
|
+
3) No Modified Version of the Font Software may use the Reserved Font
|
|
64
|
+
Name(s) unless explicit written permission is granted by the corresponding
|
|
65
|
+
Copyright Holder. This restriction only applies to the primary font name as
|
|
66
|
+
presented to the users.
|
|
67
|
+
|
|
68
|
+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
|
69
|
+
Software shall not be used to promote, endorse or advertise any
|
|
70
|
+
Modified Version, except to acknowledge the contribution(s) of the
|
|
71
|
+
Copyright Holder(s) and the Author(s) or with their explicit written
|
|
72
|
+
permission.
|
|
73
|
+
|
|
74
|
+
5) The Font Software, modified or unmodified, in part or in whole,
|
|
75
|
+
must be distributed entirely under this license, and must not be
|
|
76
|
+
distributed under any other license. The requirement for fonts to
|
|
77
|
+
remain under this license does not apply to any document created
|
|
78
|
+
using the Font Software.
|
|
79
|
+
|
|
80
|
+
TERMINATION
|
|
81
|
+
This license becomes null and void if any of the above conditions are
|
|
82
|
+
not met.
|
|
83
|
+
|
|
84
|
+
DISCLAIMER
|
|
85
|
+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
86
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
87
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
|
88
|
+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
|
89
|
+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
90
|
+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
91
|
+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
92
|
+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
|
93
|
+
OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/src/hook-prompt.mjs
CHANGED
|
@@ -104,11 +104,21 @@ function notify(title, message) {
|
|
|
104
104
|
// A milestone reached is said in neutral words (the operator's decisions 5 and 6 of 2026-10-01): its message, from the
|
|
105
105
|
// campaign, is on the dashboard, and no milestone is a reward ready, or a code. Until then this said "Reward ready at",
|
|
106
106
|
// and the announcement below "KitCode break code ready".
|
|
107
|
+
//
|
|
108
|
+
// Since 2026-10-22 (rewards/3-repeat-vouchers, phase 2) the milestones are those of the cycle counted in (reward.mjs): from cycle
|
|
109
|
+
// 2 on the context names it, "KitCode progress (cycle 2): …", and the milestones it lists and announces are that cycle's; the =
|
|
110
|
+
// and the time are the totals, which keep counting. Cycle 1 reads as it always has. The cycle is the record's, read with the
|
|
111
|
+
// rest of the file: no receipt is checked here (hard rule 7).
|
|
112
|
+
function cycleOf(reward) {
|
|
113
|
+
return Number.isSafeInteger(reward?.cycle) && reward.cycle > 1 ? reward.cycle : null;
|
|
114
|
+
}
|
|
115
|
+
|
|
107
116
|
function progressContextFor(reward) {
|
|
108
117
|
const nextBreak = reward.nextBreak ?? nextBreakReminder(reward);
|
|
109
118
|
const ready = reward.milestones.filter((entry) => entry.status === 'ready' && !entry.redeemed);
|
|
119
|
+
const cycle = cycleOf(reward);
|
|
110
120
|
const parts = [
|
|
111
|
-
`KitCode progress: ${reward.totalEquals} = counted, ${formatBreakDuration(reward.earnedSeconds)} active.`,
|
|
121
|
+
`KitCode progress${cycle === null ? '' : ` (cycle ${cycle})`}: ${reward.totalEquals} = counted, ${formatBreakDuration(reward.earnedSeconds)} active.`,
|
|
112
122
|
];
|
|
113
123
|
|
|
114
124
|
if (ready.length > 0) {
|
|
@@ -140,7 +150,8 @@ function hookOutputFor({tiers = [], reward = null}) {
|
|
|
140
150
|
};
|
|
141
151
|
}
|
|
142
152
|
|
|
143
|
-
const
|
|
153
|
+
const cycle = cycleOf(reward);
|
|
154
|
+
const labels = `${tiers.map((tier) => `${tier.percent}%`).join(', ')}${cycle === null ? '' : `, cycle ${cycle}`}`;
|
|
144
155
|
const command = 'kitcode dashboard';
|
|
145
156
|
const additionalContext = [
|
|
146
157
|
progressContext,
|
|
@@ -206,7 +217,8 @@ export async function runPromptHook({source}) {
|
|
|
206
217
|
}
|
|
207
218
|
|
|
208
219
|
const label = announced.map((tier) => `${tier.percent}%`).join(', ');
|
|
209
|
-
|
|
220
|
+
const cycle = cycleOf(reward);
|
|
221
|
+
notify('KitCode milestone reached', `Milestone ${label}${cycle === null ? '' : ` of cycle ${cycle}`} unlocked. Open kitcode dashboard.`);
|
|
210
222
|
process.stdout.write(`${JSON.stringify(hookOutputFor({tiers: announced, reward}))}\n`);
|
|
211
223
|
} catch (error) {
|
|
212
224
|
failure = error;
|
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}`,
|