@harness-lab/cli 0.2.8 → 0.3.0

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 (51) hide show
  1. package/README.md +34 -3
  2. package/assets/workshop-bundle/SKILL.md +28 -0
  3. package/assets/workshop-bundle/bundle-manifest.json +44 -52
  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/codex-craft.md +190 -0
  7. package/assets/workshop-bundle/content/facilitation/codex-setup-verification.md +5 -5
  8. package/assets/workshop-bundle/content/facilitation/master-guide.md +137 -67
  9. package/assets/workshop-bundle/content/project-briefs/code-review-helper.md +9 -9
  10. package/assets/workshop-bundle/content/project-briefs/devtoolbox-cli.md +11 -9
  11. package/assets/workshop-bundle/content/project-briefs/doc-generator.md +10 -8
  12. package/assets/workshop-bundle/content/project-briefs/locales/en/devtoolbox-cli.md +4 -2
  13. package/assets/workshop-bundle/content/project-briefs/locales/en/doc-generator.md +5 -3
  14. package/assets/workshop-bundle/content/project-briefs/locales/en/metrics-dashboard.md +4 -2
  15. package/assets/workshop-bundle/content/project-briefs/locales/en/standup-bot.md +4 -2
  16. package/assets/workshop-bundle/content/project-briefs/metrics-dashboard.md +14 -12
  17. package/assets/workshop-bundle/content/project-briefs/standup-bot.md +11 -9
  18. package/assets/workshop-bundle/content/talks/codex-demo-script.md +12 -10
  19. package/assets/workshop-bundle/content/talks/context-is-king.md +26 -23
  20. package/assets/workshop-bundle/docs/harness-cli-foundation.md +23 -11
  21. package/assets/workshop-bundle/docs/learner-resource-kit.md +37 -37
  22. package/assets/workshop-bundle/materials/coaching-codex.md +76 -0
  23. package/assets/workshop-bundle/materials/locales/en/participant-resource-kit.md +14 -2
  24. package/assets/workshop-bundle/materials/participant-resource-kit.md +23 -11
  25. package/assets/workshop-bundle/workshop-blueprint/README.md +2 -5
  26. package/assets/workshop-bundle/workshop-blueprint/day-structure.md +14 -0
  27. package/assets/workshop-bundle/workshop-skill/analyze-checklist.md +3 -3
  28. package/assets/workshop-bundle/workshop-skill/closing-skill.md +5 -5
  29. package/assets/workshop-bundle/workshop-skill/commands.md +13 -13
  30. package/assets/workshop-bundle/workshop-skill/facilitator.md +95 -0
  31. package/assets/workshop-bundle/workshop-skill/follow-up-package.md +13 -8
  32. package/assets/workshop-bundle/workshop-skill/install.md +8 -8
  33. package/assets/workshop-bundle/workshop-skill/locales/en/follow-up-package.md +8 -3
  34. package/assets/workshop-bundle/workshop-skill/locales/en/recap.md +8 -1
  35. package/assets/workshop-bundle/workshop-skill/locales/en/reference.md +19 -3
  36. package/assets/workshop-bundle/workshop-skill/locales/en/setup.md +1 -1
  37. package/assets/workshop-bundle/workshop-skill/recap.md +12 -5
  38. package/assets/workshop-bundle/workshop-skill/reference.md +45 -29
  39. package/assets/workshop-bundle/workshop-skill/setup.md +11 -11
  40. package/assets/workshop-bundle/workshop-skill/template-agents.md +4 -4
  41. package/package.json +1 -1
  42. package/src/client.js +18 -0
  43. package/src/io.js +11 -2
  44. package/src/run-cli.js +266 -8
  45. package/src/session-store.js +1 -0
  46. package/src/skill-install.js +108 -7
  47. package/src/workshop-bundle.js +48 -3
  48. package/assets/workshop-bundle/content/czech-editorial-review-checklist.md +0 -88
  49. package/assets/workshop-bundle/content/style-examples.md +0 -127
  50. package/assets/workshop-bundle/content/style-guide.md +0 -108
  51. package/assets/workshop-bundle/workshop-blueprint/edit-boundaries.md +0 -64
package/src/run-cli.js CHANGED
@@ -16,6 +16,7 @@ function sleep(ms) {
16
16
  function parseArgs(argv) {
17
17
  const positionals = [];
18
18
  const flags = {};
19
+ const booleanFlags = new Set(["json", "help", "version", "force", "no-open", "clear"]);
19
20
 
20
21
  for (let index = 0; index < argv.length; index += 1) {
21
22
  const value = argv[index];
@@ -29,6 +30,10 @@ function parseArgs(argv) {
29
30
  }
30
31
  if (value.startsWith("--")) {
31
32
  const key = value.slice(2);
33
+ if (booleanFlags.has(key)) {
34
+ flags[key] = true;
35
+ continue;
36
+ }
32
37
  const next = argv[index + 1];
33
38
  if (!next || next.startsWith("--")) {
34
39
  flags[key] = true;
@@ -161,12 +166,47 @@ function summarizeWorkshopInstance(instance) {
161
166
  };
162
167
  }
163
168
 
169
+ function summarizeParticipantAccess(participantAccess) {
170
+ return {
171
+ instanceId: participantAccess?.instanceId ?? null,
172
+ active: participantAccess?.active ?? false,
173
+ version: participantAccess?.version ?? null,
174
+ codeId: participantAccess?.codeId ?? null,
175
+ expiresAt: participantAccess?.expiresAt ?? null,
176
+ canRevealCurrent: participantAccess?.canRevealCurrent ?? false,
177
+ source: participantAccess?.source ?? "missing",
178
+ currentCode: participantAccess?.currentCode ?? null,
179
+ };
180
+ }
181
+
182
+ function resolveCurrentInstanceTarget(session, env) {
183
+ if (typeof session?.selectedInstanceId === "string" && session.selectedInstanceId.trim().length > 0) {
184
+ return {
185
+ instanceId: session.selectedInstanceId.trim(),
186
+ source: "session",
187
+ };
188
+ }
189
+
190
+ if (typeof env?.HARNESS_WORKSHOP_INSTANCE_ID === "string" && env.HARNESS_WORKSHOP_INSTANCE_ID.trim().length > 0) {
191
+ return {
192
+ instanceId: env.HARNESS_WORKSHOP_INSTANCE_ID.trim(),
193
+ source: "env",
194
+ };
195
+ }
196
+
197
+ return {
198
+ instanceId: null,
199
+ source: "none",
200
+ };
201
+ }
202
+
164
203
  function printUsage(io, ui) {
165
204
  ui.heading("Harness CLI");
166
205
  ui.paragraph(`Version ${version}`);
167
206
  ui.blank();
168
207
  ui.section("Usage");
169
208
  ui.commandList([
209
+ "harness [--json] <command>",
170
210
  "harness --help",
171
211
  "harness --version",
172
212
  "harness version",
@@ -178,9 +218,12 @@ function printUsage(io, ui) {
178
218
  "harness auth logout",
179
219
  "harness auth status",
180
220
  "harness skill install [--target PATH] [--force]",
221
+ "harness workshop current-instance",
222
+ "harness workshop select-instance <instance-id> [--clear]",
181
223
  "harness workshop status",
182
224
  "harness workshop list-instances",
183
225
  "harness workshop show-instance <instance-id>",
226
+ "harness workshop participant-access [<instance-id>] [--rotate] [--code VALUE]",
184
227
  "harness workshop archive [--notes TEXT]",
185
228
  "harness workshop create-instance [<instance-id>] [--template-id ID] [--content-lang cs|en] [--event-title TEXT] [--city CITY]",
186
229
  "harness workshop update-instance <instance-id> [--content-lang cs|en] [--event-title TEXT] [--city CITY]",
@@ -563,6 +606,22 @@ async function handleAuthLogout(io, ui, env, deps) {
563
606
  return 0;
564
607
  }
565
608
 
609
+ function renderSelectedInstanceBanner(ui, target) {
610
+ if (ui.jsonMode) {
611
+ return;
612
+ }
613
+ const instanceId = target.instanceId ?? "none";
614
+ const label = target.source === "session"
615
+ ? "Selected instance (locally selected)"
616
+ : target.instanceId
617
+ ? `Selected instance (from ${target.source})`
618
+ : "Selected instance";
619
+ ui.keyValue(label, instanceId);
620
+ if (!target.instanceId) {
621
+ ui.keyValue("", "no instance is currently selected — run `harness workshop select-instance <id>` to pin one");
622
+ }
623
+ }
624
+
566
625
  async function handleWorkshopStatus(io, ui, env, deps) {
567
626
  const session = await requireSession(io, ui, env);
568
627
  if (!session) {
@@ -571,9 +630,42 @@ async function handleWorkshopStatus(io, ui, env, deps) {
571
630
 
572
631
  try {
573
632
  const client = createHarnessClient({ fetchFn: deps.fetchFn, session });
633
+ const target = resolveCurrentInstanceTarget(session, env);
634
+
635
+ renderSelectedInstanceBanner(ui, target);
636
+
637
+ if (target.source === "session" && target.instanceId) {
638
+ const [instanceResult, agenda] = await Promise.all([
639
+ client.getWorkshopInstance(target.instanceId),
640
+ client.getWorkshopAgenda(target.instanceId),
641
+ ]);
642
+ ui.json("Workshop Status", {
643
+ ok: true,
644
+ selectedInstance: {
645
+ instanceId: target.instanceId,
646
+ source: target.source,
647
+ selected: true,
648
+ },
649
+ targetInstanceId: target.instanceId,
650
+ targetSource: target.source,
651
+ ...summarizeWorkshopInstance(instanceResult.instance),
652
+ workshopMeta: instanceResult.instance?.workshopMeta ?? null,
653
+ currentPhase: agenda.phase,
654
+ agendaItems: Array.isArray(agenda.items) ? agenda.items.length : null,
655
+ });
656
+ return 0;
657
+ }
658
+
574
659
  const [workshop, agenda] = await Promise.all([client.getWorkshopStatus(), client.getAgenda()]);
575
660
  ui.json("Workshop Status", {
576
661
  ok: true,
662
+ selectedInstance: {
663
+ instanceId: target.instanceId ?? null,
664
+ source: target.source,
665
+ selected: Boolean(target.instanceId),
666
+ },
667
+ targetInstanceId: target.instanceId,
668
+ targetSource: target.source,
577
669
  workshopId: workshop.workshopId,
578
670
  workshopMeta: workshop.workshopMeta,
579
671
  currentPhase: agenda.phase,
@@ -589,6 +681,107 @@ async function handleWorkshopStatus(io, ui, env, deps) {
589
681
  }
590
682
  }
591
683
 
684
+ async function handleWorkshopCurrentInstance(io, ui, env, deps) {
685
+ const session = await requireSession(io, ui, env);
686
+ if (!session) {
687
+ return 1;
688
+ }
689
+
690
+ const target = resolveCurrentInstanceTarget(session, env);
691
+ if (!target.instanceId) {
692
+ ui.json("Workshop Current Instance", {
693
+ ok: true,
694
+ instanceId: null,
695
+ source: target.source,
696
+ selectedInstanceId: session.selectedInstanceId ?? null,
697
+ });
698
+ return 0;
699
+ }
700
+
701
+ try {
702
+ const client = createHarnessClient({ fetchFn: deps.fetchFn, session });
703
+ const result = await client.getWorkshopInstance(target.instanceId);
704
+ ui.json("Workshop Current Instance", {
705
+ ok: true,
706
+ source: target.source,
707
+ selectedInstanceId: session.selectedInstanceId ?? null,
708
+ ...summarizeWorkshopInstance(result.instance),
709
+ instance: result.instance,
710
+ });
711
+ return 0;
712
+ } catch (error) {
713
+ if (error instanceof HarnessApiError) {
714
+ ui.status("error", `Current instance lookup failed: ${error.message}`, { stream: "stderr" });
715
+ return 1;
716
+ }
717
+ throw error;
718
+ }
719
+ }
720
+
721
+ async function handleWorkshopSelectInstance(io, ui, env, positionals, flags, deps) {
722
+ const session = await requireSession(io, ui, env);
723
+ if (!session) {
724
+ return 1;
725
+ }
726
+
727
+ if (flags.clear === true) {
728
+ const nextSession = { ...session };
729
+ delete nextSession.selectedInstanceId;
730
+ if (!(await persistSession(io, ui, env, nextSession))) {
731
+ return 1;
732
+ }
733
+
734
+ const target = resolveCurrentInstanceTarget(nextSession, env);
735
+ ui.json("Workshop Select Instance", {
736
+ ok: true,
737
+ selectedInstanceId: null,
738
+ currentInstanceId: target.instanceId,
739
+ source: target.source,
740
+ cleared: true,
741
+ });
742
+ return 0;
743
+ }
744
+
745
+ const instanceId = await readRequiredCommandValue(
746
+ io,
747
+ flags,
748
+ ["id", "instance-id"],
749
+ "Instance id: ",
750
+ readOptionalPositional(positionals, 2),
751
+ );
752
+ if (!instanceId) {
753
+ ui.status("error", "Instance id is required.", { stream: "stderr" });
754
+ return 1;
755
+ }
756
+
757
+ try {
758
+ const client = createHarnessClient({ fetchFn: deps.fetchFn, session });
759
+ const result = await client.getWorkshopInstance(instanceId);
760
+ const nextSession = {
761
+ ...session,
762
+ selectedInstanceId: result.instance?.id ?? instanceId,
763
+ };
764
+ if (!(await persistSession(io, ui, env, nextSession))) {
765
+ return 1;
766
+ }
767
+
768
+ ui.json("Workshop Select Instance", {
769
+ ok: true,
770
+ source: "session",
771
+ selectedInstanceId: nextSession.selectedInstanceId,
772
+ ...summarizeWorkshopInstance(result.instance),
773
+ instance: result.instance,
774
+ });
775
+ return 0;
776
+ } catch (error) {
777
+ if (error instanceof HarnessApiError) {
778
+ ui.status("error", `Select instance failed: ${error.message}`, { stream: "stderr" });
779
+ return 1;
780
+ }
781
+ throw error;
782
+ }
783
+ }
784
+
592
785
  async function handleWorkshopListInstances(io, ui, env, deps) {
593
786
  const session = await requireSession(io, ui, env);
594
787
  if (!session) {
@@ -625,7 +818,7 @@ async function handleWorkshopShowInstance(io, ui, env, positionals, flags, deps)
625
818
  flags,
626
819
  ["id", "instance-id"],
627
820
  "Instance id: ",
628
- readOptionalPositional(positionals, 2),
821
+ readOptionalPositional(positionals, 2) ?? session.selectedInstanceId,
629
822
  );
630
823
  if (!instanceId) {
631
824
  ui.status("error", "Instance id is required.", { stream: "stderr" });
@@ -650,6 +843,55 @@ async function handleWorkshopShowInstance(io, ui, env, positionals, flags, deps)
650
843
  }
651
844
  }
652
845
 
846
+ async function handleWorkshopParticipantAccess(io, ui, env, positionals, flags, deps) {
847
+ const session = await requireSession(io, ui, env);
848
+ if (!session) {
849
+ return 1;
850
+ }
851
+
852
+ const instanceId = await readRequiredCommandValue(
853
+ io,
854
+ flags,
855
+ ["id", "instance-id"],
856
+ "Instance id: ",
857
+ readOptionalPositional(positionals, 2) ?? session.selectedInstanceId,
858
+ );
859
+ if (!instanceId) {
860
+ ui.status("error", "Instance id is required.", { stream: "stderr" });
861
+ return 1;
862
+ }
863
+
864
+ try {
865
+ const client = createHarnessClient({ fetchFn: deps.fetchFn, session });
866
+ if (flags.rotate === true) {
867
+ const result = await client.issueWorkshopParticipantAccess(instanceId, {
868
+ ...(typeof flags.code === "string" ? { code: flags.code } : {}),
869
+ });
870
+ ui.json("Workshop Participant Access", {
871
+ ok: true,
872
+ issuedCode: result.issuedCode ?? null,
873
+ ...summarizeParticipantAccess(result.participantAccess),
874
+ participantAccess: result.participantAccess,
875
+ });
876
+ return 0;
877
+ }
878
+
879
+ const result = await client.getWorkshopParticipantAccess(instanceId);
880
+ ui.json("Workshop Participant Access", {
881
+ ok: true,
882
+ ...summarizeParticipantAccess(result.participantAccess),
883
+ participantAccess: result.participantAccess,
884
+ });
885
+ return 0;
886
+ } catch (error) {
887
+ if (error instanceof HarnessApiError) {
888
+ ui.status("error", `Participant access failed: ${error.message}`, { stream: "stderr" });
889
+ return 1;
890
+ }
891
+ throw error;
892
+ }
893
+ }
894
+
653
895
  async function handleWorkshopArchive(io, ui, env, flags, deps) {
654
896
  const session = await requireSession(io, ui, env);
655
897
  if (!session) {
@@ -724,7 +966,7 @@ async function handleWorkshopUpdateInstance(io, ui, env, positionals, flags, dep
724
966
  flags,
725
967
  ["id"],
726
968
  "Instance id: ",
727
- readOptionalPositional(positionals, 2),
969
+ readOptionalPositional(positionals, 2) ?? session.selectedInstanceId,
728
970
  );
729
971
  if (!instanceId) {
730
972
  ui.status("error", "Instance id is required.", { stream: "stderr" });
@@ -739,7 +981,7 @@ async function handleWorkshopUpdateInstance(io, ui, env, positionals, flags, dep
739
981
  if (!hasWorkshopMetadataInput(payload)) {
740
982
  ui.status(
741
983
  "error",
742
- "At least one metadata field is required. Use flags such as --event-title, --date-range, --venue-name, or --room-name.",
984
+ "At least one metadata field is required. Use flags such as --content-lang, --event-title, --date-range, --venue-name, or --room-name.",
743
985
  { stream: "stderr" },
744
986
  );
745
987
  return 1;
@@ -774,7 +1016,7 @@ async function handleWorkshopPrepare(io, ui, env, positionals, flags, deps) {
774
1016
  flags,
775
1017
  ["id", "instance-id"],
776
1018
  "Instance id: ",
777
- readOptionalPositional(positionals, 2),
1019
+ readOptionalPositional(positionals, 2) ?? session.selectedInstanceId,
778
1020
  );
779
1021
  if (!instanceId) {
780
1022
  ui.status("error", "Instance id is required.", { stream: "stderr" });
@@ -810,7 +1052,7 @@ async function handleWorkshopResetInstance(io, ui, env, positionals, flags, deps
810
1052
  flags,
811
1053
  ["id", "instance-id"],
812
1054
  "Instance id: ",
813
- readOptionalPositional(positionals, 2),
1055
+ readOptionalPositional(positionals, 2) ?? session.selectedInstanceId,
814
1056
  );
815
1057
  if (!instanceId) {
816
1058
  ui.status("error", "Instance id is required.", { stream: "stderr" });
@@ -845,7 +1087,7 @@ async function handleWorkshopRemoveInstance(io, ui, env, positionals, flags, dep
845
1087
  flags,
846
1088
  ["id", "instance-id"],
847
1089
  "Instance id: ",
848
- readOptionalPositional(positionals, 2),
1090
+ readOptionalPositional(positionals, 2) ?? session.selectedInstanceId,
849
1091
  );
850
1092
  if (!instanceId) {
851
1093
  ui.status("error", "Instance id is required.", { stream: "stderr" });
@@ -884,7 +1126,11 @@ async function handleWorkshopPhaseSet(io, ui, env, positionals, deps) {
884
1126
 
885
1127
  try {
886
1128
  const client = createHarnessClient({ fetchFn: deps.fetchFn, session });
887
- const result = await client.setCurrentPhase(phaseId);
1129
+ const target = resolveCurrentInstanceTarget(session, env);
1130
+ const result =
1131
+ target.source === "session" && target.instanceId
1132
+ ? await client.setCurrentPhaseForInstance(target.instanceId, phaseId)
1133
+ : await client.setCurrentPhase(phaseId);
888
1134
  ui.json("Workshop Phase", result);
889
1135
  return 0;
890
1136
  } catch (error) {
@@ -899,8 +1145,8 @@ async function handleWorkshopPhaseSet(io, ui, env, positionals, deps) {
899
1145
  export async function runCli(argv, io, deps = {}) {
900
1146
  const fetchFn = deps.fetchFn ?? globalThis.fetch;
901
1147
  const mergedDeps = { fetchFn, sleepFn: deps.sleepFn, openUrl: deps.openUrl, cwd: deps.cwd };
902
- const ui = createCliUi(io);
903
1148
  const { positionals, flags } = parseArgs(argv);
1149
+ const ui = createCliUi(io, { jsonMode: flags.json === true || flags.output === "json" });
904
1150
  const [scope, action, subaction] = positionals;
905
1151
 
906
1152
  if (flags.help === true) {
@@ -943,6 +1189,14 @@ export async function runCli(argv, io, deps = {}) {
943
1189
  return handleSkillInstall(io, ui, mergedDeps, flags);
944
1190
  }
945
1191
 
1192
+ if (scope === "workshop" && action === "current-instance") {
1193
+ return handleWorkshopCurrentInstance(io, ui, io.env, mergedDeps);
1194
+ }
1195
+
1196
+ if (scope === "workshop" && action === "select-instance") {
1197
+ return handleWorkshopSelectInstance(io, ui, io.env, positionals, flags, mergedDeps);
1198
+ }
1199
+
946
1200
  if (scope === "workshop" && action === "status") {
947
1201
  return handleWorkshopStatus(io, ui, io.env, mergedDeps);
948
1202
  }
@@ -955,6 +1209,10 @@ export async function runCli(argv, io, deps = {}) {
955
1209
  return handleWorkshopShowInstance(io, ui, io.env, positionals, flags, mergedDeps);
956
1210
  }
957
1211
 
1212
+ if (scope === "workshop" && action === "participant-access") {
1213
+ return handleWorkshopParticipantAccess(io, ui, io.env, positionals, flags, mergedDeps);
1214
+ }
1215
+
958
1216
  if (scope === "workshop" && action === "archive") {
959
1217
  return handleWorkshopArchive(io, ui, io.env, flags, mergedDeps);
960
1218
  }
@@ -314,6 +314,7 @@ export function sanitizeSession(session, env) {
314
314
  authType: session.authType,
315
315
  username: session.username ?? null,
316
316
  email: session.email ?? null,
317
+ selectedInstanceId: session.selectedInstanceId ?? null,
317
318
  loggedInAt: session.loggedInAt,
318
319
  expiresAt: session.expiresAt ?? null,
319
320
  mode: session.mode ?? "local-dev",
@@ -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,
@@ -225,6 +253,20 @@ async function writeWorkshopBundleManifest(bundleRoot, manifest) {
225
253
  );
226
254
  }
227
255
 
256
+ async function pruneBundleFiles(bundleRoot, manifest) {
257
+ const expectedFiles = new Set([
258
+ WORKSHOP_BUNDLE_MANIFEST,
259
+ ...manifest.files.map((file) => file.path),
260
+ ]);
261
+ const currentFiles = await listFilesRecursive(bundleRoot);
262
+
263
+ for (const file of currentFiles) {
264
+ if (!expectedFiles.has(file.relativePath)) {
265
+ await fs.rm(file.absolutePath, { force: true });
266
+ }
267
+ }
268
+ }
269
+
228
270
  export async function createWorkshopBundleFromSource(sourceRoot, targetRoot, options = {}) {
229
271
  if (options.clean === true) {
230
272
  await fs.rm(targetRoot, { recursive: true, force: true });
@@ -234,6 +276,9 @@ export async function createWorkshopBundleFromSource(sourceRoot, targetRoot, opt
234
276
  await fs.rm(path.join(targetRoot, "workshop-skill", "SKILL.md"), { force: true });
235
277
  await copyBundleFiles(sourceRoot, targetRoot);
236
278
  const manifest = await createWorkshopBundleManifestFromSource(sourceRoot);
279
+ if (options.prune === true) {
280
+ await pruneBundleFiles(targetRoot, manifest);
281
+ }
237
282
  await writeWorkshopBundleManifest(targetRoot, manifest);
238
283
  }
239
284
 
@@ -250,7 +295,7 @@ export async function syncPackagedWorkshopBundle() {
250
295
  export async function syncRepoBundledWorkshopSkill() {
251
296
  const sourceRoot = getRepoWorkshopSourceRoot();
252
297
  const bundleRoot = getRepoBundledWorkshopSkillPath();
253
- await createWorkshopBundleFromSource(sourceRoot, bundleRoot, { clean: true });
298
+ await createWorkshopBundleFromSource(sourceRoot, bundleRoot, { prune: true });
254
299
  return {
255
300
  sourceRoot,
256
301
  bundleRoot,