@b00dea/cockpit 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +99 -0
- package/README.md +3 -0
- package/cockpit.schema.json +73 -3
- package/dist/autonomy.d.ts +3 -2
- package/dist/autonomy.js +30 -13
- package/dist/browser/commands.d.ts +57 -2
- package/dist/browser/commands.js +114 -10
- package/dist/browser/runtime.d.ts +1 -0
- package/dist/browser/runtime.js +4 -1
- package/dist/browser/surfaces.d.ts +7 -7
- package/dist/browser/surfaces.js +5 -0
- package/dist/canonical-json.d.ts +2 -0
- package/dist/canonical-json.js +5 -0
- package/dist/cli-main.d.ts +40 -3
- package/dist/cli-main.js +241 -62
- package/dist/command-table.d.ts +19 -11
- package/dist/command-table.js +190 -38
- package/dist/config/load.d.ts +16 -4
- package/dist/config/load.js +25 -3
- package/dist/config/unconfirmed-toml.d.ts +9 -0
- package/dist/config/unconfirmed-toml.js +29 -0
- package/dist/diagnostics.js +10 -1
- package/dist/docs/check.d.ts +16 -0
- package/dist/docs/check.js +197 -0
- package/dist/docs/decision-record.d.ts +34 -0
- package/dist/docs/decision-record.js +135 -0
- package/dist/docs/decisions.d.ts +16 -0
- package/dist/docs/decisions.js +103 -0
- package/dist/docs/markdown.d.ts +36 -0
- package/dist/docs/markdown.js +88 -0
- package/dist/docs/references.d.ts +32 -0
- package/dist/docs/references.js +148 -0
- package/dist/environment/application/confirm.d.ts +35 -0
- package/dist/environment/application/confirm.js +122 -0
- package/dist/environment/application/destructive.js +6 -2
- package/dist/environment/application/doctor.d.ts +12 -7
- package/dist/environment/application/doctor.js +20 -8
- package/dist/environment/application/guard.d.ts +12 -6
- package/dist/environment/application/guard.js +87 -32
- package/dist/environment/application/health.d.ts +14 -5
- package/dist/environment/application/health.js +8 -2
- package/dist/environment/application/lifecycle-context.d.ts +10 -3
- package/dist/environment/application/lifecycle-state.js +4 -3
- package/dist/environment/application/lifecycle.d.ts +2 -1
- package/dist/environment/application/lifecycle.js +11 -3
- package/dist/environment/application/services.d.ts +60 -0
- package/dist/environment/application/services.js +89 -0
- package/dist/environment/application/siblings.d.ts +43 -0
- package/dist/environment/application/siblings.js +269 -0
- package/dist/environment/claims.d.ts +10 -10
- package/dist/environment/claims.js +3 -11
- package/dist/environment/compose-topology.d.ts +15 -7
- package/dist/environment/compose-topology.js +100 -23
- package/dist/environment/contract.d.ts +42 -9
- package/dist/environment/contract.js +33 -0
- package/dist/environment/executable.d.ts +12 -0
- package/dist/environment/executable.js +46 -23
- package/dist/environment/generated-application/check.js +2 -0
- package/dist/environment/generated-application/prepare.js +2 -0
- package/dist/environment/generated-application/shared.d.ts +10 -2
- package/dist/environment/generated-application/shared.js +6 -1
- package/dist/environment/generated-application/surface-outputs.js +23 -12
- package/dist/environment/generated-application/surfaces.js +16 -11
- package/dist/environment/generated-application/sync.js +19 -4
- package/dist/environment/git-observation.d.ts +25 -6
- package/dist/environment/git-observation.js +37 -28
- package/dist/environment/health-composition.d.ts +3 -2
- package/dist/environment/health-composition.js +19 -2
- package/dist/environment/init-application.d.ts +39 -14
- package/dist/environment/init-application.js +39 -10
- package/dist/environment/lifecycle-composition.d.ts +4 -2
- package/dist/environment/lifecycle-composition.js +84 -17
- package/dist/environment/lifecycle-stage.d.ts +8 -3
- package/dist/environment/lifecycle-stage.js +11 -1
- package/dist/environment/logs.d.ts +1 -1
- package/dist/environment/map-limit.d.ts +2 -0
- package/dist/environment/map-limit.js +13 -0
- package/dist/environment/mcp-observation.d.ts +9 -0
- package/dist/environment/mcp-observation.js +46 -0
- package/dist/environment/observations.d.ts +1 -1
- package/dist/environment/observations.js +2 -0
- package/dist/environment/operator-application.d.ts +35 -6
- package/dist/environment/operator-application.js +42 -0
- package/dist/environment/presence.d.ts +4 -0
- package/dist/environment/presence.js +26 -0
- package/dist/environment/read-composition.d.ts +8 -3
- package/dist/environment/read-composition.js +53 -8
- package/dist/environment/readiness-composition.d.ts +20 -1
- package/dist/environment/readiness-composition.js +23 -2
- package/dist/environment/reclaim.d.ts +56 -19
- package/dist/environment/reclaim.js +147 -42
- package/dist/environment/runtime.d.ts +64 -18
- package/dist/environment/runtime.js +73 -3
- package/dist/environment/served-checkout-grants.d.ts +53 -0
- package/dist/environment/served-checkout-grants.js +182 -0
- package/dist/environment/sibling-checkouts.d.ts +119 -0
- package/dist/environment/sibling-checkouts.js +254 -0
- package/dist/environment/sibling-observation.d.ts +27 -0
- package/dist/environment/sibling-observation.js +45 -0
- package/dist/environment/sibling-pull.d.ts +23 -0
- package/dist/environment/sibling-pull.js +95 -0
- package/dist/environment/stack-inputs.d.ts +18 -0
- package/dist/environment/stack-inputs.js +210 -0
- package/dist/environment/stack-inventory-prune.d.ts +31 -0
- package/dist/environment/stack-inventory-prune.js +129 -0
- package/dist/environment/stack-inventory-report.d.ts +44 -0
- package/dist/environment/stack-inventory-report.js +78 -0
- package/dist/environment/stack-inventory.d.ts +94 -0
- package/dist/environment/stack-inventory.js +418 -0
- package/dist/environment/state.d.ts +48 -9
- package/dist/environment/state.js +142 -2
- package/dist/environment/tool-registration.js +3 -7
- package/dist/environment-exec/execute.d.ts +6 -3
- package/dist/environment-exec/execute.js +4 -0
- package/dist/fix-catalogue.d.ts +1 -1
- package/dist/generated-environment/adopt.js +4 -8
- package/dist/guards/answer-confirmation.d.ts +41 -0
- package/dist/guards/answer-confirmation.js +109 -0
- package/dist/guards/evaluator.d.ts +20 -3
- package/dist/guards/evaluator.js +39 -7
- package/dist/guards/local-identity.d.ts +6 -2
- package/dist/guards/local-identity.js +10 -4
- package/dist/guards/policy-change.d.ts +43 -0
- package/dist/guards/policy-change.js +159 -0
- package/dist/guards/policy-pin.d.ts +1 -1
- package/dist/guards/policy-pin.js +14 -12
- package/dist/init/command.d.ts +4 -3
- package/dist/init/command.js +36 -16
- package/dist/init/derive.d.ts +8 -6
- package/dist/init/derive.js +140 -36
- package/dist/init/draft.d.ts +13 -11
- package/dist/init/draft.js +54 -31
- package/dist/init/render.d.ts +18 -6
- package/dist/init/render.js +31 -9
- package/dist/repository-path.d.ts +9 -0
- package/dist/repository-path.js +16 -0
- package/dist/schema.d.ts +42 -75
- package/dist/schema.js +62 -11
- package/dist/surfaces/harness-registry.d.ts +80 -0
- package/dist/surfaces/harness-registry.js +29 -0
- package/dist/surfaces/mcp.d.ts +67 -17
- package/dist/surfaces/mcp.js +194 -31
- package/dist/surfaces/projection.d.ts +7 -7
- package/dist/surfaces/projection.js +41 -6
- package/dist/surfaces/render.d.ts +5 -44
- package/dist/surfaces/render.js +5 -2
- package/dist/team-metadata.js +2 -1
- package/dist/testing/test-protection.d.ts +34 -0
- package/dist/testing/test-protection.js +137 -0
- package/dist/testing/weakening.d.ts +15 -0
- package/dist/testing/weakening.js +30 -0
- package/dist/work-orders/application-closure.d.ts +7 -0
- package/dist/work-orders/application-closure.js +76 -0
- package/dist/work-orders/application-context.d.ts +162 -0
- package/dist/work-orders/application-context.js +137 -0
- package/dist/work-orders/application-decisions.d.ts +27 -0
- package/dist/work-orders/application-decisions.js +121 -0
- package/dist/work-orders/application-grading.d.ts +11 -0
- package/dist/work-orders/application-grading.js +212 -0
- package/dist/work-orders/application.d.ts +4 -97
- package/dist/work-orders/application.js +94 -254
- package/dist/work-orders/composition.d.ts +35 -58
- package/dist/work-orders/composition.js +216 -24
- package/dist/work-orders/contract.d.ts +22 -6
- package/dist/work-orders/contract.js +34 -16
- package/dist/work-orders/decisions.d.ts +23 -0
- package/dist/work-orders/decisions.js +55 -0
- package/dist/work-orders/store.d.ts +54 -5
- package/dist/work-orders/store.js +118 -24
- package/docs/README.md +6 -3
- package/docs/capability-matrix.md +17 -9
- package/docs/cli-reference.md +209 -1
- package/docs/command-list.md +12 -1
- package/docs/concepts.md +6 -4
- package/docs/configuration-essentials.md +7 -2
- package/docs/configuration-reference.md +18 -3
- package/docs/docs-check.md +76 -0
- package/docs/environment.md +194 -6
- package/docs/faq.md +5 -3
- package/docs/getting-started.md +109 -25
- package/docs/harness-surfaces.md +69 -2
- package/docs/safety-and-autonomy.md +56 -12
- package/docs/troubleshooting.md +29 -8
- package/docs/tutorial.md +3 -2
- package/docs/work-and-browser.md +180 -14
- package/libexec/browser-operation.cjs +86 -2
- package/package.json +2 -2
- package/surfaces/skills/cockpit-browser.skill.md +7 -2
- package/surfaces/skills/cockpit-explain.skill.md +1 -1
- package/surfaces/skills/cockpit-fix.skill.md +1 -1
- package/surfaces/skills/cockpit-setup.skill.md +3 -3
- package/surfaces/skills/cockpit-work.skill.md +1 -1
- package/surfaces/skills/procedures/explain.md +9 -4
- package/surfaces/skills/procedures/fix.md +25 -11
- package/surfaces/skills/procedures/setup.md +113 -40
- package/surfaces/skills/procedures/work.md +45 -16
|
@@ -16,7 +16,7 @@ export interface GuideObservation {
|
|
|
16
16
|
readonly contentSha256: string;
|
|
17
17
|
readonly contentIdentity: string;
|
|
18
18
|
}
|
|
19
|
-
|
|
19
|
+
type GuideObservationResult = {
|
|
20
20
|
readonly kind: 'complete';
|
|
21
21
|
readonly observations: readonly GuideObservation[];
|
|
22
22
|
readonly missing: readonly string[];
|
|
@@ -24,7 +24,7 @@ export type GuideObservationResult = {
|
|
|
24
24
|
readonly kind: 'refusal' | 'unavailable';
|
|
25
25
|
readonly diagnostic: ProjectionDiagnostic;
|
|
26
26
|
};
|
|
27
|
-
|
|
27
|
+
interface ProjectionSiblingInput {
|
|
28
28
|
slug: string;
|
|
29
29
|
mount: Mount;
|
|
30
30
|
directoryAuthority: string;
|
|
@@ -58,21 +58,21 @@ export interface ProjectionPlannerInput {
|
|
|
58
58
|
/** The tools cockpit.toml lists; grants and start-up costs cover exactly these. Every shipped tool when absent. */
|
|
59
59
|
targets?: readonly SurfaceTarget[];
|
|
60
60
|
}
|
|
61
|
-
|
|
61
|
+
interface LoadCostCharacterization {
|
|
62
62
|
measuredOn: string;
|
|
63
63
|
provenance: string;
|
|
64
64
|
metric: 'lines';
|
|
65
65
|
scopeSemantics: 'conditional' | 'full';
|
|
66
66
|
}
|
|
67
|
-
|
|
68
|
-
readonly code: 'PROJECTION_INPUT_INVALID' | 'PROJECTION_GUIDE_UNAVAILABLE' | 'PROJECTION_GUIDE_CHANGED' | 'PROJECTION_GUIDE_ESCAPE' | 'PROJECTION_GUIDE_READ_FAILED' | 'PROJECTION_GUIDE_CLOSE_FAILED' | 'PROJECTION_ROOT_AMBIGUOUS' | 'PROJECTION_RESULT_BOUNDS' | 'PROJECTION_MOUNT_MISMATCH' | 'PROJECTION_GRANT_MISMATCH';
|
|
67
|
+
interface ProjectionDiagnostic {
|
|
68
|
+
readonly code: 'PROJECTION_INPUT_INVALID' | 'PROJECTION_GUIDE_UNAVAILABLE' | 'PROJECTION_GUIDE_CHANGED' | 'PROJECTION_GUIDE_ESCAPE' | 'PROJECTION_GUIDE_SECRET' | 'PROJECTION_GUIDE_READ_FAILED' | 'PROJECTION_GUIDE_CLOSE_FAILED' | 'PROJECTION_ROOT_AMBIGUOUS' | 'PROJECTION_RESULT_BOUNDS' | 'PROJECTION_MOUNT_MISMATCH' | 'PROJECTION_GRANT_MISMATCH';
|
|
69
69
|
readonly message: string;
|
|
70
70
|
readonly target?: string;
|
|
71
71
|
readonly expected?: string;
|
|
72
72
|
readonly actual?: string;
|
|
73
73
|
}
|
|
74
74
|
/** A sibling guide copied into the region of the root AGENTS.md that cockpit owns. */
|
|
75
|
-
|
|
75
|
+
interface ProjectedGuide {
|
|
76
76
|
readonly sibling: string;
|
|
77
77
|
/** The guide's path inside its sibling, as declared. */
|
|
78
78
|
readonly guide: string;
|
|
@@ -82,7 +82,7 @@ export interface ProjectedGuide {
|
|
|
82
82
|
/** The block written into the root AGENTS.md: a heading, a provenance marker and the guide's text. */
|
|
83
83
|
readonly bytes: string;
|
|
84
84
|
}
|
|
85
|
-
|
|
85
|
+
interface ShippedInstructionCapability {
|
|
86
86
|
readonly target: SurfaceTarget;
|
|
87
87
|
readonly mechanism: 'root-agents-guide' | 'rulesync-rules';
|
|
88
88
|
readonly capability: 'instructed' | 'generated';
|
|
@@ -6,6 +6,7 @@ import { SURFACE_TARGETS } from './contract.js';
|
|
|
6
6
|
import { GUARD_SURFACE_TARGETS as INSTRUCTION_TARGETS, HARNESS_REGISTRY } from './harness-registry.js';
|
|
7
7
|
import { INSTRUCTION_POINTER_MARKERS } from '../environment/instruction-pointer.js';
|
|
8
8
|
import { exactKeys } from '../exact-keys.js';
|
|
9
|
+
import { foldGuardSpelling } from '../guards/evaluator.js';
|
|
9
10
|
const GUIDE_BYTE_LIMIT = 65_536;
|
|
10
11
|
const GUIDE_COUNT_LIMIT = 256;
|
|
11
12
|
const OBSERVATION_CONTENT_BYTE_LIMIT = 786_432;
|
|
@@ -51,15 +52,32 @@ function contained(root, path) {
|
|
|
51
52
|
const fromRoot = relative(root, path);
|
|
52
53
|
return fromRoot === '' || !fromRoot.startsWith('..') && !isAbsolute(fromRoot);
|
|
53
54
|
}
|
|
55
|
+
// A guide is copied into every agent's instructions, so it never reaches what the guard keeps out of
|
|
56
|
+
// their context: a `.env` or `.env.*` file, anything below a `.secrets` directory, or a declared
|
|
57
|
+
// environment instance. `fromRoot` is the path inside its sibling root; `instances` are folded.
|
|
58
|
+
function secretGuide(fromRoot, path, instances) {
|
|
59
|
+
const segments = foldGuardSpelling(fromRoot).split('/');
|
|
60
|
+
const name = segments.at(-1);
|
|
61
|
+
if (instances.has(foldGuardSpelling(path)))
|
|
62
|
+
return 'a declared environment instance';
|
|
63
|
+
if (segments.includes('.secrets'))
|
|
64
|
+
return 'below a .secrets directory';
|
|
65
|
+
if (name === '.env' || name.startsWith('.env.'))
|
|
66
|
+
return 'an environment file';
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
54
69
|
function snapshotRequests(input) {
|
|
55
70
|
const snapshot = snapshotJson(input);
|
|
56
|
-
if (!isObject(snapshot) || !exactKeys(snapshot, ['allowedRoots', 'requests']) || !Array.isArray(snapshot.requests)
|
|
71
|
+
if (!isObject(snapshot) || !exactKeys(snapshot, ['allowedRoots', 'requests'], ['instances']) || !Array.isArray(snapshot.requests)
|
|
57
72
|
|| snapshot.requests.length > GUIDE_COUNT_LIMIT)
|
|
58
73
|
return undefined;
|
|
59
74
|
const allowedRoots = stringArray(snapshot.allowedRoots);
|
|
60
75
|
if (!allowedRoots || allowedRoots.length === 0 || allowedRoots.length > GUIDE_COUNT_LIMIT
|
|
61
76
|
|| new Set(allowedRoots).size !== allowedRoots.length || allowedRoots.some(root => !isAbsolute(root)))
|
|
62
77
|
return undefined;
|
|
78
|
+
const instances = snapshot.instances === undefined ? [] : stringArray(snapshot.instances);
|
|
79
|
+
if (!instances || instances.length > GUIDE_COUNT_LIMIT || instances.some(path => !isAbsolute(path)))
|
|
80
|
+
return undefined;
|
|
63
81
|
const result = [];
|
|
64
82
|
const identities = new Set();
|
|
65
83
|
for (const request of snapshot.requests) {
|
|
@@ -77,7 +95,7 @@ function snapshotRequests(input) {
|
|
|
77
95
|
identities.add(identity);
|
|
78
96
|
result.push({ slug: request.slug, root: request.root, sourcePath });
|
|
79
97
|
}
|
|
80
|
-
return { requests: result, allowedRoots };
|
|
98
|
+
return { requests: result, allowedRoots, instances };
|
|
81
99
|
}
|
|
82
100
|
function statIdentity(stat) {
|
|
83
101
|
return `${stat.dev}:${stat.ino}:${stat.mode}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`;
|
|
@@ -168,7 +186,15 @@ async function pathIdentity(sourcePath) {
|
|
|
168
186
|
const target = await lstat(contentPath, { bigint: true });
|
|
169
187
|
return { entry, entryType, ...(linkTarget === undefined ? {} : { linkTarget }), contentPath, target };
|
|
170
188
|
}
|
|
171
|
-
|
|
189
|
+
function secretGuideDiagnostic(request, reason, resolved) {
|
|
190
|
+
return diagnostic('PROJECTION_GUIDE_SECRET', `guide ${request.sourcePath} ${resolved === undefined ? 'is' : `resolves to ${resolved},`} ${reason}, `
|
|
191
|
+
+ 'which the guard keeps out of agent context, and sync would copy it into the root AGENTS.md; '
|
|
192
|
+
+ `${resolved === undefined ? '' : 'replace the link with a file of guidance, or '}remove it from the guides of sibling ${request.slug} in cockpit.toml`, { target: request.sourcePath });
|
|
193
|
+
}
|
|
194
|
+
async function readGuide(request, requestRoot, instances) {
|
|
195
|
+
const named = secretGuide(relative(request.root, request.sourcePath), request.sourcePath, instances);
|
|
196
|
+
if (named)
|
|
197
|
+
return secretGuideDiagnostic(request, named);
|
|
172
198
|
if (!await rootAuthorityStable(requestRoot)) {
|
|
173
199
|
return diagnostic('PROJECTION_GUIDE_CHANGED', `declared sibling root changed before guide observation: ${request.root}`, { target: request.root });
|
|
174
200
|
}
|
|
@@ -189,6 +215,10 @@ async function readGuide(request, requestRoot) {
|
|
|
189
215
|
target: request.sourcePath, expected: requestRoot.canonical, actual: pathBefore.contentPath,
|
|
190
216
|
});
|
|
191
217
|
}
|
|
218
|
+
// A link spelled like a guide reaches its target's bytes.
|
|
219
|
+
const resolved = secretGuide(relative(requestRoot.canonical, pathBefore.contentPath), pathBefore.contentPath, instances);
|
|
220
|
+
if (resolved)
|
|
221
|
+
return secretGuideDiagnostic(request, resolved, pathBefore.contentPath);
|
|
192
222
|
let handle;
|
|
193
223
|
let result;
|
|
194
224
|
try {
|
|
@@ -307,6 +337,11 @@ export async function observeSiblingGuides(input) {
|
|
|
307
337
|
if (new Set(allowedRoots.map(root => root.canonical)).size !== allowedRoots.length) {
|
|
308
338
|
return finishRootObservation(allowedRoots, observedOutcome({ kind: 'refusal', diagnostic: diagnostic('PROJECTION_ROOT_AMBIGUOUS', 'declared sibling roots resolve to the same canonical authority') }));
|
|
309
339
|
}
|
|
340
|
+
// Each instance under every spelling a guide can reach it by: as declared, and below its root's real path.
|
|
341
|
+
const instances = new Set(captured.instances.flatMap(path => {
|
|
342
|
+
const root = allowedRoots.find(candidate => contained(candidate.declared, path));
|
|
343
|
+
return root ? [path, join(root.canonical, relative(root.declared, path))] : [path];
|
|
344
|
+
}).map(foldGuardSpelling));
|
|
310
345
|
const observations = [];
|
|
311
346
|
const missing = [];
|
|
312
347
|
let contentBytes = 0;
|
|
@@ -321,11 +356,11 @@ export async function observeSiblingGuides(input) {
|
|
|
321
356
|
return finishRootObservation(allowedRoots, observedOutcome({ kind: 'refusal', diagnostic: diagnostic('PROJECTION_INPUT_INVALID', `guide request is not bound to its most-specific sibling root: ${request.sourcePath}`, { target: request.sourcePath }) }));
|
|
322
357
|
}
|
|
323
358
|
const requestRoot = allowedRoots.find(root => root.declared === request.root);
|
|
324
|
-
const observed = await readGuide(request, requestRoot);
|
|
359
|
+
const observed = await readGuide(request, requestRoot, instances);
|
|
325
360
|
if (observed === 'missing')
|
|
326
361
|
missing.push(request.sourcePath);
|
|
327
362
|
else if ('code' in observed) {
|
|
328
|
-
const refusal = ['PROJECTION_GUIDE_CHANGED', 'PROJECTION_GUIDE_ESCAPE', 'PROJECTION_GUIDE_READ_FAILED',
|
|
363
|
+
const refusal = ['PROJECTION_GUIDE_CHANGED', 'PROJECTION_GUIDE_ESCAPE', 'PROJECTION_GUIDE_SECRET', 'PROJECTION_GUIDE_READ_FAILED',
|
|
329
364
|
'PROJECTION_GUIDE_CLOSE_FAILED', 'PROJECTION_RESULT_BOUNDS'].includes(observed.code);
|
|
330
365
|
return finishRootObservation(allowedRoots, observedOutcome({ kind: refusal ? 'refusal' : 'unavailable', diagnostic: observed }));
|
|
331
366
|
}
|
|
@@ -365,7 +400,7 @@ function capturePlannerInput(input) {
|
|
|
365
400
|
return undefined;
|
|
366
401
|
const activeProfiles = stringArray(value.activeProfiles);
|
|
367
402
|
if (!activeProfiles || new Set(activeProfiles).size !== activeProfiles.length
|
|
368
|
-
|| activeProfiles.some(profile => !/^[a-z0-9][a-z0-
|
|
403
|
+
|| activeProfiles.some(profile => !/^[a-z0-9][a-z0-9_.-]*$/.test(profile)))
|
|
369
404
|
return undefined;
|
|
370
405
|
const siblings = [];
|
|
371
406
|
const siblingSlugs = new Set();
|
|
@@ -1,10 +1,5 @@
|
|
|
1
|
-
import { type CustomHarness as CustomHarnessValue } from '../schema.js';
|
|
2
1
|
import { type SurfaceFeature, type SurfaceTarget } from './contract.js';
|
|
3
|
-
import { type
|
|
4
|
-
import type { SurfaceMergeResult } from './merge.js';
|
|
5
|
-
import type { McpCollectionOutcome } from './mcp.js';
|
|
6
|
-
import type { ProjectionPlanResult } from './projection.js';
|
|
7
|
-
import type { RestrictionExpansionResult } from './restrictions.js';
|
|
2
|
+
import { type SurfaceGenerationWarning } from './generator-adapter.js';
|
|
8
3
|
import { type SurfaceDesiredEntry, type SurfaceDiagnostic } from './ownership.js';
|
|
9
4
|
export type SurfaceInputEvidence = Readonly<{
|
|
10
5
|
kind: 'guide';
|
|
@@ -29,7 +24,7 @@ export type SurfaceInputEvidence = Readonly<{
|
|
|
29
24
|
sourceSha256: string;
|
|
30
25
|
resultSha256: string;
|
|
31
26
|
}>;
|
|
32
|
-
|
|
27
|
+
interface NativeSurfaceFragment {
|
|
33
28
|
readonly target: SurfaceTarget;
|
|
34
29
|
readonly path: string;
|
|
35
30
|
readonly baseSha256: string;
|
|
@@ -52,7 +47,7 @@ export interface CustomSurfaceReceipt {
|
|
|
52
47
|
scope: 'always' | 'scoped';
|
|
53
48
|
}>[];
|
|
54
49
|
}
|
|
55
|
-
|
|
50
|
+
type SurfaceRenderResult = Readonly<{
|
|
56
51
|
kind: 'complete';
|
|
57
52
|
entries: readonly SurfaceDesiredEntry[];
|
|
58
53
|
inputs: readonly SurfaceInputEvidence[];
|
|
@@ -65,43 +60,8 @@ export interface NativeFragmentSet {
|
|
|
65
60
|
readonly modelSha256: string;
|
|
66
61
|
readonly fragments: readonly NativeSurfaceFragment[];
|
|
67
62
|
}
|
|
68
|
-
/** A reference file pulled on demand by one shipped skill; rendered beside that skill's entry point on every built-in harness. */
|
|
69
|
-
export interface SkillReference {
|
|
70
|
-
readonly skill: string;
|
|
71
|
-
readonly path: string;
|
|
72
|
-
readonly bytes: string;
|
|
73
|
-
}
|
|
74
|
-
export interface SurfaceRenderInput {
|
|
75
|
-
readonly configuredHarnesses: readonly string[];
|
|
76
|
-
readonly merge: Extract<SurfaceMergeResult, {
|
|
77
|
-
kind: 'complete';
|
|
78
|
-
}>;
|
|
79
|
-
readonly generated: Extract<SurfaceGenerationOutcome, {
|
|
80
|
-
kind: 'complete';
|
|
81
|
-
}>;
|
|
82
|
-
readonly projection: Extract<ProjectionPlanResult, {
|
|
83
|
-
kind: 'complete';
|
|
84
|
-
}>;
|
|
85
|
-
readonly mcp: Extract<McpCollectionOutcome, {
|
|
86
|
-
kind: 'complete';
|
|
87
|
-
}>;
|
|
88
|
-
readonly restrictions: Extract<RestrictionExpansionResult, {
|
|
89
|
-
kind: 'complete';
|
|
90
|
-
}>;
|
|
91
|
-
readonly nativeMcp: NativeFragmentSet;
|
|
92
|
-
readonly nativeGrants: NativeFragmentSet;
|
|
93
|
-
readonly browser: unknown | null;
|
|
94
|
-
readonly browserReference: string | null;
|
|
95
|
-
readonly skillReferences: readonly SkillReference[];
|
|
96
|
-
readonly testing: unknown | null;
|
|
97
|
-
readonly customHarnesses: readonly CustomHarnessValue[];
|
|
98
|
-
readonly customReceipts: readonly CustomSurfaceReceipt[];
|
|
99
|
-
readonly sourceEvidence: readonly Extract<SurfaceInputEvidence, {
|
|
100
|
-
kind: 'guide' | 'mcp';
|
|
101
|
-
}>[];
|
|
102
|
-
}
|
|
103
63
|
export declare function surfaceModelSha256(value: unknown): string | undefined;
|
|
104
|
-
|
|
64
|
+
type HookSettingsComposition = Readonly<{
|
|
105
65
|
kind: 'complete';
|
|
106
66
|
value: Record<string, unknown>;
|
|
107
67
|
}> | Readonly<{
|
|
@@ -116,3 +76,4 @@ export type HookSettingsComposition = Readonly<{
|
|
|
116
76
|
*/
|
|
117
77
|
export declare function composeHookSettings(values: readonly unknown[]): HookSettingsComposition;
|
|
118
78
|
export declare function renderSurfaceSet(input: unknown): SurfaceRenderResult;
|
|
79
|
+
export {};
|
package/dist/surfaces/render.js
CHANGED
|
@@ -364,8 +364,10 @@ export function renderSurfaceSet(input) {
|
|
|
364
364
|
if (harness.servers.length > 0)
|
|
365
365
|
mcpTargets.add(harness.target);
|
|
366
366
|
}
|
|
367
|
-
|
|
367
|
+
// A remote server may fit none of the listed tools, and says so; a local one fits every tool.
|
|
368
|
+
if (captured.mcp.result.servers.some(server => object(server) && server.transport !== 'http') && mcpTargets.size === 0) {
|
|
368
369
|
return refuse('SURFACE_INPUT_INVALID', 'MCP model has no matching native transport');
|
|
370
|
+
}
|
|
369
371
|
const mcpModelDigest = surfaceModelSha256(captured.mcp.result);
|
|
370
372
|
if (!mcpModelDigest)
|
|
371
373
|
return refuse('SURFACE_BOUNDS', 'MCP model cannot be captured for native binding');
|
|
@@ -475,8 +477,9 @@ export function renderSurfaceSet(input) {
|
|
|
475
477
|
for (const wrapper of captured.mcp.result.wrappers) {
|
|
476
478
|
if (!validFile(wrapper))
|
|
477
479
|
return refuse('SURFACE_INPUT_INVALID', 'MCP wrapper output is malformed');
|
|
480
|
+
// A local server's wrapper starts it; a remote server's wrapper runs its headers helper.
|
|
478
481
|
const server = captured.mcp.result.servers.find(row => object(row) && typeof row.name === 'string'
|
|
479
|
-
&& wrapper.path === `.cockpit/mcp/${sha256Hex(row.name)}.sh`);
|
|
482
|
+
&& wrapper.path === `.cockpit/mcp/${sha256Hex(row.name)}${row.transport === 'http' ? '.headers' : ''}.sh`);
|
|
480
483
|
const harnesses = object(server) ? captured.mcp.result.harnesses.filter(row => object(row)
|
|
481
484
|
&& Array.isArray(row.servers) && row.servers.some(transport => object(transport) && transport.name === server.name))
|
|
482
485
|
.map(row => row.target) : [];
|
package/dist/team-metadata.js
CHANGED
|
@@ -253,7 +253,8 @@ async function ignoreBlock(root, config) {
|
|
|
253
253
|
const siblingsRoot = config.cockpit.siblings_root;
|
|
254
254
|
if (/[\0\r\n]/.test(siblingsRoot))
|
|
255
255
|
return undefined;
|
|
256
|
-
|
|
256
|
+
// Claude Code's machine-local settings, where `cockpit use` grants a served checkout.
|
|
257
|
+
const patterns = ['/node_modules/', '/.cockpit/local/', '/.claude/settings.local.json', '/env/.secrets/*', '!/env/.secrets/.gitignore'];
|
|
257
258
|
const inside = (path, kind = 'directory') => {
|
|
258
259
|
const local = relative(root, path);
|
|
259
260
|
if (local === '' || local === '..' || local.startsWith(`..${sep}`) || isAbsolute(local))
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface DiffFile {
|
|
2
|
+
old: string;
|
|
3
|
+
next: string;
|
|
4
|
+
hunk: boolean;
|
|
5
|
+
removed: boolean;
|
|
6
|
+
removedLines: string[];
|
|
7
|
+
added: string[];
|
|
8
|
+
renamed: boolean;
|
|
9
|
+
deleted: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface WeakeningFinding {
|
|
12
|
+
readonly path: string;
|
|
13
|
+
readonly kind: WeakeningKind;
|
|
14
|
+
readonly reason: string;
|
|
15
|
+
}
|
|
16
|
+
type WeakeningKind = 'test-file-removed' | 'test-skipped' | 'check-suppressed' | 'tests-removed' | 'assertions-removed' | 'assertions-loosened' | 'assertion-vacuous' | 'test-selection-narrowed';
|
|
17
|
+
/** Reads `git diff` output (taken with --unified=0 and core.quotePath=true) into one record per file. */
|
|
18
|
+
export declare function parseDiff(diff: string): DiffFile[];
|
|
19
|
+
/**
|
|
20
|
+
* Why added lines turn off or short-circuit what runs. Only explicit added-line forms are measured;
|
|
21
|
+
* this is not JavaScript dataflow analysis.
|
|
22
|
+
*/
|
|
23
|
+
export declare function additiveBypassReasons(added: readonly string[]): string[];
|
|
24
|
+
export interface TestRoles {
|
|
25
|
+
readonly isTest: (path: string) => boolean;
|
|
26
|
+
/** A runner's configuration, or a manifest that carries a test script. */
|
|
27
|
+
readonly isTestConfig: (path: string) => boolean;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The findings a diff earns over the files `roles` calls tests. A diff cannot prove that a
|
|
31
|
+
* replacement keeps its force, so a removal is flagged and a person acknowledges the legitimate ones.
|
|
32
|
+
*/
|
|
33
|
+
export declare function classifyTestWeakening(files: readonly DiffFile[], roles: TestRoles): WeakeningFinding[];
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// Detects a change that makes tests pass by weakening them. It has no imports on purpose: CI runs
|
|
2
|
+
// the base revision's copy of scripts/check-test-protection.mjs beside this file, so the detector
|
|
3
|
+
// that judges a change is never the one the change edited, and the same code judges a consumer's
|
|
4
|
+
// checkout when `work grade` runs.
|
|
5
|
+
function gitPath(value) {
|
|
6
|
+
if (!value.startsWith('"'))
|
|
7
|
+
return value;
|
|
8
|
+
// Git quotes paths as C strings, including octal UTF-8 bytes.
|
|
9
|
+
const bytes = [];
|
|
10
|
+
for (let i = 1; i < value.length - 1; i++) {
|
|
11
|
+
if (value[i] !== '\\') {
|
|
12
|
+
bytes.push(...Buffer.from(value[i]));
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
const octal = /^[0-7]{1,3}/.exec(value.slice(i + 1))?.[0];
|
|
16
|
+
if (octal) {
|
|
17
|
+
bytes.push(Number.parseInt(octal, 8));
|
|
18
|
+
i += octal.length;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const escaped = value[++i];
|
|
22
|
+
const controls = { t: '\t', n: '\n', r: '\r', b: '\b', f: '\f', v: '\v', '\\': '\\', '"': '"' };
|
|
23
|
+
if (!(escaped in controls))
|
|
24
|
+
throw new Error('unsupported Git path escape');
|
|
25
|
+
bytes.push(...Buffer.from(controls[escaped]));
|
|
26
|
+
}
|
|
27
|
+
return Buffer.from(bytes).toString('utf8');
|
|
28
|
+
}
|
|
29
|
+
/** Reads `git diff` output (taken with --unified=0 and core.quotePath=true) into one record per file. */
|
|
30
|
+
export function parseDiff(diff) {
|
|
31
|
+
const files = [];
|
|
32
|
+
let file;
|
|
33
|
+
for (const line of diff.split('\n')) {
|
|
34
|
+
if (line.startsWith('diff --git ')) {
|
|
35
|
+
const names = /^diff --git ("(?:\\.|[^"])*"|a\/\S+) ("(?:\\.|[^"])*"|b\/\S+)$/.exec(line);
|
|
36
|
+
file = { old: names ? gitPath(names[1]).slice(2) : '', next: names ? gitPath(names[2]).slice(2) : '', hunk: false, removed: false, removedLines: [], renamed: false, deleted: false, added: [] };
|
|
37
|
+
files.push(file);
|
|
38
|
+
}
|
|
39
|
+
else if (file) {
|
|
40
|
+
if (line.startsWith('@@ '))
|
|
41
|
+
file.hunk = true;
|
|
42
|
+
else if (!file.hunk && line.startsWith('--- '))
|
|
43
|
+
file.old = gitPath(line.slice(4)).replace(/^a\//, '');
|
|
44
|
+
else if (!file.hunk && line.startsWith('+++ '))
|
|
45
|
+
file.next = gitPath(line.slice(4)).replace(/^b\//, '');
|
|
46
|
+
else if (line.startsWith('rename from ')) {
|
|
47
|
+
file.old = gitPath(line.slice(12));
|
|
48
|
+
file.renamed = true;
|
|
49
|
+
}
|
|
50
|
+
else if (line.startsWith('rename to '))
|
|
51
|
+
file.next = gitPath(line.slice(10));
|
|
52
|
+
else if (line.startsWith('deleted file mode '))
|
|
53
|
+
file.deleted = true;
|
|
54
|
+
else if (line.startsWith('-') && line.slice(1).trim()) {
|
|
55
|
+
file.removed = true;
|
|
56
|
+
file.removedLines.push(line.slice(1));
|
|
57
|
+
}
|
|
58
|
+
else if (line.startsWith('+'))
|
|
59
|
+
file.added.push(line.slice(1));
|
|
60
|
+
else if (line.startsWith('Binary files '))
|
|
61
|
+
file.removed = true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return files;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Why added lines turn off or short-circuit what runs. Only explicit added-line forms are measured;
|
|
68
|
+
* this is not JavaScript dataflow analysis.
|
|
69
|
+
*/
|
|
70
|
+
export function additiveBypassReasons(added) {
|
|
71
|
+
const reasons = [];
|
|
72
|
+
if (/(?:\.\s*|\[\s*['"])(?:skip|todo|only|skipIf|runIf|fails)\b|\b(?:skip|todo|only)\s*:\s*true/.test(added.join('\n'))) {
|
|
73
|
+
reasons.push('test selection or expected-failure semantics added');
|
|
74
|
+
}
|
|
75
|
+
if (added.some(line => /^[ \t]*return[ \t]*;?[ \t]*(?:\/\/.*)?$/.test(line))) {
|
|
76
|
+
reasons.push('potential additive control-flow bypass: bare return added');
|
|
77
|
+
}
|
|
78
|
+
if (added.some(line => /^[ \t]*(?:if\s*\(\s*(?:true|1)\s*\)\s*(?:\{\s*)?return\s*;?(?:\s*\})?\s*(?:\/\/.*)?$|if\s*\(\s*(?:false|0|null|undefined)\s*\)\s*\{|(?:false\s*&&|true\s*\|\|)\s*(?:describe|test|it|suite)\s*\(|process\s*\.\s*exit\s*\(\s*(?:0\s*)?\))/.test(line))) {
|
|
79
|
+
reasons.push('potential additive control-flow bypass: explicit static bypass added');
|
|
80
|
+
}
|
|
81
|
+
return reasons;
|
|
82
|
+
}
|
|
83
|
+
// JS/TS first, then the Python forms that mean the same. A match in an added comment still counts:
|
|
84
|
+
// a directive is a comment.
|
|
85
|
+
const SKIPPED = /\b(?:it|test|describe|suite|context|bench)\s*\.\s*(?:skip|only|todo|skipIf|runIf|fails|failing|fixme)\b|\b(?:xit|xtest|xdescribe|xcontext|fit|fdescribe)\s*\(|\b(?:skip|only)\s*:\s*true|\bpytest\s*\.\s*(?:mark\s*\.\s*(?:skip|skipif|xfail)|skip)\b|\bunittest\s*\.\s*(?:skip|skipIf|skipUnless|expectedFailure)\b|^\s*@(?:skip|skipIf|skipUnless|expectedFailure)\b|\bself\s*\.\s*skipTest\s*\(/;
|
|
86
|
+
const SUPPRESSED = /@ts-ignore|@ts-nocheck|eslint-disable|\bnoqa\b|\btype:\s*ignore\b|\bpylint:\s*disable\b/;
|
|
87
|
+
const TEST_DECLARATION = /^\s*(?:(?:it|test)\s*(?:\.\s*\w+)*\s*(?:\.\s*each\s*\([^)]*\))?\s*\(|def\s+test_|async\s+def\s+test_)/;
|
|
88
|
+
const ASSERTION = /\bexpect\s*\(|\bassert\b|\bassert[A-Z]\w*\s*\(|\.should\b|\bt\.(?:is|true|deepEqual|equal|throws)\s*\(/;
|
|
89
|
+
const STRONG = /\.(?:toBe|toEqual|toStrictEqual|toMatchObject|toHaveBeenCalledWith|toHaveLength|toThrow\w*|toContain\w*|toMatch)\s*\(|\bassert(?:Equal|Equals|In|Raises|IsNone)\b|\bassert\s+\S+\s*(?:==|!=|in)\s/;
|
|
90
|
+
const WEAK = /\.(?:toBeDefined|toBeTruthy|toBeFalsy|toBeUndefined)\s*\(|\bexpect\.(?:anything|any)\s*\(|\.not\.toBeNull\s*\(|\bassert\s+\w+\s*$|\bassertTrue\s*\(\s*\w+\s*\)/;
|
|
91
|
+
const VACUOUS = /\bexpect\s*\(\s*(?:true|1|false|0|null|'[^']*'|"[^"]*")\s*\)\s*\.\s*(?:toBe|toEqual|toStrictEqual)\s*\(|\bassert\s+(?:True|1|not\s+False)\s*$|\bassert\s*\(?\s*true\s*\)?\s*;?\s*$|\bassertTrue\s*\(\s*True\s*\)/;
|
|
92
|
+
// What a test runner's own configuration says about which files it runs.
|
|
93
|
+
const NARROWING = /\b(?:exclude|testPathIgnorePatterns|testIgnore|ignore|deselect|norecursedirs|skip)\b|--ignore\b|--deselect\b|--exclude\b|--passWithNoTests|\|\|\s*true\b|\bexit\s+0\b/;
|
|
94
|
+
const WIDENING = /\b(?:include|testMatch|testRegex|testpaths|projects)\b/;
|
|
95
|
+
const TEST_SCRIPT = /^\s*"(?:pre|post)?test[^"]*"\s*:/;
|
|
96
|
+
const count = (lines, pattern) => lines.filter(line => pattern.test(line)).length;
|
|
97
|
+
/**
|
|
98
|
+
* The findings a diff earns over the files `roles` calls tests. A diff cannot prove that a
|
|
99
|
+
* replacement keeps its force, so a removal is flagged and a person acknowledges the legitimate ones.
|
|
100
|
+
*/
|
|
101
|
+
export function classifyTestWeakening(files, roles) {
|
|
102
|
+
const findings = [];
|
|
103
|
+
for (const file of files) {
|
|
104
|
+
const path = file.deleted || file.next === '/dev/null' ? file.old : file.next;
|
|
105
|
+
const finding = (kind, reason) => findings.push({ path, kind, reason });
|
|
106
|
+
const wasTest = roles.isTest(file.old);
|
|
107
|
+
if (wasTest && (file.deleted || file.next === '/dev/null' || file.renamed && !roles.isTest(file.next))) {
|
|
108
|
+
finding('test-file-removed', 'a test file was deleted or moved out of the tests');
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (roles.isTest(path)) {
|
|
112
|
+
if (count(file.added, SKIPPED))
|
|
113
|
+
finding('test-skipped', 'a test was skipped, focused, marked todo or expected to fail');
|
|
114
|
+
if (count(file.added, SUPPRESSED))
|
|
115
|
+
finding('check-suppressed', 'a type or lint check was suppressed in a test');
|
|
116
|
+
if (count(file.removedLines, TEST_DECLARATION) > count(file.added, TEST_DECLARATION))
|
|
117
|
+
finding('tests-removed', 'more tests were removed than added');
|
|
118
|
+
if (count(file.removedLines, ASSERTION) > count(file.added, ASSERTION))
|
|
119
|
+
finding('assertions-removed', 'more assertions were removed than added');
|
|
120
|
+
else if (count(file.removedLines, STRONG) > count(file.added, STRONG) && count(file.added, WEAK) > count(file.removedLines, WEAK)) {
|
|
121
|
+
finding('assertions-loosened', 'specific assertions were replaced by weaker ones');
|
|
122
|
+
}
|
|
123
|
+
if (count(file.added, VACUOUS))
|
|
124
|
+
finding('assertion-vacuous', 'an assertion that cannot fail was added');
|
|
125
|
+
}
|
|
126
|
+
else if (roles.isTestConfig(path)) {
|
|
127
|
+
const manifest = /(?:^|\/)package\.json$/.test(path);
|
|
128
|
+
const scriptsAdded = manifest ? file.added.filter(line => TEST_SCRIPT.test(line)) : file.added;
|
|
129
|
+
if (count(scriptsAdded, NARROWING))
|
|
130
|
+
finding('test-selection-narrowed', 'test configuration now excludes, ignores or tolerates the absence of tests');
|
|
131
|
+
else if (manifest ? count(file.removedLines, TEST_SCRIPT) > count(scriptsAdded, TEST_SCRIPT)
|
|
132
|
+
: count(file.removedLines, WIDENING) > count(file.added, WIDENING))
|
|
133
|
+
finding('test-selection-narrowed', 'test configuration no longer selects what it selected');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return findings;
|
|
137
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type DiffFile, type WeakeningFinding } from './test-protection.js';
|
|
2
|
+
export interface TestFilePatterns {
|
|
3
|
+
readonly tests: readonly string[];
|
|
4
|
+
readonly configuration: readonly string[];
|
|
5
|
+
/** Where the test-file patterns came from, stated beside every finding. */
|
|
6
|
+
readonly source: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function testFilePatterns(declared: readonly string[] | undefined): TestFilePatterns;
|
|
9
|
+
export declare function weakeningFindings(files: readonly DiffFile[], patterns: TestFilePatterns): WeakeningFinding[];
|
|
10
|
+
export interface RepositoryWeakening extends WeakeningFinding {
|
|
11
|
+
readonly repository: string;
|
|
12
|
+
}
|
|
13
|
+
/** The digest an acknowledgement binds: exactly these findings, and no others, are acknowledged. */
|
|
14
|
+
export declare function testWeakeningDigest(findings: readonly RepositoryWeakening[]): string;
|
|
15
|
+
export declare const describeWeakening: (row: RepositoryWeakening) => string;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Owns which files a checkout treats as tests and how a finding is identified: the patterns a
|
|
2
|
+
// consumer declared under [testing] test_files or, failing that, the common layouts, plus the digest
|
|
3
|
+
// a person's acknowledgement binds to. The detection itself is test-protection.ts.
|
|
4
|
+
import { sha256Hex } from '../sha256.js';
|
|
5
|
+
import { scopeMatcher } from '../work-orders/contract.js';
|
|
6
|
+
import { classifyTestWeakening } from './test-protection.js';
|
|
7
|
+
const DEFAULT_TEST_FILES = Object.freeze([
|
|
8
|
+
'**/*.test.*', '**/*.spec.*', '**/__tests__/**', '**/tests/**', '**/test/**', '**/e2e/**',
|
|
9
|
+
'**/test_*.py', '**/*_test.py', '**/conftest.py', '**/*_test.go', '**/*Test.java', '**/*_spec.rb',
|
|
10
|
+
]);
|
|
11
|
+
// What decides which tests run; a declared test_files list does not replace these.
|
|
12
|
+
const TEST_CONFIGURATION = Object.freeze([
|
|
13
|
+
'**/vitest.config.*', '**/vitest.workspace.*', '**/jest.config.*', '**/playwright.config.*', '**/.mocharc*',
|
|
14
|
+
'**/pytest.ini', '**/tox.ini', '**/package.json',
|
|
15
|
+
]);
|
|
16
|
+
export function testFilePatterns(declared) {
|
|
17
|
+
return declared?.length
|
|
18
|
+
? { tests: declared, configuration: TEST_CONFIGURATION, source: '[testing] test_files in cockpit.toml' }
|
|
19
|
+
: { tests: DEFAULT_TEST_FILES, configuration: TEST_CONFIGURATION, source: 'the built-in test-file patterns for common layouts (no [testing] test_files declared)' };
|
|
20
|
+
}
|
|
21
|
+
export function weakeningFindings(files, patterns) {
|
|
22
|
+
const roles = { isTest: scopeMatcher(patterns.tests), isTestConfig: scopeMatcher(patterns.configuration) };
|
|
23
|
+
return classifyTestWeakening(files, roles);
|
|
24
|
+
}
|
|
25
|
+
/** The digest an acknowledgement binds: exactly these findings, and no others, are acknowledged. */
|
|
26
|
+
export function testWeakeningDigest(findings) {
|
|
27
|
+
const rows = findings.map(row => `${row.repository}\0${row.path}\0${row.kind}`).sort();
|
|
28
|
+
return sha256Hex(JSON.stringify(rows));
|
|
29
|
+
}
|
|
30
|
+
export const describeWeakening = (row) => `${row.repository === '.' ? row.path : `${row.repository}:${row.path}`}: ${row.kind} (${row.reason})`;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { WorkRecord } from './store.js';
|
|
2
|
+
import { type WorkApplicationOutcome, type WorkContext, type WorkRequest } from './application-context.js';
|
|
3
|
+
export declare const pendingCi: (record: WorkRecord) => string[];
|
|
4
|
+
export declare function signoff(request: Extract<WorkRequest, {
|
|
5
|
+
operation: 'work-signoff';
|
|
6
|
+
}>, context: WorkContext): Promise<WorkApplicationOutcome>;
|
|
7
|
+
export declare function verify(context: WorkContext): Promise<WorkApplicationOutcome>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// How an order closes: `work signoff` closes it as done, partial or abandoned, and `work verify`, the CI
|
|
2
|
+
// job, checks every closure and confirms the steps that awaited CI.
|
|
3
|
+
import { runningInCi } from '../autonomy.js';
|
|
4
|
+
import { RefusalError } from '../environment/contract.js';
|
|
5
|
+
import { complete, now, refusal, TERMINAL, unsatisfied } from './application-context.js';
|
|
6
|
+
import { decisions, recordProblem, unresolvedDecisions } from './application-decisions.js';
|
|
7
|
+
const CLOSURE = Object.freeze({ independent: 'graded', checks: 'checks-passed', self: 'self-reported' });
|
|
8
|
+
export const pendingCi = (record) => [
|
|
9
|
+
...record.acknowledgements.filter(row => row.by === 'ci').map(() => 'acknowledge'),
|
|
10
|
+
...record.accepted.by === 'ci' ? ['accept'] : [], ...record.amendments.filter(row => row.by === 'ci').map(() => 'amend'),
|
|
11
|
+
...record.notes.filter(row => row.kind === 'answer' && row.by === 'ci').map(() => 'answer'), ...record.closure?.by === 'ci' ? ['signoff'] : [],
|
|
12
|
+
];
|
|
13
|
+
export async function signoff(request, context) {
|
|
14
|
+
const { composition, operation, policy, store, current, gated } = context;
|
|
15
|
+
const record = await store.read(request.workOrderId);
|
|
16
|
+
if (!record)
|
|
17
|
+
return refusal(operation, `${request.workOrderId} is not accepted`);
|
|
18
|
+
if (TERMINAL.has(record.state))
|
|
19
|
+
return refusal(operation, `${request.workOrderId} is already closed (${record.state})`);
|
|
20
|
+
let how;
|
|
21
|
+
if (request.close === 'done') {
|
|
22
|
+
const { order } = await current(request.workOrderId);
|
|
23
|
+
if (record.state !== 'graded' || !record.grade?.passed || record.grade.digest !== record.accepted.digest) {
|
|
24
|
+
return refusal(operation, `${order.id} is ${record.state}, not graded; run npx --no cockpit work grade --config <your cockpit.toml> --work-order ${order.id}`);
|
|
25
|
+
}
|
|
26
|
+
const open = await Promise.all(order.depends_on.map(async (dependency) => (await store.read(dependency))?.state === 'done' ? undefined : dependency));
|
|
27
|
+
if (open.some(Boolean))
|
|
28
|
+
return refusal(operation, `${order.id} depends on ${open.filter(Boolean).join(', ')}, which ${open.filter(Boolean).length === 1 ? 'is' : 'are'} not done`);
|
|
29
|
+
how = CLOSURE[record.grade.mode];
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
if (!request.note)
|
|
33
|
+
return refusal(operation, `closing ${request.workOrderId} as ${request.close} needs --note saying what was and was not done`);
|
|
34
|
+
how = request.close;
|
|
35
|
+
}
|
|
36
|
+
// Every closure, partial and abandoned too, settles the order's decisions: work that stops still made them.
|
|
37
|
+
const open = await unresolvedDecisions(record, composition);
|
|
38
|
+
if (open.length)
|
|
39
|
+
return refusal(operation, `${request.workOrderId} cannot close while ${open.join('; ')}`);
|
|
40
|
+
// A grade found the agent's own amendments lowered the bar; a re-grade must not let it close the order unseen.
|
|
41
|
+
const failedReview = record.amendmentsFailedReview;
|
|
42
|
+
const decider = failedReview && policy.gates.signoff === 'agent' ? 'person' : policy.gates.signoff;
|
|
43
|
+
return gated('signoff', decider, [`Close ${request.workOrderId} as ${request.close} (${how})`, ...request.note ? [`Note: ${request.note}`] : [],
|
|
44
|
+
...failedReview ? [`A grade found the agent's amendments lower the bar: ${failedReview.note}`] : [],
|
|
45
|
+
...record.grade ? record.grade.criteria.map(row => `${row.id}: ${row.verdict} — ${row.note}`) : [],
|
|
46
|
+
...decisions(record).map(row => `Decision ${row.decision} (${row.title}): ${row.resolution.record ? `recorded in ${row.resolution.record}` : `no lasting decision — ${row.resolution.text}`}`)], 'signoff', async (by) => store.update(request.workOrderId, async (existing) => {
|
|
47
|
+
if (!existing || existing.state !== record.state || existing.accepted.digest !== record.accepted.digest || existing.notes.length !== record.notes.length) {
|
|
48
|
+
throw new RefusalError(`${request.workOrderId} changed while this prompt was open`);
|
|
49
|
+
}
|
|
50
|
+
const state = request.close;
|
|
51
|
+
return { record: { ...existing, state, closure: { at: now(), by, how, ...request.note ? { note: request.note } : {} } },
|
|
52
|
+
value: complete(operation, { id: request.workOrderId, state, closedAs: how, by }) };
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
export async function verify(context) {
|
|
56
|
+
const { composition, operation, store, observe } = context;
|
|
57
|
+
const problems = [];
|
|
58
|
+
const awaiting = [];
|
|
59
|
+
for (const record of await store.list()) {
|
|
60
|
+
const row = await observe(record.id);
|
|
61
|
+
if (!TERMINAL.has(record.state) && row.digest !== record.accepted.digest)
|
|
62
|
+
problems.push(`${record.id} changed since it was accepted; accept the change or revert it`);
|
|
63
|
+
if (record.state === 'done' && (!record.grade?.passed || record.grade.digest !== record.accepted.digest || !record.closure))
|
|
64
|
+
problems.push(`${record.id} is done without a passing grade of its accepted contract`);
|
|
65
|
+
for (const row of decisions(record)) {
|
|
66
|
+
const cited = row.resolution?.record;
|
|
67
|
+
const problem = cited && recordProblem(await composition.readDocsFile(cited), cited, record.id, false);
|
|
68
|
+
if (problem)
|
|
69
|
+
problems.push(`${record.id} decision ${row.decision}: ${problem}`);
|
|
70
|
+
}
|
|
71
|
+
awaiting.push(...pendingCi(record).map(step => `${record.id} ${step}`));
|
|
72
|
+
}
|
|
73
|
+
const ci = runningInCi(composition.env);
|
|
74
|
+
const value = { ci, problems, ...ci ? { confirmed: awaiting } : { awaitingCi: awaiting } };
|
|
75
|
+
return problems.length ? unsatisfied(operation, value, problems.join('; ')) : complete(operation, value);
|
|
76
|
+
}
|