@b00dea/cockpit 0.0.0-stage → 0.1.0
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 +375 -0
- package/LICENSE +21 -0
- package/README.md +22 -2
- package/bin/cockpit.mjs +33 -0
- package/cockpit.schema.json +2574 -0
- package/dist/autonomy.d.ts +44 -0
- package/dist/autonomy.js +156 -0
- package/dist/bounded-read.d.ts +14 -0
- package/dist/bounded-read.js +42 -0
- package/dist/browser/commands.d.ts +62 -0
- package/dist/browser/commands.js +349 -0
- package/dist/browser/package-paths.d.ts +1 -0
- package/dist/browser/package-paths.js +6 -0
- package/dist/browser/proxy.d.ts +22 -0
- package/dist/browser/proxy.js +37 -0
- package/dist/browser/runtime.d.ts +117 -0
- package/dist/browser/runtime.js +183 -0
- package/dist/browser/surfaces.d.ts +141 -0
- package/dist/browser/surfaces.js +556 -0
- package/dist/browser/ui-gate.d.ts +15 -0
- package/dist/browser/ui-gate.js +18 -0
- package/dist/cli-guard.d.ts +5 -0
- package/dist/cli-guard.js +71 -0
- package/dist/cli-main.d.ts +139 -0
- package/dist/cli-main.js +1405 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +16 -0
- package/dist/command-table.d.ts +113 -0
- package/dist/command-table.js +512 -0
- package/dist/config/load.d.ts +171 -0
- package/dist/config/load.js +345 -0
- package/dist/config/sibling-toml.d.ts +48 -0
- package/dist/config/sibling-toml.js +160 -0
- package/dist/config/surface-tools-toml.d.ts +9 -0
- package/dist/config/surface-tools-toml.js +37 -0
- package/dist/config/verified-write.d.ts +43 -0
- package/dist/config/verified-write.js +89 -0
- package/dist/diagnostics.d.ts +26 -0
- package/dist/diagnostics.js +335 -0
- package/dist/emit.d.ts +22 -0
- package/dist/emit.js +43 -0
- package/dist/entrypoint-kinds.d.ts +3 -0
- package/dist/entrypoint-kinds.js +11 -0
- package/dist/environment/application/destructive.d.ts +6 -0
- package/dist/environment/application/destructive.js +425 -0
- package/dist/environment/application/doctor.d.ts +89 -0
- package/dist/environment/application/doctor.js +235 -0
- package/dist/environment/application/guard.d.ts +43 -0
- package/dist/environment/application/guard.js +622 -0
- package/dist/environment/application/health.d.ts +134 -0
- package/dist/environment/application/health.js +379 -0
- package/dist/environment/application/lifecycle-context.d.ts +228 -0
- package/dist/environment/application/lifecycle-context.js +154 -0
- package/dist/environment/application/lifecycle-state.d.ts +25 -0
- package/dist/environment/application/lifecycle-state.js +201 -0
- package/dist/environment/application/lifecycle.d.ts +17 -0
- package/dist/environment/application/lifecycle.js +729 -0
- package/dist/environment/application/logs.d.ts +45 -0
- package/dist/environment/application/logs.js +127 -0
- package/dist/environment/application/setup.d.ts +129 -0
- package/dist/environment/application/setup.js +741 -0
- package/dist/environment/application/shared.d.ts +11 -0
- package/dist/environment/application/shared.js +61 -0
- package/dist/environment/claims.d.ts +389 -0
- package/dist/environment/claims.js +1709 -0
- package/dist/environment/codex-hook-trust.d.ts +11 -0
- package/dist/environment/codex-hook-trust.js +185 -0
- package/dist/environment/compose-environment.d.ts +1 -0
- package/dist/environment/compose-environment.js +6 -0
- package/dist/environment/compose-ownership.d.ts +21 -0
- package/dist/environment/compose-ownership.js +117 -0
- package/dist/environment/compose-topology.d.ts +117 -0
- package/dist/environment/compose-topology.js +413 -0
- package/dist/environment/contract.d.ts +489 -0
- package/dist/environment/contract.js +761 -0
- package/dist/environment/declaration.d.ts +14 -0
- package/dist/environment/declaration.js +39 -0
- package/dist/environment/destructive-composition.d.ts +29 -0
- package/dist/environment/destructive-composition.js +333 -0
- package/dist/environment/docker-daemon.d.ts +30 -0
- package/dist/environment/docker-daemon.js +78 -0
- package/dist/environment/docker-endpoint.d.ts +10 -0
- package/dist/environment/docker-endpoint.js +19 -0
- package/dist/environment/env-schema-observation.d.ts +46 -0
- package/dist/environment/env-schema-observation.js +241 -0
- package/dist/environment/evidence.d.ts +136 -0
- package/dist/environment/evidence.js +437 -0
- package/dist/environment/executable.d.ts +26 -0
- package/dist/environment/executable.js +167 -0
- package/dist/environment/generated-application/authorities.d.ts +5 -0
- package/dist/environment/generated-application/authorities.js +61 -0
- package/dist/environment/generated-application/bootstrap-skill.d.ts +16 -0
- package/dist/environment/generated-application/bootstrap-skill.js +328 -0
- package/dist/environment/generated-application/check.d.ts +13 -0
- package/dist/environment/generated-application/check.js +189 -0
- package/dist/environment/generated-application/custom-surface.d.ts +26 -0
- package/dist/environment/generated-application/custom-surface.js +289 -0
- package/dist/environment/generated-application/prepare.d.ts +29 -0
- package/dist/environment/generated-application/prepare.js +276 -0
- package/dist/environment/generated-application/publication.d.ts +33 -0
- package/dist/environment/generated-application/publication.js +547 -0
- package/dist/environment/generated-application/shared.d.ts +484 -0
- package/dist/environment/generated-application/shared.js +349 -0
- package/dist/environment/generated-application/surface-observation.d.ts +4 -0
- package/dist/environment/generated-application/surface-observation.js +69 -0
- package/dist/environment/generated-application/surface-outputs.d.ts +71 -0
- package/dist/environment/generated-application/surface-outputs.js +389 -0
- package/dist/environment/generated-application/surfaces.d.ts +3 -0
- package/dist/environment/generated-application/surfaces.js +699 -0
- package/dist/environment/generated-application/sync.d.ts +6 -0
- package/dist/environment/generated-application/sync.js +109 -0
- package/dist/environment/git-observation.d.ts +183 -0
- package/dist/environment/git-observation.js +544 -0
- package/dist/environment/health-composition.d.ts +11 -0
- package/dist/environment/health-composition.js +628 -0
- package/dist/environment/host-observation.d.ts +56 -0
- package/dist/environment/host-observation.js +52 -0
- package/dist/environment/identity.d.ts +48 -0
- package/dist/environment/identity.js +85 -0
- package/dist/environment/init-application.d.ts +308 -0
- package/dist/environment/init-application.js +697 -0
- package/dist/environment/install-policy-observation.d.ts +71 -0
- package/dist/environment/install-policy-observation.js +284 -0
- package/dist/environment/install-policy.d.ts +48 -0
- package/dist/environment/install-policy.js +96 -0
- package/dist/environment/instance.d.ts +33 -0
- package/dist/environment/instance.js +122 -0
- package/dist/environment/instruction-pointer.d.ts +19 -0
- package/dist/environment/instruction-pointer.js +144 -0
- package/dist/environment/lifecycle-composition.d.ts +29 -0
- package/dist/environment/lifecycle-composition.js +675 -0
- package/dist/environment/lifecycle-stage.d.ts +115 -0
- package/dist/environment/lifecycle-stage.js +398 -0
- package/dist/environment/lifecycle.d.ts +75 -0
- package/dist/environment/lifecycle.js +354 -0
- package/dist/environment/logs.d.ts +132 -0
- package/dist/environment/logs.js +519 -0
- package/dist/environment/observations.d.ts +46 -0
- package/dist/environment/observations.js +344 -0
- package/dist/environment/operator-application.d.ts +50 -0
- package/dist/environment/operator-application.js +155 -0
- package/dist/environment/ports.d.ts +182 -0
- package/dist/environment/ports.js +1205 -0
- package/dist/environment/posture-observation.d.ts +68 -0
- package/dist/environment/posture-observation.js +253 -0
- package/dist/environment/read-composition.d.ts +27 -0
- package/dist/environment/read-composition.js +352 -0
- package/dist/environment/readiness-composition.d.ts +38 -0
- package/dist/environment/readiness-composition.js +130 -0
- package/dist/environment/reclaim.d.ts +333 -0
- package/dist/environment/reclaim.js +1835 -0
- package/dist/environment/repository-identity.d.ts +13 -0
- package/dist/environment/repository-identity.js +108 -0
- package/dist/environment/runtime-measurement-record.d.ts +38 -0
- package/dist/environment/runtime-measurement-record.js +111 -0
- package/dist/environment/runtime-measurement.d.ts +27 -0
- package/dist/environment/runtime-measurement.js +224 -0
- package/dist/environment/runtime-probe-residue.d.ts +43 -0
- package/dist/environment/runtime-probe-residue.js +148 -0
- package/dist/environment/runtime-probe.d.ts +72 -0
- package/dist/environment/runtime-probe.js +725 -0
- package/dist/environment/runtime.d.ts +505 -0
- package/dist/environment/runtime.js +2335 -0
- package/dist/environment/seed.d.ts +16 -0
- package/dist/environment/seed.js +43 -0
- package/dist/environment/selftest-application.d.ts +86 -0
- package/dist/environment/selftest-application.js +713 -0
- package/dist/environment/sensitive.d.ts +3 -0
- package/dist/environment/sensitive.js +48 -0
- package/dist/environment/setup-observation.d.ts +62 -0
- package/dist/environment/setup-observation.js +1473 -0
- package/dist/environment/setup.d.ts +43 -0
- package/dist/environment/setup.js +176 -0
- package/dist/environment/sibling-instructions.d.ts +33 -0
- package/dist/environment/sibling-instructions.js +224 -0
- package/dist/environment/sibling-registration.d.ts +45 -0
- package/dist/environment/sibling-registration.js +538 -0
- package/dist/environment/state.d.ts +371 -0
- package/dist/environment/state.js +2013 -0
- package/dist/environment/templates.d.ts +140 -0
- package/dist/environment/templates.js +685 -0
- package/dist/environment/tool-registration.d.ts +9 -0
- package/dist/environment/tool-registration.js +96 -0
- package/dist/environment/upgrade-application.d.ts +19 -0
- package/dist/environment/upgrade-application.js +175 -0
- package/dist/environment-exec/execute.d.ts +102 -0
- package/dist/environment-exec/execute.js +1175 -0
- package/dist/environment-exec/libexec-program.d.ts +6 -0
- package/dist/environment-exec/libexec-program.js +22 -0
- package/dist/environment-exec/verified-digest.d.ts +4 -0
- package/dist/environment-exec/verified-digest.js +16 -0
- package/dist/exact-keys.d.ts +7 -0
- package/dist/exact-keys.js +15 -0
- package/dist/fix-catalogue.d.ts +32 -0
- package/dist/fix-catalogue.js +60 -0
- package/dist/freshness.d.ts +29 -0
- package/dist/freshness.js +75 -0
- package/dist/generated-environment/adopt.d.ts +16 -0
- package/dist/generated-environment/adopt.js +275 -0
- package/dist/generated-environment/command.d.ts +47 -0
- package/dist/generated-environment/command.js +75 -0
- package/dist/generated-environment/transaction.d.ts +186 -0
- package/dist/generated-environment/transaction.js +1770 -0
- package/dist/grading/evaluate.d.ts +71 -0
- package/dist/grading/evaluate.js +403 -0
- package/dist/grading/grade.d.ts +33 -0
- package/dist/grading/grade.js +57 -0
- package/dist/guards/evaluator.d.ts +113 -0
- package/dist/guards/evaluator.js +1226 -0
- package/dist/guards/harness-adapter.d.ts +17 -0
- package/dist/guards/harness-adapter.js +57 -0
- package/dist/guards/host.d.ts +8 -0
- package/dist/guards/host.js +10 -0
- package/dist/guards/invocation-parser.d.ts +66 -0
- package/dist/guards/invocation-parser.js +595 -0
- package/dist/guards/local-identity.d.ts +40 -0
- package/dist/guards/local-identity.js +123 -0
- package/dist/guards/policy-pin.d.ts +13 -0
- package/dist/guards/policy-pin.js +175 -0
- package/dist/guards/protected-locations.d.ts +62 -0
- package/dist/guards/protected-locations.js +461 -0
- package/dist/guards/read-only.d.ts +37 -0
- package/dist/guards/read-only.js +315 -0
- package/dist/init/command.d.ts +40 -0
- package/dist/init/command.js +310 -0
- package/dist/init/derive.d.ts +51 -0
- package/dist/init/derive.js +715 -0
- package/dist/init/draft.d.ts +138 -0
- package/dist/init/draft.js +1205 -0
- package/dist/init/render.d.ts +35 -0
- package/dist/init/render.js +82 -0
- package/dist/init/scaffold.d.ts +53 -0
- package/dist/init/scaffold.js +121 -0
- package/dist/invocation.d.ts +13 -0
- package/dist/invocation.js +20 -0
- package/dist/package-metadata.d.ts +9 -0
- package/dist/package-metadata.js +19 -0
- package/dist/readiness.d.ts +53 -0
- package/dist/readiness.js +88 -0
- package/dist/references/named.d.ts +24 -0
- package/dist/references/named.js +27 -0
- package/dist/refusal.d.ts +15 -0
- package/dist/refusal.js +7 -0
- package/dist/registry-derivation.d.ts +31 -0
- package/dist/registry-derivation.js +32 -0
- package/dist/registry-resolve/command.d.ts +47 -0
- package/dist/registry-resolve/command.js +226 -0
- package/dist/registry-resolve/registry.d.ts +68 -0
- package/dist/registry-resolve/registry.js +292 -0
- package/dist/registry-resolve/render-shell.d.ts +18 -0
- package/dist/registry-resolve/render-shell.js +36 -0
- package/dist/repair.d.ts +29 -0
- package/dist/repair.js +8 -0
- package/dist/schema-json.d.ts +1 -0
- package/dist/schema-json.js +177 -0
- package/dist/schema.d.ts +1322 -0
- package/dist/schema.js +934 -0
- package/dist/setup/command.d.ts +4 -0
- package/dist/setup/command.js +4 -0
- package/dist/sha256.d.ts +1 -0
- package/dist/sha256.js +4 -0
- package/dist/surfaces/adoption.d.ts +41 -0
- package/dist/surfaces/adoption.js +217 -0
- package/dist/surfaces/consumer-instructions.d.ts +20 -0
- package/dist/surfaces/consumer-instructions.js +43 -0
- package/dist/surfaces/contract.d.ts +42 -0
- package/dist/surfaces/contract.js +169 -0
- package/dist/surfaces/core-declarations.d.ts +24 -0
- package/dist/surfaces/core-declarations.js +79 -0
- package/dist/surfaces/development-markers.d.ts +1 -0
- package/dist/surfaces/development-markers.js +18 -0
- package/dist/surfaces/generator-adapter.d.ts +71 -0
- package/dist/surfaces/generator-adapter.js +809 -0
- package/dist/surfaces/harness-registry.d.ts +180 -0
- package/dist/surfaces/harness-registry.js +117 -0
- package/dist/surfaces/mcp.d.ts +95 -0
- package/dist/surfaces/mcp.js +186 -0
- package/dist/surfaces/merge.d.ts +30 -0
- package/dist/surfaces/merge.js +105 -0
- package/dist/surfaces/ownership.d.ts +190 -0
- package/dist/surfaces/ownership.js +795 -0
- package/dist/surfaces/projection.d.ts +136 -0
- package/dist/surfaces/projection.js +681 -0
- package/dist/surfaces/render.d.ts +118 -0
- package/dist/surfaces/render.js +633 -0
- package/dist/surfaces/restrictions.d.ts +130 -0
- package/dist/surfaces/restrictions.js +447 -0
- package/dist/surfaces/selftest.d.ts +162 -0
- package/dist/surfaces/selftest.js +393 -0
- package/dist/surfaces/skill-contract.d.ts +85 -0
- package/dist/surfaces/skill-contract.js +449 -0
- package/dist/surfaces/sync.d.ts +57 -0
- package/dist/surfaces/sync.js +164 -0
- package/dist/team-metadata.d.ts +41 -0
- package/dist/team-metadata.js +512 -0
- package/dist/testing/gates.d.ts +91 -0
- package/dist/testing/gates.js +182 -0
- package/dist/testing/hooks.d.ts +59 -0
- package/dist/testing/hooks.js +139 -0
- package/dist/testing/verdict.d.ts +29 -0
- package/dist/testing/verdict.js +62 -0
- package/dist/work-orders/application.d.ts +99 -0
- package/dist/work-orders/application.js +383 -0
- package/dist/work-orders/composition.d.ts +124 -0
- package/dist/work-orders/composition.js +1010 -0
- package/dist/work-orders/contract.d.ts +61 -0
- package/dist/work-orders/contract.js +122 -0
- package/dist/work-orders/store.d.ts +158 -0
- package/dist/work-orders/store.js +197 -0
- package/docs/README.md +65 -0
- package/docs/architecture.md +136 -0
- package/docs/capability-matrix.md +50 -0
- package/docs/cli-reference.md +599 -0
- package/docs/command-list.md +51 -0
- package/docs/compose-patterns.md +99 -0
- package/docs/concepts.md +171 -0
- package/docs/configuration-essentials.md +172 -0
- package/docs/configuration-reference.md +621 -0
- package/docs/environment.md +760 -0
- package/docs/extending.md +43 -0
- package/docs/faq.md +111 -0
- package/docs/getting-started.md +608 -0
- package/docs/harness-surfaces.md +433 -0
- package/docs/joining-a-team.md +76 -0
- package/docs/release-policy.md +49 -0
- package/docs/safety-and-autonomy.md +419 -0
- package/docs/troubleshooting.md +361 -0
- package/docs/tutorial.md +170 -0
- package/docs/work-and-browser.md +254 -0
- package/libexec/browser-operation.cjs +186 -0
- package/libexec/fence-marker.cjs +101 -0
- package/libexec/fence-marker.d.cts +24 -0
- package/libexec/fenced-group-runner.cjs +77 -0
- package/libexec/fenced-supervisor.cjs +137 -0
- package/libexec/generated-filesystem.cjs +731 -0
- package/libexec/install-filesystem.cjs +225 -0
- package/libexec/rulesync-generator.NOTICE.md +1112 -0
- package/libexec/rulesync-generator.cjs +743 -0
- package/libexec/surface-generator-worker.cjs +266 -0
- package/libexec/surface-output-reader.cjs +99 -0
- package/libexec/surface-scratch-cleanup.cjs +14 -0
- package/libexec/validate-harness-evidence.d.mts +183 -0
- package/libexec/validate-harness-evidence.mjs +1312 -0
- package/package.json +74 -4
- package/surfaces/README.md +33 -0
- package/surfaces/agents/evaluator.agent.md +76 -0
- package/surfaces/guards/bash-guard.sh +48 -0
- package/surfaces/guards/claude.adapter.json +25 -0
- package/surfaces/guards/codex.adapter.json +24 -0
- package/surfaces/guards/opencode.adapter.json +25 -0
- package/surfaces/references/browser.md +63 -0
- package/surfaces/references/test-data-and-personas.md +84 -0
- package/surfaces/references/testing-tiers.md +11 -0
- package/surfaces/skills/cockpit-browser.skill.md +54 -0
- package/surfaces/skills/cockpit-explain.skill.md +32 -0
- package/surfaces/skills/cockpit-fix.skill.md +37 -0
- package/surfaces/skills/cockpit-setup.skill.md +39 -0
- package/surfaces/skills/cockpit-work.skill.md +34 -0
- package/surfaces/skills/collection.json +5 -0
- package/surfaces/skills/procedures/explain.md +61 -0
- package/surfaces/skills/procedures/fix.md +81 -0
- package/surfaces/skills/procedures/setup.md +379 -0
- package/surfaces/skills/procedures/work.md +105 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type EnvironmentCommandOutcome, type FilesystemCheck, type Observation, type SetupActionKind } from './contract.js';
|
|
2
|
+
import type { Sibling } from '../schema.js';
|
|
3
|
+
import { type ResolvedExecutable } from './executable.js';
|
|
4
|
+
type ObservedPathKind = 'absent' | 'directory' | 'repository' | 'symlink';
|
|
5
|
+
export interface SetupPathEvidence {
|
|
6
|
+
kind: ObservedPathKind;
|
|
7
|
+
missingParent?: string;
|
|
8
|
+
remotes?: Readonly<Record<string, string>>;
|
|
9
|
+
filesystemChecks?: readonly FilesystemCheck[];
|
|
10
|
+
}
|
|
11
|
+
export type SetupObservation = Observation<SetupPathEvidence>;
|
|
12
|
+
export interface SetupResult {
|
|
13
|
+
sibling: string;
|
|
14
|
+
target: string;
|
|
15
|
+
observed: ObservedPathKind;
|
|
16
|
+
action: SetupActionKind;
|
|
17
|
+
changed: boolean;
|
|
18
|
+
}
|
|
19
|
+
interface PlanSetupBaseInput {
|
|
20
|
+
siblingsRoot: string;
|
|
21
|
+
sibling: Sibling;
|
|
22
|
+
env: Readonly<Record<string, string>>;
|
|
23
|
+
gitExecutable: ResolvedExecutable;
|
|
24
|
+
}
|
|
25
|
+
export type PlanSetupInput = PlanSetupBaseInput & ({
|
|
26
|
+
observation: SetupObservation;
|
|
27
|
+
observe?: never;
|
|
28
|
+
} | {
|
|
29
|
+
observation?: never;
|
|
30
|
+
observe(target: string, siblingsRoot: string, env: Readonly<Record<string, string>>, gitExecutable: ResolvedExecutable): Promise<SetupObservation>;
|
|
31
|
+
});
|
|
32
|
+
export declare function gitEnvironment(ambient: Readonly<Record<string, string | undefined>>): Readonly<Record<string, string>>;
|
|
33
|
+
export type LocalRemoteResolution = {
|
|
34
|
+
readonly kind: 'resolved';
|
|
35
|
+
readonly remote: string;
|
|
36
|
+
} | {
|
|
37
|
+
readonly kind: 'unresolvable';
|
|
38
|
+
readonly declared: string;
|
|
39
|
+
readonly errno: string;
|
|
40
|
+
};
|
|
41
|
+
export declare function resolveLocalRemote(remote: string, siblingsRoot: string): LocalRemoteResolution;
|
|
42
|
+
export declare function planSetup(input: PlanSetupInput): Promise<EnvironmentCommandOutcome<SetupResult>>;
|
|
43
|
+
export {};
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { realpathSync } from 'node:fs';
|
|
2
|
+
import { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
3
|
+
import { ENVIRONMENT_CODES, forwardedRefusal, } from './contract.js';
|
|
4
|
+
import { resolvedGitExecutable } from './executable.js';
|
|
5
|
+
function refusal(code, message, target) {
|
|
6
|
+
return forwardedRefusal([{ severity: 'error', code, message, target }]);
|
|
7
|
+
}
|
|
8
|
+
const APPROVED_GIT_ENVIRONMENT = [
|
|
9
|
+
'PATH', 'HOME', 'SSH_AUTH_SOCK', 'TMPDIR', 'LANG', 'LC_ALL',
|
|
10
|
+
];
|
|
11
|
+
const NONE_POSTURE = {
|
|
12
|
+
absent: { code: ENVIRONMENT_CODES.SETUP_NONE_ABSENT, found: 'nothing exists there yet' },
|
|
13
|
+
directory: { code: ENVIRONMENT_CODES.SETUP_NONE_DIRECTORY, found: 'it is a plain directory, not a Git checkout' },
|
|
14
|
+
repository: { code: ENVIRONMENT_CODES.SETUP_NONE_REPOSITORY, found: 'it is an existing Git checkout' },
|
|
15
|
+
};
|
|
16
|
+
export function gitEnvironment(ambient) {
|
|
17
|
+
const approved = {};
|
|
18
|
+
for (const name of APPROVED_GIT_ENVIRONMENT) {
|
|
19
|
+
const value = ambient[name];
|
|
20
|
+
if (value !== undefined)
|
|
21
|
+
approved[name] = value;
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
GIT_CONFIG_GLOBAL: '/dev/null',
|
|
25
|
+
GIT_CONFIG_NOSYSTEM: '1',
|
|
26
|
+
GIT_CONFIG_SYSTEM: '/dev/null',
|
|
27
|
+
GIT_TERMINAL_PROMPT: '0',
|
|
28
|
+
...approved,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function resolveLocalRemote(remote, siblingsRoot) {
|
|
32
|
+
const resolved = (value) => ({ kind: 'resolved', remote: value });
|
|
33
|
+
if (remote === '' || remote.startsWith('-') || isAbsolute(remote) || remote.startsWith('~'))
|
|
34
|
+
return resolved(remote);
|
|
35
|
+
if (/^[A-Za-z][A-Za-z0-9+.-]*:/.test(remote))
|
|
36
|
+
return resolved(remote);
|
|
37
|
+
const colon = remote.indexOf(':');
|
|
38
|
+
if (colon !== -1 && !remote.slice(0, colon).includes('/'))
|
|
39
|
+
return resolved(remote);
|
|
40
|
+
const path = resolve(siblingsRoot, remote);
|
|
41
|
+
try {
|
|
42
|
+
return resolved(realpathSync(path));
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
const errno = error.code;
|
|
46
|
+
if (errno === 'ENOENT')
|
|
47
|
+
return resolved(path);
|
|
48
|
+
return { kind: 'unresolvable', declared: remote, errno: errno ?? 'unknown error' };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function unresolvableRemoteRefusal(value, which, target) {
|
|
52
|
+
return {
|
|
53
|
+
kind: 'refusal',
|
|
54
|
+
status: 2,
|
|
55
|
+
diagnostics: [{
|
|
56
|
+
severity: 'error',
|
|
57
|
+
code: ENVIRONMENT_CODES.SETUP_REMOTE_UNRESOLVABLE,
|
|
58
|
+
message: `cannot resolve ${which} local remote ${JSON.stringify(value.declared)} (${value.errno})`,
|
|
59
|
+
target,
|
|
60
|
+
fix: 'make the remote path readable, or declare the remote as a URL or an absolute path to an existing repository, then run npx --no cockpit setup --config <your cockpit.toml> again',
|
|
61
|
+
}],
|
|
62
|
+
result: null,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function fallbackChecks(observation, siblingsRoot, target) {
|
|
66
|
+
if (observation.kind === 'absent') {
|
|
67
|
+
return [
|
|
68
|
+
{ path: siblingsRoot, expected: 'directory' },
|
|
69
|
+
{ path: target, expected: 'absent' },
|
|
70
|
+
];
|
|
71
|
+
}
|
|
72
|
+
if (observation.kind === 'repository')
|
|
73
|
+
return [{ path: target, expected: 'directory' }];
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
function step(id, argv, cwd, env, target, filesystemChecks, executable) {
|
|
77
|
+
return {
|
|
78
|
+
id, argv, argv0: executable.invocationPath, cwd, env, timeoutMs: 120_000, stdoutLimitBytes: 65_536, stderrLimitBytes: 65_536,
|
|
79
|
+
failureOutput: 'stderr-tail',
|
|
80
|
+
paths: [target], ports: [], labels: { operation: 'setup' },
|
|
81
|
+
expected: [{ kind: 'path', target, state: 'repository' }],
|
|
82
|
+
consumedFiles: [{ path: executable.canonicalPath, sha256: executable.sha256, maxBytes: 536_870_912, argvIndexes: [0], executable: true, executionMode: executable.executionMode }],
|
|
83
|
+
filesystemChecks, compensation: [],
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function preview(result, mutation, diagnostics = []) {
|
|
87
|
+
const plan = { steps: [mutation] };
|
|
88
|
+
return { kind: 'preview', status: 0, diagnostics, result, plan };
|
|
89
|
+
}
|
|
90
|
+
export async function planSetup(input) {
|
|
91
|
+
const siblingsRoot = resolve(input.siblingsRoot);
|
|
92
|
+
const target = resolve(siblingsRoot, input.sibling.dir);
|
|
93
|
+
const fromRoot = relative(siblingsRoot, target);
|
|
94
|
+
const gitExecutable = resolvedGitExecutable(input.gitExecutable);
|
|
95
|
+
if (gitExecutable === undefined) {
|
|
96
|
+
return refusal(ENVIRONMENT_CODES.SETUP_PATH_TYPE_INVALID, 'Git executable identity is unresolved', target);
|
|
97
|
+
}
|
|
98
|
+
const resolvedGitEnvironment = gitEnvironment(input.env);
|
|
99
|
+
if (isAbsolute(input.sibling.dir) || fromRoot === '..' || fromRoot.startsWith(`..${sep}`) || isAbsolute(fromRoot)) {
|
|
100
|
+
return refusal(ENVIRONMENT_CODES.SETUP_PATH_OUTSIDE_ROOT, 'sibling target is outside the declared siblings root', target);
|
|
101
|
+
}
|
|
102
|
+
const observed = input.observation ?? await input.observe(target, siblingsRoot, resolvedGitEnvironment, gitExecutable);
|
|
103
|
+
if (observed.state === 'failed') {
|
|
104
|
+
return refusal(observed.code, observed.reason, target);
|
|
105
|
+
}
|
|
106
|
+
if (observed.state === 'unavailable')
|
|
107
|
+
return refusal(ENVIRONMENT_CODES.SETUP_PATH_TYPE_INVALID, observed.reason, target);
|
|
108
|
+
const observation = observed.value;
|
|
109
|
+
if (observation.kind === 'symlink') {
|
|
110
|
+
return refusal(ENVIRONMENT_CODES.SETUP_PATH_SYMLINK, 'sibling target or one of its path components is a symlink', target);
|
|
111
|
+
}
|
|
112
|
+
const result = (action, changed) => ({
|
|
113
|
+
sibling: input.sibling.slug,
|
|
114
|
+
target,
|
|
115
|
+
observed: observation.kind,
|
|
116
|
+
action,
|
|
117
|
+
changed,
|
|
118
|
+
});
|
|
119
|
+
if (input.sibling.acquire === 'none') {
|
|
120
|
+
return {
|
|
121
|
+
kind: 'complete',
|
|
122
|
+
status: 0,
|
|
123
|
+
diagnostics: [{
|
|
124
|
+
severity: 'info',
|
|
125
|
+
code: NONE_POSTURE[observation.kind].code,
|
|
126
|
+
message: `acquire = "none", so setup leaves this path alone: ${NONE_POSTURE[observation.kind].found}`,
|
|
127
|
+
target,
|
|
128
|
+
}],
|
|
129
|
+
result: result('report', false),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
const declared = resolveLocalRemote(input.sibling.remote, siblingsRoot);
|
|
133
|
+
if (declared.kind === 'unresolvable')
|
|
134
|
+
return unresolvableRemoteRefusal(declared, 'declared', target);
|
|
135
|
+
const remote = declared.remote;
|
|
136
|
+
if (observation.kind === 'absent') {
|
|
137
|
+
if (input.sibling.acquire === 'manual') {
|
|
138
|
+
return {
|
|
139
|
+
kind: 'complete',
|
|
140
|
+
status: 0,
|
|
141
|
+
diagnostics: [{
|
|
142
|
+
severity: 'warning',
|
|
143
|
+
code: ENVIRONMENT_CODES.SETUP_MANUAL_MISSING,
|
|
144
|
+
message: 'manual checkout is absent',
|
|
145
|
+
target,
|
|
146
|
+
fix: 'create the checkout, then run npx --no cockpit setup --config <your cockpit.toml> again',
|
|
147
|
+
}],
|
|
148
|
+
result: result('report', false),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
if (observation.missingParent) {
|
|
152
|
+
return refusal(ENVIRONMENT_CODES.SETUP_PARENT_MISSING, 'automatic checkout parent directory is absent', observation.missingParent);
|
|
153
|
+
}
|
|
154
|
+
const branch = input.sibling.branch ? ['--branch', input.sibling.branch] : [];
|
|
155
|
+
const checks = observation.filesystemChecks ?? fallbackChecks(observation, siblingsRoot, target);
|
|
156
|
+
return preview(result('clone', true), step(`setup:${input.sibling.slug}:clone`, [gitExecutable.canonicalPath, 'clone', ...branch, '--', remote, target], siblingsRoot, resolvedGitEnvironment, target, checks, gitExecutable));
|
|
157
|
+
}
|
|
158
|
+
if (observation.kind !== 'repository') {
|
|
159
|
+
return refusal(ENVIRONMENT_CODES.SETUP_PATH_TYPE_INVALID, 'repository acquisition target exists but is not a repository', target);
|
|
160
|
+
}
|
|
161
|
+
const observedOrigin = observation.remotes?.origin;
|
|
162
|
+
const observedResolution = observedOrigin === undefined ? undefined : resolveLocalRemote(observedOrigin, siblingsRoot);
|
|
163
|
+
if (observedResolution?.kind === 'unresolvable')
|
|
164
|
+
return unresolvableRemoteRefusal(observedResolution, 'existing origin', target);
|
|
165
|
+
const current = observedResolution?.remote;
|
|
166
|
+
if (current === remote)
|
|
167
|
+
return { kind: 'complete', status: 0, diagnostics: [], result: result('report', false) };
|
|
168
|
+
if (input.sibling.acquire === 'auto') {
|
|
169
|
+
return refusal(ENVIRONMENT_CODES.SETUP_REMOTE_MISMATCH, current === undefined
|
|
170
|
+
? 'existing automatic checkout has no origin remote'
|
|
171
|
+
: 'existing automatic checkout has a different origin remote', target);
|
|
172
|
+
}
|
|
173
|
+
const verb = current === undefined ? 'add' : 'set-url';
|
|
174
|
+
const checks = observation.filesystemChecks ?? fallbackChecks(observation, siblingsRoot, target);
|
|
175
|
+
return preview(result('wire-remote', true), step(`setup:${input.sibling.slug}:remote-${verb}`, [gitExecutable.canonicalPath, '-C', target, 'remote', verb, '--', 'origin', remote], siblingsRoot, resolvedGitEnvironment, target, checks, gitExecutable));
|
|
176
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type EnvironmentDiagnostic } from './contract.js';
|
|
2
|
+
/**
|
|
3
|
+
* One warning for each way a repository's root instruction files leave a listed agent tool without
|
|
4
|
+
* them. A team that uses only Claude Code loses nothing when a repository keeps CLAUDE.md alone.
|
|
5
|
+
*/
|
|
6
|
+
export declare function inspectSiblingInstructions(repository: Readonly<{
|
|
7
|
+
slug: string;
|
|
8
|
+
root: string;
|
|
9
|
+
}>, tools: readonly string[]): Promise<readonly EnvironmentDiagnostic[]>;
|
|
10
|
+
export type SiblingInstructionFilesChange = 'claude-import' | 'move-to-agents';
|
|
11
|
+
export type SiblingInstructionFilesResult = Readonly<{
|
|
12
|
+
kind: 'none';
|
|
13
|
+
root: string;
|
|
14
|
+
}> | Readonly<{
|
|
15
|
+
kind: 'offered' | 'written';
|
|
16
|
+
root: string;
|
|
17
|
+
change: SiblingInstructionFilesChange;
|
|
18
|
+
}> | Readonly<{
|
|
19
|
+
kind: 'refusal';
|
|
20
|
+
root: string;
|
|
21
|
+
reason: string;
|
|
22
|
+
}>;
|
|
23
|
+
/**
|
|
24
|
+
* Makes AGENTS.md the file every tool loads and CLAUDE.md the import Claude Code needs, in the two
|
|
25
|
+
* cases that need no judgement: AGENTS.md alone gains a CLAUDE.md reading `@AGENTS.md`, and CLAUDE.md
|
|
26
|
+
* alone, when it imports nothing of its own, moves byte for byte to AGENTS.md and becomes that import.
|
|
27
|
+
* Without `write` it only reports the change. It writes only ordinary files directly in a canonical
|
|
28
|
+
* root, never through a link, and refuses when a file changed after it was read.
|
|
29
|
+
*/
|
|
30
|
+
export declare function synchronizeSiblingInstructionFiles(input: Readonly<{
|
|
31
|
+
siblingRoot: string;
|
|
32
|
+
write: boolean;
|
|
33
|
+
}>): Promise<SiblingInstructionFilesResult>;
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
// Which root instruction files each agent tool loads in a repository, measured 2026-11-03 by asking
|
|
2
|
+
// each tool for words only a file it had loaded could tell it, beside a decoy file it must not load:
|
|
3
|
+
// Codex 0.166.0 and OpenCode 1.19.5 loaded AGENTS.md; Claude Code 2.1.317 loaded only CLAUDE.md (its
|
|
4
|
+
// built-in AGENTS.md reader is an option user settings can turn on and project settings cannot). With
|
|
5
|
+
// no AGENTS.md, OpenCode fell back to CLAUDE.md and Codex loaded nothing. None of the three loaded
|
|
6
|
+
// .cursorrules, .cursor/rules, GEMINI.md, .github/copilot-instructions.md or .windsurfrules. So a
|
|
7
|
+
// repository's instructions reach every tool alike only when AGENTS.md holds them and CLAUDE.md
|
|
8
|
+
// imports it; anything else leaves some tool without them.
|
|
9
|
+
import { randomBytes } from 'node:crypto';
|
|
10
|
+
import { sha256Hex } from '../sha256.js';
|
|
11
|
+
import { constants } from 'node:fs';
|
|
12
|
+
import { lstat, open, realpath, rename, unlink } from 'node:fs/promises';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { readBoundedInput } from '../config/load.js';
|
|
15
|
+
import { ENVIRONMENT_CODES, errorCause, isMissing, RefusalError } from './contract.js';
|
|
16
|
+
const CLAUDE_IMPORT = '@AGENTS.md\n';
|
|
17
|
+
const TOOL_ONLY_FILES = ['.cursorrules', '.cursor/rules', 'GEMINI.md', '.github/copilot-instructions.md', '.windsurfrules'];
|
|
18
|
+
async function readInstructionFile(path) {
|
|
19
|
+
try {
|
|
20
|
+
await lstat(path);
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (isMissing(error))
|
|
24
|
+
return { kind: 'absent' };
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
// A link is read where it resolves, so CLAUDE.md linked to AGENTS.md counts as one file.
|
|
28
|
+
const real = await realpath(path);
|
|
29
|
+
const read = await readBoundedInput(real);
|
|
30
|
+
if (read.kind !== 'complete')
|
|
31
|
+
throw new RefusalError(`${path} is larger than cockpit reads`);
|
|
32
|
+
return { kind: 'present', real, bytes: read.bytes, text: new TextDecoder('utf-8', { fatal: true }).decode(read.bytes) };
|
|
33
|
+
}
|
|
34
|
+
async function classify(root) {
|
|
35
|
+
const agents = await readInstructionFile(join(root, 'AGENTS.md'));
|
|
36
|
+
const claude = await readInstructionFile(join(root, 'CLAUDE.md'));
|
|
37
|
+
// Claude Code resolves `@path` at the start of a line or after whitespace; an email address has neither.
|
|
38
|
+
const claudeImports = claude.kind === 'present' && /(^|\s)@\S/.test(claude.text);
|
|
39
|
+
if (agents.kind === 'absent')
|
|
40
|
+
return { state: claude.kind === 'absent' ? 'neither' : 'claude-only', claudeImports, agents, claude };
|
|
41
|
+
if (claude.kind === 'absent')
|
|
42
|
+
return { state: 'agents-only', claudeImports, agents, claude };
|
|
43
|
+
const linked = claude.real === agents.real || /^[ \t]*@(\.\/)?AGENTS\.md[ \t]*$/m.test(claude.text);
|
|
44
|
+
return { state: linked ? 'linked' : 'unlinked', claudeImports, agents, claude };
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* One warning for each way a repository's root instruction files leave a listed agent tool without
|
|
48
|
+
* them. A team that uses only Claude Code loses nothing when a repository keeps CLAUDE.md alone.
|
|
49
|
+
*/
|
|
50
|
+
export async function inspectSiblingInstructions(repository, tools) {
|
|
51
|
+
const claude = tools.includes('claudecode');
|
|
52
|
+
// OpenCode falls back to CLAUDE.md when AGENTS.md is absent; Codex does not.
|
|
53
|
+
const agentsReaders = tools.includes('codexcli') || tools.includes('opencode');
|
|
54
|
+
const target = `sibling:${repository.slug}`;
|
|
55
|
+
const findings = [];
|
|
56
|
+
let classified;
|
|
57
|
+
const toolOnly = [];
|
|
58
|
+
try {
|
|
59
|
+
classified = await classify(repository.root);
|
|
60
|
+
for (const name of TOOL_ONLY_FILES) {
|
|
61
|
+
try {
|
|
62
|
+
await lstat(join(repository.root, name));
|
|
63
|
+
toolOnly.push(name);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
if (!isMissing(error))
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
return [{
|
|
73
|
+
severity: 'warning',
|
|
74
|
+
code: ENVIRONMENT_CODES.EVIDENCE_UNAVAILABLE,
|
|
75
|
+
message: `which agent tools load the instructions of sibling ${repository.slug} could not be checked: ${errorCause(error)}`,
|
|
76
|
+
target,
|
|
77
|
+
fix: `make the instruction files at the root of ${repository.root} readable UTF-8 files, then rerun this command`,
|
|
78
|
+
}];
|
|
79
|
+
}
|
|
80
|
+
if (classified.state === 'agents-only' && claude)
|
|
81
|
+
findings.push({
|
|
82
|
+
severity: 'warning',
|
|
83
|
+
// A literal, so the diagnostic inventory captures the fix beside the code.
|
|
84
|
+
code: 'SIBLING_INSTRUCTIONS_AGENTS_ONLY',
|
|
85
|
+
message: `sibling ${repository.slug} has AGENTS.md but no CLAUDE.md, so Claude Code started there loads none of its instructions`,
|
|
86
|
+
target,
|
|
87
|
+
fix: 'run npx --no cockpit setup --config <your cockpit.toml> to see the CLAUDE.md it proposes (one line, @AGENTS.md); once a person approves it, run npx --no cockpit setup --config <your cockpit.toml> --write-instruction-files',
|
|
88
|
+
});
|
|
89
|
+
if (classified.state === 'claude-only' && tools.includes('codexcli'))
|
|
90
|
+
findings.push({
|
|
91
|
+
severity: 'warning',
|
|
92
|
+
// A literal, so the diagnostic inventory captures the fix beside the code.
|
|
93
|
+
code: 'SIBLING_INSTRUCTIONS_CLAUDE_ONLY',
|
|
94
|
+
message: `sibling ${repository.slug} has CLAUDE.md but no AGENTS.md, so Codex started there loads none of its instructions`
|
|
95
|
+
+ (classified.claudeImports ? '; CLAUDE.md imports other files with @, which only Claude Code follows, so setup proposes no move' : ''),
|
|
96
|
+
target,
|
|
97
|
+
fix: 'run npx --no cockpit setup --config <your cockpit.toml> to see the move it proposes (CLAUDE.md copied byte for byte to AGENTS.md, CLAUDE.md replaced by @AGENTS.md) and, once a person approves it, rerun it with --write-instruction-files; when CLAUDE.md imports other files with @, setup proposes nothing and a person moves the shared instructions into AGENTS.md by hand',
|
|
98
|
+
});
|
|
99
|
+
if (classified.state === 'unlinked' && claude && agentsReaders)
|
|
100
|
+
findings.push({
|
|
101
|
+
severity: 'warning',
|
|
102
|
+
// A literal, so the diagnostic inventory captures the fix beside the code.
|
|
103
|
+
code: 'SIBLING_INSTRUCTIONS_UNLINKED',
|
|
104
|
+
message: `sibling ${repository.slug} has both AGENTS.md and CLAUDE.md, but CLAUDE.md does not import AGENTS.md, so Claude Code loads only CLAUDE.md and Codex only AGENTS.md`,
|
|
105
|
+
target,
|
|
106
|
+
fix: 'a person merges the instructions every tool needs into AGENTS.md, then makes CLAUDE.md a line reading @AGENTS.md followed only by instructions that apply to Claude Code alone',
|
|
107
|
+
});
|
|
108
|
+
if (classified.state === 'neither')
|
|
109
|
+
findings.push({
|
|
110
|
+
severity: 'warning',
|
|
111
|
+
// A literal, so the diagnostic inventory captures the fix beside the code.
|
|
112
|
+
code: 'SIBLING_INSTRUCTIONS_MISSING',
|
|
113
|
+
message: `sibling ${repository.slug} has neither AGENTS.md nor CLAUDE.md, so no agent tool started there loads instructions for it`,
|
|
114
|
+
target,
|
|
115
|
+
fix: 'a person writes AGENTS.md with what an agent must know to work in the repository, then runs npx --no cockpit setup --config <your cockpit.toml> --write-instruction-files to add the CLAUDE.md that imports it',
|
|
116
|
+
});
|
|
117
|
+
if (toolOnly.length > 0)
|
|
118
|
+
findings.push({
|
|
119
|
+
severity: 'warning',
|
|
120
|
+
// A literal, so the diagnostic inventory captures the fix beside the code.
|
|
121
|
+
code: 'SIBLING_INSTRUCTIONS_TOOL_ONLY',
|
|
122
|
+
message: `sibling ${repository.slug} keeps instructions in ${toolOnly.join(', ')}, which Claude Code, Codex and OpenCode do not load`,
|
|
123
|
+
target,
|
|
124
|
+
fix: 'a person moves the instructions every tool needs from those files into AGENTS.md, and keeps only settings specific to one tool where they are',
|
|
125
|
+
});
|
|
126
|
+
return findings;
|
|
127
|
+
}
|
|
128
|
+
async function createExclusive(path, bytes, mode) {
|
|
129
|
+
const handle = await open(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, mode);
|
|
130
|
+
try {
|
|
131
|
+
await handle.writeFile(bytes);
|
|
132
|
+
await handle.sync();
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
await handle.close();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async function syncDirectory(path) {
|
|
139
|
+
const directory = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
140
|
+
try {
|
|
141
|
+
await directory.sync();
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
await directory.close();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
async function holds(path, expected) {
|
|
148
|
+
const read = await readBoundedInput(path);
|
|
149
|
+
return read.kind === 'complete' && Buffer.from(read.bytes).equals(Buffer.from(expected));
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Makes AGENTS.md the file every tool loads and CLAUDE.md the import Claude Code needs, in the two
|
|
153
|
+
* cases that need no judgement: AGENTS.md alone gains a CLAUDE.md reading `@AGENTS.md`, and CLAUDE.md
|
|
154
|
+
* alone, when it imports nothing of its own, moves byte for byte to AGENTS.md and becomes that import.
|
|
155
|
+
* Without `write` it only reports the change. It writes only ordinary files directly in a canonical
|
|
156
|
+
* root, never through a link, and refuses when a file changed after it was read.
|
|
157
|
+
*/
|
|
158
|
+
export async function synchronizeSiblingInstructionFiles(input) {
|
|
159
|
+
const root = input.siblingRoot;
|
|
160
|
+
const agentsPath = join(root, 'AGENTS.md');
|
|
161
|
+
const claudePath = join(root, 'CLAUDE.md');
|
|
162
|
+
let created;
|
|
163
|
+
try {
|
|
164
|
+
if (await realpath(root) !== root)
|
|
165
|
+
return { kind: 'refusal', root, reason: 'the sibling root must be a canonical directory without symlink redirects' };
|
|
166
|
+
const classified = await classify(root);
|
|
167
|
+
const change = classified.state === 'agents-only' ? 'claude-import'
|
|
168
|
+
: classified.state === 'claude-only' && !classified.claudeImports ? 'move-to-agents' : undefined;
|
|
169
|
+
if (!change)
|
|
170
|
+
return { kind: 'none', root };
|
|
171
|
+
const before = await lstat(change === 'claude-import' ? agentsPath : claudePath);
|
|
172
|
+
if (!before.isFile())
|
|
173
|
+
return { kind: 'refusal', root, reason: `${change === 'claude-import' ? 'AGENTS.md' : 'CLAUDE.md'} is not an ordinary file` };
|
|
174
|
+
if (!input.write)
|
|
175
|
+
return { kind: 'offered', root, change };
|
|
176
|
+
if (change === 'claude-import') {
|
|
177
|
+
await createExclusive(claudePath, CLAUDE_IMPORT, 0o644);
|
|
178
|
+
created = claudePath;
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
const original = classified.claude.kind === 'present' ? classified.claude.bytes : new Uint8Array();
|
|
182
|
+
await createExclusive(agentsPath, original, Number(before.mode) & 0o777);
|
|
183
|
+
created = agentsPath;
|
|
184
|
+
const temporary = join(root, `.CLAUDE.md.cockpit-instructions-${randomBytes(8).toString('hex')}`);
|
|
185
|
+
try {
|
|
186
|
+
await createExclusive(temporary, CLAUDE_IMPORT, Number(before.mode) & 0o777);
|
|
187
|
+
const current = await lstat(claudePath);
|
|
188
|
+
const reread = await readBoundedInput(claudePath);
|
|
189
|
+
if (!current.isFile() || current.dev !== before.dev || current.ino !== before.ino || current.size !== before.size
|
|
190
|
+
|| current.mtimeMs !== before.mtimeMs || reread.kind !== 'complete' || sha256Hex(reread.bytes) !== sha256Hex(original)) {
|
|
191
|
+
throw new RefusalError('CLAUDE.md changed before it could be replaced');
|
|
192
|
+
}
|
|
193
|
+
await rename(temporary, claudePath);
|
|
194
|
+
}
|
|
195
|
+
finally {
|
|
196
|
+
await unlink(temporary).catch((cleanup) => { if (!isMissing(cleanup))
|
|
197
|
+
throw cleanup; });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// From here the instructions live only in what was written; nothing is undone any more.
|
|
201
|
+
created = undefined;
|
|
202
|
+
await syncDirectory(root);
|
|
203
|
+
if (!await holds(claudePath, CLAUDE_IMPORT))
|
|
204
|
+
return { kind: 'refusal', root, reason: 'CLAUDE.md does not hold the import after writing' };
|
|
205
|
+
if (change === 'move-to-agents' && !await holds(agentsPath, classified.claude.kind === 'present' ? classified.claude.bytes : '')) {
|
|
206
|
+
return { kind: 'refusal', root, reason: 'AGENTS.md does not hold the moved instructions after writing' };
|
|
207
|
+
}
|
|
208
|
+
return { kind: 'written', root, change };
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
// A file this run created is removed when the change could not complete, so a half-made move
|
|
212
|
+
// never leaves two copies of the instructions that can drift apart.
|
|
213
|
+
if (created) {
|
|
214
|
+
try {
|
|
215
|
+
await unlink(created);
|
|
216
|
+
}
|
|
217
|
+
catch (cleanup) {
|
|
218
|
+
if (!isMissing(cleanup))
|
|
219
|
+
return { kind: 'refusal', root, reason: `${errorCause(error)}; ${created} could not be removed: ${errorCause(cleanup)}` };
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return { kind: 'refusal', root, reason: errorCause(error) };
|
|
223
|
+
}
|
|
224
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type ExecuteProbeOutcome } from '../environment-exec/execute.js';
|
|
2
|
+
import type { Diagnostic } from '../diagnostics.js';
|
|
3
|
+
import { type ExecutionStep } from './contract.js';
|
|
4
|
+
export interface SiblingAddInput {
|
|
5
|
+
readonly configPath: string;
|
|
6
|
+
readonly slug: string;
|
|
7
|
+
readonly create: boolean;
|
|
8
|
+
readonly from?: string;
|
|
9
|
+
readonly remote?: string;
|
|
10
|
+
readonly dir?: string;
|
|
11
|
+
readonly purpose?: string;
|
|
12
|
+
readonly varName?: string;
|
|
13
|
+
/** How the stack uses the checkout; defaulted from the declared runtime when absent. */
|
|
14
|
+
readonly mount?: string;
|
|
15
|
+
readonly instructionPointer?: boolean;
|
|
16
|
+
readonly env: Readonly<Record<string, string | undefined>>;
|
|
17
|
+
}
|
|
18
|
+
export interface SiblingRemoveInput {
|
|
19
|
+
readonly configPath: string;
|
|
20
|
+
readonly slug: string;
|
|
21
|
+
readonly env: Readonly<Record<string, string | undefined>>;
|
|
22
|
+
}
|
|
23
|
+
export interface SiblingDependencies {
|
|
24
|
+
readonly execute: (step: ExecutionStep) => Promise<ExecuteProbeOutcome>;
|
|
25
|
+
}
|
|
26
|
+
export interface SiblingOutcome {
|
|
27
|
+
readonly status: number;
|
|
28
|
+
readonly result: unknown;
|
|
29
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
30
|
+
readonly artifact?: string | null;
|
|
31
|
+
readonly destination?: string | null;
|
|
32
|
+
}
|
|
33
|
+
export type GitRepositoryInitializationOutcome = Readonly<{
|
|
34
|
+
kind: 'complete';
|
|
35
|
+
}> | Readonly<{
|
|
36
|
+
kind: 'refusal';
|
|
37
|
+
reason: string;
|
|
38
|
+
}>;
|
|
39
|
+
/** Initialises one local repository through the authenticated process seam used by sibling creation. */
|
|
40
|
+
export declare function initializeLocalGitRepository(input: Readonly<{
|
|
41
|
+
path: string;
|
|
42
|
+
env: Readonly<Record<string, string | undefined>>;
|
|
43
|
+
}>, dependencies: SiblingDependencies): Promise<GitRepositoryInitializationOutcome>;
|
|
44
|
+
export declare function runSiblingAdd(input: SiblingAddInput, dependencies: SiblingDependencies): Promise<SiblingOutcome>;
|
|
45
|
+
export declare function runSiblingRemove(input: SiblingRemoveInput): Promise<SiblingOutcome>;
|