@jskit-ai/jskit-cli 0.2.79 → 0.2.81

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.
@@ -0,0 +1,163 @@
1
+ import { mkdir, rename } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import {
4
+ SESSION_ID_PATTERN,
5
+ SESSION_STATE_RELATIVE_PATH
6
+ } from "./constants.js";
7
+ import {
8
+ fileExists,
9
+ normalizeText
10
+ } from "./io.js";
11
+
12
+ function formatDatePart(value) {
13
+ return String(value).padStart(2, "0");
14
+ }
15
+
16
+ function createSessionId(now = new Date()) {
17
+ const year = now.getFullYear();
18
+ const month = formatDatePart(now.getMonth() + 1);
19
+ const day = formatDatePart(now.getDate());
20
+ const hour = formatDatePart(now.getHours());
21
+ const minute = formatDatePart(now.getMinutes());
22
+ const second = formatDatePart(now.getSeconds());
23
+ return `${year}-${month}-${day}_${hour}-${minute}-${second}`;
24
+ }
25
+
26
+ async function createAvailableSessionId(targetRoot, now = new Date()) {
27
+ const baseSessionId = createSessionId(now);
28
+ const basePaths = resolveSessionPaths({ targetRoot, sessionId: baseSessionId });
29
+ if (
30
+ !(await fileExists(basePaths.sessionRoot)) &&
31
+ !(await fileExists(basePaths.completedSessionRoot)) &&
32
+ !(await fileExists(basePaths.abandonedSessionRoot))
33
+ ) {
34
+ return baseSessionId;
35
+ }
36
+
37
+ for (let index = 1; index <= 36 ** 4 - 1; index += 1) {
38
+ const suffix = index.toString(36).padStart(4, "0");
39
+ const candidate = `${baseSessionId}-${suffix}`;
40
+ const candidatePaths = resolveSessionPaths({ targetRoot, sessionId: candidate });
41
+ if (
42
+ !(await fileExists(candidatePaths.sessionRoot)) &&
43
+ !(await fileExists(candidatePaths.completedSessionRoot)) &&
44
+ !(await fileExists(candidatePaths.abandonedSessionRoot))
45
+ ) {
46
+ return candidate;
47
+ }
48
+ }
49
+
50
+ throw new Error(`No available session id found for timestamp ${baseSessionId}.`);
51
+ }
52
+
53
+ function isValidSessionId(sessionId = "") {
54
+ return SESSION_ID_PATTERN.test(normalizeText(sessionId));
55
+ }
56
+
57
+ function normalizeSessionId(sessionId = "") {
58
+ const normalized = normalizeText(sessionId);
59
+ if (!isValidSessionId(normalized)) {
60
+ throw new Error(`Invalid session id "${sessionId}". Expected YYYY-MM-DD_HH-MM-SS.`);
61
+ }
62
+ return normalized;
63
+ }
64
+
65
+ function resolveSessionPaths({ targetRoot, sessionId = "" } = {}) {
66
+ const normalizedTargetRoot = path.resolve(normalizeText(targetRoot) || process.cwd());
67
+ const sessionStateRoot = path.join(normalizedTargetRoot, SESSION_STATE_RELATIVE_PATH);
68
+ const sessionsRoot = path.join(sessionStateRoot, "active");
69
+ const completedSessionsRoot = path.join(sessionStateRoot, "completed");
70
+ const abandonedSessionsRoot = path.join(sessionStateRoot, "abandoned");
71
+ const normalizedSessionId = sessionId ? normalizeSessionId(sessionId) : "";
72
+ const sessionRoot = normalizedSessionId ? path.join(sessionsRoot, normalizedSessionId) : "";
73
+ const worktree = normalizedSessionId ? path.join(sessionRoot, "worktree") : "";
74
+ const branch = normalizedSessionId ? `jskit-studio/${normalizedSessionId}` : "";
75
+
76
+ return Object.freeze({
77
+ abandonedSessionRoot: normalizedSessionId ? path.join(abandonedSessionsRoot, normalizedSessionId) : "",
78
+ abandonedSessionsRoot,
79
+ branch,
80
+ completedSessionRoot: normalizedSessionId ? path.join(completedSessionsRoot, normalizedSessionId) : "",
81
+ completedSessionsRoot,
82
+ sessionId: normalizedSessionId,
83
+ sessionRoot,
84
+ sessionsRoot,
85
+ sessionStateRoot,
86
+ targetRoot: normalizedTargetRoot,
87
+ worktree
88
+ });
89
+ }
90
+
91
+ async function resolveExistingSessionRoot(paths) {
92
+ if (paths.archive && await fileExists(paths.sessionRoot)) {
93
+ return {
94
+ archive: paths.archive,
95
+ root: paths.sessionRoot
96
+ };
97
+ }
98
+ if (await fileExists(paths.sessionRoot)) {
99
+ return {
100
+ archive: "active",
101
+ root: paths.sessionRoot
102
+ };
103
+ }
104
+ if (await fileExists(paths.completedSessionRoot)) {
105
+ return {
106
+ archive: "completed",
107
+ root: paths.completedSessionRoot
108
+ };
109
+ }
110
+ if (await fileExists(paths.abandonedSessionRoot)) {
111
+ return {
112
+ archive: "abandoned",
113
+ root: paths.abandonedSessionRoot
114
+ };
115
+ }
116
+ return {
117
+ archive: "",
118
+ root: ""
119
+ };
120
+ }
121
+
122
+ async function pathsForExistingSession(paths) {
123
+ const existing = await resolveExistingSessionRoot(paths);
124
+ if (!existing.root || existing.root === paths.sessionRoot) {
125
+ return paths;
126
+ }
127
+ return Object.freeze({
128
+ ...paths,
129
+ archive: existing.archive,
130
+ sessionRoot: existing.root
131
+ });
132
+ }
133
+
134
+ async function archiveSession(paths, archive) {
135
+ const archiveRoot = archive === "completed" ? paths.completedSessionRoot : paths.abandonedSessionRoot;
136
+ if (!archiveRoot || paths.sessionRoot === archiveRoot) {
137
+ return paths;
138
+ }
139
+ if (!(await fileExists(paths.sessionRoot))) {
140
+ return pathsForExistingSession(paths);
141
+ }
142
+ if (await fileExists(archiveRoot)) {
143
+ throw new Error(`Cannot archive session ${paths.sessionId}; target already exists: ${archiveRoot}`);
144
+ }
145
+ await mkdir(path.dirname(archiveRoot), { recursive: true });
146
+ await rename(paths.sessionRoot, archiveRoot);
147
+ return Object.freeze({
148
+ ...paths,
149
+ archive,
150
+ sessionRoot: archiveRoot
151
+ });
152
+ }
153
+
154
+ export {
155
+ archiveSession,
156
+ createAvailableSessionId,
157
+ createSessionId,
158
+ isValidSessionId,
159
+ normalizeSessionId,
160
+ resolveExistingSessionRoot,
161
+ resolveSessionPaths,
162
+ pathsForExistingSession
163
+ };
@@ -0,0 +1,362 @@
1
+ import {
2
+ access,
3
+ appendFile,
4
+ mkdir
5
+ } from "node:fs/promises";
6
+ import { constants as fsConstants } from "node:fs";
7
+ import path from "node:path";
8
+ import {
9
+ SESSION_STATE_RELATIVE_PATH
10
+ } from "./constants.js";
11
+ import {
12
+ createError,
13
+ createPrecondition
14
+ } from "./responses.js";
15
+ import {
16
+ readTextIfExists,
17
+ readTrimmedFile,
18
+ runCommand,
19
+ runGit
20
+ } from "./io.js";
21
+ import {
22
+ resolveExistingSessionRoot
23
+ } from "./paths.js";
24
+ import {
25
+ hasWorktree
26
+ } from "./worktrees.js";
27
+
28
+ async function assertTargetRootWritable(targetRoot) {
29
+ try {
30
+ await access(targetRoot, fsConstants.R_OK | fsConstants.W_OK);
31
+ return {
32
+ ok: true,
33
+ precondition: createPrecondition({
34
+ id: "target_root_writable",
35
+ ok: true,
36
+ message: "Target root exists and is readable/writable."
37
+ })
38
+ };
39
+ } catch (error) {
40
+ return {
41
+ ok: false,
42
+ error: createError({
43
+ code: "target_root_not_writable",
44
+ message: `Target root is not readable/writable: ${error?.message || error}`,
45
+ repairCommand: `test -w ${targetRoot}`
46
+ }),
47
+ precondition: createPrecondition({
48
+ id: "target_root_writable",
49
+ ok: false,
50
+ message: "Target root exists and is readable/writable."
51
+ })
52
+ };
53
+ }
54
+ }
55
+
56
+ async function resolveGitCommonDirectory(targetRoot) {
57
+ const result = await runGit(targetRoot, ["rev-parse", "--git-common-dir"]);
58
+ if (!result.ok || !result.stdout) {
59
+ return "";
60
+ }
61
+ return path.resolve(targetRoot, result.stdout);
62
+ }
63
+
64
+ async function assertGitRepository(targetRoot) {
65
+ const result = await runGit(targetRoot, ["rev-parse", "--is-inside-work-tree"]);
66
+ if (result.ok && result.stdout === "true") {
67
+ return {
68
+ ok: true,
69
+ precondition: createPrecondition({
70
+ id: "git_repository",
71
+ ok: true,
72
+ message: "Target root is inside a git work tree."
73
+ })
74
+ };
75
+ }
76
+ return {
77
+ ok: false,
78
+ error: createError({
79
+ code: "git_repository_missing",
80
+ message: "Target root is not inside a git work tree.",
81
+ repairCommand: "git init"
82
+ }),
83
+ precondition: createPrecondition({
84
+ id: "git_repository",
85
+ ok: false,
86
+ message: "Target root is inside a git work tree."
87
+ })
88
+ };
89
+ }
90
+
91
+ async function assertGitCurrentBranch(targetRoot) {
92
+ const [branchResult, headResult] = await Promise.all([
93
+ runGit(targetRoot, ["branch", "--show-current"]),
94
+ runGit(targetRoot, ["rev-parse", "--verify", "HEAD"])
95
+ ]);
96
+ if (branchResult.ok && branchResult.stdout && headResult.ok) {
97
+ return {
98
+ ok: true,
99
+ precondition: createPrecondition({
100
+ id: "git_current_branch",
101
+ ok: true,
102
+ message: "Target repository has a named current branch with an initial commit."
103
+ })
104
+ };
105
+ }
106
+ return {
107
+ ok: false,
108
+ error: createError({
109
+ code: "git_current_branch_missing",
110
+ message: "Target repository does not have a named current branch with an initial commit.",
111
+ repairCommand: "git checkout -b main && git add . && git commit -m \"Initial commit\""
112
+ }),
113
+ precondition: createPrecondition({
114
+ id: "git_current_branch",
115
+ ok: false,
116
+ message: "Target repository has a named current branch with an initial commit."
117
+ })
118
+ };
119
+ }
120
+
121
+ async function assertGhAuth(targetRoot) {
122
+ const result = await runCommand("gh", ["auth", "status"], {
123
+ cwd: targetRoot,
124
+ timeout: 15000
125
+ });
126
+ if (result.ok) {
127
+ return {
128
+ ok: true,
129
+ precondition: createPrecondition({
130
+ id: "github_auth",
131
+ ok: true,
132
+ message: "GitHub CLI is authenticated."
133
+ })
134
+ };
135
+ }
136
+ return {
137
+ ok: false,
138
+ error: createError({
139
+ code: "github_auth_missing",
140
+ message: "GitHub CLI is not authenticated.",
141
+ repairCommand: "gh auth login"
142
+ }),
143
+ precondition: createPrecondition({
144
+ id: "github_auth",
145
+ ok: false,
146
+ message: "GitHub CLI is authenticated."
147
+ })
148
+ };
149
+ }
150
+
151
+ async function assertGithubOrigin(targetRoot) {
152
+ const result = await runGit(targetRoot, ["remote", "get-url", "origin"]);
153
+ if (result.ok && /github\.com[:/]/u.test(result.stdout)) {
154
+ return {
155
+ ok: true,
156
+ precondition: createPrecondition({
157
+ id: "github_origin",
158
+ ok: true,
159
+ message: "Origin remote points at GitHub."
160
+ })
161
+ };
162
+ }
163
+ return {
164
+ ok: false,
165
+ error: createError({
166
+ code: "github_origin_missing",
167
+ message: "Origin remote is missing or is not a GitHub remote.",
168
+ repairCommand: "git remote add origin https://github.com/<owner>/<repo>.git"
169
+ }),
170
+ precondition: createPrecondition({
171
+ id: "github_origin",
172
+ ok: false,
173
+ message: "Origin remote points at GitHub."
174
+ })
175
+ };
176
+ }
177
+
178
+ async function applyPreconditions(paths, checks = []) {
179
+ const preconditions = [];
180
+ for (const check of checks) {
181
+ const result = await check();
182
+ if (result?.precondition) {
183
+ preconditions.push(result.precondition);
184
+ }
185
+ if (result?.ok !== true) {
186
+ return {
187
+ ok: false,
188
+ error: result.error,
189
+ preconditions
190
+ };
191
+ }
192
+ }
193
+ void paths;
194
+ return {
195
+ ok: true,
196
+ preconditions
197
+ };
198
+ }
199
+
200
+ async function ensureStudioGitExclude(targetRoot) {
201
+ const gitCommonDirectory = await resolveGitCommonDirectory(targetRoot);
202
+ if (!gitCommonDirectory) {
203
+ return;
204
+ }
205
+ const excludePath = path.join(gitCommonDirectory, "info", "exclude");
206
+ await mkdir(path.dirname(excludePath), { recursive: true });
207
+ const current = await readTextIfExists(excludePath);
208
+ const lines = current.split(/\r?\n/u).map((line) => line.trim());
209
+ if (!lines.includes(`${SESSION_STATE_RELATIVE_PATH}/`)) {
210
+ await appendFile(excludePath, `${current.endsWith("\n") || current.length === 0 ? "" : "\n"}${SESSION_STATE_RELATIVE_PATH}/\n`, "utf8");
211
+ }
212
+ }
213
+
214
+ async function assertSessionExists(paths) {
215
+ const existing = await resolveExistingSessionRoot(paths);
216
+ if (existing.root) {
217
+ return {
218
+ ok: true,
219
+ precondition: createPrecondition({
220
+ id: "session_exists",
221
+ ok: true,
222
+ message: "Session state directory exists."
223
+ })
224
+ };
225
+ }
226
+ return {
227
+ ok: false,
228
+ error: createError({
229
+ code: "session_missing",
230
+ message: `Session does not exist: ${paths.sessionId}`,
231
+ repairCommand: "jskit session create"
232
+ }),
233
+ precondition: createPrecondition({
234
+ id: "session_exists",
235
+ ok: false,
236
+ message: "Session state directory exists."
237
+ })
238
+ };
239
+ }
240
+
241
+ async function assertIssueTextExists(paths) {
242
+ const issueText = await readTrimmedFile(path.join(paths.sessionRoot, "issue.md"));
243
+ if (issueText) {
244
+ return {
245
+ ok: true,
246
+ precondition: createPrecondition({
247
+ id: "issue_text_exists",
248
+ ok: true,
249
+ message: "Issue text exists."
250
+ })
251
+ };
252
+ }
253
+ return {
254
+ ok: false,
255
+ error: createError({
256
+ code: "issue_text_missing",
257
+ message: "Cannot create a GitHub issue before issue.md exists.",
258
+ repairCommand: `jskit session ${paths.sessionId} step --issue -`
259
+ }),
260
+ precondition: createPrecondition({
261
+ id: "issue_text_exists",
262
+ ok: false,
263
+ message: "Issue text exists."
264
+ })
265
+ };
266
+ }
267
+
268
+ async function assertIssueUrlExists(paths) {
269
+ const issueUrl = await readTrimmedFile(path.join(paths.sessionRoot, "issue_url"));
270
+ if (issueUrl) {
271
+ return {
272
+ ok: true,
273
+ precondition: createPrecondition({
274
+ id: "issue_url_exists",
275
+ ok: true,
276
+ message: "GitHub issue URL exists."
277
+ })
278
+ };
279
+ }
280
+ return {
281
+ ok: false,
282
+ error: createError({
283
+ code: "issue_url_missing",
284
+ message: "Cannot create a plan before the GitHub issue exists.",
285
+ repairCommand: `jskit session ${paths.sessionId} step`
286
+ }),
287
+ precondition: createPrecondition({
288
+ id: "issue_url_exists",
289
+ ok: false,
290
+ message: "GitHub issue URL exists."
291
+ })
292
+ };
293
+ }
294
+
295
+ async function assertWorktreeExists(paths) {
296
+ if (await hasWorktree(paths)) {
297
+ return {
298
+ ok: true,
299
+ precondition: createPrecondition({
300
+ id: "worktree_exists",
301
+ ok: true,
302
+ message: "Session worktree exists."
303
+ })
304
+ };
305
+ }
306
+ return {
307
+ ok: false,
308
+ error: createError({
309
+ code: "worktree_missing",
310
+ message: "Session worktree does not exist.",
311
+ repairCommand: `jskit session ${paths.sessionId} step`
312
+ }),
313
+ precondition: createPrecondition({
314
+ id: "worktree_exists",
315
+ ok: false,
316
+ message: "Session worktree exists."
317
+ })
318
+ };
319
+ }
320
+
321
+ async function assertPrUrlExists(paths) {
322
+ const prUrl = await readTrimmedFile(path.join(paths.sessionRoot, "pr_url"));
323
+ if (prUrl) {
324
+ return {
325
+ ok: true,
326
+ precondition: createPrecondition({
327
+ id: "pr_url_exists",
328
+ ok: true,
329
+ message: "PR URL exists."
330
+ })
331
+ };
332
+ }
333
+ return {
334
+ ok: false,
335
+ error: createError({
336
+ code: "pr_url_missing",
337
+ message: "Cannot merge before pr_url exists.",
338
+ repairCommand: `jskit session ${paths.sessionId} step`
339
+ }),
340
+ precondition: createPrecondition({
341
+ id: "pr_url_exists",
342
+ ok: false,
343
+ message: "PR URL exists."
344
+ })
345
+ };
346
+ }
347
+
348
+ export {
349
+ applyPreconditions,
350
+ assertGhAuth,
351
+ assertGitCurrentBranch,
352
+ assertGitRepository,
353
+ assertGithubOrigin,
354
+ assertIssueTextExists,
355
+ assertIssueUrlExists,
356
+ assertPrUrlExists,
357
+ assertSessionExists,
358
+ assertTargetRootWritable,
359
+ assertWorktreeExists,
360
+ ensureStudioGitExclude,
361
+ hasWorktree
362
+ };
@@ -0,0 +1,41 @@
1
+ import path from "node:path";
2
+ import {
3
+ PROMPT_DIRECTORY,
4
+ SESSION_STATE_RELATIVE_PATH
5
+ } from "./constants.js";
6
+ import {
7
+ fileExists,
8
+ normalizeText,
9
+ readTextIfExists
10
+ } from "./io.js";
11
+
12
+ async function readPromptTemplate(targetRoot, name) {
13
+ const normalizedName = normalizeText(name);
14
+ const overridePath = path.join(targetRoot, SESSION_STATE_RELATIVE_PATH, "prompts", normalizedName);
15
+ if (await fileExists(overridePath)) {
16
+ return readTextIfExists(overridePath);
17
+ }
18
+ return readTextIfExists(path.join(PROMPT_DIRECTORY, normalizedName));
19
+ }
20
+
21
+ function renderTemplate(source, values = {}) {
22
+ return String(source || "").replace(/\{\{\s*([A-Za-z0-9_]+)\s*\}\}/gu, (_match, key) => {
23
+ return String(values[key] ?? "");
24
+ });
25
+ }
26
+
27
+ async function renderPrompt(paths, templateName, values = {}) {
28
+ const template = await readPromptTemplate(paths.targetRoot, templateName);
29
+ return renderTemplate(template, {
30
+ branch: paths.branch,
31
+ session_id: paths.sessionId,
32
+ worktree: paths.worktree,
33
+ ...values
34
+ }).trim();
35
+ }
36
+
37
+ export {
38
+ readPromptTemplate,
39
+ renderPrompt,
40
+ renderTemplate
41
+ };
@@ -0,0 +1,52 @@
1
+ Create a durable JSKIT app blueprint from this app brief.
2
+
3
+ App brief:
4
+
5
+ {{app_brief}}
6
+
7
+ Produce a concise but useful app-level blueprint. It must describe product intent, platform choices, and architectural boundaries. It must not become an implementation workboard for one issue.
8
+
9
+ Before writing the blueprint, classify the app state if local files are available:
10
+
11
+ - empty
12
+ - non_jskit_repo
13
+ - partial_jskit_app
14
+ - jskit_app
15
+
16
+ Use these markers for a real JSKIT app when they exist:
17
+
18
+ - package.json
19
+ - config/public.js
20
+ - src/main.js
21
+ - packages/main/package.descriptor.mjs
22
+ - .jskit/lock.json
23
+
24
+ If the app is empty or only a fresh minimal scaffold, keep platform choices explicit and provisional until decided. Do not treat a missing `config.tenancyMode` line or untouched minimal scaffold as a final tenancy decision.
25
+
26
+ Cover:
27
+
28
+ - App purpose and what the app will do in general.
29
+ - Primary users and actors.
30
+ - Type of multihoming or tenancy: none, personal, workspaces, or another explicit model from the brief.
31
+ - Database engine, if the app needs persistence.
32
+ - Auth provider, if the app needs auth.
33
+ - The role of each surface: app, admin, console, settings, public, workspace, or any app-specific surface from the brief.
34
+ - Global view of the main product areas and navigation destinations.
35
+ - Key domain concepts and data objects, without inventing database schemas unless the brief clearly requires them.
36
+ - Ownership model per persistent entity when it is already clear: public, user, workspace, or workspace_user.
37
+ - Baseline JSKIT package workflows to accept as defaults and any intended overrides.
38
+ - Package install, generator, and custom-code areas at a high level.
39
+ - CRUDs likely to need server ownership, and any narrow exceptions that should be called out later.
40
+ - Important non-goals and constraints.
41
+ - First useful screen and what it should show.
42
+ - Settings, admin, and operator expectations when relevant.
43
+ - Verification expectations, including UI checks for user-facing screens.
44
+
45
+ If the brief is ambiguous, state the assumption in the blueprint instead of asking questions. Do not invent detailed feature behavior that the brief does not support.
46
+
47
+ When the blueprint is ready, output only the final markdown surrounded by these exact markers:
48
+
49
+ [app_blueprint]
50
+ # App Blueprint
51
+ ...
52
+ [/app_blueprint]
@@ -0,0 +1,26 @@
1
+ The JSKIT session doctor or verification step failed.
2
+
3
+ Session:
4
+
5
+ {{session_id}}
6
+
7
+ Worktree:
8
+
9
+ {{worktree}}
10
+
11
+ Failure output:
12
+
13
+ {{doctor_output}}
14
+
15
+ Fix the root cause in this worktree. Do not silence the failure or remove checks to make the step pass.
16
+
17
+ Diagnosis rules:
18
+
19
+ - Identify whether the failure is dependency/setup, JSKIT metadata, generated contract drift, routing/surface wiring, CRUD ownership, UI verification receipt, test-auth, or ordinary application code.
20
+ - Prefer repairing the JSKIT-owned contract or generated metadata over adding local-path hacks.
21
+ - Do not run `npm install` only because optional agent docs are missing. Run installs only when the failure or a JSKIT setup/session step requires dependency repair.
22
+ - If a generator/package command is the correct repair, use the `jskit` command rather than hand-recreating generated structure.
23
+ - For UI receipt failures, run the relevant Playwright check and record it with `jskit app verify-ui --command "<playwright command>" --feature "<label>" --auth-mode <mode>` when possible.
24
+ - If login is required, use the app's development auth bootstrap path rather than a live external auth flow.
25
+
26
+ Do not push, open a PR, merge, or remove the worktree. When the fix is ready, report the root cause, files changed, verification, and anything still unverified. The user or Studio will rerun the JSKIT session step.
@@ -0,0 +1,35 @@
1
+ Execute the approved implementation plan for JSKIT session {{session_id}}.
2
+
3
+ GitHub issue: {{issue_url}}
4
+ Issue number: {{issue_number}}
5
+ Issue title: {{issue_title}}
6
+ Issue body file: {{issue_file}}
7
+ Plan file: {{plan_file}}
8
+ Worktree: {{worktree}}
9
+
10
+ Implement the plan in the session worktree. Keep the change scoped to the issue and approved plan.
11
+
12
+ Implementation rules:
13
+
14
+ - Inspect the current app before editing. Do not assume a JSKIT app shape without checking files.
15
+ - Prefer existing JSKIT helpers, package runtime seams, generated scaffolds, and documented generators over new local helpers.
16
+ - If the plan calls for a generator or package install, use the planned `jskit` command unless inspection proves it does not apply. If you skip a generator, explain the exact gap.
17
+ - For non-CRUD route pages, use `jskit generate ui-generator page ...` when it fits instead of hand-writing both route files and placement entries.
18
+ - For CRUD work, scaffold the server side first with `crud-server-generator` before CRUD UI or CRUD route work.
19
+ - Do not hand-write a separate CRUD migration for a table owned by `crud-server-generator`.
20
+ - Do not hand-build CRUD endpoints or page trees before the server CRUD package and shared resource file exist.
21
+ - Keep direct knex exceptional and minimal. Prefer internal json-rest-api seams outside generated CRUD packages and explicit weird-custom feature lanes.
22
+ - Keep runtime, UI, and data concerns separated.
23
+ - Avoid accidental scope expansion.
24
+ - Do not create old workflow files such as `.jskit/WORKBOARD.md`; the session files and receipts are the workflow record.
25
+ - If user-facing UI changes, bring the screen to Material Design and Vuetify quality before calling it done. Include coherent responsive layout, loading, empty, error, disabled, and success states where relevant.
26
+ - If verification needs login, use the app's local development auth bootstrap path rather than a live external auth login.
27
+
28
+ After making changes:
29
+
30
+ - Review for repeated code, unnecessary helpers, fragmented functions, placeholder work, missing states, broken route wiring, ownership mistakes, and weak JSKIT reuse.
31
+ - Run the smallest relevant checks you can run safely in the worktree.
32
+ - For changed user-facing UI, run or clearly identify the Playwright verification path. When possible, record UI verification with `jskit app verify-ui --command "<playwright command>" --feature "<label>" --auth-mode <mode>`.
33
+ - Summarize changed files and checks run.
34
+
35
+ Do not create commits, branches, issues, pull requests, merges, or worktree cleanup yourself. JSKIT session will handle those steps.
@@ -0,0 +1,8 @@
1
+ Codex thread id: {{codex_thread_id}}
2
+ Issue session status: finished
3
+ PR: {{pr_url}}
4
+ Local transcript: {{transcript_log}}
5
+
6
+ Summary:
7
+
8
+ Session {{session_id}} finished and merged.