@try-works/dsh-recursive-mode 0.4.2 → 0.4.4
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 +20 -15
- package/lib/client/index.d.ts +3 -1
- package/lib/client/settings-view.d.ts +97 -0
- package/lib/client/settings.d.ts +25 -1
- package/lib/client.js +530 -11
- package/lib/enforcement.d.ts +37 -0
- package/lib/index.js +123 -18
- package/lib/memory-feedback.d.ts +25 -2
- package/package.json +1 -1
- package/preset/recursive/agent.cordis.yml +5 -4
- package/preset/recursive.patch.yml +5 -4
- package/src/client/index.ts +3 -1
- package/src/client/settings-view.ts +211 -0
- package/src/client/settings.tsx +194 -8
- package/src/client/slots.ts +15 -2
- package/src/client/styles.ts +237 -0
- package/src/enforcement.ts +74 -3
- package/src/index.ts +47 -10
- package/src/memory-feedback.ts +50 -5
- package/src/phase-rules.ts +10 -1
- package/src/policy-globs.ts +25 -3
- package/src/policy-write.ts +1 -1
- package/src/runtime.ts +29 -0
package/lib/client.js
CHANGED
|
@@ -975,16 +975,6 @@ window.__ModuleLoader__.load({
|
|
|
975
975
|
}, "View phase"))), facts.gateBlocked && (0, react.createElement)("div", { className: "rec-detail-section" }, (0, react.createElement)("h3", {}, "Gate"), (0, react.createElement)("p", { className: "rec-detail-text" }, "Blocked (" + (facts.gateKind ?? "") + ")"))), theme, toggle);
|
|
976
976
|
}
|
|
977
977
|
//#endregion
|
|
978
|
-
//#region src/client/settings.tsx
|
|
979
|
-
/**
|
|
980
|
-
* Recursive settings page (Phase D R8, §11.8): a settings.section list entry
|
|
981
|
-
* surfacing the host-owned enforcement config. The client is read-mostly; all
|
|
982
|
-
* writes ride the host config path (never run files).
|
|
983
|
-
*/
|
|
984
|
-
function RecursiveSettings({ close }) {
|
|
985
|
-
return (0, react.createElement)("div", { className: "rec-settings" }, (0, react.createElement)("h2", {}, "Recursive"), (0, react.createElement)("p", {}, "Enforcement policy, scratch format, provider defaults, and preset install path are configured on the host. The client is read-only (§11.9)."), (0, react.createElement)("button", { onClick: close }, "Close"));
|
|
986
|
-
}
|
|
987
|
-
//#endregion
|
|
988
978
|
//#region src/client/use-live.ts
|
|
989
979
|
/**
|
|
990
980
|
* useLiveProjection(scope): the client bridge to the live host route (SP2 R1).
|
|
@@ -1032,6 +1022,289 @@ window.__ModuleLoader__.load({
|
|
|
1032
1022
|
return state;
|
|
1033
1023
|
}
|
|
1034
1024
|
//#endregion
|
|
1025
|
+
//#region src/client/settings-view.ts
|
|
1026
|
+
/**
|
|
1027
|
+
* The `/state` payload is {root, projection, revision} (host-api.ts /
|
|
1028
|
+
* live-route.ts, both typed by LiveProjectionValue). Nothing else rides it, so
|
|
1029
|
+
* anything else is reported as not carried rather than guessed.
|
|
1030
|
+
*/
|
|
1031
|
+
const NOT_CARRIED = [
|
|
1032
|
+
{
|
|
1033
|
+
id: "enforcement",
|
|
1034
|
+
label: "Enforcement modes (preStep / toolGuards / tamper)",
|
|
1035
|
+
value: null
|
|
1036
|
+
},
|
|
1037
|
+
{
|
|
1038
|
+
id: "router-defaults",
|
|
1039
|
+
label: "Router defaults (defaults.subagent) and per-role routes",
|
|
1040
|
+
value: null
|
|
1041
|
+
},
|
|
1042
|
+
{
|
|
1043
|
+
id: "scratch-format",
|
|
1044
|
+
label: "Scratch format",
|
|
1045
|
+
value: null
|
|
1046
|
+
}
|
|
1047
|
+
];
|
|
1048
|
+
/** A carried string, or null when the producer carried nothing to print. */
|
|
1049
|
+
function text(value) {
|
|
1050
|
+
if (value === void 0 || value === null) return null;
|
|
1051
|
+
return value.trim() === "" ? null : value;
|
|
1052
|
+
}
|
|
1053
|
+
/**
|
|
1054
|
+
* Read a field the WIRE may omit even where the type calls it required (an
|
|
1055
|
+
* older or partial producer). The comparison itself has to happen here: with
|
|
1056
|
+
* `strict`, `card.subagents === undefined` is a no-overlap error at the call
|
|
1057
|
+
* site, but a missing field must still read as "not reported", not crash.
|
|
1058
|
+
*/
|
|
1059
|
+
function carried(value) {
|
|
1060
|
+
return value;
|
|
1061
|
+
}
|
|
1062
|
+
/** `none` for a present-but-empty map, null for a map the card did not carry. */
|
|
1063
|
+
function tamperValue(card) {
|
|
1064
|
+
const tampers = carried(card.tampers);
|
|
1065
|
+
if (tampers === void 0) return null;
|
|
1066
|
+
const facts = Object.values(tampers);
|
|
1067
|
+
if (facts.length === 0) return "none";
|
|
1068
|
+
return facts.map((f) => (text(f.path) ?? "") + ": " + (text(f.reason) ?? "no reason carried")).join(" · ");
|
|
1069
|
+
}
|
|
1070
|
+
/** Scalar facts for one run, in a fixed order. */
|
|
1071
|
+
function runRows(card) {
|
|
1072
|
+
const facts = cardFacts(card);
|
|
1073
|
+
const gate = card.gateBlocked;
|
|
1074
|
+
return [
|
|
1075
|
+
{
|
|
1076
|
+
id: "state",
|
|
1077
|
+
label: "run state",
|
|
1078
|
+
value: text(card.state)
|
|
1079
|
+
},
|
|
1080
|
+
{
|
|
1081
|
+
id: "state-reason",
|
|
1082
|
+
label: "state reason",
|
|
1083
|
+
value: text(card.stateReason)
|
|
1084
|
+
},
|
|
1085
|
+
{
|
|
1086
|
+
id: "lock",
|
|
1087
|
+
label: "lock position",
|
|
1088
|
+
value: facts.lockValidity
|
|
1089
|
+
},
|
|
1090
|
+
{
|
|
1091
|
+
id: "locked",
|
|
1092
|
+
label: "locked phase groups",
|
|
1093
|
+
value: String(facts.lockedCount) + " of " + String(facts.totalPhases) + " present"
|
|
1094
|
+
},
|
|
1095
|
+
{
|
|
1096
|
+
id: "current-phase",
|
|
1097
|
+
label: "current phase",
|
|
1098
|
+
value: text(facts.currentPhase)
|
|
1099
|
+
},
|
|
1100
|
+
{
|
|
1101
|
+
id: "gate",
|
|
1102
|
+
label: "gate block",
|
|
1103
|
+
value: gate === void 0 ? null : (text(gate.kind) ?? "kind not carried") + ": " + (gate.failures.length === 0 ? "no failures listed" : gate.failures.join("; "))
|
|
1104
|
+
},
|
|
1105
|
+
{
|
|
1106
|
+
id: "tampers",
|
|
1107
|
+
label: "tamper facts",
|
|
1108
|
+
value: tamperValue(card)
|
|
1109
|
+
},
|
|
1110
|
+
{
|
|
1111
|
+
id: "repo",
|
|
1112
|
+
label: "repo",
|
|
1113
|
+
value: text(card.repo)
|
|
1114
|
+
},
|
|
1115
|
+
{
|
|
1116
|
+
id: "template",
|
|
1117
|
+
label: "template",
|
|
1118
|
+
value: text(card.template)
|
|
1119
|
+
},
|
|
1120
|
+
{
|
|
1121
|
+
id: "merged",
|
|
1122
|
+
label: "merged to repo root",
|
|
1123
|
+
value: text(card.mergedToRepoRoot)
|
|
1124
|
+
}
|
|
1125
|
+
];
|
|
1126
|
+
}
|
|
1127
|
+
/** The folded phase chain, with the card's own `position` read off the wire row. */
|
|
1128
|
+
function phaseRows(card) {
|
|
1129
|
+
return expandPhaseRows(card).map((row) => {
|
|
1130
|
+
const wired = row.fileName === null ? void 0 : card.phases[row.fileName];
|
|
1131
|
+
return {
|
|
1132
|
+
phase: row.phase,
|
|
1133
|
+
fileName: row.fileName,
|
|
1134
|
+
status: row.status,
|
|
1135
|
+
position: wired?.position ?? null,
|
|
1136
|
+
lockedAt: text(row.lockedAt),
|
|
1137
|
+
lockHash: text(row.lockHash),
|
|
1138
|
+
present: row.present,
|
|
1139
|
+
pill: phaseStatusPill(row.status)
|
|
1140
|
+
};
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
/** One run card -> its report. */
|
|
1144
|
+
function settingsRunView(card) {
|
|
1145
|
+
const subagents = carried(card.subagents);
|
|
1146
|
+
const pendingWork = carried(card.pendingWork);
|
|
1147
|
+
return {
|
|
1148
|
+
runId: card.runId,
|
|
1149
|
+
worktreeRoot: card.worktreeRoot,
|
|
1150
|
+
state: card.state,
|
|
1151
|
+
pill: cardPill(card),
|
|
1152
|
+
rows: runRows(card),
|
|
1153
|
+
phases: phaseRows(card),
|
|
1154
|
+
subagents: subagents === void 0 ? null : Object.values(subagents).map((s) => ({
|
|
1155
|
+
childId: s.childId,
|
|
1156
|
+
role: text(s.role),
|
|
1157
|
+
provider: text(s.provider),
|
|
1158
|
+
status: text(s.status)
|
|
1159
|
+
})),
|
|
1160
|
+
pendingWork: pendingWork === void 0 ? null : pendingWork.map((p) => ({
|
|
1161
|
+
kind: p.kind,
|
|
1162
|
+
delegationId: p.delegationId,
|
|
1163
|
+
detail: p.detail
|
|
1164
|
+
}))
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
/**
|
|
1168
|
+
* The panel model for one live snapshot (null = the route has not answered yet).
|
|
1169
|
+
* Every run in the projection is reported; nothing is filtered or defaulted.
|
|
1170
|
+
*/
|
|
1171
|
+
function settingsView(snapshot) {
|
|
1172
|
+
if (snapshot === null) return {
|
|
1173
|
+
status: "no-frame",
|
|
1174
|
+
root: null,
|
|
1175
|
+
revision: null,
|
|
1176
|
+
runs: [],
|
|
1177
|
+
notCarried: [...NOT_CARRIED]
|
|
1178
|
+
};
|
|
1179
|
+
return {
|
|
1180
|
+
status: snapshot.root === null ? "no-root" : "connected",
|
|
1181
|
+
root: text(snapshot.root),
|
|
1182
|
+
revision: typeof snapshot.revision === "number" ? snapshot.revision : null,
|
|
1183
|
+
runs: listRuns(snapshot.projection).map(settingsRunView),
|
|
1184
|
+
notCarried: [...NOT_CARRIED]
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
/** The route-answer states, each said plainly instead of implied by an empty panel. */
|
|
1188
|
+
const STATUS_TEXT = {
|
|
1189
|
+
"no-frame": "The host route has not answered for this workspace yet. Every value below is unknown — not a default.",
|
|
1190
|
+
"no-root": "The host route answered, but resolved no recursive root for this workspace (no .recursive/ run layer found).",
|
|
1191
|
+
connected: "Live."
|
|
1192
|
+
};
|
|
1193
|
+
const STATUS_VALUE = {
|
|
1194
|
+
"no-frame": "no frame yet",
|
|
1195
|
+
"no-root": "answered · no recursive root",
|
|
1196
|
+
connected: "connected"
|
|
1197
|
+
};
|
|
1198
|
+
/** A label/value report block (`dl` — the semantics of a definition list). */
|
|
1199
|
+
function rows(className, items) {
|
|
1200
|
+
return (0, react.createElement)("dl", { className }, items.flatMap((row) => [(0, react.createElement)("dt", {
|
|
1201
|
+
key: row.id + "-k",
|
|
1202
|
+
className: "rec-settings-label"
|
|
1203
|
+
}, row.label), (0, react.createElement)("dd", {
|
|
1204
|
+
key: row.id + "-v",
|
|
1205
|
+
className: row.value === null ? "rec-settings-value rec-settings-absent" : "rec-settings-value"
|
|
1206
|
+
}, row.value ?? "not reported")]));
|
|
1207
|
+
}
|
|
1208
|
+
/** The frame's own facts: what the route answered, and how much it carried. */
|
|
1209
|
+
function sourceRows(view) {
|
|
1210
|
+
return [
|
|
1211
|
+
{
|
|
1212
|
+
id: "status",
|
|
1213
|
+
label: "route status",
|
|
1214
|
+
value: STATUS_VALUE[view.status]
|
|
1215
|
+
},
|
|
1216
|
+
{
|
|
1217
|
+
id: "root",
|
|
1218
|
+
label: "workspace root",
|
|
1219
|
+
value: view.root
|
|
1220
|
+
},
|
|
1221
|
+
{
|
|
1222
|
+
id: "revision",
|
|
1223
|
+
label: "revision",
|
|
1224
|
+
value: view.revision === null ? null : String(view.revision)
|
|
1225
|
+
},
|
|
1226
|
+
{
|
|
1227
|
+
id: "runs",
|
|
1228
|
+
label: "runs in the projection",
|
|
1229
|
+
value: view.status === "no-frame" ? null : String(view.runs.length)
|
|
1230
|
+
}
|
|
1231
|
+
];
|
|
1232
|
+
}
|
|
1233
|
+
function phaseNode(row) {
|
|
1234
|
+
const lock = [];
|
|
1235
|
+
if (row.lockedAt !== null) lock.push((0, react.createElement)("span", { key: "at" }, "locked at " + row.lockedAt));
|
|
1236
|
+
if (row.lockHash !== null) lock.push((0, react.createElement)("code", {
|
|
1237
|
+
key: "hash",
|
|
1238
|
+
className: "rec-lockhash"
|
|
1239
|
+
}, "#" + row.lockHash.slice(0, 8)));
|
|
1240
|
+
return (0, react.createElement)("div", {
|
|
1241
|
+
key: row.phase,
|
|
1242
|
+
className: "rec-settings-phase"
|
|
1243
|
+
}, (0, react.createElement)("span", { className: "rec-settings-phase-id" }, row.phase), (0, react.createElement)("span", { className: "rec-settings-phase-name" }, row.fileName ?? "not present in this run"), (0, react.createElement)("span", { className: "rec-settings-phase-status" }, row.status), (0, react.createElement)("span", { className: row.position === null ? "rec-settings-phase-pos rec-settings-absent" : "rec-settings-phase-pos" }, row.position ?? "not reported"), lock.length > 0 && (0, react.createElement)("span", { className: "rec-settings-phase-lock" }, lock));
|
|
1244
|
+
}
|
|
1245
|
+
function subagentNode(s) {
|
|
1246
|
+
return (0, react.createElement)("div", {
|
|
1247
|
+
key: s.childId,
|
|
1248
|
+
className: "rec-settings-item"
|
|
1249
|
+
}, (0, react.createElement)("span", { className: "rec-settings-item-id" }, s.childId), (0, react.createElement)("span", { className: "rec-settings-item-text" }, "role " + (s.role ?? "not reported") + " · provider " + (s.provider ?? "not reported") + " · status " + (s.status ?? "not reported")));
|
|
1250
|
+
}
|
|
1251
|
+
function pendingNode(p) {
|
|
1252
|
+
return (0, react.createElement)("div", {
|
|
1253
|
+
key: p.kind + "\0" + p.delegationId,
|
|
1254
|
+
className: "rec-settings-item"
|
|
1255
|
+
}, (0, react.createElement)("span", { className: "rec-settings-item-id" }, p.kind), (0, react.createElement)("span", { className: "rec-settings-item-text" }, p.delegationId + " — " + p.detail));
|
|
1256
|
+
}
|
|
1257
|
+
/**
|
|
1258
|
+
* A collection field: null = the card carried no such key (not reported),
|
|
1259
|
+
* [] = the card carried an empty one (that empty value IS the report).
|
|
1260
|
+
*/
|
|
1261
|
+
function collection(items, render, none) {
|
|
1262
|
+
if (items === null) return (0, react.createElement)("p", { className: "rec-settings-value rec-settings-absent" }, "not reported — the card carries no such field");
|
|
1263
|
+
if (items.length === 0) return (0, react.createElement)("p", { className: "rec-settings-value rec-settings-none" }, none);
|
|
1264
|
+
return (0, react.createElement)("div", { className: "rec-settings-items" }, items.map(render));
|
|
1265
|
+
}
|
|
1266
|
+
function runSection(run) {
|
|
1267
|
+
const present = run.phases.filter((p) => p.present).length;
|
|
1268
|
+
return (0, react.createElement)("section", {
|
|
1269
|
+
key: run.worktreeRoot + "\0" + run.runId,
|
|
1270
|
+
className: "rec-settings-run"
|
|
1271
|
+
}, (0, react.createElement)("div", { className: "rec-settings-run-header" }, (0, react.createElement)("h3", { className: "rec-settings-run-title" }, run.runId), (0, react.createElement)("span", {
|
|
1272
|
+
className: "rec-settings-pill",
|
|
1273
|
+
"data-pill": run.pill
|
|
1274
|
+
}, PILL_LABELS[run.pill]), (0, react.createElement)("span", { className: "rec-settings-run-root" }, run.worktreeRoot)), rows("rec-settings-rows", run.rows), (0, react.createElement)("h4", { className: "rec-settings-h4" }, "Phases (" + String(present) + " present)"), (0, react.createElement)("p", { className: "rec-settings-hint" }, "status is the folded artifact status; position is the host's single derived phase position (T21)."), run.phases.length === 0 ? (0, react.createElement)("p", { className: "rec-settings-value rec-settings-none" }, "none — the card carries no phase rows") : (0, react.createElement)("div", { className: "rec-settings-phases" }, run.phases.map(phaseNode)), (0, react.createElement)("h4", { className: "rec-settings-h4" }, "Subagents (role and provider as the host recorded them)"), collection(run.subagents, subagentNode, "none — the card carries an empty subagent map"), (0, react.createElement)("h4", { className: "rec-settings-h4" }, "Pending work (unresolved in-flight delegations)"), collection(run.pendingWork, pendingNode, "none — the card carries an empty pending set"));
|
|
1275
|
+
}
|
|
1276
|
+
/** The panel: a report of one live frame. Pure — snapshot in, tree out. */
|
|
1277
|
+
function RecursiveSettings({ close, snapshot = null }) {
|
|
1278
|
+
const view = settingsView(snapshot);
|
|
1279
|
+
return (0, react.createElement)("div", { className: "rec-settings" }, (0, react.createElement)("header", { className: "rec-settings-header" }, (0, react.createElement)("h2", { className: "rec-settings-title" }, "Recursive"), (0, react.createElement)("span", { className: "rec-settings-tag" }, "live projection · read-only"), (0, react.createElement)("button", {
|
|
1280
|
+
type: "button",
|
|
1281
|
+
className: "rec-settings-close",
|
|
1282
|
+
onClick: close
|
|
1283
|
+
}, "Close")), (0, react.createElement)("p", { className: "rec-settings-lede" }, "Read verbatim from the host route (GET /.recursive/api/state + GET /.recursive/api/events). This client writes nothing and reads no files; anything the projection does not carry is printed as \"not reported\"."), rows("rec-settings-source", sourceRows(view)), view.status !== "connected" && (0, react.createElement)("p", { className: "rec-settings-hint" }, STATUS_TEXT[view.status]), view.status === "connected" && view.runs.length === 0 && (0, react.createElement)("p", { className: "rec-settings-value rec-settings-none" }, "none — the projection carries no runs in this workspace"), view.runs.map(runSection), (0, react.createElement)("section", { className: "rec-settings-notcarried" }, (0, react.createElement)("h3", { className: "rec-settings-h3" }, "Not carried by this route"), (0, react.createElement)("p", { className: "rec-settings-hint" }, "Configured on the host, but the payload carries only {root, projection, revision} — so this panel reports them as unknown rather than guessing."), rows("rec-settings-rows", view.notCarried)));
|
|
1284
|
+
}
|
|
1285
|
+
/**
|
|
1286
|
+
* The live seat: same scope resolution as the board seats (sessionId PRIMARY,
|
|
1287
|
+
* workspace path as the cwd hint, session cwd while workspaces hydrate). Hooks
|
|
1288
|
+
* are called unconditionally — the seat only renders this component when both
|
|
1289
|
+
* selector hooks are present (slots.ts), so the hook order is fixed.
|
|
1290
|
+
*/
|
|
1291
|
+
function RecursiveSettingsLive({ close, useSessions, useWorkspaces }) {
|
|
1292
|
+
const sessions = useSessions((s) => s);
|
|
1293
|
+
const wsPath = currentWorkspacePath(useWorkspaces((s) => s) ?? {
|
|
1294
|
+
items: [],
|
|
1295
|
+
recentWorkspaceId: void 0
|
|
1296
|
+
}, sessions);
|
|
1297
|
+
const cwd = wsPath !== "" ? wsPath : currentSessionCwd(sessions);
|
|
1298
|
+
const snapshot = useLiveProjection({
|
|
1299
|
+
sessionId: sessions.current,
|
|
1300
|
+
cwd
|
|
1301
|
+
});
|
|
1302
|
+
return (0, react.createElement)(RecursiveSettings, {
|
|
1303
|
+
close,
|
|
1304
|
+
snapshot
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
//#endregion
|
|
1035
1308
|
//#region src/client/open-state.ts
|
|
1036
1309
|
/**
|
|
1037
1310
|
* Shared board open-state (run 08 R1): module-level store behind the launcher,
|
|
@@ -1955,6 +2228,243 @@ window.__ModuleLoader__.load({
|
|
|
1955
2228
|
border-color: var(--dsw-alias-state-warn-primary);
|
|
1956
2229
|
}
|
|
1957
2230
|
|
|
2231
|
+
/* ===== Settings section (Settings -> Recursive): the live projection report =====
|
|
2232
|
+
This panel lives INSIDE the settings shell, so it consumes the shell's own
|
|
2233
|
+
--dsw-alias-* theme tokens (like .rec-badge above) instead of the board's
|
|
2234
|
+
scoped --board-* paper tokens: no theme toggle, and it follows the app theme. */
|
|
2235
|
+
.rec-settings {
|
|
2236
|
+
display: flex;
|
|
2237
|
+
flex-direction: column;
|
|
2238
|
+
gap: 16px;
|
|
2239
|
+
max-width: 940px;
|
|
2240
|
+
padding: 2px 2px 10px;
|
|
2241
|
+
color: var(--dsw-alias-label-primary);
|
|
2242
|
+
font-size: 13px;
|
|
2243
|
+
line-height: 1.5;
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
.rec-settings-header {
|
|
2247
|
+
display: flex;
|
|
2248
|
+
align-items: center;
|
|
2249
|
+
gap: 10px;
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
.rec-settings-title {
|
|
2253
|
+
margin: 0;
|
|
2254
|
+
font-size: 16px;
|
|
2255
|
+
font-weight: 600;
|
|
2256
|
+
letter-spacing: -0.01em;
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
.rec-settings-tag {
|
|
2260
|
+
flex: none;
|
|
2261
|
+
padding: 2px 8px;
|
|
2262
|
+
font-size: 11px;
|
|
2263
|
+
font-weight: 500;
|
|
2264
|
+
letter-spacing: 0.03em;
|
|
2265
|
+
text-transform: uppercase;
|
|
2266
|
+
color: var(--dsw-alias-label-tertiary);
|
|
2267
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
2268
|
+
border-radius: 999px;
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
.rec-settings-close {
|
|
2272
|
+
margin-left: auto;
|
|
2273
|
+
padding: 5px 12px;
|
|
2274
|
+
font: inherit;
|
|
2275
|
+
font-size: 12px;
|
|
2276
|
+
color: var(--dsw-alias-label-primary);
|
|
2277
|
+
background: transparent;
|
|
2278
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
2279
|
+
border-radius: 8px;
|
|
2280
|
+
cursor: pointer;
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
.rec-settings-close:hover {
|
|
2284
|
+
background: var(--dsw-alias-bg-layer-3);
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
.rec-settings-lede,
|
|
2288
|
+
.rec-settings-hint,
|
|
2289
|
+
.rec-settings-none {
|
|
2290
|
+
margin: 0;
|
|
2291
|
+
color: var(--dsw-alias-label-secondary);
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
.rec-settings-hint,
|
|
2295
|
+
.rec-settings-none {
|
|
2296
|
+
font-size: 12px;
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
.rec-settings-section,
|
|
2300
|
+
.rec-settings-notcarried,
|
|
2301
|
+
.rec-settings-run {
|
|
2302
|
+
display: flex;
|
|
2303
|
+
flex-direction: column;
|
|
2304
|
+
gap: 8px;
|
|
2305
|
+
padding: 12px 14px;
|
|
2306
|
+
background: var(--dsw-alias-bg-layer-2);
|
|
2307
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
2308
|
+
border-radius: 10px;
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
.rec-settings-h3 {
|
|
2312
|
+
margin: 0;
|
|
2313
|
+
font-size: 12px;
|
|
2314
|
+
font-weight: 600;
|
|
2315
|
+
letter-spacing: 0.04em;
|
|
2316
|
+
text-transform: uppercase;
|
|
2317
|
+
color: var(--dsw-alias-label-tertiary);
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
.rec-settings-h4 {
|
|
2321
|
+
margin: 6px 0 0;
|
|
2322
|
+
font-size: 12px;
|
|
2323
|
+
font-weight: 600;
|
|
2324
|
+
color: var(--dsw-alias-label-secondary);
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
.rec-settings-run-header {
|
|
2328
|
+
display: flex;
|
|
2329
|
+
align-items: center;
|
|
2330
|
+
gap: 10px;
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
.rec-settings-run-title {
|
|
2334
|
+
margin: 0;
|
|
2335
|
+
font-size: 14px;
|
|
2336
|
+
font-weight: 600;
|
|
2337
|
+
overflow-wrap: anywhere;
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
.rec-settings-run-root {
|
|
2341
|
+
flex: 1;
|
|
2342
|
+
min-width: 0;
|
|
2343
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
2344
|
+
font-size: 11px;
|
|
2345
|
+
color: var(--dsw-alias-label-tertiary);
|
|
2346
|
+
overflow: hidden;
|
|
2347
|
+
text-overflow: ellipsis;
|
|
2348
|
+
white-space: nowrap;
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
/* Solid state pill, same vocabulary as .rec-pill but on the SHELL tokens
|
|
2352
|
+
(the board's --board-* aliases are scoped to .rec-board/.rec-inspector). */
|
|
2353
|
+
.rec-settings-pill {
|
|
2354
|
+
flex: none;
|
|
2355
|
+
padding: 3px 10px;
|
|
2356
|
+
font-size: 11px;
|
|
2357
|
+
font-weight: 500;
|
|
2358
|
+
line-height: 1;
|
|
2359
|
+
border-radius: 999px;
|
|
2360
|
+
color: var(--dsw-alias-label-primary-foreground);
|
|
2361
|
+
background: var(--dsw-alias-label-tertiary);
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
.rec-settings-pill[data-pill='locked'] { background: var(--dsw-alias-state-success-primary); }
|
|
2365
|
+
.rec-settings-pill[data-pill='in-progress'] { background: var(--dsw-alias-state-business-primary); }
|
|
2366
|
+
.rec-settings-pill[data-pill='paused'] { background: var(--dsw-alias-state-warn-primary); }
|
|
2367
|
+
.rec-settings-pill[data-pill='blocked'] { background: var(--dsw-alias-state-error-primary); }
|
|
2368
|
+
.rec-settings-pill[data-pill='tampered'] { background: var(--dsw-alias-state-error-primary); }
|
|
2369
|
+
.rec-settings-pill[data-pill='advisory'] { background: var(--dsw-alias-state-warn-secondary); }
|
|
2370
|
+
.rec-settings-pill[data-pill='neutral'] { background: var(--dsw-alias-label-tertiary); }
|
|
2371
|
+
|
|
2372
|
+
dl.rec-settings-source,
|
|
2373
|
+
dl.rec-settings-rows {
|
|
2374
|
+
display: grid;
|
|
2375
|
+
grid-template-columns: minmax(160px, 300px) 1fr;
|
|
2376
|
+
gap: 4px 16px;
|
|
2377
|
+
margin: 0;
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2380
|
+
.rec-settings-label {
|
|
2381
|
+
font-size: 12px;
|
|
2382
|
+
color: var(--dsw-alias-label-tertiary);
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
.rec-settings-value {
|
|
2386
|
+
margin: 0;
|
|
2387
|
+
color: var(--dsw-alias-label-primary);
|
|
2388
|
+
overflow-wrap: anywhere;
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2391
|
+
/* An absent value is a STATEMENT, not an empty row — always visibly marked. */
|
|
2392
|
+
.rec-settings-absent {
|
|
2393
|
+
color: var(--dsw-alias-state-warn-label);
|
|
2394
|
+
font-style: italic;
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
.rec-settings-phases {
|
|
2398
|
+
display: flex;
|
|
2399
|
+
flex-direction: column;
|
|
2400
|
+
gap: 4px;
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
.rec-settings-phase {
|
|
2404
|
+
display: flex;
|
|
2405
|
+
align-items: baseline;
|
|
2406
|
+
flex-wrap: wrap;
|
|
2407
|
+
gap: 10px;
|
|
2408
|
+
padding: 4px 10px;
|
|
2409
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
2410
|
+
border-radius: 6px;
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
.rec-settings-phase-id {
|
|
2414
|
+
flex: none;
|
|
2415
|
+
min-width: 40px;
|
|
2416
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
2417
|
+
font-size: 11px;
|
|
2418
|
+
color: var(--dsw-alias-label-secondary);
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2421
|
+
.rec-settings-phase-name {
|
|
2422
|
+
flex: 1;
|
|
2423
|
+
min-width: 140px;
|
|
2424
|
+
overflow-wrap: anywhere;
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
.rec-settings-phase-status,
|
|
2428
|
+
.rec-settings-phase-pos {
|
|
2429
|
+
flex: none;
|
|
2430
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
2431
|
+
font-size: 11px;
|
|
2432
|
+
letter-spacing: 0.02em;
|
|
2433
|
+
color: var(--dsw-alias-label-secondary);
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
.rec-settings-phase-lock {
|
|
2437
|
+
flex: none;
|
|
2438
|
+
font-size: 11px;
|
|
2439
|
+
color: var(--dsw-alias-label-tertiary);
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
.rec-settings-items {
|
|
2443
|
+
display: flex;
|
|
2444
|
+
flex-direction: column;
|
|
2445
|
+
gap: 4px;
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
.rec-settings-item {
|
|
2449
|
+
display: flex;
|
|
2450
|
+
align-items: baseline;
|
|
2451
|
+
gap: 10px;
|
|
2452
|
+
font-size: 12px;
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2455
|
+
.rec-settings-item-id {
|
|
2456
|
+
flex: none;
|
|
2457
|
+
min-width: 96px;
|
|
2458
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
2459
|
+
color: var(--dsw-alias-label-secondary);
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
.rec-settings-item-text {
|
|
2463
|
+
flex: 1;
|
|
2464
|
+
min-width: 0;
|
|
2465
|
+
overflow-wrap: anywhere;
|
|
2466
|
+
}
|
|
2467
|
+
|
|
1958
2468
|
@media (prefers-reduced-motion: reduce) {
|
|
1959
2469
|
.rec-card, .rec-back, .rec-close, .rec-theme-toggle { transition: none; }
|
|
1960
2470
|
}
|
|
@@ -2100,7 +2610,14 @@ window.__ModuleLoader__.load({
|
|
|
2100
2610
|
id: "recursive",
|
|
2101
2611
|
order: 90,
|
|
2102
2612
|
label: "Recursive"
|
|
2103
|
-
}, (props) =>
|
|
2613
|
+
}, (props) => {
|
|
2614
|
+
if (props.useSessions === void 0 || props.useWorkspaces === void 0) return (0, react.createElement)(RecursiveSettings, { close: props.close });
|
|
2615
|
+
return (0, react.createElement)(RecursiveSettingsLive, {
|
|
2616
|
+
close: props.close,
|
|
2617
|
+
useSessions: props.useSessions,
|
|
2618
|
+
useWorkspaces: props.useWorkspaces
|
|
2619
|
+
});
|
|
2620
|
+
})));
|
|
2104
2621
|
return () => {
|
|
2105
2622
|
for (const d of disposers) d();
|
|
2106
2623
|
};
|
|
@@ -2169,6 +2686,7 @@ window.__ModuleLoader__.load({
|
|
|
2169
2686
|
exports.DocViewer = DocViewer;
|
|
2170
2687
|
exports.Inspector = Inspector;
|
|
2171
2688
|
exports.RecursiveSettings = RecursiveSettings;
|
|
2689
|
+
exports.RecursiveSettingsLive = RecursiveSettingsLive;
|
|
2172
2690
|
exports.RecursiveView = RecursiveView;
|
|
2173
2691
|
exports.apply = apply;
|
|
2174
2692
|
exports.currentSessionCwd = currentSessionCwd;
|
|
@@ -2179,6 +2697,7 @@ window.__ModuleLoader__.load({
|
|
|
2179
2697
|
exports.listRuns = listRuns;
|
|
2180
2698
|
exports.parseDoc = parseDoc;
|
|
2181
2699
|
exports.parseInline = parseInline;
|
|
2700
|
+
exports.settingsView = settingsView;
|
|
2182
2701
|
exports.subscribeLiveEvents = subscribeLiveEvents;
|
|
2183
2702
|
exports.useLiveProjection = useLiveProjection;
|
|
2184
2703
|
return module.exports;
|
package/lib/enforcement.d.ts
CHANGED
|
@@ -124,10 +124,47 @@ export declare function evaluateToolGuard(exec: ToolExecLike, worktreeRoot: stri
|
|
|
124
124
|
* decisions pass through unchanged.
|
|
125
125
|
*/
|
|
126
126
|
export declare function coerceAskToDecision(decision: ToolGuardDecision, mode?: EnforcementMode): ToolGuardDecision;
|
|
127
|
+
/**
|
|
128
|
+
* The ADMISSION test for the tamper path: the resolved absolute path when
|
|
129
|
+
* `targetPath` names a run-tree `*.md` — a tamper CANDIDATE — and `null`
|
|
130
|
+
* otherwise. Pure path arithmetic on every branch (no filesystem work), so a
|
|
131
|
+
* caller may use it as a cheap shape check before paying for `existsSync`.
|
|
132
|
+
*
|
|
133
|
+
* ⚠ THE BLIND SPOT THIS FUNCTION EXISTS TO CLOSE. The admission test used to be a
|
|
134
|
+
* substring test on the target STRING alone, looking for `/.recursive/run/`. A
|
|
135
|
+
* repo-relative path has no separator before `.recursive`, so
|
|
136
|
+
* `.recursive/run/<id>/00-requirements.md` — the spelling a model actually types,
|
|
137
|
+
* and its backslash form — was rejected before ANYTHING was examined, and
|
|
138
|
+
* tampering with a locked artifact through that spelling was invisible
|
|
139
|
+
* (measured: `detectTamper` returned a record for the absolute path and `null`
|
|
140
|
+
* for the relative one, on the same file). The identical defect, in the identical
|
|
141
|
+
* spelling, was fixed one module over in `policy-globs.ts` `lockedWriteRule`; this
|
|
142
|
+
* is that fix's shape, reused rather than reinvented.
|
|
143
|
+
*
|
|
144
|
+
* So the marker is looked for on the path the target RESOLVES to as well as on
|
|
145
|
+
* the string as written. The `||` is load-bearing and the string test is KEPT
|
|
146
|
+
* rather than replaced, because a resolved-only test would SHRINK the admitted
|
|
147
|
+
* set: an absolute target that literally carries the marker but resolves away
|
|
148
|
+
* from it (`…/.recursive/run/../…`) was caught before and must stay caught. The
|
|
149
|
+
* net effect is a strict SUPERSET of the previous behaviour, so no tamper that
|
|
150
|
+
* was visible before can become invisible.
|
|
151
|
+
*
|
|
152
|
+
* EXPORTED because `src/index.ts`'s `fs/observed` listener must apply the SAME
|
|
153
|
+
* admission test before calling `detectTamper`. That listener used to carry a
|
|
154
|
+
* hand-copied mirror of this test, and a mirror is exactly what leaves half the
|
|
155
|
+
* defect behind: widening `detectTamper` alone changes nothing, because the
|
|
156
|
+
* listener rejects the spelling first. One function cannot disagree with itself.
|
|
157
|
+
*/
|
|
158
|
+
export declare function tamperCandidatePath(targetPath: string, worktreeRoot: string): string | null;
|
|
127
159
|
/**
|
|
128
160
|
* Layer 8 - fs/observed lock-tamper detection.
|
|
129
161
|
* A locked *.md whose observed version differs from the stored LockHash is
|
|
130
162
|
* a tamper. Returns a tamper reason (or null when clean/not-applicable).
|
|
163
|
+
*
|
|
164
|
+
* The admission test lives in `tamperCandidatePath` (above), shared with the
|
|
165
|
+
* `fs/observed` listener in `src/index.ts` — see the note there for why sharing
|
|
166
|
+
* it is the point and not a tidiness preference. What this function reports is
|
|
167
|
+
* unchanged: the same record shape, carrying the target AS WRITTEN.
|
|
131
168
|
*/
|
|
132
169
|
export declare function detectTamper(targetPath: string, worktreeRoot: string, activeRunId: string): {
|
|
133
170
|
runId: string;
|