@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.
Files changed (70) hide show
  1. package/README.md +51 -2
  2. package/dist/admission-fixture.d.ts.map +1 -1
  3. package/dist/admission-fixture.js +15 -1
  4. package/dist/admission-fixture.js.map +1 -1
  5. package/dist/admission.d.ts.map +1 -1
  6. package/dist/admission.js +6 -1
  7. package/dist/admission.js.map +1 -1
  8. package/dist/apply-plan-cli.d.ts +1 -1
  9. package/dist/apply-plan-cli.d.ts.map +1 -1
  10. package/dist/apply-plan-cli.js +1 -1
  11. package/dist/approval-sheet.d.ts.map +1 -1
  12. package/dist/approval-sheet.js +6 -0
  13. package/dist/approval-sheet.js.map +1 -1
  14. package/dist/change-set-contract.d.ts +22 -0
  15. package/dist/change-set-contract.d.ts.map +1 -1
  16. package/dist/change-set-contract.js +45 -2
  17. package/dist/change-set-contract.js.map +1 -1
  18. package/dist/existing-declaration-adoption.check.d.ts +2 -0
  19. package/dist/existing-declaration-adoption.check.d.ts.map +1 -0
  20. package/dist/existing-declaration-adoption.check.js +10 -0
  21. package/dist/existing-declaration-adoption.check.js.map +1 -0
  22. package/dist/generated/plan-contracts.generated.d.ts.map +1 -1
  23. package/dist/generated/plan-contracts.generated.js +264 -3
  24. package/dist/generated/plan-contracts.generated.js.map +1 -1
  25. package/dist/index.d.ts +2 -1
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +1 -1
  28. package/dist/index.js.map +1 -1
  29. package/dist/ledger-contract.d.ts +5 -2
  30. package/dist/ledger-contract.d.ts.map +1 -1
  31. package/dist/ledger-contract.js +25 -2
  32. package/dist/ledger-contract.js.map +1 -1
  33. package/dist/ledger-trust.d.ts.map +1 -1
  34. package/dist/ledger-trust.js +8 -3
  35. package/dist/ledger-trust.js.map +1 -1
  36. package/dist/materialize.d.ts.map +1 -1
  37. package/dist/materialize.js +43 -2
  38. package/dist/materialize.js.map +1 -1
  39. package/dist/plan-bundle.d.ts +7 -1
  40. package/dist/plan-bundle.d.ts.map +1 -1
  41. package/dist/plan-bundle.js +64 -9
  42. package/dist/plan-bundle.js.map +1 -1
  43. package/dist/plan-command.d.ts +1 -1
  44. package/dist/plan-command.d.ts.map +1 -1
  45. package/dist/plan-command.js +33 -3
  46. package/dist/plan-command.js.map +1 -1
  47. package/dist/setup-template-scripts.d.ts +5 -3
  48. package/dist/setup-template-scripts.d.ts.map +1 -1
  49. package/dist/setup-template-scripts.js +17 -6
  50. package/dist/setup-template-scripts.js.map +1 -1
  51. package/dist/setup-templates.d.ts +4 -3
  52. package/dist/setup-templates.d.ts.map +1 -1
  53. package/dist/setup-templates.js +24 -13
  54. package/dist/setup-templates.js.map +1 -1
  55. package/package.json +1 -1
  56. package/src/admission-fixture.ts +14 -1
  57. package/src/admission.ts +4 -1
  58. package/src/apply-plan-cli.ts +1 -1
  59. package/src/approval-sheet.ts +6 -0
  60. package/src/change-set-contract.ts +51 -1
  61. package/src/existing-declaration-adoption.check.ts +12 -0
  62. package/src/generated/plan-contracts.generated.ts +264 -3
  63. package/src/index.ts +3 -1
  64. package/src/ledger-contract.ts +25 -2
  65. package/src/ledger-trust.ts +5 -0
  66. package/src/materialize.ts +31 -2
  67. package/src/plan-bundle.ts +60 -10
  68. package/src/plan-command.ts +25 -2
  69. package/src/setup-template-scripts.ts +15 -6
  70. package/src/setup-templates.ts +20 -12
@@ -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 own = row === undefined ? (baseAt === undefined ? null : "unowned-existing") : baseAt === undefined ? "deleted" : baseAt.value !== row ? "client-edited" : null;
523
- const satisfiedInBase =
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: `clossys/apply-${short}`, pullRequest: { title: `Clossys: apply plan ${short}` }, changeSetDigest: digest };
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
  }
@@ -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
- if (argv.length !== 0) return refuse("usage");
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 takes no input and returns the exact bytes of one file, so
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 = PATH_SCOPE_SCRIPT.slice(0, -1)
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
- return [...PATH_SCOPE_WORKFLOW_HEAD, ...body, `${RUN_BODY_INDENT}${HEREDOC_TERMINATOR}`, ""].join("\n");
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
  }
@@ -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.3.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\.2\.(?:0|[1-9][0-9]{0,8})$/u;
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
- return [...workflowTop(manager), ...FIRST_STEPS, ...install, ...LAST_STEPS].join("\n");
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 decision = renderAdoptionDecisionWorkflow(ownValue(input, "packageManager"));
436
- if (!decision.ok) return decision;
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
- * and `add-ci-template` and `add-path-scope-job` take no input. The returned
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
  }