@remits/remits-cli 0.1.121 → 0.1.124
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/index.js +249 -36
- package/package.json +1 -1
- package/skills/remits-cli/SKILL.md +6 -0
- package/skills/remits-cli/references/branch-variants.md +8 -1
- package/skills/remits-cli/references/command-reference.md +1 -1
- package/skills/remits-cli/references/component-integrity.md +27 -7
- package/skills/remits-cli/references/development-loop.md +27 -0
package/index.js
CHANGED
|
@@ -19,6 +19,9 @@ const openBrowser = (openModule && typeof openModule === 'function')
|
|
|
19
19
|
|
|
20
20
|
//const DEFAULT_BASE_URL = process.env.REMITS_BASE_URL || 'http://localhost:8080';
|
|
21
21
|
const DEFAULT_BASE_URL = process.env.REMITS_BASE_URL || 'https://remits-529558023549.us-east5.run.app';
|
|
22
|
+
|
|
23
|
+
// How many compile failures print their full message before the rest are listed by identifier only.
|
|
24
|
+
const COMPILE_FAILURES_SHOWN = 5;
|
|
22
25
|
const SESSION_DIR = path.join(os.homedir(), '.remits-cli');
|
|
23
26
|
const SESSIONS_FILE = path.join(SESSION_DIR, 'sessions.json');
|
|
24
27
|
const CONFIG_FILE = path.join(SESSION_DIR, 'config.json');
|
|
@@ -1816,6 +1819,19 @@ function resolveSessionContext(cwd, flags) {
|
|
|
1816
1819
|
};
|
|
1817
1820
|
}
|
|
1818
1821
|
|
|
1822
|
+
/**
|
|
1823
|
+
* Record the FILE identity of a component (type + filename id, or type + filename stem) on an object without
|
|
1824
|
+
* sending it anywhere. Non-enumerable, so JSON payloads and the content hash never see it.
|
|
1825
|
+
*
|
|
1826
|
+
* It exists because a component is named twice: by its files, and by its `.meta.yml` `name:`. For a `new_`
|
|
1827
|
+
* file there is no id, so the two can disagree (`new_PDFStatement` vs `name: PDF Statement`), and the file
|
|
1828
|
+
* identity is the only thing both the payload and the git changed set agree on.
|
|
1829
|
+
*/
|
|
1830
|
+
function withFileKey(target, key) {
|
|
1831
|
+
Object.defineProperty(target, 'fileKey', { value: key, enumerable: false, configurable: true });
|
|
1832
|
+
return target;
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1819
1835
|
function collectComponents(cwd) {
|
|
1820
1836
|
const mapping = {
|
|
1821
1837
|
schemas: 'schema',
|
|
@@ -1863,7 +1879,7 @@ function collectComponents(cwd) {
|
|
|
1863
1879
|
//
|
|
1864
1880
|
// A partial stage (mcp_component_edit writing one field) deliberately does NOT set this and
|
|
1865
1881
|
// keeps the merge semantics it needs.
|
|
1866
|
-
byKey.set(key, { type, id, name, metadataAuthoritative: true });
|
|
1882
|
+
byKey.set(key, withFileKey({ type, id, name, metadataAuthoritative: true }, key));
|
|
1867
1883
|
}
|
|
1868
1884
|
const component = byKey.get(key);
|
|
1869
1885
|
const filePath = path.join(dir, fileName);
|
|
@@ -2030,11 +2046,11 @@ function changedComponentsFromWorkingTree(cwd) {
|
|
|
2030
2046
|
if (!entry.statuses.includes(status)) entry.statuses.push(status);
|
|
2031
2047
|
if (!entry.paths.includes(info.path)) entry.paths.push(info.path);
|
|
2032
2048
|
}
|
|
2033
|
-
return Array.from(byKey.
|
|
2049
|
+
return Array.from(byKey.entries()).map(([key, entry]) => withFileKey({
|
|
2034
2050
|
...entry,
|
|
2035
2051
|
fields: entry.fields.sort(),
|
|
2036
2052
|
paths: entry.paths.sort()
|
|
2037
|
-
})).sort((a, b) => {
|
|
2053
|
+
}, key)).sort((a, b) => {
|
|
2038
2054
|
const typeCmp = String(a.type).localeCompare(String(b.type));
|
|
2039
2055
|
if (typeCmp !== 0) return typeCmp;
|
|
2040
2056
|
return String(a.id || a.name || '').localeCompare(String(b.id || b.name || ''));
|
|
@@ -2068,7 +2084,7 @@ function changedComponentsSinceRef(cwd, ref) {
|
|
|
2068
2084
|
const info = componentPathInfo(cwd, filePath);
|
|
2069
2085
|
if (!info) continue;
|
|
2070
2086
|
if (!byKey.has(info.key)) {
|
|
2071
|
-
byKey.set(info.key, { type: info.type, id: info.id, name: info.name, fields: [], paths: [] });
|
|
2087
|
+
byKey.set(info.key, withFileKey({ type: info.type, id: info.id, name: info.name, fields: [], paths: [] }, info.key));
|
|
2072
2088
|
}
|
|
2073
2089
|
const entry = byKey.get(info.key);
|
|
2074
2090
|
if (!entry.fields.includes(info.field)) entry.fields.push(info.field);
|
|
@@ -2756,7 +2772,7 @@ async function pushComponentsCommand(flags) {
|
|
|
2756
2772
|
const dataMode = resolveDataMode(flags, session);
|
|
2757
2773
|
const requestedMode = String(flags.mode || 'stage').toLowerCase();
|
|
2758
2774
|
const mode = requestedMode === 'push' ? 'stage' : requestedMode;
|
|
2759
|
-
const
|
|
2775
|
+
const changedFromGit = changedComponentsFromWorkingTree(cwd);
|
|
2760
2776
|
|
|
2761
2777
|
// WHAT this stage is. Three shapes, and the difference between them is the difference between a lane
|
|
2762
2778
|
// that reads as "7 components in flight" and one that reads as "115 staged":
|
|
@@ -2776,6 +2792,8 @@ async function pushComponentsCommand(flags) {
|
|
|
2776
2792
|
const emptyWorksetPolicy = normalizeEmptyWorksetPolicy(flags);
|
|
2777
2793
|
|
|
2778
2794
|
let components = collectComponents(cwd);
|
|
2795
|
+
// Named the way the payload is named, BEFORE anything compares the two — see alignChangedSetWithComponents.
|
|
2796
|
+
const changedFromWorkingTree = alignChangedSetWithComponents(changedFromGit, components);
|
|
2779
2797
|
// Changes git reported that a component payload cannot carry — a deleted component file has nothing to
|
|
2780
2798
|
// stage, and Redis staging has no way to say "hide this during CLI-scoped runs". Removing its staged
|
|
2781
2799
|
// entry falls back to the committed row, so the component still resolves. Reported out loud rather than
|
|
@@ -2785,10 +2803,8 @@ async function pushComponentsCommand(flags) {
|
|
|
2785
2803
|
if (changedFromWorkingTree === null) {
|
|
2786
2804
|
throw new Error('--changed-only needs a git working tree to establish the changed set, and this directory is not one.');
|
|
2787
2805
|
}
|
|
2788
|
-
const wanted = new Set(changedFromWorkingTree.map(
|
|
2789
|
-
|
|
2790
|
-
components = components.filter((component) => wanted.has(
|
|
2791
|
-
component.type + ':' + (component.id ? 'id:' + component.id : 'name:' + String(component.name || '').toLowerCase())));
|
|
2806
|
+
const wanted = new Set(changedFromWorkingTree.map(stageIdentityKey));
|
|
2807
|
+
components = components.filter((component) => wanted.has(stageIdentityKey(component)));
|
|
2792
2808
|
if (!components.length && !(worksetReplace && emptyWorksetPolicy === 'clear')) {
|
|
2793
2809
|
// Deliberately NOT an error, and deliberately not a clear. An agent that has not edited anything yet
|
|
2794
2810
|
// is in an ordinary state; failing its loop teaches it nothing, and reconciling the lane to an empty
|
|
@@ -2817,7 +2833,7 @@ async function pushComponentsCommand(flags) {
|
|
|
2817
2833
|
// or, worse, conclude staging was broken and go on reading trunk. Scaled by payload size, floored at
|
|
2818
2834
|
// the old default, and overridable.
|
|
2819
2835
|
const api = buildAxios(baseUrl, session.token, stageTimeoutMs(components, flags));
|
|
2820
|
-
const
|
|
2836
|
+
const stagePayload = {
|
|
2821
2837
|
token: session.token,
|
|
2822
2838
|
accountId,
|
|
2823
2839
|
branchName,
|
|
@@ -2846,11 +2862,50 @@ async function pushComponentsCommand(flags) {
|
|
|
2846
2862
|
// Kept for a platform that predates stageMode. Same meaning it always had.
|
|
2847
2863
|
replace: !changedOnly,
|
|
2848
2864
|
components
|
|
2849
|
-
}
|
|
2865
|
+
};
|
|
2866
|
+
|
|
2867
|
+
let response;
|
|
2868
|
+
try {
|
|
2869
|
+
response = await stageOrRefuse(api, cwd, stagePayload);
|
|
2870
|
+
} catch (err) {
|
|
2871
|
+
// A REFUSED stage is evidence too. Recording it only on the success path is how a lane ends up with
|
|
2872
|
+
// an envelope that says nothing happened between two packets, when in fact a stage was attempted and
|
|
2873
|
+
// the platform rejected it. The packet is best-effort: a failing stage must surface the STAGE error,
|
|
2874
|
+
// never an error from writing the record of it.
|
|
2875
|
+
try {
|
|
2876
|
+
await appendVerificationPacket(api, cwd, session, accountId, flags, {
|
|
2877
|
+
type: 'stage',
|
|
2878
|
+
success: false,
|
|
2879
|
+
claim: 'Components refused at staging',
|
|
2880
|
+
world: buildCommandWorld(err.responseBody || {}, { accountId, dataMode, branchName, workspace, host: normalizeBaseUrl(baseUrl), sourceLayer: 'staged' }),
|
|
2881
|
+
revision: collectVerificationSource(cwd, flags),
|
|
2882
|
+
assertions: [
|
|
2883
|
+
{ id: 'stage', stageMode },
|
|
2884
|
+
{ id: 'stage_refused', value: err.message }
|
|
2885
|
+
],
|
|
2886
|
+
evidenceCategories: ['stage'],
|
|
2887
|
+
rawRefs: { command: 'components stage' },
|
|
2888
|
+
stage: (err.responseBody || {}).stage,
|
|
2889
|
+
compileValidation: (err.responseBody || {}).compileValidation,
|
|
2890
|
+
changedFromWorkingTree: changedFromWorkingTree || [],
|
|
2891
|
+
unrepresentableChanges: unstageable
|
|
2892
|
+
}, { quiet: true });
|
|
2893
|
+
} catch (packetError) {
|
|
2894
|
+
// Deliberately swallowed and named, not silent.
|
|
2895
|
+
console.error('Could not record the refused-stage verification packet: ' + packetError.message);
|
|
2896
|
+
}
|
|
2897
|
+
throw err;
|
|
2898
|
+
}
|
|
2850
2899
|
response.changedFromWorkingTree = changedFromWorkingTree || [];
|
|
2851
2900
|
response.changedFromWorkingTreeAvailable = changedFromWorkingTree !== null;
|
|
2852
2901
|
response.requestedStageMode = stageMode;
|
|
2853
2902
|
response.unrepresentableChanges = unstageable;
|
|
2903
|
+
if (response.success === false) {
|
|
2904
|
+
// A 200 that says `success:false`. The 422 path throws out of stageOrRefuse above and never lands
|
|
2905
|
+
// here, so this stays as the belt-and-braces case — print whatever detail came with it first.
|
|
2906
|
+
printCompileValidation(response.compileValidation, { stderr: true });
|
|
2907
|
+
throw new Error(response.message || 'Server stage failed');
|
|
2908
|
+
}
|
|
2854
2909
|
|
|
2855
2910
|
await appendVerificationPacket(api, cwd, session, accountId, flags, {
|
|
2856
2911
|
type: 'stage',
|
|
@@ -2901,16 +2956,71 @@ function normalizeEmptyWorksetPolicy(flags) {
|
|
|
2901
2956
|
*
|
|
2902
2957
|
* Returns [] when git could not answer, because "nothing is unrepresentable" would be a claim.
|
|
2903
2958
|
*/
|
|
2959
|
+
/**
|
|
2960
|
+
* The ONE identity a stage compares components by: type + id, or type + name for an id-less `new_` file.
|
|
2961
|
+
* The workset filter and the unrepresentable check used to spell this out separately.
|
|
2962
|
+
*/
|
|
2963
|
+
function stageIdentityKey(entry) {
|
|
2964
|
+
return entry.type + ':' + (entry.id ? 'id:' + entry.id : 'name:' + String(entry.name || '').toLowerCase());
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2967
|
+
/**
|
|
2968
|
+
* Name each git-changed component the way its payload is named.
|
|
2969
|
+
*
|
|
2970
|
+
* The changed set is built from FILENAMES; the payload takes `name` from the `.meta.yml`. For a `new_` file
|
|
2971
|
+
* there is no id, so `new_PDFStatement.groovy` + `name: PDF Statement` produced two identities for one
|
|
2972
|
+
* component: `--workset` filtered it out, it was reported as "no-local-files", and the platform (which
|
|
2973
|
+
* matches the changed set against the stored name) never marked it as workset — so a full snapshot staged
|
|
2974
|
+
* it WITHOUT compiling it. Both sides carry the file identity (`fileKey`), so align on that.
|
|
2975
|
+
*/
|
|
2976
|
+
function alignChangedSetWithComponents(changed, components) {
|
|
2977
|
+
if (!Array.isArray(changed)) return changed;
|
|
2978
|
+
const byFileKey = new Map();
|
|
2979
|
+
(components || []).forEach((component) => {
|
|
2980
|
+
if (component && component.fileKey) byFileKey.set(component.fileKey, component);
|
|
2981
|
+
});
|
|
2982
|
+
return changed.map((entry) => {
|
|
2983
|
+
const component = entry && entry.fileKey ? byFileKey.get(entry.fileKey) : null;
|
|
2984
|
+
if (!component || entry.id || !component.name || component.name === entry.name) return entry;
|
|
2985
|
+
return withFileKey(Object.assign({}, entry, { name: component.name }), entry.fileKey);
|
|
2986
|
+
});
|
|
2987
|
+
}
|
|
2988
|
+
|
|
2989
|
+
/**
|
|
2990
|
+
* The local components, for identity alignment ONLY on paths that did not already collect them (the sync
|
|
2991
|
+
* gates). Best-effort: a sidecar that does not parse must not stop a sync the server is about to judge.
|
|
2992
|
+
*/
|
|
2993
|
+
function collectComponentsForIdentity(cwd) {
|
|
2994
|
+
try {
|
|
2995
|
+
return collectComponents(cwd);
|
|
2996
|
+
} catch (_) {
|
|
2997
|
+
return [];
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
|
|
2904
3001
|
function unrepresentableChanges(changedFromWorkingTree, collected) {
|
|
2905
3002
|
if (!Array.isArray(changedFromWorkingTree)) return [];
|
|
2906
|
-
const
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
.
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
3003
|
+
const contentFields = ['source', 'prompt', 'html', 'javascript', 'previewData', 'inputSchema', 'messages', 'schema'];
|
|
3004
|
+
const present = new Map((collected || []).map((component) => [stageIdentityKey(component), component]));
|
|
3005
|
+
const report = [];
|
|
3006
|
+
changedFromWorkingTree.forEach((entry) => {
|
|
3007
|
+
const deleted = (entry.statuses || []).some((s) => String(s).includes('D'));
|
|
3008
|
+
const component = present.get(stageIdentityKey(entry));
|
|
3009
|
+
if (!component) {
|
|
3010
|
+
report.push(Object.assign({}, entry, { unrepresentable: true, reason: deleted ? 'deleted' : 'no-local-files' }));
|
|
3011
|
+
return;
|
|
3012
|
+
}
|
|
3013
|
+
// The component is still present (its other files remain), so it IS staged — but a content file git
|
|
3014
|
+
// reports as deleted has nothing to send, and staged content fields LAYER: the lane keeps whatever it
|
|
3015
|
+
// held before. Reported alongside the component, which stays in the workset.
|
|
3016
|
+
const deletedFields = deleted
|
|
3017
|
+
? (entry.fields || []).filter((field) => contentFields.includes(field) && component[field] == null)
|
|
3018
|
+
: [];
|
|
3019
|
+
if (deletedFields.length) {
|
|
3020
|
+
report.push(Object.assign({}, entry, { unrepresentable: true, reason: 'content-file-deleted', fields: deletedFields }));
|
|
3021
|
+
}
|
|
3022
|
+
});
|
|
3023
|
+
return report;
|
|
2914
3024
|
}
|
|
2915
3025
|
|
|
2916
3026
|
function printUnrepresentableChanges(unstageable) {
|
|
@@ -2918,12 +3028,23 @@ function printUnrepresentableChanges(unstageable) {
|
|
|
2918
3028
|
console.log('');
|
|
2919
3029
|
console.log('NOT REPRESENTABLE IN REDIS STAGING — ' + unstageable.length + ' change(s):');
|
|
2920
3030
|
unstageable.slice(0, 20).forEach((entry) => {
|
|
3031
|
+
const fields = Array.isArray(entry.fields) && entry.reason === 'content-file-deleted' ? ': ' + entry.fields.join(', ') : '';
|
|
2921
3032
|
console.log(' ' + entry.type + ' ' + (entry.id || entry.name || '(unknown)') +
|
|
2922
|
-
' (' + entry.reason + ') ' + (entry.paths || []).join(', '));
|
|
3033
|
+
' (' + entry.reason + fields + ') ' + (entry.paths || []).join(', '));
|
|
2923
3034
|
});
|
|
2924
3035
|
console.log(' A staged entry cannot hide a component. Clearing it falls back to the committed row, so');
|
|
2925
3036
|
console.log(' the component still resolves in a CLI-scoped run. Prove a deletion through the durable');
|
|
2926
3037
|
console.log(' plan instead: remits-cli components sync --dry-run --summary --fail-on-errors');
|
|
3038
|
+
const partial = unstageable.filter((entry) => entry.reason === 'content-file-deleted');
|
|
3039
|
+
if (partial.length) {
|
|
3040
|
+
console.log(' A deleted CONTENT file of a component that is still present is not a staged removal: the');
|
|
3041
|
+
console.log(' lane keeps the content it already held, and compile validation judges THAT. Drop it with:');
|
|
3042
|
+
partial.slice(0, 5).forEach((entry) => {
|
|
3043
|
+
console.log(entry.id
|
|
3044
|
+
? ' remits-cli components clear --component-type ' + entry.type + ' --component-id ' + entry.id
|
|
3045
|
+
: ' remits-cli components clear --all (' + entry.type + ' ' + entry.name + ' has no id; this clears only your lane)');
|
|
3046
|
+
});
|
|
3047
|
+
}
|
|
2927
3048
|
}
|
|
2928
3049
|
|
|
2929
3050
|
/**
|
|
@@ -2947,6 +3068,21 @@ async function stageOrRefuse(api, cwd, payload) {
|
|
|
2947
3068
|
refusal.refused = true;
|
|
2948
3069
|
throw refusal;
|
|
2949
3070
|
}
|
|
3071
|
+
if (body && body.message) {
|
|
3072
|
+
// The server's `message` is ONE line derived from the FIRST failure. The rest of the detail is in
|
|
3073
|
+
// the body, and the caller cannot see the body — an agent handed "compile failed for Reader 41"
|
|
3074
|
+
// over a stage with nine broken components fixes one, re-stages, and pays nine round trips. Render
|
|
3075
|
+
// the whole list HERE, where the body is still in hand, then throw the one-line summary.
|
|
3076
|
+
printCompileValidation(body.compileValidation, { stderr: true });
|
|
3077
|
+
// A refusal can be ABOUT an unrepresentable change: a deleted source file leaves the lane holding the
|
|
3078
|
+
// content it had, and that is what was judged. Without this the refusal names a file that no longer
|
|
3079
|
+
// exists and "fix it and re-stage" cannot work. Read from the payload, which still carries `fields`.
|
|
3080
|
+
printUnrepresentableChanges((payload.changedSet || []).filter((entry) => entry && entry.unrepresentable));
|
|
3081
|
+
const failure = new Error(body.message);
|
|
3082
|
+
failure.responseBody = body;
|
|
3083
|
+
failure.status = status;
|
|
3084
|
+
throw failure;
|
|
3085
|
+
}
|
|
2950
3086
|
throw err;
|
|
2951
3087
|
}
|
|
2952
3088
|
}
|
|
@@ -2967,6 +3103,67 @@ function printComponentPolicy(response) {
|
|
|
2967
3103
|
(policy.overrides || []).forEach((id) => console.log('Policy override recorded for rule: ' + id));
|
|
2968
3104
|
}
|
|
2969
3105
|
|
|
3106
|
+
/**
|
|
3107
|
+
* What the platform compiled, and what it could not.
|
|
3108
|
+
*
|
|
3109
|
+
* Prints on BOTH outcomes, including `attempted: 0`. A silent pass and a pass that validated nothing look
|
|
3110
|
+
* identical otherwise, and they are not the same claim: when git cannot identify the changed set the
|
|
3111
|
+
* server has no workset to validate and says so (`skipped[].reason`). Reporting that as silence would be
|
|
3112
|
+
* the same confident-wrong-number this surface exists to remove.
|
|
3113
|
+
*/
|
|
3114
|
+
function printCompileValidation(validation, options) {
|
|
3115
|
+
// Failures go to stderr so `--json` stdout stays parseable: a caller that asked for JSON is a program,
|
|
3116
|
+
// and a refusal must not put prose in the middle of its document.
|
|
3117
|
+
const emit = (options && options.stderr) ? console.error : console.log;
|
|
3118
|
+
if (!validation || validation.attempted == null) return;
|
|
3119
|
+
const failures = Array.isArray(validation.failures) ? validation.failures : [];
|
|
3120
|
+
if (validation.attempted === 0) {
|
|
3121
|
+
const reasons = (Array.isArray(validation.skipped) ? validation.skipped : [])
|
|
3122
|
+
.map((entry) => (entry && entry.reason) || '')
|
|
3123
|
+
.filter((reason) => reason && reason !== 'no-compilable-source');
|
|
3124
|
+
if (reasons.length) {
|
|
3125
|
+
emit('Compile validation: NOT RUN — ' + reasons.join(', ') +
|
|
3126
|
+
'. Nothing in this stage was compile-checked.');
|
|
3127
|
+
if (reasons.includes('workset-unknown')) {
|
|
3128
|
+
emit(' git could not identify the changed set, so there was no workset to validate.');
|
|
3129
|
+
emit(' Stage from a git working tree, or re-stage the component explicitly, to get the check.');
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
return;
|
|
3133
|
+
}
|
|
3134
|
+
const status = validation.success === true ? 'passed' : 'failed';
|
|
3135
|
+
emit('Compile validation: ' + status + ' — ' + validation.passed + '/' + validation.attempted +
|
|
3136
|
+
' compiled in ' + validation.durationMs + 'ms' +
|
|
3137
|
+
(validation.concurrency ? ' (' + validation.concurrency + ' parallel)' : ''));
|
|
3138
|
+
// A skip is neither a pass nor a failure, and it used to be printed as a pass ("1/1 compiled" over a
|
|
3139
|
+
// component whose source was blank). Named, so the reader knows exactly what was NOT looked at.
|
|
3140
|
+
const unchecked = Array.isArray(validation.unchecked) ? validation.unchecked : [];
|
|
3141
|
+
if (unchecked.length) {
|
|
3142
|
+
emit(' ' + unchecked.length + ' NOT compile-checked: ' + unchecked.map((entry) =>
|
|
3143
|
+
(entry.type || 'component') + ' ' + (entry.id || entry.name || '?') + ' (' + (entry.reason || 'unchecked') + ')').join(', '));
|
|
3144
|
+
}
|
|
3145
|
+
const shown = failures.slice(0, COMPILE_FAILURES_SHOWN);
|
|
3146
|
+
shown.forEach((failure) => {
|
|
3147
|
+
emit(' ' + (failure.type || 'component') + ' ' + (failure.id || failure.name || '') +
|
|
3148
|
+
': ' + (failure.message || 'compile failed'));
|
|
3149
|
+
});
|
|
3150
|
+
if (failures.length > shown.length) {
|
|
3151
|
+
// Named, not counted. "3 more failure(s)" sends an agent back for another round trip to learn WHICH
|
|
3152
|
+
// three; the identifiers are already in hand and cost one line.
|
|
3153
|
+
emit(' ' + (failures.length - shown.length) + ' more failed: ' +
|
|
3154
|
+
failures.slice(COMPILE_FAILURES_SHOWN)
|
|
3155
|
+
.map((failure) => (failure.type || 'component') + ' ' + (failure.id || failure.name || '?'))
|
|
3156
|
+
.join(', '));
|
|
3157
|
+
emit(' Re-run with --json for every compile message.');
|
|
3158
|
+
}
|
|
3159
|
+
if (validation.success !== true) {
|
|
3160
|
+
// The lane is written BEFORE it is validated, because the compile has to resolve through the staged
|
|
3161
|
+
// overlay to see the source it is judging. So a refusal does not mean "nothing was staged".
|
|
3162
|
+
emit(' The rejected source IS in the lane: staging writes, then compiles what it wrote.');
|
|
3163
|
+
emit(' A run in this lane will resolve the broken component until you fix it and re-stage.');
|
|
3164
|
+
}
|
|
3165
|
+
}
|
|
3166
|
+
|
|
2970
3167
|
function shouldPrintFullComponentResponse(flags) {
|
|
2971
3168
|
return flagEnabled(flags.verbose);
|
|
2972
3169
|
}
|
|
@@ -3088,6 +3285,7 @@ function printStageSummary(response, flags) {
|
|
|
3088
3285
|
if (overlay != null) {
|
|
3089
3286
|
console.log('Materialized overlay now held by the lane:', overlay, 'component(s)');
|
|
3090
3287
|
}
|
|
3288
|
+
printCompileValidation(response.compileValidation);
|
|
3091
3289
|
|
|
3092
3290
|
// The trap `--changed-only` leaves behind: it merges, so a lane inherited from an earlier full snapshot
|
|
3093
3291
|
// keeps every one of those entries resolving ahead of committed source. Silence here is what made
|
|
@@ -3584,7 +3782,9 @@ async function syncComponentsCommand(rawFlags) {
|
|
|
3584
3782
|
const forceTombstones = flagEnabled(flags['force-tombstones']) || flagEnabled(flags.forceTombstones);
|
|
3585
3783
|
const dryRun = flagEnabled(flags['dry-run']) || flagEnabled(flags.dryRun);
|
|
3586
3784
|
const api = buildAxios(baseUrl, session.token);
|
|
3587
|
-
|
|
3785
|
+
// Aligned for the same reason a stage is: the server's plan names a `new_` component by its sidecar
|
|
3786
|
+
// `name:`, and the changed set would otherwise name it by its filename and the gate would refuse it.
|
|
3787
|
+
const changedFromWorkingTree = alignChangedSetWithComponents(changedComponentsFromWorkingTree(cwd), collectComponentsForIdentity(cwd));
|
|
3588
3788
|
const preflightRequested = syncPreflightRequested(flags);
|
|
3589
3789
|
const namesOnly = flagEnabled(flags['names-only']) || flagEnabled(flags.namesOnly);
|
|
3590
3790
|
|
|
@@ -3992,7 +4192,7 @@ function evaluateSyncGates(response, flags, changedFromWorkingTree, cwd) {
|
|
|
3992
4192
|
|
|
3993
4193
|
if (flagEnabled(flags['changed-only']) || flagEnabled(flags.changedOnly)) {
|
|
3994
4194
|
const since = flags['changed-since'] || flags.changedSince;
|
|
3995
|
-
const fromRef = since ? changedComponentsSinceRef(cwd, String(since)) : null;
|
|
4195
|
+
const fromRef = since ? alignChangedSetWithComponents(changedComponentsSinceRef(cwd, String(since)), collectComponentsForIdentity(cwd)) : null;
|
|
3996
4196
|
if (since && fromRef === null) {
|
|
3997
4197
|
violations.push('--changed-since: could not diff against "' + since + '". Check the ref exists (git fetch first for a remote ref).');
|
|
3998
4198
|
checks.changedOnly = false;
|
|
@@ -4501,7 +4701,22 @@ async function commitComponentsCommand(flags) {
|
|
|
4501
4701
|
const landing = await acquireLandingLease(flags, accountId, branchName);
|
|
4502
4702
|
try {
|
|
4503
4703
|
if (!skipGit) {
|
|
4504
|
-
console.log('Phase 1/
|
|
4704
|
+
console.log('Phase 1/4: staged compile validation');
|
|
4705
|
+
// MERGE semantics on purpose (`--changed-only`, never `--workset`). A workset stage RECONCILES the
|
|
4706
|
+
// lane — it deletes every entry outside the git changed set — and the lane is frequently shared. A
|
|
4707
|
+
// validation pass must not be able to delete another agent's staged work as a side effect of somebody
|
|
4708
|
+
// running `components commit`. This adds the changed components to the lane and leaves the rest alone.
|
|
4709
|
+
await pushComponentsCommand(Object.assign({}, flags, {
|
|
4710
|
+
branch: branchName,
|
|
4711
|
+
'account-id': accountId,
|
|
4712
|
+
mode: 'stage',
|
|
4713
|
+
workset: false,
|
|
4714
|
+
'replace-lane': false,
|
|
4715
|
+
replaceLane: false,
|
|
4716
|
+
'changed-only': true
|
|
4717
|
+
}));
|
|
4718
|
+
|
|
4719
|
+
console.log('Phase 2/4: local git commit/push');
|
|
4505
4720
|
const status = runGit(cwd, 'git status --porcelain');
|
|
4506
4721
|
if (status || allowEmpty) {
|
|
4507
4722
|
runGit(cwd, 'git add -A');
|
|
@@ -4528,14 +4743,12 @@ async function commitComponentsCommand(flags) {
|
|
|
4528
4743
|
console.log('Skipping local git phase. Prefer `remits-cli components sync` if you only need the server sync step.');
|
|
4529
4744
|
}
|
|
4530
4745
|
|
|
4531
|
-
console.log('Phase
|
|
4746
|
+
console.log('Phase 3/4: server sync');
|
|
4532
4747
|
if (flagEnabled(flags.safe)) {
|
|
4533
|
-
//
|
|
4534
|
-
//
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
console.log(' --safe: the branch is already pushed. The gate below stops the platform from WRITING a');
|
|
4538
|
-
console.log(' surprising plan; it cannot un-push. Use `components sync --safe` alone for a pre-push gate.');
|
|
4748
|
+
// Compile validation already ran before the push. The sync safety gates still run after the push
|
|
4749
|
+
// because they need the server's remote-branch plan.
|
|
4750
|
+
console.log(' --safe: compile validation ran before git push; the sync plan gate below runs before');
|
|
4751
|
+
console.log(' the platform writes trunk rows or ComponentVariant overlays.');
|
|
4539
4752
|
}
|
|
4540
4753
|
const syncResponse = await syncComponentsCommand({
|
|
4541
4754
|
...flags,
|
|
@@ -4544,7 +4757,7 @@ async function commitComponentsCommand(flags) {
|
|
|
4544
4757
|
});
|
|
4545
4758
|
|
|
4546
4759
|
if (!skipGit) {
|
|
4547
|
-
console.log('Phase
|
|
4760
|
+
console.log('Phase 4/4: local fast-forward pull');
|
|
4548
4761
|
runGit(cwd, 'git fetch origin ' + shellQuote(branchName));
|
|
4549
4762
|
const expectedSha = syncResponse && syncResponse.sync && syncResponse.sync.postSyncSha;
|
|
4550
4763
|
if (expectedSha) {
|
|
@@ -11144,13 +11357,13 @@ function printComponentsHelp(subcommand) {
|
|
|
11144
11357
|
if (subcommand === 'commit') {
|
|
11145
11358
|
console.log('Usage: remits-cli components commit [--safe] [--message "msg"] [--allow-empty true|false] [--skip-git true|false] [--branch BRANCH] [--data-mode test|prod] [--force-tombstones]');
|
|
11146
11359
|
console.log('');
|
|
11147
|
-
console.log('Runs local git add/commit/push,
|
|
11360
|
+
console.log('Runs staged compile validation, local git add/commit/push, server sync, then fast-forward pull.');
|
|
11361
|
+
console.log('Prefer explicit stage + git + components sync when you need inspectable phases.');
|
|
11148
11362
|
console.log('components commit does not support --dry-run.');
|
|
11149
11363
|
console.log('');
|
|
11150
|
-
console.log('
|
|
11151
|
-
console.log('
|
|
11152
|
-
console.log('
|
|
11153
|
-
console.log('`remits-cli components sync --safe`.');
|
|
11364
|
+
console.log('Phase 1 merge-stages the git-changed components and compile-validates their runtime source.');
|
|
11365
|
+
console.log('It uses --changed-only semantics, so it never reconciles a lane you share with another agent.');
|
|
11366
|
+
console.log('--safe also gates the server sync plan before any trunk rows or ComponentVariant overlays are written.');
|
|
11154
11367
|
return;
|
|
11155
11368
|
}
|
|
11156
11369
|
console.log('Usage: remits-cli components <stage|status|lanes|entries|clear|sync|commit|promotion|branches|branch>');
|
package/package.json
CHANGED
|
@@ -60,6 +60,12 @@ reference named after it.
|
|
|
60
60
|
- **edit → stage → run, every time.** The platform executes whatever is in the staging cache at the
|
|
61
61
|
moment a run starts. Edit a file, run a test without `remits-cli components stage`, and the test runs
|
|
62
62
|
the OLD code. This is the single most common mistake. (`development-loop.md`)
|
|
63
|
+
- **Staging and sync fail early on broken runtime-compiled Groovy.** Stage validates changed source in the
|
|
64
|
+
active lane; sync validates changed/new compiled components before writing DB rows or branch variants.
|
|
65
|
+
Agent files are Utility components internally. A stage refused BY COMPILE VALIDATION still wrote the
|
|
66
|
+
lane — the compile has to resolve through the staged overlay to see the source it is judging — so fix and
|
|
67
|
+
re-stage before running anything in that lane. (A policy or edit-lease refusal is the opposite: it
|
|
68
|
+
returns before any write.) (`development-loop.md`, `component-integrity.md`, `branch-variants.md`)
|
|
63
69
|
- **Stage your WORKSET, not the whole repo: `remits-cli components stage --workset`.** It uploads only
|
|
64
70
|
the components git reports changed and makes the lane hold exactly them. A plain `components stage` is
|
|
65
71
|
a FULL SNAPSHOT — it puts every component in the repo into the lane, so "115 staged" tells a human
|
|
@@ -222,6 +222,12 @@ remits-cli components sync --dry-run # inspect overrides/additions/tombstones wi
|
|
|
222
222
|
remits-cli components sync # writes ComponentVariant overlays ONLY
|
|
223
223
|
```
|
|
224
224
|
|
|
225
|
+
`components stage` validates changed runtime-compiled source through the active branch/workspace lane,
|
|
226
|
+
and `components sync` validates changed/new Groovy source before writing a `ComponentVariant`. This is
|
|
227
|
+
intentional: a branch overlay that cannot compile should fail at staging/sync time, not later when the
|
|
228
|
+
subscriber's workflow first resolves it. `Agent` files in `components/agents/` are the `Utility`
|
|
229
|
+
component kind internally; the CLI and validator normalize both names to the same target.
|
|
230
|
+
|
|
225
231
|
Promotion back to trunk is a **git** operation followed by a **trunk** sync — the platform merges nothing
|
|
226
232
|
for you, and merging a branch promotes its **deletions** as hard deletes. Do not improvise it: follow
|
|
227
233
|
`features/subscriber-branch-promotions.md`. For a real trunk promotion with many `new_` files, use
|
|
@@ -330,7 +336,8 @@ The analogous hazard is different, and you must still respect it:
|
|
|
330
336
|
`removed`, `unchanged`, `skipped`, and `errors` without writing rows, caching the sync SHA, or clearing
|
|
331
337
|
staging. Existing overlay ids appear as `variantId`; an `unchanged` row with `pruned:true` means the
|
|
332
338
|
branch has converged back to trunk and the overlay would be removed. It is rejected on trunk, and
|
|
333
|
-
`components commit --dry-run` is unsupported because `commit` performs
|
|
339
|
+
`components commit --dry-run` is unsupported because `commit` performs compile validation and local git
|
|
340
|
+
writes before syncing.
|
|
334
341
|
- **The sync refuses a wholesale removal.** Above roughly a third of a kind — or **100% of a kind at any
|
|
335
342
|
size** — it aborts that kind, reports why, and points at a rebase. Rebasing is almost always the real
|
|
336
343
|
fix. Only when the removals are genuinely deliberate, re-run with `--force-tombstones`.
|
|
@@ -198,7 +198,7 @@ remits-cli components lanes [--json] # every indexed sta
|
|
|
198
198
|
remits-cli components entries --lane-id <id> [--json|--verbose] # authoritative staged files for one lane
|
|
199
199
|
remits-cli components clear [--branch <name>] [--component-type <type>] [--component-id <id>] [--all] [--json|--verbose] # id alone scopes to one component when unambiguous (ids are type-local; add --component-type if the same id is staged in multiple families); no filter clears the whole branch scope; --all forces the full wipe
|
|
200
200
|
remits-cli components sync [--safe [--yes]] [--branch <name>] [--data-mode test|prod] [--force-tombstones] [--dry-run] [--summary] [--changed-only [--changed-since <ref>]] # gated; on trunk = full repo->DB reconcile, on a variant branch = ComponentVariant overlays only
|
|
201
|
-
remits-cli components commit [--safe] [--message "msg"] [--data-mode test|prod] [--force-tombstones] #
|
|
201
|
+
remits-cli components commit [--safe] [--message "msg"] [--data-mode test|prod] [--force-tombstones] # phase 1 merge-stages + compile-validates changed source (never reconciles the lane); --safe gates sync writes
|
|
202
202
|
remits-cli components branches [--json] # branches carrying committed variants, with counts + drift
|
|
203
203
|
remits-cli components branch <name> [--json] # one branch: overridden / added / removed, drift flags, subscribers
|
|
204
204
|
remits-cli components branch <name> --diff <componentId> --component-type <kind> [--json]
|
|
@@ -87,21 +87,41 @@ every live component present at its real id, and nothing extra.
|
|
|
87
87
|
|
|
88
88
|
| Command | What it touches | Danger |
|
|
89
89
|
|---|---|---|
|
|
90
|
-
| `remits-cli components stage` (alias: deprecated `push`) | **Redis staging cache only.** Never mutates the DB or git. The safe iteration surface. | none |
|
|
90
|
+
| `remits-cli components stage` (alias: deprecated `push`) | **Redis staging cache only.** Never mutates the DB or git. The safe iteration surface. A plain stage or `--workset` RECONCILES the lane (entries outside the manifest are dropped); `--changed-only` merges. | none for the DB; a lane you share with another agent is reconciled by the first two |
|
|
91
91
|
| `remits-cli components status` | Reads this lane's staging scope (account/user/branch/workspace) and branch resolution, and lists every other lane on the branch. | none |
|
|
92
92
|
| `remits-cli components clear` | Clears THIS lane's staging cache without changing DB or git. Never touches another workspace lane. | none |
|
|
93
93
|
| `remits-cli components sync` **on trunk** | **Server-side git→DB reconcile of the whole account** (create/update/**delete**/rename). Reads the pushed remote; ignores local files. | **high** |
|
|
94
94
|
| `remits-cli components sync` **on a variant branch** | Writes `ComponentVariant` overlays for that branch only. Never touches trunk rows or the account's trunk branch. When the checkout identifies a subscribing account, the branch-local `account-info.json` is refreshed for that subscriber; `--dry-run` reports the plan without writes. | medium (a missing file becomes a **tombstone** that hides the component from subscribers) |
|
|
95
|
-
| `remits-cli components commit` | **One shot:** `git add -A` + commit + `git push` + **`components sync`** + `git pull --ff-only`. Blindly stages the *entire* working tree (including any drift) and reconciles it into prod. Inherits the danger of whichever sync mode the branch selects. | **highest on trunk** |
|
|
95
|
+
| `remits-cli components commit` | **One shot:** changed-source compile validation (a `--changed-only` MERGE stage, so it never reconciles the lane) + `git add -A` + commit + `git push` + **`components sync`** + `git pull --ff-only`. Blindly stages the *entire* working tree (including any drift) and reconciles it into prod. Inherits the danger of whichever sync mode the branch selects. | **highest on trunk** |
|
|
96
96
|
|
|
97
97
|
Key implications:
|
|
98
|
-
- **`stage`
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
98
|
+
- **`stage` never touches the DB or git** — stage and test as much as you want. It is safe *for your
|
|
99
|
+
components*; it is not inert *for the lane*. A full snapshot and `--workset` both reconcile the lane to
|
|
100
|
+
their manifest, so on a lane shared with another agent they drop that agent's staged entries. Use
|
|
101
|
+
`--changed-only` when you mean "add mine, leave theirs".
|
|
102
|
+
- **A COMPILE-VALIDATION refusal still wrote the lane; a policy refusal does not.** Read the status code,
|
|
103
|
+
because the two refusals leave opposite states behind:
|
|
104
|
+
- **422 (compile validation)** — the entries are already stored. Validation runs after the write because
|
|
105
|
+
the compile has to resolve through the staged overlay to see the source it is judging. The response
|
|
106
|
+
says so with `laneHoldsRejectedSource`. Until you fix the source and re-stage, a run in that lane
|
|
107
|
+
resolves the broken component.
|
|
108
|
+
- **409 (account policy, or another agent's edit lease)** — refused before anything was written. The lane
|
|
109
|
+
is exactly as it was; nothing to undo.
|
|
110
|
+
- **"Compile validation: NOT RUN" is a real answer.** The check keys off the git changed set. If git
|
|
111
|
+
cannot identify one — you are not in a working tree, or git failed — there is no workset to validate and
|
|
112
|
+
the CLI says `workset-unknown` rather than implying a pass.
|
|
113
|
+
- **`components commit` is the most dangerous command**, not a mere convenience wrapper: it first
|
|
114
|
+
merge-stages and compile-validates the changed runtime source, then `git add -A`, commits, pushes, and
|
|
115
|
+
immediately syncs. Never run it while the tree contains drift or unexplained changes. Prefer the explicit,
|
|
116
|
+
observable `components stage → git commit → git push → components sync → git pull` sequence so each
|
|
117
|
+
phase can be inspected.
|
|
103
118
|
- **`components sync` acts on the pushed remote**, so local edits are invisible to it until committed **and
|
|
104
119
|
pushed**, and a drifted **remote** is dangerous even when your local tree looks fine.
|
|
120
|
+
- **Runtime-compiled component source is validated before durable writes.** When a trunk sync or variant
|
|
121
|
+
sync sees changed/new Groovy source for a `Reader`, `Action`, `Embeddable`, `HtmlTemplate`, `Rule`,
|
|
122
|
+
`Test`, `Agent`/`Utility`, or `Tool`, the platform compiles it before accepting the DB row or
|
|
123
|
+
`ComponentVariant` overlay. A compile failure lands in `syncResults.errors` and the branch SHA cache is
|
|
124
|
+
not advanced, so fix the source and retry the same sync.
|
|
105
125
|
- After a successful non-dry-run `components sync` / `components commit`, the server clears the full
|
|
106
126
|
staging scope for that lane (account/user/branch/workspace). This is the expected clean state: old Redis aliases should not keep shadowing
|
|
107
127
|
the newly synced DB rows. `components sync --dry-run` intentionally leaves staging untouched.
|
|
@@ -218,6 +218,33 @@ remits-cli components stage --workset # every edit: stage what you changed
|
|
|
218
218
|
|
|
219
219
|
This uploads your local component changes to the platform's staging cache (Redis, 240-minute TTL). It does NOT commit anything. The platform cannot see your local edits until you stage them.
|
|
220
220
|
|
|
221
|
+
For runtime-compiled components (`Reader`, `Action`, `Embeddable`, `HtmlTemplate`, `Rule`, `Test`,
|
|
222
|
+
`Agent`/`Utility`, and `Tool`), staging also validates the Groovy source that is part of the submitted
|
|
223
|
+
workset. Validation resolves through the same branch/workspace staging lane a later run will use and
|
|
224
|
+
compiles candidates in a small bounded pool, so a syntax/compile error is reported by `components stage`
|
|
225
|
+
instead of waiting for the next workflow to trip over it. A full repository stage does not compile every
|
|
226
|
+
component in the repo; it validates only entries known to be in the current workset, plus explicit partial
|
|
227
|
+
source updates.
|
|
228
|
+
|
|
229
|
+
Two things to read correctly when it refuses:
|
|
230
|
+
|
|
231
|
+
- **The lane already holds what it rejected — on a 422.** The compile has to resolve through the staged
|
|
232
|
+
overlay to see the source it is judging, so the entries are written first and compiled second. `422`
|
|
233
|
+
means "staged, and rejected", not "nothing happened" — fix the source and re-stage before running
|
|
234
|
+
anything in that lane. A `409` is the other kind of refusal (account policy, or another agent's edit
|
|
235
|
+
lease) and writes nothing at all.
|
|
236
|
+
- **Every failure is printed, not just the first.** The one-line error is the first failure; the full list
|
|
237
|
+
(identified by type and id) follows it. Fix them in one pass rather than one round trip each.
|
|
238
|
+
- **`N NOT compile-checked` is not a pass.** A `new_` component whose source file is empty or blank cannot
|
|
239
|
+
be judged, so it is listed by name with its reason instead of being counted as compiled. It will not run.
|
|
240
|
+
- **Deleting a component's source file while its sidecar stays is `content-file-deleted`.** The lane keeps
|
|
241
|
+
the source it already held, and that is what gets compiled and run. Clear that one component with the
|
|
242
|
+
command the CLI prints (`components clear --component-type <type> --component-id <id>`).
|
|
243
|
+
|
|
244
|
+
If git cannot identify the changed set — you are not in a working tree, or git failed — there is no
|
|
245
|
+
workset to validate, and the CLI prints `Compile validation: NOT RUN — workset-unknown` rather than
|
|
246
|
+
silently implying a pass.
|
|
247
|
+
|
|
221
248
|
##### Stage your workset, not the whole repo
|
|
222
249
|
|
|
223
250
|
There are three stage modes, and the difference decides what a run in your lane resolves and what a
|