@harness-lab/cli 0.2.9 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +5 -0
  2. package/assets/workshop-bundle/SKILL.md +16 -0
  3. package/assets/workshop-bundle/bundle-manifest.json +46 -54
  4. package/assets/workshop-bundle/content/challenge-cards/deck.md +19 -17
  5. package/assets/workshop-bundle/content/challenge-cards/locales/en/deck.md +7 -5
  6. package/assets/workshop-bundle/content/challenge-cards/print-spec.md +1 -1
  7. package/assets/workshop-bundle/content/codex-craft.md +190 -0
  8. package/assets/workshop-bundle/content/facilitation/codex-setup-verification.md +5 -5
  9. package/assets/workshop-bundle/content/facilitation/master-guide.md +133 -66
  10. package/assets/workshop-bundle/content/project-briefs/code-review-helper.md +9 -9
  11. package/assets/workshop-bundle/content/project-briefs/devtoolbox-cli.md +11 -9
  12. package/assets/workshop-bundle/content/project-briefs/doc-generator.md +10 -8
  13. package/assets/workshop-bundle/content/project-briefs/locales/en/devtoolbox-cli.md +4 -2
  14. package/assets/workshop-bundle/content/project-briefs/locales/en/doc-generator.md +5 -3
  15. package/assets/workshop-bundle/content/project-briefs/locales/en/metrics-dashboard.md +4 -2
  16. package/assets/workshop-bundle/content/project-briefs/locales/en/standup-bot.md +4 -2
  17. package/assets/workshop-bundle/content/project-briefs/metrics-dashboard.md +14 -12
  18. package/assets/workshop-bundle/content/project-briefs/standup-bot.md +11 -9
  19. package/assets/workshop-bundle/content/talks/codex-demo-script.md +12 -10
  20. package/assets/workshop-bundle/content/talks/context-is-king.md +25 -25
  21. package/assets/workshop-bundle/docs/harness-cli-foundation.md +2 -0
  22. package/assets/workshop-bundle/docs/learner-resource-kit.md +37 -37
  23. package/assets/workshop-bundle/materials/coaching-codex.md +76 -0
  24. package/assets/workshop-bundle/materials/locales/en/participant-resource-kit.md +14 -2
  25. package/assets/workshop-bundle/materials/participant-resource-kit.md +23 -11
  26. package/assets/workshop-bundle/workshop-blueprint/README.md +2 -5
  27. package/assets/workshop-bundle/workshop-blueprint/day-structure.md +14 -0
  28. package/assets/workshop-bundle/workshop-skill/analyze-checklist.md +3 -3
  29. package/assets/workshop-bundle/workshop-skill/closing-skill.md +6 -6
  30. package/assets/workshop-bundle/workshop-skill/commands.md +17 -13
  31. package/assets/workshop-bundle/workshop-skill/facilitator.md +33 -0
  32. package/assets/workshop-bundle/workshop-skill/follow-up-package.md +13 -8
  33. package/assets/workshop-bundle/workshop-skill/install.md +8 -8
  34. package/assets/workshop-bundle/workshop-skill/locales/en/commands.md +4 -0
  35. package/assets/workshop-bundle/workshop-skill/locales/en/follow-up-package.md +8 -3
  36. package/assets/workshop-bundle/workshop-skill/locales/en/recap.md +8 -1
  37. package/assets/workshop-bundle/workshop-skill/locales/en/reference.md +20 -3
  38. package/assets/workshop-bundle/workshop-skill/locales/en/setup.md +1 -1
  39. package/assets/workshop-bundle/workshop-skill/recap.md +12 -5
  40. package/assets/workshop-bundle/workshop-skill/reference.md +53 -29
  41. package/assets/workshop-bundle/workshop-skill/setup.md +11 -11
  42. package/assets/workshop-bundle/workshop-skill/template-agents.md +4 -4
  43. package/package.json +1 -1
  44. package/src/client.js +9 -0
  45. package/src/io.js +1 -0
  46. package/src/run-cli.js +197 -0
  47. package/src/skill-install.js +108 -7
  48. package/src/workshop-bundle.js +30 -2
  49. package/assets/workshop-bundle/content/czech-editorial-review-checklist.md +0 -88
  50. package/assets/workshop-bundle/content/style-examples.md +0 -127
  51. package/assets/workshop-bundle/content/style-guide.md +0 -108
  52. package/assets/workshop-bundle/workshop-blueprint/edit-boundaries.md +0 -64
@@ -6,15 +6,15 @@ Nejdůležitější pravidlo:
6
6
  - napište, kam má agent sáhnout jako první
7
7
  - napište, co je zdroj pravdy
8
8
  - napište, jak se práce ověří
9
- - když se text nafukuje, přidejte navazující dokument a odkažte na něj
9
+ - když se text nafukuje, přidejte navazující dokument a odkažte na něj
10
10
 
11
11
  ## Goal
12
12
 
13
- Popiš, co má agent v tomto repozitáři vytvořit nebo udržovat.
13
+ Popiš, co má agent v tomto repozitáři vytvořit nebo udržovat.
14
14
 
15
15
  ## Context
16
16
 
17
- - Klíčové soubory a složky
17
+ - Klíčové soubory a složky
18
18
  - Rozhodnutí, která už padla
19
19
  - Systémy nebo integrace, na které se navazuje
20
20
  - Kam má agent sáhnout jako první
@@ -23,7 +23,7 @@ Popiš, co má agent v tomto repozitáři vytvořit nebo udržovat.
23
23
  ## Constraints
24
24
 
25
25
  - Build/test/lint příkazy
26
- - Jazykové, architektonické a bezpečnostní standardy
26
+ - Jazykové, architektonické a bezpečnostní standardy
27
27
  - Co agent nesmí dělat bez explicitního souhlasu
28
28
  - Public/private nebo auth boundary, pokud existuje
29
29
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harness-lab/cli",
3
- "version": "0.2.9",
3
+ "version": "0.3.1",
4
4
  "description": "Participant-facing Harness Lab CLI for facilitator auth and workshop operations",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/client.js CHANGED
@@ -96,6 +96,15 @@ export function createHarnessClient({ fetchFn, session }) {
96
96
  getWorkshopInstance(instanceId) {
97
97
  return request(`/api/workshop/instances/${encodeURIComponent(instanceId)}`);
98
98
  },
99
+ getWorkshopParticipantAccess(instanceId) {
100
+ return request(`/api/workshop/instances/${encodeURIComponent(instanceId)}/participant-access`);
101
+ },
102
+ issueWorkshopParticipantAccess(instanceId, input = {}) {
103
+ return request(`/api/workshop/instances/${encodeURIComponent(instanceId)}/participant-access`, {
104
+ method: "POST",
105
+ body: { action: "rotate", ...input },
106
+ });
107
+ },
99
108
  getAgenda() {
100
109
  return request("/api/agenda");
101
110
  },
package/src/io.js CHANGED
@@ -200,5 +200,6 @@ export function createCliUi(io, options = {}) {
200
200
  numberedList,
201
201
  commandList,
202
202
  json,
203
+ jsonMode,
203
204
  };
204
205
  }
package/src/run-cli.js CHANGED
@@ -1,3 +1,5 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
1
3
  import { getDefaultDashboardUrl } from "./config.js";
2
4
  import { createHarnessClient, HarnessApiError } from "./client.js";
3
5
  import { createCliUi, prompt, writeLine } from "./io.js";
@@ -166,6 +168,19 @@ function summarizeWorkshopInstance(instance) {
166
168
  };
167
169
  }
168
170
 
171
+ function summarizeParticipantAccess(participantAccess) {
172
+ return {
173
+ instanceId: participantAccess?.instanceId ?? null,
174
+ active: participantAccess?.active ?? false,
175
+ version: participantAccess?.version ?? null,
176
+ codeId: participantAccess?.codeId ?? null,
177
+ expiresAt: participantAccess?.expiresAt ?? null,
178
+ canRevealCurrent: participantAccess?.canRevealCurrent ?? false,
179
+ source: participantAccess?.source ?? "missing",
180
+ currentCode: participantAccess?.currentCode ?? null,
181
+ };
182
+ }
183
+
169
184
  function resolveCurrentInstanceTarget(session, env) {
170
185
  if (typeof session?.selectedInstanceId === "string" && session.selectedInstanceId.trim().length > 0) {
171
186
  return {
@@ -210,6 +225,7 @@ function printUsage(io, ui) {
210
225
  "harness workshop status",
211
226
  "harness workshop list-instances",
212
227
  "harness workshop show-instance <instance-id>",
228
+ "harness workshop participant-access [<instance-id>] [--rotate] [--code VALUE]",
213
229
  "harness workshop archive [--notes TEXT]",
214
230
  "harness workshop create-instance [<instance-id>] [--template-id ID] [--content-lang cs|en] [--event-title TEXT] [--city CITY]",
215
231
  "harness workshop update-instance <instance-id> [--content-lang cs|en] [--event-title TEXT] [--city CITY]",
@@ -217,6 +233,7 @@ function printUsage(io, ui) {
217
233
  "harness workshop prepare <instance-id>",
218
234
  "harness workshop remove-instance <instance-id>",
219
235
  "harness workshop phase set <phase-id>",
236
+ "harness workshop learnings [--tag TAG] [--instance ID] [--cohort NAME] [--limit N]",
220
237
  ]);
221
238
  }
222
239
 
@@ -592,6 +609,22 @@ async function handleAuthLogout(io, ui, env, deps) {
592
609
  return 0;
593
610
  }
594
611
 
612
+ function renderSelectedInstanceBanner(ui, target) {
613
+ if (ui.jsonMode) {
614
+ return;
615
+ }
616
+ const instanceId = target.instanceId ?? "none";
617
+ const label = target.source === "session"
618
+ ? "Selected instance (locally selected)"
619
+ : target.instanceId
620
+ ? `Selected instance (from ${target.source})`
621
+ : "Selected instance";
622
+ ui.keyValue(label, instanceId);
623
+ if (!target.instanceId) {
624
+ ui.keyValue("", "no instance is currently selected — run `harness workshop select-instance <id>` to pin one");
625
+ }
626
+ }
627
+
595
628
  async function handleWorkshopStatus(io, ui, env, deps) {
596
629
  const session = await requireSession(io, ui, env);
597
630
  if (!session) {
@@ -602,6 +635,8 @@ async function handleWorkshopStatus(io, ui, env, deps) {
602
635
  const client = createHarnessClient({ fetchFn: deps.fetchFn, session });
603
636
  const target = resolveCurrentInstanceTarget(session, env);
604
637
 
638
+ renderSelectedInstanceBanner(ui, target);
639
+
605
640
  if (target.source === "session" && target.instanceId) {
606
641
  const [instanceResult, agenda] = await Promise.all([
607
642
  client.getWorkshopInstance(target.instanceId),
@@ -609,6 +644,11 @@ async function handleWorkshopStatus(io, ui, env, deps) {
609
644
  ]);
610
645
  ui.json("Workshop Status", {
611
646
  ok: true,
647
+ selectedInstance: {
648
+ instanceId: target.instanceId,
649
+ source: target.source,
650
+ selected: true,
651
+ },
612
652
  targetInstanceId: target.instanceId,
613
653
  targetSource: target.source,
614
654
  ...summarizeWorkshopInstance(instanceResult.instance),
@@ -622,6 +662,11 @@ async function handleWorkshopStatus(io, ui, env, deps) {
622
662
  const [workshop, agenda] = await Promise.all([client.getWorkshopStatus(), client.getAgenda()]);
623
663
  ui.json("Workshop Status", {
624
664
  ok: true,
665
+ selectedInstance: {
666
+ instanceId: target.instanceId ?? null,
667
+ source: target.source,
668
+ selected: Boolean(target.instanceId),
669
+ },
625
670
  targetInstanceId: target.instanceId,
626
671
  targetSource: target.source,
627
672
  workshopId: workshop.workshopId,
@@ -801,6 +846,55 @@ async function handleWorkshopShowInstance(io, ui, env, positionals, flags, deps)
801
846
  }
802
847
  }
803
848
 
849
+ async function handleWorkshopParticipantAccess(io, ui, env, positionals, flags, deps) {
850
+ const session = await requireSession(io, ui, env);
851
+ if (!session) {
852
+ return 1;
853
+ }
854
+
855
+ const instanceId = await readRequiredCommandValue(
856
+ io,
857
+ flags,
858
+ ["id", "instance-id"],
859
+ "Instance id: ",
860
+ readOptionalPositional(positionals, 2) ?? session.selectedInstanceId,
861
+ );
862
+ if (!instanceId) {
863
+ ui.status("error", "Instance id is required.", { stream: "stderr" });
864
+ return 1;
865
+ }
866
+
867
+ try {
868
+ const client = createHarnessClient({ fetchFn: deps.fetchFn, session });
869
+ if (flags.rotate === true) {
870
+ const result = await client.issueWorkshopParticipantAccess(instanceId, {
871
+ ...(typeof flags.code === "string" ? { code: flags.code } : {}),
872
+ });
873
+ ui.json("Workshop Participant Access", {
874
+ ok: true,
875
+ issuedCode: result.issuedCode ?? null,
876
+ ...summarizeParticipantAccess(result.participantAccess),
877
+ participantAccess: result.participantAccess,
878
+ });
879
+ return 0;
880
+ }
881
+
882
+ const result = await client.getWorkshopParticipantAccess(instanceId);
883
+ ui.json("Workshop Participant Access", {
884
+ ok: true,
885
+ ...summarizeParticipantAccess(result.participantAccess),
886
+ participantAccess: result.participantAccess,
887
+ });
888
+ return 0;
889
+ } catch (error) {
890
+ if (error instanceof HarnessApiError) {
891
+ ui.status("error", `Participant access failed: ${error.message}`, { stream: "stderr" });
892
+ return 1;
893
+ }
894
+ throw error;
895
+ }
896
+ }
897
+
804
898
  async function handleWorkshopArchive(io, ui, env, flags, deps) {
805
899
  const session = await requireSession(io, ui, env);
806
900
  if (!session) {
@@ -1051,6 +1145,101 @@ async function handleWorkshopPhaseSet(io, ui, env, positionals, deps) {
1051
1145
  }
1052
1146
  }
1053
1147
 
1148
+ async function handleWorkshopLearningsQuery(io, ui, env, flags) {
1149
+ const dataDir = env.HARNESS_DATA_DIR ?? path.join(process.cwd(), "data");
1150
+ const logPath = env.HARNESS_LEARNINGS_LOG_PATH ?? path.join(dataDir, "learnings-log.jsonl");
1151
+
1152
+ let rawLines;
1153
+ try {
1154
+ const content = await fs.readFile(logPath, "utf8");
1155
+ rawLines = content.split("\n").filter((line) => line.trim().length > 0);
1156
+ } catch (error) {
1157
+ if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
1158
+ ui.json("Workshop Learnings", { ok: true, signals: [], totalMatched: 0, source: logPath });
1159
+ return 0;
1160
+ }
1161
+ ui.status("error", `Could not read learnings log at ${logPath}: ${error instanceof Error ? error.message : String(error)}`, { stream: "stderr" });
1162
+ return 1;
1163
+ }
1164
+
1165
+ let entries;
1166
+ try {
1167
+ entries = rawLines.map((line) => JSON.parse(line));
1168
+ } catch (error) {
1169
+ ui.status("error", `Learnings log has malformed JSON lines: ${error instanceof Error ? error.message : String(error)}`, { stream: "stderr" });
1170
+ return 1;
1171
+ }
1172
+
1173
+ const filterTag = readStringFlag(flags, "tag");
1174
+ const filterInstance = readStringFlag(flags, "instance");
1175
+ const filterCohort = readStringFlag(flags, "cohort");
1176
+ const limit = Number(readStringFlag(flags, "limit") ?? "20");
1177
+
1178
+ let matched = entries;
1179
+ if (filterTag) {
1180
+ matched = matched.filter((entry) =>
1181
+ Array.isArray(entry.signal?.tags) && entry.signal.tags.some((tag) => tag === filterTag),
1182
+ );
1183
+ }
1184
+ if (filterInstance) {
1185
+ matched = matched.filter((entry) => entry.instanceId === filterInstance);
1186
+ }
1187
+ if (filterCohort) {
1188
+ matched = matched.filter((entry) => entry.cohort === filterCohort);
1189
+ }
1190
+
1191
+ const totalMatched = matched.length;
1192
+ const limited = Number.isFinite(limit) && limit > 0 ? matched.slice(-limit) : matched;
1193
+
1194
+ if (ui.jsonMode) {
1195
+ ui.json("Workshop Learnings", {
1196
+ ok: true,
1197
+ totalMatched,
1198
+ returned: limited.length,
1199
+ source: logPath,
1200
+ signals: limited.map((entry) => ({
1201
+ cohort: entry.cohort,
1202
+ instanceId: entry.instanceId,
1203
+ capturedAt: entry.signal?.capturedAt ?? entry.loggedAt,
1204
+ capturedBy: entry.signal?.capturedBy ?? "unknown",
1205
+ teamId: entry.signal?.teamId ?? null,
1206
+ tags: entry.signal?.tags ?? [],
1207
+ freeText: entry.signal?.freeText ?? "",
1208
+ })),
1209
+ });
1210
+ return 0;
1211
+ }
1212
+
1213
+ ui.heading("Workshop Learnings");
1214
+ if (limited.length === 0) {
1215
+ ui.paragraph(totalMatched === 0
1216
+ ? "No signals captured yet. Use the rotation capture panel in the facilitator dashboard during the continuation shift."
1217
+ : `No signals matched the current filters (${totalMatched} total in log).`,
1218
+ );
1219
+ ui.blank();
1220
+ ui.keyValue("Source", logPath);
1221
+ return 0;
1222
+ }
1223
+
1224
+ ui.paragraph(`${totalMatched} signal${totalMatched === 1 ? "" : "s"} matched${totalMatched > limited.length ? ` (showing last ${limited.length})` : ""}`);
1225
+ ui.blank();
1226
+
1227
+ for (const entry of limited) {
1228
+ const signal = entry.signal ?? {};
1229
+ const capturedAt = signal.capturedAt ?? entry.loggedAt ?? "";
1230
+ const time = capturedAt ? new Date(capturedAt).toLocaleString("en-US", { dateStyle: "short", timeStyle: "short" }) : "";
1231
+ const team = signal.teamId ? ` [${signal.teamId}]` : "";
1232
+ const tags = Array.isArray(signal.tags) && signal.tags.length > 0 ? ` {${signal.tags.join(", ")}}` : "";
1233
+
1234
+ ui.section(`${entry.cohort ?? "?"} · ${time}${team}${tags}`);
1235
+ ui.paragraph(signal.freeText ?? "(no observation text)", { indent: " " });
1236
+ ui.blank();
1237
+ }
1238
+
1239
+ ui.keyValue("Source", logPath);
1240
+ return 0;
1241
+ }
1242
+
1054
1243
  export async function runCli(argv, io, deps = {}) {
1055
1244
  const fetchFn = deps.fetchFn ?? globalThis.fetch;
1056
1245
  const mergedDeps = { fetchFn, sleepFn: deps.sleepFn, openUrl: deps.openUrl, cwd: deps.cwd };
@@ -1118,6 +1307,10 @@ export async function runCli(argv, io, deps = {}) {
1118
1307
  return handleWorkshopShowInstance(io, ui, io.env, positionals, flags, mergedDeps);
1119
1308
  }
1120
1309
 
1310
+ if (scope === "workshop" && action === "participant-access") {
1311
+ return handleWorkshopParticipantAccess(io, ui, io.env, positionals, flags, mergedDeps);
1312
+ }
1313
+
1121
1314
  if (scope === "workshop" && action === "archive") {
1122
1315
  return handleWorkshopArchive(io, ui, io.env, flags, mergedDeps);
1123
1316
  }
@@ -1146,6 +1339,10 @@ export async function runCli(argv, io, deps = {}) {
1146
1339
  return handleWorkshopPhaseSet(io, ui, io.env, positionals, mergedDeps);
1147
1340
  }
1148
1341
 
1342
+ if (scope === "workshop" && action === "learnings") {
1343
+ return handleWorkshopLearningsQuery(io, ui, io.env, flags);
1344
+ }
1345
+
1149
1346
  printUsage(io, ui);
1150
1347
  return 1;
1151
1348
  }
@@ -1,5 +1,6 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import process from "node:process";
3
4
  import {
4
5
  createWorkshopBundleManifestFromDirectory,
5
6
  createWorkshopBundleManifestFromSource,
@@ -12,6 +13,8 @@ import {
12
13
  WORKSHOP_SKILL_NAME,
13
14
  } from "./workshop-bundle.js";
14
15
 
16
+ const MIN_NODE_MAJOR = 22;
17
+
15
18
  export class SkillInstallError extends Error {
16
19
  constructor(message, options = {}) {
17
20
  super(message);
@@ -20,6 +23,81 @@ export class SkillInstallError extends Error {
20
23
  }
21
24
  }
22
25
 
26
+ function assertSupportedNodeVersion() {
27
+ const raw = process.versions?.node;
28
+ if (!raw) {
29
+ return;
30
+ }
31
+ const major = Number.parseInt(raw.split(".")[0], 10);
32
+ if (Number.isFinite(major) && major < MIN_NODE_MAJOR) {
33
+ throw new SkillInstallError(
34
+ `Harness CLI requires Node.js ${MIN_NODE_MAJOR} or newer. This process is running Node.js ${raw}. Upgrade with your version manager (for example \`nvm install --lts\`) and re-run \`harness skill install\`.`,
35
+ { code: "unsupported_node_version" },
36
+ );
37
+ }
38
+ }
39
+
40
+ function translateFileSystemError(error, context) {
41
+ if (!error || typeof error !== "object" || !("code" in error)) {
42
+ return null;
43
+ }
44
+
45
+ const targetPath = error.path ? ` (${error.path})` : "";
46
+
47
+ if (error.code === "EACCES" || error.code === "EPERM") {
48
+ return new SkillInstallError(
49
+ `Harness CLI could not ${context}${targetPath} because the current user does not have write permission. Try running from a directory you own, or adjust the directory permissions. On macOS and Linux, that usually means avoiding system paths like /usr. On Windows, avoid running from a protected location such as C:\\Program Files.`,
50
+ { code: "install_permission_denied" },
51
+ );
52
+ }
53
+
54
+ if (error.code === "ENOSPC") {
55
+ return new SkillInstallError(
56
+ `Harness CLI could not ${context}${targetPath} because the disk is full. Free some space and re-run \`harness skill install\`.`,
57
+ { code: "install_no_space" },
58
+ );
59
+ }
60
+
61
+ if (error.code === "ENAMETOOLONG" || error.code === "ENOTDIR") {
62
+ return new SkillInstallError(
63
+ `Harness CLI could not ${context}${targetPath}. The target path is too long or part of it is not a directory. On Windows, this often happens when the repository lives in a deeply nested folder — move the repo closer to the drive root (for example C:\\repos\\your-project) and try again.`,
64
+ { code: "install_path_invalid" },
65
+ );
66
+ }
67
+
68
+ if (error.code === "EROFS") {
69
+ return new SkillInstallError(
70
+ `Harness CLI could not ${context}${targetPath} because the file system is read-only. Re-run \`harness skill install\` from a writable directory.`,
71
+ { code: "install_read_only" },
72
+ );
73
+ }
74
+
75
+ if (error.code === "EBUSY") {
76
+ return new SkillInstallError(
77
+ `Harness CLI could not ${context}${targetPath} because the target is busy (another process may hold it open). Close editors or agents pointing at \`.agents/skills/\` and re-run \`harness skill install\`.`,
78
+ { code: "install_target_busy" },
79
+ );
80
+ }
81
+
82
+ return null;
83
+ }
84
+
85
+ async function fsWithActionableError(operation, context) {
86
+ try {
87
+ return await operation();
88
+ } catch (error) {
89
+ if (error instanceof SkillInstallError) {
90
+ throw error;
91
+ }
92
+ const translated = translateFileSystemError(error, context);
93
+ if (translated) {
94
+ translated.cause = error;
95
+ throw translated;
96
+ }
97
+ throw error;
98
+ }
99
+ }
100
+
23
101
  async function resolveBundleSource() {
24
102
  const packagedBundlePath = getPackagedWorkshopBundlePath();
25
103
  if (await pathExists(path.join(packagedBundlePath, "SKILL.md"))) {
@@ -43,8 +121,14 @@ async function resolveBundleSource() {
43
121
  }
44
122
 
45
123
  async function ensureDirectory(targetPath) {
46
- await fs.mkdir(targetPath, { recursive: true });
47
- const stat = await fs.stat(targetPath);
124
+ await fsWithActionableError(
125
+ () => fs.mkdir(targetPath, { recursive: true }),
126
+ `create the install target directory`,
127
+ );
128
+ const stat = await fsWithActionableError(
129
+ () => fs.stat(targetPath),
130
+ `inspect the install target directory`,
131
+ );
48
132
  if (!stat.isDirectory()) {
49
133
  throw new SkillInstallError(`Install target is not a directory: ${targetPath}`, {
50
134
  code: "invalid_target",
@@ -71,14 +155,22 @@ async function getSourceBundleManifest(resolvedBundle) {
71
155
 
72
156
  async function installFromResolvedBundle(resolvedBundle, installPath) {
73
157
  if (resolvedBundle.mode === "packaged_bundle") {
74
- await fs.cp(resolvedBundle.sourcePath, installPath, { recursive: true });
158
+ await fsWithActionableError(
159
+ () => fs.cp(resolvedBundle.sourcePath, installPath, { recursive: true }),
160
+ `copy the workshop bundle into the install target`,
161
+ );
75
162
  return;
76
163
  }
77
164
 
78
- await createWorkshopBundleFromSource(resolvedBundle.sourceRoot, installPath);
165
+ await fsWithActionableError(
166
+ () => createWorkshopBundleFromSource(resolvedBundle.sourceRoot, installPath),
167
+ `build the workshop bundle from source into the install target`,
168
+ );
79
169
  }
80
170
 
81
171
  export async function installWorkshopSkill(startDir, options = {}) {
172
+ assertSupportedNodeVersion();
173
+
82
174
  const resolvedBundle = await resolveBundleSource();
83
175
  if (!resolvedBundle) {
84
176
  throw new SkillInstallError(
@@ -106,7 +198,10 @@ export async function installWorkshopSkill(startDir, options = {}) {
106
198
  };
107
199
  }
108
200
 
109
- await fs.rm(installPath, { recursive: true, force: true });
201
+ await fsWithActionableError(
202
+ () => fs.rm(installPath, { recursive: true, force: true }),
203
+ `remove the previous workshop bundle before refreshing it`,
204
+ );
110
205
 
111
206
  return {
112
207
  ...(await installFreshBundle(resolvedBundle, installPath, targetRoot)),
@@ -115,7 +210,10 @@ export async function installWorkshopSkill(startDir, options = {}) {
115
210
  }
116
211
 
117
212
  if (existingInstall && options.force === true) {
118
- await fs.rm(installPath, { recursive: true, force: true });
213
+ await fsWithActionableError(
214
+ () => fs.rm(installPath, { recursive: true, force: true }),
215
+ `remove the previous workshop bundle before reinstalling`,
216
+ );
119
217
  }
120
218
 
121
219
  const result = await installFreshBundle(resolvedBundle, installPath, targetRoot);
@@ -130,7 +228,10 @@ export async function installWorkshopSkill(startDir, options = {}) {
130
228
  }
131
229
 
132
230
  async function installFreshBundle(resolvedBundle, installPath, targetRoot) {
133
- await fs.mkdir(path.dirname(installPath), { recursive: true });
231
+ await fsWithActionableError(
232
+ () => fs.mkdir(path.dirname(installPath), { recursive: true }),
233
+ `create the parent directory for the installed skill`,
234
+ );
134
235
  await installFromResolvedBundle(resolvedBundle, installPath);
135
236
 
136
237
  return {
@@ -26,8 +26,26 @@ const FILE_COPIES = [
26
26
  ["docs/locales/en/learner-reference-gallery.md", "docs/locales/en/learner-reference-gallery.md"],
27
27
  ["materials/participant-resource-kit.md", "materials/participant-resource-kit.md"],
28
28
  ["materials/locales/en/participant-resource-kit.md", "materials/locales/en/participant-resource-kit.md"],
29
+ ["materials/coaching-codex.md", "materials/coaching-codex.md"],
29
30
  ];
30
31
 
32
+ // Files that live inside DIRECTORY_COPIES source trees but must not ship to
33
+ // participants. These are author/maintainer/copy-editor artifacts that have no
34
+ // runtime purpose inside the installed workshop skill. Keep the set minimal —
35
+ // every entry should be a file that (a) has no workshop-skill reference and
36
+ // (b) is clearly authoring/governance content rather than participant-facing.
37
+ const EXCLUDED_BUNDLE_PATHS = new Set([
38
+ "content/style-guide.md",
39
+ "content/style-examples.md",
40
+ "content/czech-reject-list.md",
41
+ "content/czech-editorial-review-checklist.md",
42
+ "workshop-blueprint/edit-boundaries.md",
43
+ ]);
44
+
45
+ function isExcludedBundlePath(bundleRelativePath) {
46
+ return EXCLUDED_BUNDLE_PATHS.has(normalizePathForManifest(bundleRelativePath));
47
+ }
48
+
31
49
  export function getPackageRoot() {
32
50
  return packageRoot;
33
51
  }
@@ -66,24 +84,31 @@ async function copyDirectoryTree(sourceRoot, targetRoot) {
66
84
  await copyDirectoryRecursive(
67
85
  path.join(sourceRoot, sourceRelativePath),
68
86
  path.join(targetRoot, targetRelativePath),
87
+ targetRelativePath,
69
88
  );
70
89
  }
71
90
  }
72
91
 
73
- async function copyDirectoryRecursive(sourcePath, targetPath) {
92
+ async function copyDirectoryRecursive(sourcePath, targetPath, bundleRelativePrefix) {
74
93
  const entries = await fs.readdir(sourcePath, { withFileTypes: true });
75
94
  await fs.mkdir(targetPath, { recursive: true });
76
95
 
77
96
  for (const entry of entries) {
78
97
  const sourceEntryPath = path.join(sourcePath, entry.name);
79
98
  const targetEntryPath = path.join(targetPath, entry.name);
99
+ const entryBundleRelativePath = bundleRelativePrefix
100
+ ? path.join(bundleRelativePrefix, entry.name)
101
+ : entry.name;
80
102
 
81
103
  if (entry.isDirectory()) {
82
- await copyDirectoryRecursive(sourceEntryPath, targetEntryPath);
104
+ await copyDirectoryRecursive(sourceEntryPath, targetEntryPath, entryBundleRelativePath);
83
105
  continue;
84
106
  }
85
107
 
86
108
  if (entry.isFile()) {
109
+ if (isExcludedBundlePath(entryBundleRelativePath)) {
110
+ continue;
111
+ }
87
112
  await fs.copyFile(sourceEntryPath, targetEntryPath);
88
113
  }
89
114
  }
@@ -187,6 +212,9 @@ export async function createWorkshopBundleManifestFromSource(sourceRoot) {
187
212
  if (targetRelative === "workshop-skill/SKILL.md") {
188
213
  continue;
189
214
  }
215
+ if (isExcludedBundlePath(targetRelative)) {
216
+ continue;
217
+ }
190
218
 
191
219
  entries.push({
192
220
  absolutePath: file.absolutePath,
@@ -1,88 +0,0 @@
1
- # Czech Editorial Review Checklist
2
-
3
- Použijte tento checklist při revizi českého obsahu pro účastníky, hlavně pro:
4
-
5
- - workshop agenda a presenter scenes
6
- - pokyny pro participant room
7
- - project briefs
8
- - challenge cards
9
- - setup, reference, recap, follow-up a learner-kit materiály
10
-
11
- Tento checklist doplňuje [`content/style-guide.md`](./style-guide.md) a [`content/style-examples.md`](./style-examples.md). Není to náhrada. Je to poslední quality gate před tím, než se text bere jako workshop-ready.
12
-
13
- ## 1. Přirozenost češtiny
14
-
15
- - Zní text jako přirozená čeština, ne jako překlad?
16
- - Přečetl by to český developer bez škobrtnutí?
17
- - Nejsou ve větách doslovné anglické kalky?
18
- - Nejsou věty zbytečně dlouhé nebo toporné?
19
-
20
- ## 2. Kvalita míchání češtiny a angličtiny
21
-
22
- - Zůstává česká věta česká?
23
- - Jsou anglické termíny použité jen tam, kde jsou v developerské praxi opravdu přirozené?
24
- - Není v textu náhodně promíchaná angličtina jen proto, že původní zdroj byl anglický?
25
- - Když se méně známý anglický termín objevuje poprvé, je stručně ukotvený česky?
26
-
27
- ## 3. Jasnost instrukce
28
-
29
- - Je z textu jasné, co má člověk udělat právě teď?
30
- - Mají věty konkrétní slovesa jako `spusťte`, `zkontrolujte`, `doplňte`, `ověřte`?
31
- - Neschovává se akce za abstraktní formulace typu „je vhodné realizovat“?
32
- - Nejsou odstavce přeplněné více cíli najednou?
33
-
34
- ## 4. Workshop voice
35
-
36
- - Zní text jako zkušený peer, ne jako marketér nebo korporát?
37
- - Drží se text klidného, věcného a praktického tónu?
38
- - Nezní text jako slide, slogan nebo generický AI obsah?
39
- - Je z textu cítit disciplína workshopu: kontext zapsaný v repu, ověření, handoff?
40
-
41
- ## 5. Terminologická disciplína
42
-
43
- - Jsou opakované workshop terms použité konzistentně?
44
- - Neobjevují se slabé nebo matoucí fráze jen proto, že znějí „AI-ish“?
45
- - Jsou výrazy jako `safe move`, `handoff`, `checkpoint`, `workflow`, `review`, `skill`, `runbook` použité záměrně, ne náhodně?
46
- - Není stejná věc jednou česky a podruhé napůl anglicky bez důvodu?
47
-
48
- ## 6. Vyhněte se těmto signálům
49
-
50
- Pokud se v textu objeví něco z toho, vraťte ho do editace:
51
-
52
- - doslovný překlad anglické vazby
53
- - česká věta s náhodně vloženými anglickými slovy mimo technické termíny
54
- - korporátní nebo školometský tón
55
- - fráze, které nic nekotví k akci
56
- - generické AI obraty bez konkrétního významu
57
-
58
- ## 7. Spoken check
59
-
60
- Přečtěte text nahlas nebo aspoň polohlasem.
61
-
62
- Text vrátit do editace, pokud:
63
-
64
- - se nedá říct plynule
65
- - obsahuje nepřirozený slovosled
66
- - zní tvrdě přeloženě
67
- - potřebuje v hlavě „opravovat“, co tím autor asi myslel
68
-
69
- ## 8. Cross-locale check
70
-
71
- Pokud text vznikal z anglického source:
72
-
73
- - není to doslovný překlad?
74
- - drží česká verze stejný význam, ale přirozenější formulací?
75
- - není česká verze výrazně slabší, plošší nebo méně konkrétní než anglická?
76
- - není anglický source sám o sobě slabý a nehodí se nejdřív přepsat?
77
-
78
- ## 9. Before publish
79
-
80
- Před schválením českého textu pro účastníky musí být možné říct `ano` na všechno:
81
-
82
- 1. Je to přirozená čeština?
83
- 2. Rozumí tomu český developer napoprvé?
84
- 3. Je jasné, co má člověk udělat nebo pochopit?
85
- 4. Drží text workshop voice bez hype a bez korporátu?
86
- 5. Je míchání češtiny a angličtiny disciplinované?
87
- 6. Neobsahuje text slabé fráze nebo „AI slop“?
88
- 7. Obstojí text při čtení nahlas?