@growthub/cli 0.9.18 → 0.10.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 (56) hide show
  1. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/reference-options/route.js +62 -0
  2. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/refresh-sources/route.js +13 -2
  3. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/resolver-templates/route.js +23 -0
  4. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +35 -5
  5. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/test-source/route.js +15 -1
  6. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +2277 -0
  7. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataTable.jsx +1 -0
  8. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/FieldEditor.jsx +1 -0
  9. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/FieldManager.jsx +9 -0
  10. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ObjectSidebar.jsx +41 -0
  11. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/RecordDrawer.jsx +1 -0
  12. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ReferencePicker.jsx +244 -0
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxRunPanel.jsx +21 -0
  14. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SourceTestPanel.jsx +15 -0
  15. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/StatusPill.jsx +13 -0
  16. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ToggleField.jsx +41 -0
  17. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/dm-shared.jsx +99 -0
  18. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/page.jsx +2 -1528
  19. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +99 -6
  20. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/docs/connector-template-authoring.md +8 -0
  21. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/docs/data-model-reference-fields.md +15 -0
  22. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/docs/mcp-chrome-tool-connectors.md +12 -0
  23. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/docs/resolver-template-library.md +17 -0
  24. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/source-resolver-registry.js +13 -0
  25. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/templates/README.md +12 -0
  26. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/templates/chrome-bridge.js +22 -0
  27. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/templates/custom-http.js +23 -0
  28. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/templates/generic-commerce.js +22 -0
  29. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/templates/generic-crm.js +23 -0
  30. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/templates/generic-project-management.js +22 -0
  31. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/templates/generic-spreadsheet.js +22 -0
  32. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/templates/mcp-tool.js +22 -0
  33. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/templates/template-registry.js +50 -0
  34. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/templates/webhook.js +22 -0
  35. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/references/collect-reference-options.js +133 -0
  36. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/references/reference-resolver-registry.js +17 -0
  37. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/references/resolver-loader.js +6 -0
  38. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/references/resolvers/README.md +8 -0
  39. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/references/resolvers/local-data-model.js +11 -0
  40. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/references/resolvers/source-records.js +34 -0
  41. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/adapters/README.md +5 -3
  42. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-intelligence.js +203 -0
  43. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/index.js +1 -0
  44. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/data-model/field-contracts.js +81 -0
  45. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/data-model/reference-option-schema.js +59 -0
  46. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/data-model/reference-options.js +29 -0
  47. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +534 -23
  48. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-schema.js +131 -1
  49. package/assets/worker-kits/growthub-custom-workspace-starter-v1/helpers/export-training-traces.mjs +144 -0
  50. package/assets/worker-kits/growthub-custom-workspace-starter-v1/helpers/grade-raw-pairs.mjs +279 -0
  51. package/assets/worker-kits/growthub-custom-workspace-starter-v1/helpers/harvest-cursor-traces.mjs +288 -0
  52. package/assets/worker-kits/growthub-custom-workspace-starter-v1/helpers/upload-graded-traces.mjs +128 -0
  53. package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +10 -0
  54. package/assets/worker-kits/growthub-custom-workspace-starter-v1/templates/seeded-configs/alignment-loop.config.json +264 -0
  55. package/dist/index.js +486 -1
  56. package/package.json +1 -1
@@ -132,7 +132,13 @@ const WIDGET_SCHEMA_CONTRACTS = {
132
132
  },
133
133
  FieldSettingsConfig: {
134
134
  hidden: "string[] of column names hidden from preview",
135
- order: "string[] of column names defining custom order"
135
+ order: "string[] of column names defining custom order",
136
+ sort: "SortClause[] optional",
137
+ filter: "FilterConfig optional",
138
+ types: "record<string,string> optional — client field-type hints",
139
+ views: "saved view snapshots optional",
140
+ activeViewId: "string optional",
141
+ favorite: "boolean optional"
136
142
  },
137
143
  SortClause: {
138
144
  fieldId: "non-empty string (column name)",
@@ -491,6 +497,45 @@ function validateFieldSettings(fieldSettings, path, errors) {
491
497
  }
492
498
  if (fieldSettings.hidden !== undefined) validateStringArray(fieldSettings.hidden, `${path}.hidden`, errors);
493
499
  if (fieldSettings.order !== undefined) validateStringArray(fieldSettings.order, `${path}.order`, errors);
500
+ validateSortClauses(fieldSettings.sort, `${path}.sort`, errors);
501
+ validateFilterClauses(fieldSettings.filter, `${path}.filter`, errors);
502
+ if (fieldSettings.types !== undefined) {
503
+ if (!isPlainObject(fieldSettings.types)) {
504
+ errors.push(`${path}.types must be a plain object`);
505
+ } else {
506
+ Object.entries(fieldSettings.types).forEach(([key, value]) => {
507
+ if (typeof key !== "string" || !key.trim()) errors.push(`${path}.types keys must be non-empty strings`);
508
+ if (typeof value !== "string" || !value.trim()) errors.push(`${path}.types.${key} must be a non-empty string`);
509
+ });
510
+ }
511
+ }
512
+ if (fieldSettings.activeViewId !== undefined && typeof fieldSettings.activeViewId !== "string") {
513
+ errors.push(`${path}.activeViewId must be a string`);
514
+ }
515
+ if (fieldSettings.favorite !== undefined && typeof fieldSettings.favorite !== "boolean") {
516
+ errors.push(`${path}.favorite must be a boolean`);
517
+ }
518
+ if (fieldSettings.views !== undefined) {
519
+ if (!Array.isArray(fieldSettings.views)) {
520
+ errors.push(`${path}.views must be an array`);
521
+ } else {
522
+ fieldSettings.views.forEach((view, index) => {
523
+ const prefix = `${path}.views[${index}]`;
524
+ if (!isPlainObject(view)) {
525
+ errors.push(`${prefix} must be a plain object`);
526
+ return;
527
+ }
528
+ if (typeof view.id !== "string" || !view.id.trim()) errors.push(`${prefix}.id must be a non-empty string`);
529
+ if (typeof view.name !== "string" || !view.name.trim()) errors.push(`${prefix}.name must be a non-empty string`);
530
+ if (view.favorite !== undefined && typeof view.favorite !== "boolean") errors.push(`${prefix}.favorite must be a boolean`);
531
+ if (view.locked !== undefined && typeof view.locked !== "boolean") errors.push(`${prefix}.locked must be a boolean`);
532
+ if (view.hidden !== undefined) validateStringArray(view.hidden, `${prefix}.hidden`, errors);
533
+ if (view.order !== undefined) validateStringArray(view.order, `${prefix}.order`, errors);
534
+ validateSortClauses(view.sort, `${prefix}.sort`, errors);
535
+ validateFilterClauses(view.filter, `${prefix}.filter`, errors);
536
+ });
537
+ }
538
+ }
494
539
  }
495
540
 
496
541
  function validateSortClauses(sort, path, errors) {
@@ -820,6 +865,65 @@ function validateCanvasConfig(canvas, errors) {
820
865
  }
821
866
  }
822
867
 
868
+ function validateDataModelRelation(relation, path, errors) {
869
+ if (!isPlainObject(relation)) {
870
+ errors.push(`${path} must be a plain object`);
871
+ return;
872
+ }
873
+ for (const key of ["id", "name", "field", "targetObjectType", "type"]) {
874
+ if (typeof relation[key] !== "string" || !relation[key].trim()) {
875
+ errors.push(`${path}.${key} must be a non-empty string`);
876
+ }
877
+ }
878
+ if (relation.type !== undefined && !["belongs-to", "has-many"].includes(relation.type)) {
879
+ errors.push(`${path}.type must be belongs-to or has-many`);
880
+ }
881
+ for (const opt of ["valueField", "labelField", "secondaryLabelField", "statusField"]) {
882
+ if (relation[opt] !== undefined && relation[opt] !== null && typeof relation[opt] !== "string") {
883
+ errors.push(`${path}.${opt} must be a string when present`);
884
+ }
885
+ }
886
+ if (relation.statusAllowlist !== undefined) {
887
+ if (!Array.isArray(relation.statusAllowlist)) {
888
+ errors.push(`${path}.statusAllowlist must be an array of strings when present`);
889
+ } else {
890
+ relation.statusAllowlist.forEach((entry, i) => {
891
+ if (typeof entry !== "string" || !entry.trim()) {
892
+ errors.push(`${path}.statusAllowlist[${i}] must be a non-empty string`);
893
+ }
894
+ });
895
+ }
896
+ }
897
+ if (relation.searchable !== undefined && typeof relation.searchable !== "boolean") {
898
+ errors.push(`${path}.searchable must be a boolean when present`);
899
+ }
900
+ if (relation.pageSize !== undefined && relation.pageSize !== "") {
901
+ const ps = Number(relation.pageSize);
902
+ if (!Number.isFinite(ps) || ps < 1 || ps > 500) {
903
+ errors.push(`${path}.pageSize must be a number between 1 and 500 when present`);
904
+ }
905
+ }
906
+ if (relation.referenceSource !== undefined) {
907
+ const rs = String(relation.referenceSource).trim();
908
+ if (!["workspace-rows", "source-records"].includes(rs)) {
909
+ errors.push(`${path}.referenceSource must be workspace-rows or source-records when present`);
910
+ }
911
+ }
912
+ if (relation.sidecarSourceId !== undefined && typeof relation.sidecarSourceId !== "string") {
913
+ errors.push(`${path}.sidecarSourceId must be a string when present`);
914
+ }
915
+ if (relation.resolver !== undefined) {
916
+ if (!isPlainObject(relation.resolver)) {
917
+ errors.push(`${path}.resolver must be a plain object when present`);
918
+ } else if (
919
+ relation.resolver.integrationId !== undefined
920
+ && (typeof relation.resolver.integrationId !== "string" || !relation.resolver.integrationId.trim())
921
+ ) {
922
+ errors.push(`${path}.resolver.integrationId must be a non-empty string when present`);
923
+ }
924
+ }
925
+ }
926
+
823
927
  function validateSandboxEnvironmentRow(row, path, errors) {
824
928
  if (!isPlainObject(row)) return;
825
929
  const lifecycleStatus = String(row.lifecycleStatus || "").trim().toLowerCase();
@@ -847,6 +951,18 @@ function validateSandboxEnvironmentRow(row, path, errors) {
847
951
  if (row.agentHost !== undefined && row.agentHost !== "" && !KNOWN_SANDBOX_AGENT_HOSTS.includes(row.agentHost)) {
848
952
  errors.push(`${path}.agentHost must be one of ${KNOWN_SANDBOX_AGENT_HOSTS.join(", ")}`);
849
953
  }
954
+ const INTELLIGENCE_ADAPTER_MODES = ["ollama", "lmstudio", "vllm", "custom-openai-compatible"];
955
+ for (const field of ["localModel", "localEndpoint", "intelligenceAdapterMode"]) {
956
+ if (row[field] !== undefined && row[field] !== null && row[field] !== "" && typeof row[field] !== "string") {
957
+ errors.push(`${path}.${field} must be a string when set`);
958
+ }
959
+ }
960
+ if (row.intelligenceAdapterMode !== undefined && String(row.intelligenceAdapterMode).trim() !== "") {
961
+ const im = String(row.intelligenceAdapterMode).trim().toLowerCase();
962
+ if (!INTELLIGENCE_ADAPTER_MODES.includes(im)) {
963
+ errors.push(`${path}.intelligenceAdapterMode must be one of ${INTELLIGENCE_ADAPTER_MODES.join(", ")}`);
964
+ }
965
+ }
850
966
  if (row.envRefs !== undefined && typeof row.envRefs !== "string" && !Array.isArray(row.envRefs)) {
851
967
  errors.push(`${path}.envRefs must be a comma-separated string or array of env-ref slugs (never values)`);
852
968
  }
@@ -877,6 +993,11 @@ function validateSandboxEnvironmentRow(row, path, errors) {
877
993
  errors.push(`${path}.timeoutMs must be a finite number between 0 and ${SANDBOX_MAX_TIMEOUT_MS}`);
878
994
  }
879
995
  }
996
+ for (const traceField of ["resolverTemplateId", "connectorKind", "executionLane"]) {
997
+ if (row[traceField] !== undefined && typeof row[traceField] !== "string") {
998
+ errors.push(`${path}.${traceField} must be a string when present`);
999
+ }
1000
+ }
880
1001
  }
881
1002
 
882
1003
  function validateDataModelConfig(dataModel, errors) {
@@ -925,6 +1046,15 @@ function validateDataModelConfig(dataModel, errors) {
925
1046
  if (object.binding?.sourceStorage === "workspace-source-records" && typeof object.sourceId !== "string") {
926
1047
  errors.push(`${prefix}.sourceId is required when binding.sourceStorage is "workspace-source-records"`);
927
1048
  }
1049
+ if (object.relations !== undefined) {
1050
+ if (!Array.isArray(object.relations)) {
1051
+ errors.push(`${prefix}.relations must be an array`);
1052
+ } else {
1053
+ object.relations.forEach((rel, relIndex) => {
1054
+ validateDataModelRelation(rel, `${prefix}.relations[${relIndex}]`, errors);
1055
+ });
1056
+ }
1057
+ }
928
1058
  validateFieldSettings(object.fieldSettings, `${prefix}.fieldSettings`, errors);
929
1059
  });
930
1060
  }
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * helpers/export-training-traces.mjs — Distillation Pipeline V1, Phase 3
4
+ *
5
+ * Reads `training-traces.rows` from the live workspace, filters rows where
6
+ * qualityScore >= --min-score AND exported == "false", emits an Unsloth-ready
7
+ * JSONL of {instruction, input, output} on disk, then PATCHes the same rows
8
+ * with exported = "true" so they are not re-exported on the next run.
9
+ *
10
+ * Output format (one JSON object per line):
11
+ * {"instruction": "<system + task>", "input": "<user prompt>", "output": "<agent output>"}
12
+ *
13
+ * Usage:
14
+ * node helpers/export-training-traces.mjs \
15
+ * --workspace http://localhost:3000 \
16
+ * --traces-object training-traces \
17
+ * --min-score 4 \
18
+ * --out ./antonio/distillation/unsloth-batch-001.jsonl \
19
+ * --instruction "You are growthub-local-expert. Respect AWaC V2 invariants and the PATCH allowlist." \
20
+ * [--dry-run]
21
+ */
22
+
23
+ import fs from "node:fs";
24
+ import path from "node:path";
25
+
26
+ function parseArgs(argv) {
27
+ const a = {
28
+ workspace: "http://localhost:3000",
29
+ tracesObject: "training-traces",
30
+ minScore: 4,
31
+ out: "",
32
+ instruction: "You are growthub-local-expert. Respect AWaC V2 invariants and the PATCH allowlist.",
33
+ dryRun: false,
34
+ };
35
+ for (let i = 0; i < argv.length; i += 1) {
36
+ const t = argv[i];
37
+ const next = () => String(argv[++i] || "").trim();
38
+ if (t === "--workspace") a.workspace = next().replace(/\/+$/, "");
39
+ else if (t === "--traces-object") a.tracesObject = next();
40
+ else if (t === "--min-score") a.minScore = Number(next()) || 4;
41
+ else if (t === "--out") a.out = next();
42
+ else if (t === "--instruction") a.instruction = next();
43
+ else if (t === "--dry-run") a.dryRun = true;
44
+ else if (t === "--help" || t === "-h") {
45
+ process.stdout.write(
46
+ "Usage: export-training-traces.mjs [--workspace URL] [--traces-object id] [--min-score N] --out <path> [--instruction TEXT] [--dry-run]\n",
47
+ );
48
+ process.exit(0);
49
+ }
50
+ }
51
+ if (!a.out) {
52
+ process.stderr.write("error: --out is required\n");
53
+ process.exit(2);
54
+ }
55
+ return a;
56
+ }
57
+
58
+ const args = parseArgs(process.argv.slice(2));
59
+ const outAbs = path.resolve(args.out);
60
+ fs.mkdirSync(path.dirname(outAbs), { recursive: true });
61
+
62
+ async function getObjects() {
63
+ const r = await fetch(`${args.workspace}/api/workspace`, { cache: "no-store" });
64
+ if (!r.ok) throw new Error(`GET /api/workspace ${r.status}`);
65
+ return (await r.json()).workspaceConfig.dataModel.objects;
66
+ }
67
+ async function patchObjects(objects) {
68
+ const r = await fetch(`${args.workspace}/api/workspace`, {
69
+ method: "PATCH",
70
+ headers: { "content-type": "application/json" },
71
+ body: JSON.stringify({ dataModel: { objects } }),
72
+ });
73
+ if (!r.ok) throw new Error(`PATCH ${r.status}: ${(await r.text()).slice(0, 300)}`);
74
+ }
75
+
76
+ const objects = await getObjects();
77
+ const tracesIdx = objects.findIndex((o) => o.id === args.tracesObject);
78
+ if (tracesIdx < 0) {
79
+ process.stderr.write(`error: object ${args.tracesObject} not found in workspace\n`);
80
+ process.exit(3);
81
+ }
82
+ const tracesObj = objects[tracesIdx];
83
+ const allRows = Array.isArray(tracesObj.rows) ? tracesObj.rows : [];
84
+
85
+ const eligible = allRows
86
+ .map((row, idx) => ({ row, idx }))
87
+ .filter(({ row }) =>
88
+ Number(row.qualityScore) >= args.minScore &&
89
+ String(row.exported || "false").toLowerCase() !== "true" &&
90
+ String(row.inputPrompt || "").trim() &&
91
+ String(row.agentOutput || "").trim(),
92
+ );
93
+
94
+ if (eligible.length === 0) {
95
+ process.stdout.write(
96
+ JSON.stringify(
97
+ {
98
+ ok: true,
99
+ out: outAbs,
100
+ eligible: 0,
101
+ exported: 0,
102
+ totalRows: allRows.length,
103
+ reason: "no rows match score >= min-score AND exported == false",
104
+ },
105
+ null,
106
+ 2,
107
+ ) + "\n",
108
+ );
109
+ process.exit(0);
110
+ }
111
+
112
+ const outStream = fs.createWriteStream(outAbs, { encoding: "utf8" });
113
+ for (const { row } of eligible) {
114
+ const sample = {
115
+ instruction: args.instruction,
116
+ input: String(row.inputPrompt),
117
+ output: String(row.agentOutput),
118
+ };
119
+ outStream.write(`${JSON.stringify(sample)}\n`);
120
+ }
121
+ await new Promise((r) => outStream.end(r));
122
+
123
+ if (!args.dryRun) {
124
+ const eligibleIdx = new Set(eligible.map((e) => e.idx));
125
+ const updatedRows = allRows.map((row, i) => (eligibleIdx.has(i) ? { ...row, exported: "true" } : row));
126
+ const nextObjects = objects.map((o, i) => (i !== tracesIdx ? o : { ...o, rows: updatedRows }));
127
+ await patchObjects(nextObjects);
128
+ }
129
+
130
+ process.stdout.write(
131
+ JSON.stringify(
132
+ {
133
+ ok: true,
134
+ out: outAbs,
135
+ totalRows: allRows.length,
136
+ eligible: eligible.length,
137
+ exported: args.dryRun ? 0 : eligible.length,
138
+ dryRun: args.dryRun,
139
+ format: "unsloth-jsonl-v1 ({instruction, input, output})",
140
+ },
141
+ null,
142
+ 2,
143
+ ) + "\n",
144
+ );
@@ -0,0 +1,279 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * helpers/grade-raw-pairs.mjs — Distillation Pipeline V1, Phase 2
4
+ *
5
+ * Grades pairs from `raw-pairs.jsonl` (Phase 1 output) by routing each one
6
+ * through the live `critic-grader` sandbox row (local-intelligence /
7
+ * gemma3:4b). The script never bypasses the workspace API: it PATCHes the
8
+ * critic row's `command`, calls `POST /api/workspace/sandbox-run`, then
9
+ * parses the strict-JSON `{score, reason}` envelope the grader returns.
10
+ *
11
+ * Quality boost: pairs whose `mergedToMain === true` (Phase 1 ground-truth
12
+ * signal that the work was squash-merged on `main`) get a floor of 4.
13
+ *
14
+ * Output: newline-delimited JSON with the original pair fields plus
15
+ * - `qualityScore` 1-5 (string for downstream parity with training-traces)
16
+ * - `qualityReason` one-sentence rationale from the grader
17
+ * - `criticRunMs` latency
18
+ * - `criticRunId` run id for traceability
19
+ * - `gradedAt` ISO timestamp
20
+ * - `boostedByMerge` true if mergedToMain forced the floor
21
+ *
22
+ * Streams to disk after each pair so partial progress survives a kill.
23
+ *
24
+ * Usage:
25
+ * node helpers/grade-raw-pairs.mjs \
26
+ * --in ./antonio/distillation/raw-pairs.jsonl \
27
+ * --out ./antonio/distillation/graded-batch-001.jsonl \
28
+ * --workspace http://localhost:3000 \
29
+ * --grader-row critic-grader \
30
+ * --sandbox-object sandboxes-alignment-loop \
31
+ * --limit 20 \
32
+ * --offset 0 \
33
+ * --max-input-chars 6000 # cap pair text to keep grader prompts safe
34
+ */
35
+
36
+ import fs from "node:fs";
37
+ import path from "node:path";
38
+
39
+ function parseArgs(argv) {
40
+ const a = {
41
+ in: "",
42
+ out: "",
43
+ workspace: "http://localhost:3000",
44
+ graderRow: "critic-grader",
45
+ sandboxObject: "sandboxes-alignment-loop",
46
+ limit: 20,
47
+ offset: 0,
48
+ maxInputChars: 6000,
49
+ mergedOnly: false,
50
+ };
51
+ for (let i = 0; i < argv.length; i += 1) {
52
+ const t = argv[i];
53
+ const next = () => String(argv[++i] || "").trim();
54
+ if (t === "--in") a.in = next();
55
+ else if (t === "--out") a.out = next();
56
+ else if (t === "--workspace") a.workspace = next().replace(/\/+$/, "");
57
+ else if (t === "--grader-row") a.graderRow = next();
58
+ else if (t === "--sandbox-object") a.sandboxObject = next();
59
+ else if (t === "--limit") a.limit = Number(next()) || 20;
60
+ else if (t === "--offset") a.offset = Number(next()) || 0;
61
+ else if (t === "--max-input-chars") a.maxInputChars = Number(next()) || 6000;
62
+ else if (t === "--merged-only") a.mergedOnly = true;
63
+ else if (t === "--help" || t === "-h") {
64
+ process.stdout.write(
65
+ "Usage: grade-raw-pairs.mjs --in <raw-pairs.jsonl> --out <graded.jsonl> [--workspace URL] [--grader-row name] [--sandbox-object id] [--limit N] [--offset N] [--max-input-chars N] [--merged-only]\n",
66
+ );
67
+ process.exit(0);
68
+ }
69
+ }
70
+ if (!a.in || !a.out) {
71
+ process.stderr.write("error: --in and --out are required\n");
72
+ process.exit(2);
73
+ }
74
+ return a;
75
+ }
76
+
77
+ const args = parseArgs(process.argv.slice(2));
78
+
79
+ async function getWorkspaceObjects() {
80
+ const r = await fetch(`${args.workspace}/api/workspace`, { cache: "no-store" });
81
+ if (!r.ok) throw new Error(`GET /api/workspace ${r.status}`);
82
+ const j = await r.json();
83
+ return j.workspaceConfig.dataModel.objects;
84
+ }
85
+
86
+ async function patchObjects(objects) {
87
+ const r = await fetch(`${args.workspace}/api/workspace`, {
88
+ method: "PATCH",
89
+ headers: { "content-type": "application/json" },
90
+ body: JSON.stringify({ dataModel: { objects } }),
91
+ });
92
+ if (!r.ok) {
93
+ const t = await r.text();
94
+ throw new Error(`PATCH /api/workspace ${r.status}: ${t.slice(0, 300)}`);
95
+ }
96
+ }
97
+
98
+ async function runGraderSandbox() {
99
+ const r = await fetch(`${args.workspace}/api/workspace/sandbox-run`, {
100
+ method: "POST",
101
+ headers: { "content-type": "application/json" },
102
+ body: JSON.stringify({ objectId: args.sandboxObject, name: args.graderRow }),
103
+ });
104
+ return r.json();
105
+ }
106
+
107
+ function setRowCommand(objects, rowName, command) {
108
+ return objects.map((obj) => {
109
+ if (obj.id !== args.sandboxObject) return obj;
110
+ return {
111
+ ...obj,
112
+ rows: (obj.rows || []).map((row) => (row.Name === rowName ? { ...row, command } : row)),
113
+ };
114
+ });
115
+ }
116
+
117
+ function parseScoreFromGraderEnvelope(stdout) {
118
+ if (!stdout) return null;
119
+ try {
120
+ const env = JSON.parse(stdout);
121
+ if (env?.result?.json && typeof env.result.json.score === "number") {
122
+ return { score: env.result.json.score, reason: String(env.result.json.reason || "") };
123
+ }
124
+ if (typeof env?.rawText === "string") {
125
+ const outer = JSON.parse(env.rawText);
126
+ const content = outer?.choices?.[0]?.message?.content;
127
+ if (typeof content === "string") {
128
+ const inner = JSON.parse(content);
129
+ if (typeof inner?.score === "number") {
130
+ return { score: inner.score, reason: String(inner.reason || "") };
131
+ }
132
+ }
133
+ }
134
+ } catch {
135
+ // fall through
136
+ }
137
+ return null;
138
+ }
139
+
140
+ function buildGraderPrompt(pair, maxChars) {
141
+ const promptHead = pair.inputPrompt.slice(0, Math.floor(maxChars / 3));
142
+ const outputHead = pair.agentOutput.slice(0, maxChars - promptHead.length - 800);
143
+ const lines = [
144
+ "You are critic-grader for AWaC V2. Score this user→assistant pair from a Cursor session on the growthub-local repo.",
145
+ "Criteria:",
146
+ " 1) Clear understanding of the user request",
147
+ " 2) Used appropriate tools / primitives",
148
+ " 3) Respects AWaC V2 invariants (PATCH allowlist, no secret leaks, no protected-boundary edits)",
149
+ " 4) Output is correct and actionable",
150
+ " 5) Production-quality (would survive code review on this repo)",
151
+ "Return ONLY strict JSON: {\"score\": <1-5 integer>, \"reason\": \"one short sentence\"}.",
152
+ "",
153
+ "USER PROMPT (truncated):",
154
+ promptHead,
155
+ "",
156
+ "ASSISTANT RESPONSE (truncated):",
157
+ outputHead,
158
+ ];
159
+ return lines.join("\n");
160
+ }
161
+
162
+ // ---------- read pairs ----------
163
+ const inAbs = path.resolve(args.in);
164
+ const outAbs = path.resolve(args.out);
165
+ fs.mkdirSync(path.dirname(outAbs), { recursive: true });
166
+
167
+ const allLines = fs.readFileSync(inAbs, "utf8").split("\n").filter(Boolean);
168
+ let pool = allLines;
169
+ if (args.mergedOnly) {
170
+ pool = allLines.filter((ln) => {
171
+ try {
172
+ return JSON.parse(ln).mergedToMain === true;
173
+ } catch {
174
+ return false;
175
+ }
176
+ });
177
+ }
178
+ const slice = pool.slice(args.offset, args.offset + args.limit);
179
+
180
+ process.stdout.write(
181
+ `[grade] in=${path.basename(inAbs)} totalLines=${allLines.length} pool=${pool.length}${args.mergedOnly ? " (mergedOnly)" : ""} offset=${args.offset} batch=${slice.length} -> ${path.basename(outAbs)}\n`,
182
+ );
183
+
184
+ const outStream = fs.createWriteStream(outAbs, { encoding: "utf8" });
185
+ const summary = {
186
+ graded: 0,
187
+ parseFailures: 0,
188
+ scoreCounts: { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 0: 0 },
189
+ boostedByMerge: 0,
190
+ scoreSum: 0,
191
+ startedAt: new Date().toISOString(),
192
+ };
193
+
194
+ for (let i = 0; i < slice.length; i += 1) {
195
+ let pair;
196
+ try {
197
+ pair = JSON.parse(slice[i]);
198
+ } catch {
199
+ process.stderr.write(`[grade] skip line ${i}: not JSON\n`);
200
+ continue;
201
+ }
202
+
203
+ const command = buildGraderPrompt(pair, args.maxInputChars);
204
+ let parsedScore = null;
205
+ let runMs = 0;
206
+ let runId = "";
207
+ let boosted = false;
208
+
209
+ try {
210
+ const objects = await getWorkspaceObjects();
211
+ await patchObjects(setRowCommand(objects, args.graderRow, command));
212
+ const startedAt = Date.now();
213
+ const run = await runGraderSandbox();
214
+ runMs = Date.now() - startedAt;
215
+ runId = run?.runId || "";
216
+ parsedScore = parseScoreFromGraderEnvelope(run?.response?.stdout);
217
+ } catch (e) {
218
+ process.stderr.write(`[grade] pair ${i + args.offset} sandbox-run error: ${e.message}\n`);
219
+ }
220
+
221
+ if (!parsedScore) {
222
+ summary.parseFailures += 1;
223
+ parsedScore = { score: 0, reason: "grader did not return parseable JSON" };
224
+ }
225
+
226
+ // Apply mergedToMain floor=4 boost
227
+ if (pair.mergedToMain === true && parsedScore.score < 4) {
228
+ boosted = true;
229
+ summary.boostedByMerge += 1;
230
+ parsedScore = { score: 4, reason: `[boosted by squash-merge to main; original: ${parsedScore.score} - ${parsedScore.reason}]` };
231
+ }
232
+
233
+ summary.graded += 1;
234
+ summary.scoreCounts[parsedScore.score] = (summary.scoreCounts[parsedScore.score] || 0) + 1;
235
+ summary.scoreSum += parsedScore.score;
236
+
237
+ const out = {
238
+ ...pair,
239
+ qualityScore: String(parsedScore.score),
240
+ qualityReason: parsedScore.reason,
241
+ criticRunMs: runMs,
242
+ criticRunId: runId,
243
+ boostedByMerge: boosted,
244
+ gradedAt: new Date().toISOString(),
245
+ };
246
+ outStream.write(`${JSON.stringify(out)}\n`);
247
+
248
+ process.stdout.write(
249
+ `[grade] ${i + 1}/${slice.length} session=${pair.sessionId.slice(0, 8)} pair=${pair.pairIndex} score=${out.qualityScore}${boosted ? "*" : ""} ms=${runMs}\n`,
250
+ );
251
+ }
252
+
253
+ await new Promise((r) => outStream.end(r));
254
+
255
+ const avg = summary.graded ? (summary.scoreSum / summary.graded).toFixed(2) : "0.00";
256
+ const highCount = (summary.scoreCounts[4] || 0) + (summary.scoreCounts[5] || 0);
257
+ const finishedAt = new Date().toISOString();
258
+
259
+ process.stdout.write(
260
+ "\n" +
261
+ JSON.stringify(
262
+ {
263
+ ok: true,
264
+ out: outAbs,
265
+ startedAt: summary.startedAt,
266
+ finishedAt,
267
+ graded: summary.graded,
268
+ parseFailures: summary.parseFailures,
269
+ boostedByMerge: summary.boostedByMerge,
270
+ averageScore: Number(avg),
271
+ scoreCounts: summary.scoreCounts,
272
+ highQualityCount: highCount,
273
+ highQualityRatio: summary.graded ? Number((highCount / summary.graded).toFixed(3)) : 0,
274
+ },
275
+ null,
276
+ 2,
277
+ ) +
278
+ "\n",
279
+ );