@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.
- package/README.md +5 -0
- package/assets/workshop-bundle/SKILL.md +16 -0
- package/assets/workshop-bundle/bundle-manifest.json +46 -54
- package/assets/workshop-bundle/content/challenge-cards/deck.md +19 -17
- package/assets/workshop-bundle/content/challenge-cards/locales/en/deck.md +7 -5
- package/assets/workshop-bundle/content/challenge-cards/print-spec.md +1 -1
- package/assets/workshop-bundle/content/codex-craft.md +190 -0
- package/assets/workshop-bundle/content/facilitation/codex-setup-verification.md +5 -5
- package/assets/workshop-bundle/content/facilitation/master-guide.md +133 -66
- package/assets/workshop-bundle/content/project-briefs/code-review-helper.md +9 -9
- package/assets/workshop-bundle/content/project-briefs/devtoolbox-cli.md +11 -9
- package/assets/workshop-bundle/content/project-briefs/doc-generator.md +10 -8
- package/assets/workshop-bundle/content/project-briefs/locales/en/devtoolbox-cli.md +4 -2
- package/assets/workshop-bundle/content/project-briefs/locales/en/doc-generator.md +5 -3
- package/assets/workshop-bundle/content/project-briefs/locales/en/metrics-dashboard.md +4 -2
- package/assets/workshop-bundle/content/project-briefs/locales/en/standup-bot.md +4 -2
- package/assets/workshop-bundle/content/project-briefs/metrics-dashboard.md +14 -12
- package/assets/workshop-bundle/content/project-briefs/standup-bot.md +11 -9
- package/assets/workshop-bundle/content/talks/codex-demo-script.md +12 -10
- package/assets/workshop-bundle/content/talks/context-is-king.md +25 -25
- package/assets/workshop-bundle/docs/harness-cli-foundation.md +2 -0
- package/assets/workshop-bundle/docs/learner-resource-kit.md +37 -37
- package/assets/workshop-bundle/materials/coaching-codex.md +76 -0
- package/assets/workshop-bundle/materials/locales/en/participant-resource-kit.md +14 -2
- package/assets/workshop-bundle/materials/participant-resource-kit.md +23 -11
- package/assets/workshop-bundle/workshop-blueprint/README.md +2 -5
- package/assets/workshop-bundle/workshop-blueprint/day-structure.md +14 -0
- package/assets/workshop-bundle/workshop-skill/analyze-checklist.md +3 -3
- package/assets/workshop-bundle/workshop-skill/closing-skill.md +6 -6
- package/assets/workshop-bundle/workshop-skill/commands.md +17 -13
- package/assets/workshop-bundle/workshop-skill/facilitator.md +33 -0
- package/assets/workshop-bundle/workshop-skill/follow-up-package.md +13 -8
- package/assets/workshop-bundle/workshop-skill/install.md +8 -8
- package/assets/workshop-bundle/workshop-skill/locales/en/commands.md +4 -0
- package/assets/workshop-bundle/workshop-skill/locales/en/follow-up-package.md +8 -3
- package/assets/workshop-bundle/workshop-skill/locales/en/recap.md +8 -1
- package/assets/workshop-bundle/workshop-skill/locales/en/reference.md +20 -3
- package/assets/workshop-bundle/workshop-skill/locales/en/setup.md +1 -1
- package/assets/workshop-bundle/workshop-skill/recap.md +12 -5
- package/assets/workshop-bundle/workshop-skill/reference.md +53 -29
- package/assets/workshop-bundle/workshop-skill/setup.md +11 -11
- package/assets/workshop-bundle/workshop-skill/template-agents.md +4 -4
- package/package.json +1 -1
- package/src/client.js +9 -0
- package/src/io.js +1 -0
- package/src/run-cli.js +197 -0
- package/src/skill-install.js +108 -7
- package/src/workshop-bundle.js +30 -2
- package/assets/workshop-bundle/content/czech-editorial-review-checklist.md +0 -88
- package/assets/workshop-bundle/content/style-examples.md +0 -127
- package/assets/workshop-bundle/content/style-guide.md +0 -108
- 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
|
|
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
|
|
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
|
|
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
|
|
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
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
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
|
}
|
package/src/skill-install.js
CHANGED
|
@@ -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
|
|
47
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 {
|
package/src/workshop-bundle.js
CHANGED
|
@@ -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?
|