@ericrisco/rsc 2.0.9 → 2.0.11
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/manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -4,6 +4,7 @@ import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { applyInstall, pruneSharedBases, removeTargetInstall } from '../install-apply.js';
|
|
6
6
|
import { targetPaths } from '../../targets/index.js';
|
|
7
|
+
import { targetHasAgents } from '../../targets/agents.js';
|
|
7
8
|
import { readState } from './state.js';
|
|
8
9
|
import { readManifest, writeManifest } from './manifest-file.js';
|
|
9
10
|
import { encodeGoal, identifyPlan } from './onboarding.js';
|
|
@@ -11,6 +12,22 @@ import { createBackup, restoreBackup } from './backups.js';
|
|
|
11
12
|
|
|
12
13
|
const sameSet = (a = [], b = []) => JSON.stringify([...a].sort()) === JSON.stringify([...b].sort());
|
|
13
14
|
|
|
15
|
+
/**
|
|
16
|
+
* What THIS target should be holding, which is not the same question as what the project decided.
|
|
17
|
+
*
|
|
18
|
+
* The policy is the PROJECT's: "these are the agents this project wants." Whether an assistant can
|
|
19
|
+
* hold a subagent at all is the TARGET's business, and nine of the seventeen cannot — amp, jules,
|
|
20
|
+
* zed, antigravity, windsurf, cline, roo, continue and aider have no such concept. The installer
|
|
21
|
+
* always knew this and correctly wrote none; the verifier did not, and demanded the project's list
|
|
22
|
+
* from every target, so onboarding could never complete on any of the nine. Since the failure rolls
|
|
23
|
+
* back, what the person got was empty directory shells and no harness — with a recovery command
|
|
24
|
+
* that leads straight back to the same wall.
|
|
25
|
+
*
|
|
26
|
+
* Asking amp for a subagent is not a divergence. It is a category error, and this is where the two
|
|
27
|
+
* categories stop being confused.
|
|
28
|
+
*/
|
|
29
|
+
export const expectedAgentsFor = (target, plan) => (targetHasAgents(target) ? (plan.policy.agents || []) : []);
|
|
30
|
+
|
|
14
31
|
function inside(root, path) {
|
|
15
32
|
return path === root || path.startsWith(`${root}${sep}`);
|
|
16
33
|
}
|
|
@@ -92,7 +109,7 @@ export function verifyOnboarding(cwd, plan, planId) {
|
|
|
92
109
|
for (const target of plan.policy.targets) {
|
|
93
110
|
const state = readState(targetPaths(target, undefined, cwd).stateFile);
|
|
94
111
|
if (!sameSet(Object.keys(state.skills || {}), plan.policy.skills)) differences.push(`${target}: installed skills differ from accepted policy`);
|
|
95
|
-
if (!sameSet(state.agents || [], plan
|
|
112
|
+
if (!sameSet(state.agents || [], expectedAgentsFor(target, plan))) differences.push(`${target}: installed agents differ from accepted policy`);
|
|
96
113
|
if (state.policy?.alwaysOn !== plan.policy.alwaysOn) differences.push(`${target}: always-on policy differs`);
|
|
97
114
|
if (state.policy?.codeHooks !== plan.policy.codeHooks) differences.push(`${target}: code-hook policy differs`);
|
|
98
115
|
if (state.policy?.memory !== plan.policy.memory) differences.push(`${target}: memory policy differs`);
|
|
@@ -56,15 +56,35 @@ function hookEventName(target, event, native) {
|
|
|
56
56
|
return names[event] || event;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
const COMPACTION_HINT = 'rsc memory: consider compacting at the next phase boundary; nothing was compacted automatically.';
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Two different things used to share one channel, and that is the whole bug.
|
|
63
|
+
*
|
|
64
|
+
* The resume CONTEXT is information for the model, and it is only ever produced on `start` —
|
|
65
|
+
* `SessionStart`, one of the few events whose output may carry `hookSpecificOutput.additionalContext`.
|
|
66
|
+
* The compaction HINT is a message for the PERSON, and it was going out through the same field on
|
|
67
|
+
* whatever event happened to be running. On `Stop`, `PreCompact` and `SessionEnd` the client rejects
|
|
68
|
+
* that outright ("Hook JSON output validation failed — hookSpecificOutput.hookEventName: expected one
|
|
69
|
+
* of …"), so the hook meant to help ended up printing an error after every turn, and after
|
|
70
|
+
* `/compact` in particular — which is exactly what the hint had just told the person to run.
|
|
71
|
+
*
|
|
72
|
+
* So the hint moves to `systemMessage`, which every event accepts. This needs no table of which
|
|
73
|
+
* event carries what, and it cannot rot when the client adds an event: anything addressed to the
|
|
74
|
+
* person travels by the universal channel, and only genuine model context uses the narrow one.
|
|
75
|
+
*/
|
|
59
76
|
function nativeOutput(target, eventName, context = '', notice = null, compactionHint = false) {
|
|
60
|
-
const hint = compactionHint ?
|
|
61
|
-
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
|
|
77
|
+
const hint = compactionHint ? COMPACTION_HINT : '';
|
|
78
|
+
if (!context && !hint && !notice) return {};
|
|
79
|
+
if (target === 'cursor') {
|
|
80
|
+
const message = [notice, hint].filter(Boolean).join('\n');
|
|
81
|
+
return { ...(context ? { additional_context: context } : {}), ...(message ? { user_message: message } : {}) };
|
|
82
|
+
}
|
|
83
|
+
if (target === 'opencode') return { context, notice: [notice, hint].filter(Boolean).join('\n') || null };
|
|
84
|
+
const message = [notice, hint].filter(Boolean).join('\n');
|
|
65
85
|
return {
|
|
66
|
-
...(
|
|
67
|
-
...(
|
|
86
|
+
...(message ? { systemMessage: message } : {}),
|
|
87
|
+
...(context ? { hookSpecificOutput: { hookEventName: eventName, additionalContext: context } } : {}),
|
|
68
88
|
};
|
|
69
89
|
}
|
|
70
90
|
|
|
@@ -106,8 +126,13 @@ export function handleLifecycle({ target, event, native = {}, cwd, settings } =
|
|
|
106
126
|
if (typeof native.cost === 'number') captureInput.cost = native.cost;
|
|
107
127
|
if (Number.isInteger(native.tool_calls)) captureInput.toolCalls = native.tool_calls;
|
|
108
128
|
const captured = capture(captureInput);
|
|
129
|
+
// `stop_hook_active` is the client saying "this turn is already running BECAUSE a stop hook
|
|
130
|
+
// asked for it". Speaking again here is how a Stop hook wakes itself: the student's session did
|
|
131
|
+
// nine rounds of hook → "—" → hook before the client broke the loop. Record the turn, say
|
|
132
|
+
// nothing.
|
|
133
|
+
const silenced = event === 'turn' && Boolean(native?.stop_hook_active);
|
|
109
134
|
return {
|
|
110
|
-
output: nativeOutput(target, eventName, '', captured.notice, captured.compactionHint),
|
|
135
|
+
output: silenced ? {} : nativeOutput(target, eventName, '', captured.notice, captured.compactionHint),
|
|
111
136
|
capture: captured,
|
|
112
137
|
remote: false,
|
|
113
138
|
degraded: false,
|
|
@@ -352,11 +352,22 @@ export function capture(input = {}) {
|
|
|
352
352
|
if (errors.length) return { record: null, path: null, notice: null, compactionHint: false, errors };
|
|
353
353
|
atomicJson(recordPath, record);
|
|
354
354
|
existing = record;
|
|
355
|
+
// Once per session, not once per turn. `editCount` only ever grows, so `editCount >= threshold`
|
|
356
|
+
// was true for every remaining event of a working afternoon — the twentieth edit turned a useful
|
|
357
|
+
// nudge into a line after every single turn, for good. Worse, compaction does not reset the
|
|
358
|
+
// count, so following the advice changed nothing and the hint kept coming.
|
|
359
|
+
//
|
|
360
|
+
// The flag lives on the ANCHOR and not on the record on purpose: the record's schema is closed
|
|
361
|
+
// and every field of it is required, so a new field there would fail validation on every record
|
|
362
|
+
// already written and `readRecords` would quietly drop the project's whole history.
|
|
363
|
+
let compactionHint = Boolean(config.compactionHint && editCount >= config.editThreshold);
|
|
364
|
+
if (compactionHint && anchor.compactionHintedAt) compactionHint = false;
|
|
365
|
+
else if (compactionHint) atomicJson(anchorPath, { ...anchor, compactionHintedAt: now });
|
|
355
366
|
return {
|
|
356
367
|
record,
|
|
357
368
|
path: recordPath,
|
|
358
369
|
notice: consumeNotice(store),
|
|
359
|
-
compactionHint
|
|
370
|
+
compactionHint,
|
|
360
371
|
};
|
|
361
372
|
}
|
|
362
373
|
|