@clossys/launcher 0.4.0 → 0.5.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/README.md +51 -2
- package/dist/admission-fixture.d.ts.map +1 -1
- package/dist/admission-fixture.js +15 -1
- package/dist/admission-fixture.js.map +1 -1
- package/dist/admission.d.ts.map +1 -1
- package/dist/admission.js +6 -1
- package/dist/admission.js.map +1 -1
- package/dist/apply-plan-cli.d.ts +1 -1
- package/dist/apply-plan-cli.d.ts.map +1 -1
- package/dist/apply-plan-cli.js +1 -1
- package/dist/approval-sheet.d.ts.map +1 -1
- package/dist/approval-sheet.js +6 -0
- package/dist/approval-sheet.js.map +1 -1
- package/dist/change-set-contract.d.ts +22 -0
- package/dist/change-set-contract.d.ts.map +1 -1
- package/dist/change-set-contract.js +45 -2
- package/dist/change-set-contract.js.map +1 -1
- package/dist/existing-declaration-adoption.check.d.ts +2 -0
- package/dist/existing-declaration-adoption.check.d.ts.map +1 -0
- package/dist/existing-declaration-adoption.check.js +10 -0
- package/dist/existing-declaration-adoption.check.js.map +1 -0
- package/dist/generated/plan-contracts.generated.d.ts.map +1 -1
- package/dist/generated/plan-contracts.generated.js +264 -3
- package/dist/generated/plan-contracts.generated.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/ledger-contract.d.ts +5 -2
- package/dist/ledger-contract.d.ts.map +1 -1
- package/dist/ledger-contract.js +25 -2
- package/dist/ledger-contract.js.map +1 -1
- package/dist/ledger-trust.d.ts.map +1 -1
- package/dist/ledger-trust.js +8 -3
- package/dist/ledger-trust.js.map +1 -1
- package/dist/materialize.d.ts.map +1 -1
- package/dist/materialize.js +43 -2
- package/dist/materialize.js.map +1 -1
- package/dist/plan-bundle.d.ts +7 -1
- package/dist/plan-bundle.d.ts.map +1 -1
- package/dist/plan-bundle.js +64 -9
- package/dist/plan-bundle.js.map +1 -1
- package/dist/plan-command.d.ts +1 -1
- package/dist/plan-command.d.ts.map +1 -1
- package/dist/plan-command.js +33 -3
- package/dist/plan-command.js.map +1 -1
- package/dist/setup-template-scripts.d.ts +5 -3
- package/dist/setup-template-scripts.d.ts.map +1 -1
- package/dist/setup-template-scripts.js +17 -6
- package/dist/setup-template-scripts.js.map +1 -1
- package/dist/setup-templates.d.ts +4 -3
- package/dist/setup-templates.d.ts.map +1 -1
- package/dist/setup-templates.js +24 -13
- package/dist/setup-templates.js.map +1 -1
- package/package.json +1 -1
- package/src/admission-fixture.ts +14 -1
- package/src/admission.ts +4 -1
- package/src/apply-plan-cli.ts +1 -1
- package/src/approval-sheet.ts +6 -0
- package/src/change-set-contract.ts +51 -1
- package/src/existing-declaration-adoption.check.ts +12 -0
- package/src/generated/plan-contracts.generated.ts +264 -3
- package/src/index.ts +3 -1
- package/src/ledger-contract.ts +25 -2
- package/src/ledger-trust.ts +5 -0
- package/src/materialize.ts +31 -2
- package/src/plan-bundle.ts +60 -10
- package/src/plan-command.ts +25 -2
- package/src/setup-template-scripts.ts +15 -6
- package/src/setup-templates.ts +20 -12
package/src/plan-bundle.ts
CHANGED
|
@@ -51,11 +51,11 @@ import { bundleDigest, changeSetDigest } from "./change-set-digest.js";
|
|
|
51
51
|
import {
|
|
52
52
|
AUTHORIZATION_ABSENT, AUTHORIZATION_PLAN_MISMATCH, BRIEF_PATH, CANONICAL_KEYS, DISCOVERY_ROOTS, EXEMPTION_SURFACES, ID_TOKEN, LEDGER_PATH, derivedPlanItem, SKILLS_MANIFEST_PATH, TEMPLATE_PATHS, canonicalOrder, contentDigest,
|
|
53
53
|
dependencyPointer, discoveryLinkPath, discoveryLinkTarget, isSafeRelativePath, lockfilePath, matchesPathPattern, skillPath, validateApplyBundle, validateRepositoryChangeSet,
|
|
54
|
-
worstVerdict,
|
|
54
|
+
worstVerdict, existingDeclarationVersionMatches,
|
|
55
55
|
} from "./change-set-contract.js";
|
|
56
56
|
import type {
|
|
57
57
|
ApplyBundle, ApplyBundleRepository, ApplyCheck, ChangeSetDeferral, ChangeSetItem, ChangeSetPhase, ChangeSetRefusal, DependencyPlacement, DiscoveryRoot,
|
|
58
|
-
FileChange, KeyChange, LockfileName, PackageInvariant, PackageManagerKind, PinnedPackage, ReleaseAgeSurfaceKind, RepositoryChangeSet, RepositoryProfileObservation,
|
|
58
|
+
ExistingDeclarationAdoption, FileChange, KeyChange, LockfileName, PackageInvariant, PackageManagerKind, PinnedPackage, ReleaseAgeSurfaceKind, RepositoryChangeSet, RepositoryProfileObservation,
|
|
59
59
|
RepositoryVisibility, TemplateAct,
|
|
60
60
|
} from "./change-set-contract.js";
|
|
61
61
|
import type { InstalledLedger } from "./ledger-contract.js";
|
|
@@ -139,6 +139,20 @@ export interface RepositoryObservation {
|
|
|
139
139
|
readonly npmrcText?: string | null;
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
/** Derives proof fields from observed source and desired plan acts after explicit scoped consent. */
|
|
143
|
+
export function createExistingDeclarationAdoptions(observation: RepositoryObservation, plan: AdvisorPlan, names: readonly string[], consent: "adopt-existing-declaration"): readonly ExistingDeclarationAdoption[] {
|
|
144
|
+
const acts = plan.packages ?? [];
|
|
145
|
+
if (!validateAdvisorPlan(plan).valid || !plan.resolution) throw new TypeError("existing declaration consent needs a resolved desired plan");
|
|
146
|
+
if (observation.phase !== "setup" || consent !== "adopt-existing-declaration" || names.length === 0 || new Set(names).size !== names.length) throw new TypeError("existing declaration consent is not explicit setup scope");
|
|
147
|
+
return canonicalOrder(names.map(name => {
|
|
148
|
+
const act = acts.find(act => act.name === name && act.planItem === `${observation.id}:${name}`);
|
|
149
|
+
const entries = observation.manifestEntries.filter(entry => entry.name === name);
|
|
150
|
+
const resolved = observation.lockedPackages.filter(pkg => pkg.name === name);
|
|
151
|
+
if (!act || !name.startsWith("@clossys/") || entries.length !== 1 || entries[0]?.placement !== act.placement || resolved.length !== 1 || !existingDeclarationVersionMatches(entries[0]!.value,resolved[0]!.version)) throw new TypeError("existing declaration source is not unique and scoped");
|
|
152
|
+
return {file:"package.json" as const, placement:act.placement, name, beforeVersion:entries[0]!.value, beforeResolved:resolved[0]!, desired:{name:act.name,version:act.version,integrity:act.integrity,planItem:act.planItem,act:act.act,placement:act.placement},observedBaseCommit:observation.baseCommit,desiredSnapshotDigest:plan.resolution!.snapshotDigest,consent};
|
|
153
|
+
}),row=>[row.name]);
|
|
154
|
+
}
|
|
155
|
+
|
|
142
156
|
/** A staffed repository no change set is computed for, and why, as an id such as `not-in-inventory`. */
|
|
143
157
|
export interface SkippedRepositoryObservation {
|
|
144
158
|
readonly id: string;
|
|
@@ -147,8 +161,12 @@ export interface SkippedRepositoryObservation {
|
|
|
147
161
|
}
|
|
148
162
|
|
|
149
163
|
export interface PlanApplyBundleInputs {
|
|
164
|
+
/** Explicit setup consent, indexed by staffed repository; omitted by default. */
|
|
165
|
+
readonly existingDeclarationAdoptions?: Readonly<Record<string, readonly ExistingDeclarationAdoption[]>>;
|
|
150
166
|
/** The plan, clossys/advisor/plan.json. It must validate and have `staffing`. */
|
|
151
167
|
readonly plan: AdvisorPlan;
|
|
168
|
+
/** Explicit authoring agent namespace; omission preserves legacy changes and bytes. */
|
|
169
|
+
readonly agentProvenance?: "codex" | "claude" | "cursor";
|
|
152
170
|
/** The hub brief, clossys/advisor/brief.json: validated, and without `staffedHere`. Each repository gets its own projection of it. */
|
|
153
171
|
readonly hubBrief: EngagementBrief;
|
|
154
172
|
/** One entry per staffed repository; a staffed repository with no entry is skipped as `not-observed`. */
|
|
@@ -317,7 +335,7 @@ interface SetupTemplate {
|
|
|
317
335
|
* takes the manager, the repository id and the plan's one Starter pin, and
|
|
318
336
|
* nothing else reaches a template.
|
|
319
337
|
*/
|
|
320
|
-
function prepareSetup(observation: RepositoryObservation, acts: readonly PlanPackageAct[]): { readonly templates: readonly SetupTemplate[] } | { readonly skip: string } {
|
|
338
|
+
function prepareSetup(observation: RepositoryObservation, acts: readonly PlanPackageAct[], agentProvenance?: "codex" | "claude" | "cursor"): { readonly templates: readonly SetupTemplate[] } | { readonly skip: string } {
|
|
321
339
|
const packageManager = observation.packageManager;
|
|
322
340
|
if (packageManager !== "npm" && packageManager !== "pnpm") return { skip: "package-manager-unsupported" };
|
|
323
341
|
const pins = acts.filter((act) => act.act === "pin-starter");
|
|
@@ -335,7 +353,7 @@ function prepareSetup(observation: RepositoryObservation, acts: readonly PlanPac
|
|
|
335
353
|
}
|
|
336
354
|
const templates: SetupTemplate[] = [];
|
|
337
355
|
for (const { act, id } of TEMPLATE_ITEMS) {
|
|
338
|
-
const rendered = act === "write-starter-request" ? request : renderSetupTemplate(act, act === "add-caller-workflow" ? { packageManager } : undefined);
|
|
356
|
+
const rendered = act === "write-starter-request" ? request : renderSetupTemplate(act, act === "add-caller-workflow" ? { packageManager, ...(/^0\.3\./.test(pin.version) ? {existingDeclarationAdoptions:true} : {}) } : act === "add-path-scope-job" && agentProvenance !== undefined ? { agentProvenance } : undefined);
|
|
339
357
|
if (!rendered.ok) throw new TypeError(`the setup template ${act} does not render`);
|
|
340
358
|
templates.push({ id, files: rendered.files });
|
|
341
359
|
}
|
|
@@ -362,6 +380,30 @@ function computeChangeSet(
|
|
|
362
380
|
skillContent: ReadonlyMap<string, string>,
|
|
363
381
|
setupTemplates: readonly SetupTemplate[] | null,
|
|
364
382
|
): ComputedSet | SkippedSet {
|
|
383
|
+
const requested = inputs.existingDeclarationAdoptions?.[observation.id];
|
|
384
|
+
// Retained consent is historical after its first admitted apply.
|
|
385
|
+
const protectedRows = ledger?.history.at(-1)?.phase === "setup"
|
|
386
|
+
? ledger.existingDeclarationAdoptions?.map(({changeSet: _changeSet, ...row}) => row)
|
|
387
|
+
: undefined;
|
|
388
|
+
const adoptions = observation.phase === "setup" ? requested : protectedRows;
|
|
389
|
+
if (observation.phase !== "setup" && requested !== undefined) return {skip: {verdict: "violated", reason: "adoption-consent-not-setup"}};
|
|
390
|
+
if (adoptions !== undefined) {
|
|
391
|
+
const starter = acts.find(act => act.act === "pin-starter");
|
|
392
|
+
if (!starter || !/^0\.3\.[0-9]+$/.test(starter.version)) return {skip: {verdict: "indeterminate", reason: "adoption-starter-unsupported"}};
|
|
393
|
+
if (!adoptions.length || new Set(adoptions.map(row => row.name)).size !== adoptions.length) return {skip: {verdict: "violated", reason: "adoption-proof-invalid"}};
|
|
394
|
+
for (const row of adoptions) {
|
|
395
|
+
const act = acts.find(act => act.planItem === row.desired.planItem);
|
|
396
|
+
const desired = act && {planItem:act.planItem, act:act.act, name:act.name, version:act.version, integrity:act.integrity, placement:act.placement};
|
|
397
|
+
const entries = observation.manifestEntries.filter(entry => entry.name === row.name);
|
|
398
|
+
const consumed = observation.phase === "apply" && row.desired.act === "pin-starter";
|
|
399
|
+
const prior = consumed ? row.desired : row.beforeResolved;
|
|
400
|
+
if (row.desiredSnapshotDigest !== inputs.plan.resolution?.snapshotDigest || row.consent !== "adopt-existing-declaration" || row.file !== "package.json" || row.name !== row.desired.name || row.name !== row.beforeResolved.name || row.placement !== row.desired.placement || !row.name.startsWith("@clossys/") ||
|
|
401
|
+
!existingDeclarationVersionMatches(row.beforeVersion,row.beforeResolved.version) ||
|
|
402
|
+
(observation.phase === "setup" && row.observedBaseCommit !== observation.baseCommit) ||
|
|
403
|
+
(!desired || !["planItem","act","name","version","integrity","placement"].every(key => desired[key as keyof typeof desired] === row.desired[key as keyof typeof row.desired])) || entries.length !== 1 || entries[0]?.placement !== row.placement || entries[0]?.value !== (consumed ? row.desired.version : row.beforeVersion) ||
|
|
404
|
+
!observation.lockedPackages.some(pkg => pkg.name === prior.name && pkg.version === prior.version && pkg.integrity === prior.integrity)) return {skip:{verdict:"violated",reason:"adoption-proof-mismatch"}};
|
|
405
|
+
}
|
|
406
|
+
}
|
|
365
407
|
// Paths compare case-insensitively (code rule C3): a base file that differs only in case is the same file on many checkouts.
|
|
366
408
|
// Two (or more) observed files at the same lowercase path -- distinct case variants, or a repeated entry -- have no single
|
|
367
409
|
// base digest between them: the path is occupied by other bytes than any one of them, so it can never be kept, updated or
|
|
@@ -519,8 +561,9 @@ function computeChangeSet(
|
|
|
519
561
|
// when the base happens to already carry the version the plan wants, so it is never waved through as satisfied.
|
|
520
562
|
const row = keyRows.get(pointer);
|
|
521
563
|
const baseAt = entries.find((entry) => entry.placement === act.placement);
|
|
522
|
-
const
|
|
523
|
-
const
|
|
564
|
+
const adoption = adoptions?.find(proof => proof.name === act.name && proof.desired.act === act.act && !(observation.phase === "apply" && act.act === "pin-starter"));
|
|
565
|
+
const own = row === undefined ? (baseAt === undefined || adoption !== undefined ? null : "unowned-existing") : baseAt === undefined ? "deleted" : baseAt.value !== row ? "client-edited" : null;
|
|
566
|
+
const satisfiedInBase = adoption === undefined &&
|
|
524
567
|
entries.length === 1 &&
|
|
525
568
|
baseAt !== undefined &&
|
|
526
569
|
baseAt.value === act.version &&
|
|
@@ -543,11 +586,11 @@ function computeChangeSet(
|
|
|
543
586
|
refused.push({ file: "package.json", pointer, reason: own, item: act.planItem });
|
|
544
587
|
continue;
|
|
545
588
|
}
|
|
546
|
-
if (row === undefined || act.version !== row) {
|
|
589
|
+
if (adoption !== undefined || row === undefined || act.version !== row) {
|
|
547
590
|
// The request a setup set wrote names this pin; an apply set that changes it would leave the request naming another, and
|
|
548
591
|
// rewriting the request is not something an apply set does.
|
|
549
592
|
if (act.act === "pin-starter" && observation.phase === "apply") return { skip: { verdict: "indeterminate", reason: "starter-request-stale" } };
|
|
550
|
-
keys.push({ file: "package.json", pointer, before: row ?? null, after: act.version, item: act.planItem });
|
|
593
|
+
keys.push({ file: "package.json", pointer, before: adoption?.beforeVersion ?? row ?? null, after: act.version, item: act.planItem });
|
|
551
594
|
invariants.push({ item: act.planItem, ...pinned });
|
|
552
595
|
continue;
|
|
553
596
|
}
|
|
@@ -689,6 +732,8 @@ function computeChangeSet(
|
|
|
689
732
|
schemaVersion: 1,
|
|
690
733
|
kind: "clossys.repository-change-set",
|
|
691
734
|
producer: { name: inputs.producer.name, version: inputs.producer.version },
|
|
735
|
+
...(inputs.agentProvenance !== undefined ? { agentProvenance: inputs.agentProvenance } : {}),
|
|
736
|
+
...(adoptions !== undefined ? {existingDeclarationAdoptions: adoptions} : {}),
|
|
692
737
|
planDigest: planDigestValue,
|
|
693
738
|
repository: {
|
|
694
739
|
id: observation.id,
|
|
@@ -829,10 +874,15 @@ function computeChangeSet(
|
|
|
829
874
|
* fails its contract.
|
|
830
875
|
*/
|
|
831
876
|
export function planApplyBundle(inputs: PlanApplyBundleInputs): PlanApplyBundleResult {
|
|
877
|
+
if (inputs.agentProvenance !== undefined && !["codex", "claude", "cursor"].includes(inputs.agentProvenance)) throw new TypeError("unsupported agent provenance");
|
|
832
878
|
const planValidation = validateAdvisorPlan(inputs.plan);
|
|
833
879
|
if (!planValidation.valid) throw new TypeError(`the plan does not validate: ${planValidation.reason}`);
|
|
834
880
|
const staffing = inputs.plan.staffing;
|
|
835
881
|
if (staffing === undefined) throw new TypeError("the plan has no staffing, so no repository has a change set");
|
|
882
|
+
if (inputs.existingDeclarationAdoptions !== undefined) {
|
|
883
|
+
const mapping = inputs.existingDeclarationAdoptions;
|
|
884
|
+
if (mapping === null || Array.isArray(mapping) || typeof mapping !== "object" || Reflect.ownKeys(mapping).some(key => typeof key !== "string" || !staffing.some(row => row.repository === key) || !Object.hasOwn(Object.getOwnPropertyDescriptor(mapping,key) ?? {},"value") || !Array.isArray(mapping[key]))) throw new TypeError("existing declaration scope is not a closed staffed mapping");
|
|
885
|
+
}
|
|
836
886
|
const briefValidation = validateEngagementBrief(inputs.hubBrief);
|
|
837
887
|
if (!briefValidation.valid) throw new TypeError(`the hub brief does not validate: ${briefValidation.reason}`);
|
|
838
888
|
if (inputs.hubBrief.staffedHere !== undefined) throw new TypeError("the hub brief must not have staffedHere; each repository's brief is projected from it");
|
|
@@ -894,7 +944,7 @@ export function planApplyBundle(inputs: PlanApplyBundleInputs): PlanApplyBundleR
|
|
|
894
944
|
let setupTemplates: readonly SetupTemplate[] | null = null;
|
|
895
945
|
if (observation.phase === "setup") {
|
|
896
946
|
// A setup set holds the setup templates (code rule C11); anything that stops them being rendered or the pin being safe is a skip.
|
|
897
|
-
const prepared = prepareSetup(observation, acts);
|
|
947
|
+
const prepared = prepareSetup(observation, acts, inputs.agentProvenance);
|
|
898
948
|
if ("skip" in prepared) {
|
|
899
949
|
entries.push({ id: staffingEntry.repository, verdict: "indeterminate", reason: prepared.skip, checks: [] });
|
|
900
950
|
continue;
|
|
@@ -920,7 +970,7 @@ export function planApplyBundle(inputs: PlanApplyBundleInputs): PlanApplyBundleR
|
|
|
920
970
|
const { changeSet, checks } = result;
|
|
921
971
|
const digest = changeSetDigest(changeSet);
|
|
922
972
|
const short = digest.slice("sha256:".length, "sha256:".length + 12);
|
|
923
|
-
const set = { ...changeSet, branch:
|
|
973
|
+
const set = { ...changeSet, branch: `${changeSet.agentProvenance ?? "clossys"}/apply-${short}`, pullRequest: { title: `Clossys: apply plan ${short}` }, changeSetDigest: digest };
|
|
924
974
|
entries.push({ pending: computed.length });
|
|
925
975
|
computed.push({ id: staffingEntry.repository, staffingIndex, phase: observation.phase, set, checks });
|
|
926
976
|
}
|
package/src/plan-command.ts
CHANGED
|
@@ -37,7 +37,16 @@ import type { PlanApplyBundleResult, RepositoryObservation, SkippedRepositoryObs
|
|
|
37
37
|
import { validateAdvisorPlan, validateEngagementBrief } from "./plan-contract.js";
|
|
38
38
|
import type { AdvisorPlan, EngagementBrief } from "./plan-contract.js";
|
|
39
39
|
|
|
40
|
-
export const PLAN_USAGE = `Usage: launcher-apply-plan plan [--help]
|
|
40
|
+
export const PLAN_USAGE = `Usage: launcher-apply-plan plan [--agent codex|claude|cursor] [--adopt-existing <consent.json>] [--help]
|
|
41
|
+
|
|
42
|
+
--agent selects the authoring agent namespace for generated apply branches.
|
|
43
|
+
The choice is covered by the change-set and approval digests. Omitting it
|
|
44
|
+
preserves legacy clossys/apply branches and stored change sets. Use the same
|
|
45
|
+
choice when recomputing an approved bundle.
|
|
46
|
+
|
|
47
|
+
--adopt-existing reads an explicit repository-to-proof-row mapping for setup.
|
|
48
|
+
It supplies scope, not approval, and requires Starter 0.3.x. Omit it for apply;
|
|
49
|
+
the protected setup ledger supplies the frozen scope automatically.
|
|
41
50
|
|
|
42
51
|
Run in the hub. Computes the apply bundle for the plan file in the working
|
|
43
52
|
tree, reports whether that file is the one committed at HEAD, and prints the
|
|
@@ -436,7 +445,19 @@ export async function planMain(argv: readonly string[], options: PlanCommandOpti
|
|
|
436
445
|
stdout(`${PLAN_USAGE}\n`);
|
|
437
446
|
return 0;
|
|
438
447
|
}
|
|
439
|
-
|
|
448
|
+
let agentProvenance: "codex" | "claude" | "cursor" | undefined;
|
|
449
|
+
let adoptionPath: string | undefined;
|
|
450
|
+
for (let index=0; index<argv.length; index+=2) {
|
|
451
|
+
const flag=argv[index]; const value=argv[index+1];
|
|
452
|
+
if (flag === "--agent" && agentProvenance === undefined && value !== undefined && ["codex","claude","cursor"].includes(value)) agentProvenance=value as "codex"|"claude"|"cursor";
|
|
453
|
+
else if (flag === "--adopt-existing" && adoptionPath === undefined && value !== undefined) adoptionPath=value;
|
|
454
|
+
else return refuse("usage");
|
|
455
|
+
}
|
|
456
|
+
let existingDeclarationAdoptions: import("./plan-bundle.js").PlanApplyBundleInputs["existingDeclarationAdoptions"];
|
|
457
|
+
if (adoptionPath !== undefined) {
|
|
458
|
+
try { existingDeclarationAdoptions=readContractDocument(readFileSync(adoptionPath)) as NonNullable<typeof existingDeclarationAdoptions>; }
|
|
459
|
+
catch { return refuse("usage"); }
|
|
460
|
+
}
|
|
440
461
|
let hub: string;
|
|
441
462
|
try {
|
|
442
463
|
hub = realpathSync(options.cwd ?? process.cwd());
|
|
@@ -475,6 +496,8 @@ export async function planMain(argv: readonly string[], options: PlanCommandOpti
|
|
|
475
496
|
authorization,
|
|
476
497
|
computedAt,
|
|
477
498
|
heldChangeSets,
|
|
499
|
+
...(agentProvenance !== undefined ? { agentProvenance } : {}),
|
|
500
|
+
...(existingDeclarationAdoptions !== undefined ? {existingDeclarationAdoptions} : {}),
|
|
478
501
|
});
|
|
479
502
|
} catch {
|
|
480
503
|
return refuse("planner-refused");
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pure renderers for the setup template acts' script and workflow files.
|
|
3
3
|
*
|
|
4
|
-
* Each renderer
|
|
4
|
+
* Each renderer returns exact file bytes; path scope optionally takes the
|
|
5
|
+
* supported authoring agent provenance. With no input it preserves legacy
|
|
6
|
+
* bytes. Rendering is pure, so
|
|
5
7
|
* the text is a pure function of this module alone: no clock, no randomness,
|
|
6
8
|
* no filesystem, no environment. Everything that reads a clock or a
|
|
7
9
|
* repository does so inside the emitted script, when a workflow runs it, and
|
|
@@ -505,8 +507,14 @@ try {
|
|
|
505
507
|
* The standalone path-scope script (its text ends in one LF). The
|
|
506
508
|
* path-scope workflow embeds it; a test runs it with `node` directly.
|
|
507
509
|
*/
|
|
508
|
-
export function renderPathScopeScript(): string {
|
|
509
|
-
return PATH_SCOPE_SCRIPT;
|
|
510
|
+
export function renderPathScopeScript(agentProvenance: "codex" | "claude" | "cursor" | undefined = undefined): string {
|
|
511
|
+
if (agentProvenance === undefined) return PATH_SCOPE_SCRIPT;
|
|
512
|
+
if (!["codex", "claude", "cursor"].includes(agentProvenance)) throw new TypeError("unsupported agent provenance");
|
|
513
|
+
// Keep legacy bytes unchanged. Explicit provenance opts into the expanded
|
|
514
|
+
// branch recognizer; every supported namespace still runs the full checks.
|
|
515
|
+
return PATH_SCOPE_SCRIPT
|
|
516
|
+
.replace('const APPLY_PREFIX = "clossys/apply-";', 'const APPLY_PREFIX = "a supported apply branch";\nconst APPLY_BRANCH = /^(clossys|codex|claude|cursor)\\/apply-[0-9a-f]{12}$/;')
|
|
517
|
+
.replace('if (!headRef.startsWith(APPLY_PREFIX)) {', 'if (!APPLY_BRANCH.test(headRef)) {\n if (headRef.includes("/apply")) refuse("unsupported or malformed apply branch");');
|
|
510
518
|
}
|
|
511
519
|
|
|
512
520
|
const PATH_SCOPE_WORKFLOW_HEAD = [
|
|
@@ -563,9 +571,10 @@ const PATH_SCOPE_WORKFLOW_HEAD = [
|
|
|
563
571
|
const RUN_BODY_INDENT = " ";
|
|
564
572
|
|
|
565
573
|
/** The bytes of `.github/workflows/clossys-path-scope.yml`: the script embedded in a quoted heredoc. */
|
|
566
|
-
export function renderPathScopeWorkflow(): string {
|
|
567
|
-
const body =
|
|
574
|
+
export function renderPathScopeWorkflow(agentProvenance: "codex" | "claude" | "cursor" | undefined = undefined): string {
|
|
575
|
+
const body = renderPathScopeScript(agentProvenance).slice(0, -1)
|
|
568
576
|
.split("\n")
|
|
569
577
|
.map((line) => (line === "" ? "" : `${RUN_BODY_INDENT}${line}`));
|
|
570
|
-
|
|
578
|
+
const head = agentProvenance === undefined ? PATH_SCOPE_WORKFLOW_HEAD : PATH_SCOPE_WORKFLOW_HEAD.map((line) => line.replace("head branch starts with clossys/apply-.", "head branch is a legacy or supported agent apply branch."));
|
|
579
|
+
return [...head, ...body, `${RUN_BODY_INDENT}${HEREDOC_TERMINATOR}`, ""].join("\n");
|
|
571
580
|
}
|
package/src/setup-templates.ts
CHANGED
|
@@ -49,10 +49,10 @@ export type TemplateResult =
|
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
51
|
* The Starter versions a setup may pin, for documentation. The check below is
|
|
52
|
-
* the rule: an exact `0.2.PATCH`, with no range, prerelease, build metadata, or
|
|
52
|
+
* the rule: an exact `0.2.PATCH` or `0.3.PATCH`, with no range, prerelease, build metadata, or
|
|
53
53
|
* leading zero.
|
|
54
54
|
*/
|
|
55
|
-
export const STARTER_PIN_RANGE = ">=0.2.0 <0.
|
|
55
|
+
export const STARTER_PIN_RANGE = ">=0.2.0 <0.4.0";
|
|
56
56
|
|
|
57
57
|
const REQUEST_PATH = ".starter/request.json";
|
|
58
58
|
const DECISION_WORKFLOW_PATH = ".github/workflows/clossys-adoption-decision.yml";
|
|
@@ -61,7 +61,7 @@ const STARTER_NAME = "@clossys/starter";
|
|
|
61
61
|
const STARTER_BIN = "foundry-starter";
|
|
62
62
|
const SNAPSHOT_MAX_AGE_MS = 3_600_000;
|
|
63
63
|
|
|
64
|
-
const STARTER_VERSION = /^0\.
|
|
64
|
+
const STARTER_VERSION = /^0\.[23]\.(?:0|[1-9][0-9]{0,8})$/u;
|
|
65
65
|
|
|
66
66
|
/**
|
|
67
67
|
* Whether `version` is a Starter version the templates support: exactly the
|
|
@@ -305,9 +305,10 @@ const LAST_STEPS: readonly string[] = [
|
|
|
305
305
|
"",
|
|
306
306
|
];
|
|
307
307
|
|
|
308
|
-
function decisionWorkflow(manager: SetupPackageManager): string {
|
|
308
|
+
function decisionWorkflow(manager: SetupPackageManager, adoptionProofs = false): string {
|
|
309
309
|
const install = manager === "npm" ? NPM_INSTALL : PNPM_INSTALL;
|
|
310
|
-
|
|
310
|
+
const bytes = [...workflowTop(manager), ...FIRST_STEPS, ...install, ...LAST_STEPS].join("\n");
|
|
311
|
+
return adoptionProofs ? bytes.replace(" /clossys/.state/installed.json", " /clossys/.state/installed.json\n /package.json\n /" + (manager === "npm" ? "package-lock.json" : "pnpm-lock.yaml")) : bytes;
|
|
311
312
|
}
|
|
312
313
|
|
|
313
314
|
/** Render `.github/workflows/clossys-adoption-decision.yml` for npm or pnpm, or refuse at `packageManager`. */
|
|
@@ -416,7 +417,7 @@ export function renderProductCiWorkflow(): string {
|
|
|
416
417
|
// The dispatcher
|
|
417
418
|
// ---------------------------------------------------------------------------
|
|
418
419
|
|
|
419
|
-
const CALLER_KEYS: ReadonlySet<string> = new Set(["packageManager"]);
|
|
420
|
+
const CALLER_KEYS: ReadonlySet<string> = new Set(["packageManager", "existingDeclarationAdoptions"]);
|
|
420
421
|
|
|
421
422
|
function isTemplateAct(value: unknown): value is TemplateAct {
|
|
422
423
|
return typeof value === "string" && Object.prototype.hasOwnProperty.call(TEMPLATE_PATHS, value);
|
|
@@ -431,9 +432,10 @@ function checked(act: TemplateAct, result: TemplateResult): TemplateResult {
|
|
|
431
432
|
}
|
|
432
433
|
|
|
433
434
|
function renderCallerWorkflows(input: unknown): TemplateResult {
|
|
434
|
-
if (!isPlainRecord(input, CALLER_KEYS) || Reflect.ownKeys(input).length !== 1) return refuse("input-invalid", "input");
|
|
435
|
-
const
|
|
436
|
-
if (!
|
|
435
|
+
if (!isPlainRecord(input, CALLER_KEYS) || (Reflect.ownKeys(input).length !== 1 && (Reflect.ownKeys(input).length !== 2 || ownValue(input,"existingDeclarationAdoptions") !== true))) return refuse("input-invalid", "input");
|
|
436
|
+
const manager = ownValue(input,"packageManager");
|
|
437
|
+
if (!isPackageManager(manager)) return refuse("package-manager-unsupported", "packageManager");
|
|
438
|
+
const decision = {ok:true as const, files:[{path:DECISION_WORKFLOW_PATH, bytes:decisionWorkflow(manager,ownValue(input,"existingDeclarationAdoptions") === true)}]};
|
|
437
439
|
return {
|
|
438
440
|
ok: true,
|
|
439
441
|
files: [
|
|
@@ -452,7 +454,8 @@ function renderFixedFile(input: unknown, path: string, bytes: () => string): Tem
|
|
|
452
454
|
/**
|
|
453
455
|
* Render the files one setup act writes, or refuse. `add-caller-workflow` takes
|
|
454
456
|
* `{ packageManager }`, `write-starter-request` takes a `StarterRequestInput`,
|
|
455
|
-
*
|
|
457
|
+
* `add-ci-template` takes no input, and `add-path-scope-job` optionally takes
|
|
458
|
+
* `{ agentProvenance }`. The returned
|
|
456
459
|
* paths are always `TEMPLATE_PATHS[act]`, in that order.
|
|
457
460
|
*/
|
|
458
461
|
export function renderSetupTemplate(act: TemplateAct, input?: unknown): TemplateResult {
|
|
@@ -465,7 +468,12 @@ export function renderSetupTemplate(act: TemplateAct, input?: unknown): Template
|
|
|
465
468
|
return checked(name, renderStarterRequest(input));
|
|
466
469
|
case "add-ci-template":
|
|
467
470
|
return checked(name, renderFixedFile(input, CI_WORKFLOW_PATH, renderProductCiWorkflow));
|
|
468
|
-
case "add-path-scope-job":
|
|
469
|
-
return checked(name, renderFixedFile(input, PATH_SCOPE_WORKFLOW_PATH, renderPathScopeWorkflow));
|
|
471
|
+
case "add-path-scope-job": {
|
|
472
|
+
if (input === undefined) return checked(name, renderFixedFile(input, PATH_SCOPE_WORKFLOW_PATH, renderPathScopeWorkflow));
|
|
473
|
+
if (!isPlainRecord(input, new Set(["agentProvenance"])) || Reflect.ownKeys(input).length !== 1) return refuse("input-invalid", "input");
|
|
474
|
+
const agent = ownValue(input, "agentProvenance");
|
|
475
|
+
if (agent !== "codex" && agent !== "claude" && agent !== "cursor") return refuse("input-invalid", "input");
|
|
476
|
+
return checked(name, { ok: true, files: [{ path: PATH_SCOPE_WORKFLOW_PATH, bytes: renderPathScopeWorkflow(agent) }] });
|
|
477
|
+
}
|
|
470
478
|
}
|
|
471
479
|
}
|