agentic-team 6.1.0 → 6.3.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 +8 -3
- package/bin/build-template.js +2 -2
- package/bin/index.js +234 -39
- package/bin/platforms.js +510 -0
- package/package.json +4 -4
- package/template/.claude-plugin/plugin.json +2 -2
- package/template/.orchestra/README.md +41 -51
- package/template/.orchestra/config.yml +16 -0
- package/template/.orchestra/roles/product-manager.md +3 -2
- package/template/.orchestra/scripts/compact-diff.awk +1 -1
- package/template/.orchestra/scripts/context.mjs +373 -0
- package/template/.orchestra/stacks/_repo/setup.sh +7 -3
- package/template/.orchestra/stacks/mobile-expo/setup.sh +1 -1
- package/template/CLAUDE.md +1 -1
- package/template/agents/lead.md +171 -116
- package/template/agents/milestone-reviewer.md +5 -3
- package/template/agents/product-manager.md +3 -2
- package/template/agents/re-reviewer.md +25 -0
- package/template/agents/reviewer.md +7 -5
- package/template/agents/scaffolder.md +26 -0
- package/template/commands/help.md +1 -0
- package/template/commands/hotfix.md +3 -1
- package/template/commands/rewind.md +10 -8
- package/template/commands/setup.md +2 -1
- package/template/commands/start.md +2 -2
- package/template/commands/status.md +1 -1
- package/template/commands/sync.md +5 -2
- package/template/commands/verifier.md +2 -2
- package/template/hooks/audit-context.sh +15 -2
- package/template/hooks/enforce-role.sh +21 -3
- package/template/hooks/protect-system-files.sh +2 -2
- package/template/hooks/review-rounds.sh +1 -1
- package/template/rules/code-standards.orchestra.md +3 -2
- package/template/rules/design-parity.orchestra.md +12 -1
- package/template/rules/observability.orchestra.md +3 -0
- package/template/rules/performance.orchestra.md +5 -1
- package/template/rules/phase-limits.orchestra.md +1 -1
- package/template/rules/pull-request.orchestra.md +7 -2
- package/template/rules/role-boundaries.orchestra.md +24 -9
- package/template/rules/security.orchestra.md +1 -1
- package/template/rules/stuck-detection.orchestra.md +1 -1
- package/template/rules/testing-standards.orchestra.md +10 -5
- package/template/rules/tracker-authority.orchestra.md +9 -1
- package/template/skills/push-notifications/SKILL.md +1 -1
- package/template/skills/testing/SKILL.md +2 -1
package/README.md
CHANGED
|
@@ -24,6 +24,11 @@ That copies `.orchestra/` and `.claude/` in and adds the Orchestra section to
|
|
|
24
24
|
blueprints, your own roles, skills and rules are preserved, and `config.yml`
|
|
25
25
|
is merged rather than overwritten.
|
|
26
26
|
|
|
27
|
+
Only the project's platforms are installed — `web`, `expo`, `ios`, `android`,
|
|
28
|
+
`backend` — from `project.platforms` in `config.yml`, detected from the repo
|
|
29
|
+
when it is empty, or `--platforms ios,backend`. See
|
|
30
|
+
[Getting started](docs/getting-started.md#only-your-platforms).
|
|
31
|
+
|
|
27
32
|
## How it works
|
|
28
33
|
|
|
29
34
|
Once, in a freshly cloned empty repo:
|
|
@@ -105,10 +110,10 @@ With `tracker.provider: none` there is no board. The markdown under
|
|
|
105
110
|
├── stacks/ how each kind of app is scaffolded and built afterwards
|
|
106
111
|
├── trackers/ the GitHub adapter; ClickUp goes through its MCP connector
|
|
107
112
|
├── blueprints/ milestone templates
|
|
108
|
-
└── milestones/ your work — prd, milestone, phases, tracker.yml, context
|
|
113
|
+
└── milestones/ your work — prd, milestone, phases, tracker.yml, context/decisions/journal
|
|
109
114
|
|
|
110
115
|
.claude/
|
|
111
|
-
├── agents/ architect, scaffolder, lead, reviewer, tracker
|
|
116
|
+
├── agents/ architect, scaffolder, lead, reviewer, re-reviewer, tracker
|
|
112
117
|
├── rules/*.orchestra.md discipline, loaded into every session
|
|
113
118
|
├── skills/*/SKILL.md depth on one topic, loaded per phase
|
|
114
119
|
└── commands/orchestra/ the commands above
|
|
@@ -125,7 +130,7 @@ is enforced by `scripts/policy-check.mjs` in the gate; the rest is a review
|
|
|
125
130
|
item, and the reviewer is told which is which.
|
|
126
131
|
|
|
127
132
|
**Roles** write only inside their ownership. PM writes milestones. Lead
|
|
128
|
-
writes `context.md`. Sub-agents write what a phase's scope names. A
|
|
133
|
+
writes the milestone record — `context.md`, `decisions.md`, `journal.md`. Sub-agents write what a phase's scope names. A
|
|
129
134
|
`PreToolUse` hook blocks everything else.
|
|
130
135
|
|
|
131
136
|
## Principles
|
package/bin/build-template.js
CHANGED
|
@@ -36,7 +36,7 @@ const PLUGIN_MANIFEST = {
|
|
|
36
36
|
"AI Team Orchestration — multi-role coordination with milestones, phases, and quality gates for Claude Code",
|
|
37
37
|
version: require("../package.json").version,
|
|
38
38
|
author: { name: "Sulhadin Öney" },
|
|
39
|
-
repository: "https://github.com/sulhadin/
|
|
39
|
+
repository: "https://github.com/sulhadin/agentic-team",
|
|
40
40
|
license: "MIT",
|
|
41
41
|
};
|
|
42
42
|
|
|
@@ -79,7 +79,7 @@ const SYSTEM_PATHS = [
|
|
|
79
79
|
|
|
80
80
|
// The repo's CLAUDE.md carries a "## Development" section about THIS package.
|
|
81
81
|
// Copying it wholesale means every scaffolded project tells its own agents it
|
|
82
|
-
// is the
|
|
82
|
+
// is the agentic-team npm package. Ship only what is between the markers.
|
|
83
83
|
function packClaudeMd() {
|
|
84
84
|
const src = fs.readFileSync(path.join(rootDir, "CLAUDE.md"), "utf-8");
|
|
85
85
|
const start = src.indexOf("<!-- orchestra -->");
|
package/bin/index.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const { execFileSync } = require("node:child_process");
|
|
4
4
|
const fs = require("node:fs");
|
|
5
5
|
const path = require("node:path");
|
|
6
|
+
const platformsLib = require("./platforms");
|
|
6
7
|
|
|
7
8
|
const targetDir = process.cwd();
|
|
8
9
|
const templateDir = path.join(__dirname, "..", "template");
|
|
@@ -71,9 +72,11 @@ const ALLOW_PERMISSIONS = [
|
|
|
71
72
|
|
|
72
73
|
function parseArgs() {
|
|
73
74
|
const args = process.argv.slice(2);
|
|
74
|
-
const
|
|
75
|
+
const platforms = platformsLib.takePlatformsArg(args);
|
|
76
|
+
const flags = { skipPermissions: false, help: false, platforms: platforms?.list ?? null };
|
|
75
77
|
|
|
76
|
-
for (const arg of args) {
|
|
78
|
+
for (const [i, arg] of args.entries()) {
|
|
79
|
+
if (platforms?.used.has(i)) continue;
|
|
77
80
|
switch (arg) {
|
|
78
81
|
case "--dangerously-skip-permissions":
|
|
79
82
|
flags.skipPermissions = true;
|
|
@@ -101,11 +104,16 @@ function showHelp() {
|
|
|
101
104
|
console.log("");
|
|
102
105
|
console.log(" Options:");
|
|
103
106
|
console.log(" --public new: create a public repository");
|
|
107
|
+
console.log(
|
|
108
|
+
" --platforms <list> web,expo,ios,android,backend — install only these",
|
|
109
|
+
);
|
|
110
|
+
console.log(" (default: project.platforms, else detected)");
|
|
104
111
|
console.log(" --dangerously-skip-permissions Auto-allow all tool permissions");
|
|
105
112
|
console.log(" --help, -h Show this help");
|
|
106
113
|
console.log("");
|
|
107
114
|
console.log(" Examples:");
|
|
108
115
|
console.log(" npx agentic-team");
|
|
116
|
+
console.log(" npx agentic-team --platforms ios,backend");
|
|
109
117
|
console.log(" npx agentic-team new daily-intake");
|
|
110
118
|
console.log("");
|
|
111
119
|
}
|
|
@@ -441,22 +449,72 @@ function smartMergeDir(backupPath, restorePath, templateDirPath) {
|
|
|
441
449
|
// The manifest is that marker, for everything, in one place both sides read.
|
|
442
450
|
const MANIFEST_PATH = ".orchestra/.installed.json";
|
|
443
451
|
|
|
444
|
-
/**
|
|
445
|
-
|
|
452
|
+
/**
|
|
453
|
+
* Every file the template installs for these platforms: its repo-relative
|
|
454
|
+
* POSIX destination and its source in the package. An empty platform list is
|
|
455
|
+
* everything.
|
|
456
|
+
*/
|
|
457
|
+
function templateEntries(platforms, keepSkills) {
|
|
446
458
|
const out = [];
|
|
447
459
|
const walk = (dir, prefix) => {
|
|
448
460
|
if (!fs.existsSync(dir)) return;
|
|
449
461
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
450
462
|
const rel = `${prefix}/${entry.name}`;
|
|
451
|
-
|
|
452
|
-
|
|
463
|
+
const src = path.join(dir, entry.name);
|
|
464
|
+
if (entry.isDirectory()) walk(src, rel);
|
|
465
|
+
else if (platformsLib.selected(rel, platforms, keepSkills)) out.push({ rel, src });
|
|
453
466
|
}
|
|
454
467
|
};
|
|
455
468
|
walk(path.join(templateDir, ".orchestra"), ".orchestra");
|
|
456
469
|
for (const [pluginDir, claudeSubdir] of Object.entries(PLUGIN_TO_CLAUDE)) {
|
|
457
470
|
walk(path.join(templateDir, pluginDir), claudeSubdir);
|
|
458
471
|
}
|
|
459
|
-
return out.sort();
|
|
472
|
+
return out.sort((a, b) => (a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0));
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Copy the template's files for these platforms into the project. A rule
|
|
477
|
+
* loses the sections marked for other platforms; every file keeps its name,
|
|
478
|
+
* so the manifest and the system-file hook see the same paths either way.
|
|
479
|
+
*/
|
|
480
|
+
function installTemplate(entries, platforms) {
|
|
481
|
+
for (const { rel, src } of entries) {
|
|
482
|
+
const dest = path.join(targetDir, ...rel.split("/"));
|
|
483
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
484
|
+
// Dereference rather than recreate: a relative link that resolved in the
|
|
485
|
+
// package will not resolve in the user's tree.
|
|
486
|
+
const real = fs.realpathSync(src);
|
|
487
|
+
try {
|
|
488
|
+
if (fs.lstatSync(dest).isSymbolicLink()) fs.unlinkSync(dest);
|
|
489
|
+
} catch {
|
|
490
|
+
// Optional: on a fresh install there is nothing at dest yet.
|
|
491
|
+
}
|
|
492
|
+
if (rel.startsWith(".claude/rules/") && rel.endsWith(".md")) {
|
|
493
|
+
const text = fs.readFileSync(real, "utf-8");
|
|
494
|
+
fs.writeFileSync(dest, platformsLib.filterSections(text, platforms));
|
|
495
|
+
} else {
|
|
496
|
+
fs.copyFileSync(real, dest);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/** Names in a platform list that are not platforms. */
|
|
502
|
+
const unknownPlatforms = (list) => list.filter((p) => !platformsLib.PLATFORMS.includes(p));
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Which platforms this run installs for, and where that answer came from:
|
|
506
|
+
* the flag (already validated), then the config, then detection. A config
|
|
507
|
+
* naming something that is not a platform is refused, not narrowed around —
|
|
508
|
+
* dropping the name once retired a platform's files and rewrote the user's
|
|
509
|
+
* list without it.
|
|
510
|
+
*/
|
|
511
|
+
function resolvePlatforms(flagValue, configText, detect) {
|
|
512
|
+
if (flagValue) return { list: flagValue, source: "--platforms" };
|
|
513
|
+
const configured = platformsLib.readConfigPlatforms(configText || "");
|
|
514
|
+
const bad = unknownPlatforms(configured);
|
|
515
|
+
if (bad.length) return { error: `project.platforms in config.yml names ${bad.join(", ")}` };
|
|
516
|
+
if (configured.length) return { list: configured, source: "config.yml" };
|
|
517
|
+
return { list: detect(), source: "detected" };
|
|
460
518
|
}
|
|
461
519
|
|
|
462
520
|
function readManifest() {
|
|
@@ -464,7 +522,9 @@ function readManifest() {
|
|
|
464
522
|
if (!fs.existsSync(p)) return null;
|
|
465
523
|
try {
|
|
466
524
|
const m = JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
467
|
-
|
|
525
|
+
if (Array.isArray(m?.files)) return m;
|
|
526
|
+
console.log(" [!] .orchestra/.installed.json has no files list — nothing retired this run");
|
|
527
|
+
return null;
|
|
468
528
|
} catch {
|
|
469
529
|
// A corrupt manifest means retirement cannot run this time. Reported, and
|
|
470
530
|
// not fatal: the install itself is still correct without it.
|
|
@@ -481,18 +541,22 @@ function writeManifest(files) {
|
|
|
481
541
|
}
|
|
482
542
|
|
|
483
543
|
/**
|
|
484
|
-
* Delete what a previous
|
|
544
|
+
* Delete what a previous install put down and this one does not: a file this
|
|
545
|
+
* version no longer ships, or one for a platform the project no longer lists.
|
|
546
|
+
* `alsoRetire` adds paths no manifest records, for an install older than it.
|
|
485
547
|
*
|
|
486
548
|
* Without it a retired skill is indistinguishable from one the user wrote, so
|
|
487
549
|
* it survives every future upgrade and keeps teaching whatever it taught.
|
|
488
550
|
*/
|
|
489
|
-
function retireRemoved(previous, current) {
|
|
490
|
-
if (!previous) return 0;
|
|
551
|
+
function retireRemoved(previous, current, alsoRetire = []) {
|
|
491
552
|
const keep = new Set(current);
|
|
492
553
|
let removed = 0;
|
|
493
|
-
for (const rel of previous
|
|
554
|
+
for (const rel of new Set([...(previous?.files || []), ...alsoRetire])) {
|
|
494
555
|
if (keep.has(rel)) continue;
|
|
495
556
|
const full = path.join(targetDir, rel);
|
|
557
|
+
// The manifest is a committed file anyone can edit. An entry that leaves
|
|
558
|
+
// the project is never deleted.
|
|
559
|
+
if (!full.startsWith(targetDir + path.sep)) continue;
|
|
496
560
|
if (!fs.existsSync(full)) continue;
|
|
497
561
|
fs.unlinkSync(full);
|
|
498
562
|
// The directory it lived in is ours too when nothing is left in it.
|
|
@@ -711,11 +775,12 @@ function setupPermissions() {
|
|
|
711
775
|
* Deliberately stops before `/orchestra:setup`. Setup is a conversation — it
|
|
712
776
|
* asks what you are building — and that conversation has to happen inside the
|
|
713
777
|
* new project, where the project's own CLAUDE.md and role boundaries are
|
|
714
|
-
* loaded. Run from the
|
|
778
|
+
* loaded. Run from the agentic-team repo instead, an agent would be wearing
|
|
715
779
|
* the system maintainer's contract at the moment it starts building an app.
|
|
716
780
|
*/
|
|
717
781
|
function newProject(argv) {
|
|
718
|
-
const
|
|
782
|
+
const platforms = platformsLib.takePlatformsArg(argv);
|
|
783
|
+
const name = argv.find((a, i) => !a.startsWith("-") && !platforms?.used.has(i));
|
|
719
784
|
const isPublic = argv.includes("--public");
|
|
720
785
|
|
|
721
786
|
if (!name) {
|
|
@@ -731,6 +796,15 @@ function newProject(argv) {
|
|
|
731
796
|
return;
|
|
732
797
|
}
|
|
733
798
|
|
|
799
|
+
// Checked before the repository exists: the installer would refuse a typo
|
|
800
|
+
// only after GitHub already has an empty repository by that name.
|
|
801
|
+
if (platforms && (!platforms.list.length || unknownPlatforms(platforms.list).length)) {
|
|
802
|
+
console.log(` [x] --platforms takes a comma list of: ${platformsLib.PLATFORMS.join(", ")}`);
|
|
803
|
+
process.exitCode = 1;
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
const forward = platforms ? ["--platforms", platforms.list.join(",")] : [];
|
|
807
|
+
|
|
734
808
|
const dir = path.resolve(targetDir, name);
|
|
735
809
|
if (fs.existsSync(dir)) {
|
|
736
810
|
console.log(` [x] ${name}/ already exists here. Refusing to write into it.`);
|
|
@@ -771,7 +845,7 @@ function newProject(argv) {
|
|
|
771
845
|
// The installer runs unchanged, in the new directory. Copying its logic here
|
|
772
846
|
// would give the manifest, the upgrade path and the hook registration a
|
|
773
847
|
// second implementation to drift from.
|
|
774
|
-
execFileSync(process.execPath, [__filename], { cwd: dir, stdio: "inherit" });
|
|
848
|
+
execFileSync(process.execPath, [__filename, ...forward], { cwd: dir, stdio: "inherit" });
|
|
775
849
|
}
|
|
776
850
|
|
|
777
851
|
function run() {
|
|
@@ -781,6 +855,20 @@ function run() {
|
|
|
781
855
|
return;
|
|
782
856
|
}
|
|
783
857
|
|
|
858
|
+
// Refused before anything is touched: a typo in the flag would otherwise
|
|
859
|
+
// install for the wrong platforms and retire the right ones. An empty list
|
|
860
|
+
// means "everything", which is never what a flag asking for platforms meant.
|
|
861
|
+
if (flags.platforms && (!flags.platforms.length || unknownPlatforms(flags.platforms).length)) {
|
|
862
|
+
console.log("");
|
|
863
|
+
console.log(
|
|
864
|
+
` [x] Unknown platform: ${unknownPlatforms(flags.platforms).join(", ") || "(none given)"}`,
|
|
865
|
+
);
|
|
866
|
+
console.log(` Expected any of: ${platformsLib.PLATFORMS.join(", ")}`);
|
|
867
|
+
console.log("");
|
|
868
|
+
process.exitCode = 1;
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
|
|
784
872
|
console.log("");
|
|
785
873
|
console.log(" Orchestra — AI Team Orchestration");
|
|
786
874
|
console.log(` Installing into: ${targetDir}`);
|
|
@@ -793,6 +881,38 @@ function run() {
|
|
|
793
881
|
// Read before anything is copied: after the install the manifest describes
|
|
794
882
|
// this version, and what a previous one left behind is no longer knowable.
|
|
795
883
|
const previousManifest = readManifest();
|
|
884
|
+
// Asked separately: a manifest that exists but does not parse is not an old
|
|
885
|
+
// install, and treating it as one sweeps user files the manifest would have
|
|
886
|
+
// told apart. Read now, because the upgrade removes .orchestra/ below.
|
|
887
|
+
const manifestExisted = fs.existsSync(path.join(targetDir, MANIFEST_PATH));
|
|
888
|
+
|
|
889
|
+
// Also before the copy: the upgrade's config merge cannot keep a block-form
|
|
890
|
+
// list over the template's flow-form `platforms: []`, so the answer is
|
|
891
|
+
// taken from the user's file as it stands and written back afterwards.
|
|
892
|
+
const configBefore = path.join(orchestraDest, "config.yml");
|
|
893
|
+
let detected = null;
|
|
894
|
+
const detect = () => {
|
|
895
|
+
detected ??= platformsLib.detectPlatforms(targetDir);
|
|
896
|
+
return detected;
|
|
897
|
+
};
|
|
898
|
+
const profile = resolvePlatforms(
|
|
899
|
+
flags.platforms,
|
|
900
|
+
fs.existsSync(configBefore) ? fs.readFileSync(configBefore, "utf-8") : "",
|
|
901
|
+
detect,
|
|
902
|
+
);
|
|
903
|
+
if (profile.error) {
|
|
904
|
+
console.log(` [x] ${profile.error}.`);
|
|
905
|
+
console.log(
|
|
906
|
+
` Expected any of: ${platformsLib.PLATFORMS.join(", ")}. Nothing was changed.`,
|
|
907
|
+
);
|
|
908
|
+
console.log("");
|
|
909
|
+
process.exitCode = 1;
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
// Read before the upgrade moves milestones/ aside.
|
|
913
|
+
const phaseSkills = platformsLib.phaseSkills(targetDir);
|
|
914
|
+
const entries = templateEntries(profile.list, phaseSkills);
|
|
915
|
+
const allEntries = templateEntries([]);
|
|
796
916
|
|
|
797
917
|
if (isUpgrade) {
|
|
798
918
|
// A backup nobody finished restoring holds the only copy of the user's
|
|
@@ -923,17 +1043,8 @@ function run() {
|
|
|
923
1043
|
}
|
|
924
1044
|
}
|
|
925
1045
|
|
|
926
|
-
|
|
1046
|
+
installTemplate(entries, profile.list);
|
|
927
1047
|
console.log(" [+] .orchestra/ installed");
|
|
928
|
-
|
|
929
|
-
// Copy plugin dirs → .claude/ (plugin-to-standalone mapping)
|
|
930
|
-
for (const [pluginDir, claudeSubdir] of Object.entries(PLUGIN_TO_CLAUDE)) {
|
|
931
|
-
const src = path.join(templateDir, pluginDir);
|
|
932
|
-
if (fs.existsSync(src)) {
|
|
933
|
-
const dest = path.join(targetDir, claudeSubdir);
|
|
934
|
-
copyDirRecursive(src, dest);
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
1048
|
console.log(" [+] .claude/ orchestra files installed");
|
|
938
1049
|
|
|
939
1050
|
// ── Restore user data ──
|
|
@@ -988,30 +1099,111 @@ function run() {
|
|
|
988
1099
|
console.log("");
|
|
989
1100
|
}
|
|
990
1101
|
}
|
|
991
|
-
} else {
|
|
992
|
-
// ── Fresh install ──
|
|
993
|
-
copyDirRecursive(orchestraSrc, orchestraDest);
|
|
994
|
-
console.log(" [+] .orchestra/ installed");
|
|
995
1102
|
|
|
996
|
-
//
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1103
|
+
// A milestone written before the split keeps decisions and the journal
|
|
1104
|
+
// inside context.md. The script moves them out losslessly, new files
|
|
1105
|
+
// first, so a failure here leaves context.md whole.
|
|
1106
|
+
const contextScript = path.join(orchestraDest, "scripts", "context.mjs");
|
|
1107
|
+
if (fs.existsSync(contextScript)) {
|
|
1108
|
+
try {
|
|
1109
|
+
const out = execFileSync(process.execPath, [contextScript, "migrate"], {
|
|
1110
|
+
cwd: targetDir,
|
|
1111
|
+
encoding: "utf8",
|
|
1112
|
+
});
|
|
1113
|
+
for (const line of out.split("\n").filter(Boolean)) console.log(` [~] ${line}`);
|
|
1114
|
+
} catch (e) {
|
|
1115
|
+
// Not fatal: the install itself is complete and context.md is whole,
|
|
1116
|
+
// because the split writes the new files before it touches it. What
|
|
1117
|
+
// is left is one command, and the message names it.
|
|
1118
|
+
console.log(` [!] context.md not split (${e.message.split("\n")[0]}); run it again with`);
|
|
1119
|
+
console.log(" node .orchestra/scripts/context.mjs migrate");
|
|
1002
1120
|
}
|
|
1003
1121
|
}
|
|
1122
|
+
} else {
|
|
1123
|
+
// ── Fresh install ──
|
|
1124
|
+
installTemplate(entries, profile.list);
|
|
1125
|
+
console.log(" [+] .orchestra/ installed");
|
|
1004
1126
|
console.log(" [+] .claude/ installed");
|
|
1005
1127
|
}
|
|
1006
1128
|
|
|
1007
1129
|
// ── Record what was installed ──
|
|
1008
|
-
const installed =
|
|
1009
|
-
|
|
1130
|
+
const installed = entries.map((e) => e.rel);
|
|
1131
|
+
// The manifest retires what this project no longer lists. An install from
|
|
1132
|
+
// before the manifest has none, so its files for other platforms would look
|
|
1133
|
+
// like the user's own; only then is every such template path retired. With
|
|
1134
|
+
// a manifest, a path it does not list is the user's — somebody who writes
|
|
1135
|
+
// their own skill under a name a dropped platform freed keeps it.
|
|
1136
|
+
const unselected =
|
|
1137
|
+
isUpgrade && !manifestExisted
|
|
1138
|
+
? allEntries.map((e) => e.rel).filter((rel) => !installed.includes(rel))
|
|
1139
|
+
: [];
|
|
1140
|
+
const retired = retireRemoved(previousManifest, installed, unselected);
|
|
1010
1141
|
if (retired > 0) {
|
|
1011
|
-
console.log(` [~] Removed ${retired} system file(s) this
|
|
1142
|
+
console.log(` [~] Removed ${retired} system file(s) this install no longer ships`);
|
|
1012
1143
|
}
|
|
1013
1144
|
writeManifest(installed);
|
|
1014
1145
|
|
|
1146
|
+
// ── Record the platforms ──
|
|
1147
|
+
const cfgPath = path.join(orchestraDest, "config.yml");
|
|
1148
|
+
if (fs.existsSync(cfgPath)) {
|
|
1149
|
+
const text = fs.readFileSync(cfgPath, "utf-8");
|
|
1150
|
+
const next = platformsLib.writeConfigPlatforms(text, profile.list);
|
|
1151
|
+
if (next === null) {
|
|
1152
|
+
console.log(" [!] project: in config.yml is an inline map — platforms not written back.");
|
|
1153
|
+
console.log(" Write it as a block, with platforms: [ios] indented under project:");
|
|
1154
|
+
} else {
|
|
1155
|
+
fs.writeFileSync(cfgPath, next);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
if (profile.list.length) {
|
|
1159
|
+
const skipped = allEntries.length - entries.length;
|
|
1160
|
+
console.log(
|
|
1161
|
+
` [~] Platforms: ${profile.list.join(", ")} (${profile.source})` +
|
|
1162
|
+
(skipped ? ` — ${skipped} file(s) for other platforms not installed` : ""),
|
|
1163
|
+
);
|
|
1164
|
+
if (profile.source === "detected") {
|
|
1165
|
+
console.log(
|
|
1166
|
+
" Wrong? Edit project.platforms in .orchestra/config.yml, or pass --platforms.",
|
|
1167
|
+
);
|
|
1168
|
+
} else {
|
|
1169
|
+
// The list is trusted once written, so an app added later is said out
|
|
1170
|
+
// loud on every run rather than silently left out.
|
|
1171
|
+
const unlisted = detect().filter((p) => !profile.list.includes(p));
|
|
1172
|
+
if (unlisted.length) {
|
|
1173
|
+
console.log(
|
|
1174
|
+
` [!] The repository also looks like ${unlisted.join(", ")}, which project.platforms does not list.`,
|
|
1175
|
+
);
|
|
1176
|
+
console.log(" Add it to project.platforms if this project builds for it.");
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
} else {
|
|
1180
|
+
console.log(
|
|
1181
|
+
" [~] Platforms: none set — everything installed. Set project.platforms or pass --platforms to narrow it.",
|
|
1182
|
+
);
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
// A skill kept only because a phase names it is said, so it is not mistaken
|
|
1186
|
+
// for one the profile chose; a name that exists nowhere is a broken plan.
|
|
1187
|
+
const keptOnlyByPhase = [...phaseSkills].filter(
|
|
1188
|
+
(n) =>
|
|
1189
|
+
platformsLib.SKILLS[n] !== undefined &&
|
|
1190
|
+
!allEntries.some(
|
|
1191
|
+
(e) =>
|
|
1192
|
+
e.rel.startsWith(`.claude/skills/${n}/`) && platformsLib.selected(e.rel, profile.list),
|
|
1193
|
+
),
|
|
1194
|
+
);
|
|
1195
|
+
if (keptOnlyByPhase.length) {
|
|
1196
|
+
console.log(
|
|
1197
|
+
` [~] Kept for other platforms because a phase names them: ${keptOnlyByPhase.join(", ")}`,
|
|
1198
|
+
);
|
|
1199
|
+
}
|
|
1200
|
+
const missing = [...phaseSkills].filter(
|
|
1201
|
+
(n) => !fs.existsSync(path.join(targetDir, ".claude", "skills", n, "SKILL.md")),
|
|
1202
|
+
);
|
|
1203
|
+
if (missing.length) {
|
|
1204
|
+
console.log(` [!] A phase names a skill that does not exist: ${missing.join(", ")}`);
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1015
1207
|
// ── Handle CLAUDE.md ──
|
|
1016
1208
|
const templateClaudeMd = fs.readFileSync(path.join(templateDir, "CLAUDE.md"), "utf-8");
|
|
1017
1209
|
const targetClaudeMdPath = path.join(targetDir, "CLAUDE.md");
|
|
@@ -1092,9 +1284,12 @@ function run() {
|
|
|
1092
1284
|
// The previous message sent a fresh repo straight to the PM, while the
|
|
1093
1285
|
// CLAUDE.md it had just written says to point at setup before anything else.
|
|
1094
1286
|
// The first screen a new user reads contradicted the system it installed.
|
|
1287
|
+
// A native app at the root has neither apps/ nor a package.json, so
|
|
1288
|
+
// detection counts too.
|
|
1095
1289
|
const scaffolded =
|
|
1096
1290
|
fs.existsSync(path.join(targetDir, "apps")) ||
|
|
1097
|
-
fs.existsSync(path.join(targetDir, "package.json"))
|
|
1291
|
+
fs.existsSync(path.join(targetDir, "package.json")) ||
|
|
1292
|
+
detect().length > 0;
|
|
1098
1293
|
|
|
1099
1294
|
// Preserved, and said out loud. Silently keeping a key nothing reads is how
|
|
1100
1295
|
// somebody spends an afternoon wondering why their setting has no effect.
|