forge-orkes 0.62.0 → 0.64.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/bin/create-forge.js +250 -99
- package/experimental/conventions/README.md +87 -0
- package/experimental/conventions/install.sh +179 -0
- package/experimental/conventions/source/rules/code-quality.md +17 -0
- package/experimental/conventions/source/rules/laravel-http.md +21 -0
- package/experimental/conventions/source/rules/laravel-jobs.md +19 -0
- package/experimental/conventions/source/rules/laravel-migrations.md +19 -0
- package/experimental/conventions/source/rules/laravel-services.md +21 -0
- package/experimental/conventions/source/rules/python.md +14 -0
- package/experimental/conventions/source/rules/testing.md +15 -0
- package/experimental/conventions/source/rules/vue-inertia.md +24 -0
- package/experimental/conventions/source/skills/laravel-conventions/SKILL.md +124 -0
- package/experimental/conventions/source/skills/python-conventions/SKILL.md +100 -0
- package/experimental/conventions/source/skills/testing-standards/SKILL.md +142 -0
- package/experimental/conventions/source/skills/vue-conventions/SKILL.md +92 -0
- package/experimental/conventions/uninstall.sh +97 -0
- package/experimental/m10/README.md +130 -0
- package/experimental/m10/install.sh +198 -0
- package/experimental/m10/source/hooks/forge-branch-guard.sh +61 -0
- package/experimental/m10/source/hooks/forge-claim-check-doctor.sh +77 -0
- package/experimental/m10/source/hooks/forge-claim-check.sh +113 -0
- package/experimental/m10/source/hooks/forge-session-id.sh +22 -0
- package/experimental/m10/source/mcp-server/README.md +74 -0
- package/experimental/m10/source/mcp-server/example.mcp.json +8 -0
- package/experimental/m10/source/mcp-server/index.js +460 -0
- package/experimental/m10/source/mcp-server/package.json +17 -0
- package/experimental/m10/source/skills/orchestrating/SKILL.md +223 -0
- package/experimental/m10/source/skills/orchestrating/bootstrap-checks.md +70 -0
- package/experimental/m10/uninstall.sh +69 -0
- package/package.json +3 -2
- package/template/.claude/hooks/forge-release-fold.sh +9 -1
- package/template/.claude/settings.json +1 -7
- package/template/.claude/skills/jarvis/SKILL.md +204 -0
- package/template/.claude/skills/upgrading/SKILL.md +105 -176
- package/template/.forge/FORGE.md +2 -2
- package/template/.forge/checks/forge-board-render.sh +11 -5
- package/template/.forge/checks/forge-jarvis-answer.sh +79 -0
- package/template/.forge/checks/forge-jarvis-awareness.sh +128 -0
- package/template/.forge/checks/forge-jarvis-bridge-clean.sh +130 -0
- package/template/.forge/checks/forge-jarvis-dispatch.sh +176 -0
- package/template/.forge/checks/forge-jarvis-instruments.sh +146 -0
- package/template/.forge/checks/forge-jarvis-notify-logonly.sh +40 -0
- package/template/.forge/checks/forge-jarvis-promote.sh +103 -0
- package/template/.forge/checks/forge-jarvis-prwatch.sh +92 -0
- package/template/.forge/checks/forge-jarvis-relay.sh +215 -0
- package/template/.forge/checks/forge-jarvis.sh +160 -0
- package/template/.forge/checks/tests/forge-jarvis-answer.test.sh +75 -0
- package/template/.forge/checks/tests/forge-jarvis-bridge-clean.test.sh +94 -0
- package/template/.forge/checks/tests/forge-jarvis-dispatch.test.sh +162 -0
- package/template/.forge/checks/tests/forge-jarvis-instruments.test.sh +64 -0
- package/template/.forge/checks/tests/forge-jarvis-promote.test.sh +102 -0
- package/template/.forge/checks/tests/forge-jarvis-prwatch.test.sh +66 -0
- package/template/.forge/checks/tests/forge-jarvis-relay.test.sh +135 -0
- package/template/.forge/checks/tests/forge-jarvis.test.sh +122 -0
- package/template/.forge/migrations/0.63.0-origin-first-upgrading.md +49 -0
- package/template/.forge/templates/project.yml +11 -0
package/bin/create-forge.js
CHANGED
|
@@ -10,6 +10,26 @@ const templateDir = path.join(__dirname, '..', 'template');
|
|
|
10
10
|
const targetDir = process.cwd();
|
|
11
11
|
const pkgVersion = require('../package.json').version;
|
|
12
12
|
|
|
13
|
+
// --- CLI flags (parsed once up front) ---
|
|
14
|
+
|
|
15
|
+
// --json: machine-readable `upgrade` mode — exactly ONE JSON document on stdout
|
|
16
|
+
// (the report, or an {error, reason} object on refusal); every line of human
|
|
17
|
+
// prose is diverted to stderr so stdout stays parseable. --yes: auto-confirm
|
|
18
|
+
// stale-framework-file removal ONLY — it never bypasses the downgrade guard
|
|
19
|
+
// (that remains --force, which keeps its existing dual meaning).
|
|
20
|
+
const jsonMode = process.argv.includes('--json');
|
|
21
|
+
const yesMode = process.argv.includes('--yes');
|
|
22
|
+
|
|
23
|
+
// Route human prose: stdout normally, stderr under --json.
|
|
24
|
+
function say(...args) {
|
|
25
|
+
(jsonMode ? console.error : console.log)(...args);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// The only stdout write allowed in --json mode: one JSON document.
|
|
29
|
+
function emitJson(doc) {
|
|
30
|
+
process.stdout.write(JSON.stringify(doc, null, 2) + '\n');
|
|
31
|
+
}
|
|
32
|
+
|
|
13
33
|
// --- CLAUDE.md import management ---
|
|
14
34
|
|
|
15
35
|
// The framework prose lives in .forge/FORGE.md (a framework-owned file). CLAUDE.md
|
|
@@ -39,15 +59,15 @@ const FRAMEWORK_OWNED_DIRS = ['.claude/agents', '.claude/skills'];
|
|
|
39
59
|
// They are NOT in the shipped base template, so the framework-owned auto-clean
|
|
40
60
|
// would otherwise delete them on upgrade. Preserve them instead.
|
|
41
61
|
//
|
|
42
|
-
// The npm tarball ships
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
// re-sync
|
|
48
|
-
// promoted to base (FORGE.md)
|
|
49
|
-
//
|
|
50
|
-
//
|
|
62
|
+
// The npm tarball ships experimental/ alongside bin/ + template/, so this route
|
|
63
|
+
// CAN diff an installed experimental skill against its same-tarball source:
|
|
64
|
+
// detectExperimentalStaleness() reports unchanged|stale with the differing files.
|
|
65
|
+
// (Blind preservation is what silently froze these pre-0.33.0 — issue #5: a base
|
|
66
|
+
// migration that changed experimental-skill behavior became a no-op.) Detection
|
|
67
|
+
// only — consent for a re-sync lives in the /upgrading skill layer, and
|
|
68
|
+
// load-bearing conventions are still promoted to base (FORGE.md) rather than
|
|
69
|
+
// left to a frozen skill. This preserve-list gates auto-clean only; staleness
|
|
70
|
+
// discovery is by directory scan of the shipped experimental/ tree.
|
|
51
71
|
const EXPERIMENTAL_SKILL_PATHS = ['.claude/skills/orchestrating'];
|
|
52
72
|
|
|
53
73
|
function isExperimentalSkillPath(displayPath) {
|
|
@@ -532,10 +552,14 @@ function extractDetectionBlock(guideContent) {
|
|
|
532
552
|
* pkgVersion (this package's package.json) — never the template settings.json
|
|
533
553
|
* literal, which is a placeholder the installer stamps at write time. Non-
|
|
534
554
|
* interactive: the bin only warns and points at the guide; it never applies.
|
|
555
|
+
*
|
|
556
|
+
* Returns the applying guides as [{version, guide, reason}] — the human warning
|
|
557
|
+
* and the --json `migrations` key render the same findings.
|
|
535
558
|
*/
|
|
536
559
|
function runPostUpgradeMigrationChecks(installedVersion) {
|
|
560
|
+
const found = [];
|
|
537
561
|
const migrationsDir = path.join(targetDir, '.forge', 'migrations');
|
|
538
|
-
if (!fs.existsSync(migrationsDir)) return;
|
|
562
|
+
if (!fs.existsSync(migrationsDir)) return found;
|
|
539
563
|
|
|
540
564
|
const installed =
|
|
541
565
|
installedVersion && installedVersion !== 'unknown' ? installedVersion : '0.0.0';
|
|
@@ -558,7 +582,7 @@ function runPostUpgradeMigrationChecks(installedVersion) {
|
|
|
558
582
|
)
|
|
559
583
|
.sort((a, b) => compareVersions(a.version, b.version));
|
|
560
584
|
} catch {
|
|
561
|
-
return;
|
|
585
|
+
return found;
|
|
562
586
|
}
|
|
563
587
|
|
|
564
588
|
for (const guide of guides) {
|
|
@@ -598,18 +622,22 @@ function runPostUpgradeMigrationChecks(installedVersion) {
|
|
|
598
622
|
? reasonLine.replace(/^.*MIGRATE\s*[—-]?\s*/, '').trim()
|
|
599
623
|
: '';
|
|
600
624
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
625
|
+
const hit = { version: guide.version, guide: `.forge/migrations/${guide.name}`, reason };
|
|
626
|
+
found.push(hit);
|
|
627
|
+
|
|
628
|
+
say(` ⚠ ${hit.version} migration available${hit.reason ? ` — ${hit.reason}` : ''}`);
|
|
629
|
+
say(' ─────────────────────────────────────────────────────────────');
|
|
630
|
+
say(` Guide: ${hit.guide}`);
|
|
631
|
+
say(
|
|
605
632
|
` (canonical: https://github.com/Attuned-Media/forge/blob/main/docs/migrations/${guide.name})`
|
|
606
633
|
);
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
634
|
+
say();
|
|
635
|
+
say(' Migrations are never applied automatically. In Claude Code:');
|
|
636
|
+
say(' /forge then hand the guide to quick-tasking, or invoke Skill(quick-tasking)');
|
|
637
|
+
say(' with the guide path as the task definition.');
|
|
638
|
+
say();
|
|
612
639
|
}
|
|
640
|
+
return found;
|
|
613
641
|
}
|
|
614
642
|
|
|
615
643
|
/**
|
|
@@ -632,8 +660,87 @@ function syncFrameworkFile(srcPath, destPath, displayName, results) {
|
|
|
632
660
|
}
|
|
633
661
|
}
|
|
634
662
|
|
|
663
|
+
/**
|
|
664
|
+
* Real staleness detection for opt-in experimental skills (FR-185). The tarball
|
|
665
|
+
* ships experimental/ (m10, conventions), so the bin diffs an installed
|
|
666
|
+
* experimental skill against its own same-tarball source. Discovery is by
|
|
667
|
+
* directory scan — an installed skill dir absent from the base template is a
|
|
668
|
+
* candidate; a matching {packageRoot}/experimental/{pkg}/source/skills/{name}/
|
|
669
|
+
* makes it reportable. A skill with no experimental source (a project-local
|
|
670
|
+
* skill) keeps today's preserved/removed-from-template verdicts and is not
|
|
671
|
+
* reported here. Detection ONLY: the bin never copies or overwrites an
|
|
672
|
+
* experimental skill — re-sync consent lives in the /upgrading skill (FR-186).
|
|
673
|
+
*
|
|
674
|
+
* Returns [{name, pkg, status: 'unchanged'|'stale', files: [differing .md]}].
|
|
675
|
+
*/
|
|
676
|
+
function detectExperimentalStaleness() {
|
|
677
|
+
const found = [];
|
|
678
|
+
const installedSkillsDir = path.join(targetDir, '.claude', 'skills');
|
|
679
|
+
const templateSkillsDir = path.join(templateDir, '.claude', 'skills');
|
|
680
|
+
const experimentalRoot = path.join(__dirname, '..', 'experimental');
|
|
681
|
+
|
|
682
|
+
let installed;
|
|
683
|
+
try {
|
|
684
|
+
installed = fs
|
|
685
|
+
.readdirSync(installedSkillsDir, { withFileTypes: true })
|
|
686
|
+
.filter((e) => e.isDirectory())
|
|
687
|
+
.map((e) => e.name);
|
|
688
|
+
} catch {
|
|
689
|
+
return found; // no installed skills dir — nothing to report
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
let pkgs = [];
|
|
693
|
+
try {
|
|
694
|
+
pkgs = fs
|
|
695
|
+
.readdirSync(experimentalRoot, { withFileTypes: true })
|
|
696
|
+
.filter((e) => e.isDirectory())
|
|
697
|
+
.map((e) => e.name);
|
|
698
|
+
} catch {
|
|
699
|
+
return found; // tarball without experimental/ — nothing to diff against
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
for (const name of installed) {
|
|
703
|
+
if (fs.existsSync(path.join(templateSkillsDir, name))) continue; // base skill
|
|
704
|
+
for (const pkg of pkgs) {
|
|
705
|
+
const srcDir = path.join(experimentalRoot, pkg, 'source', 'skills', name);
|
|
706
|
+
if (!fs.existsSync(srcDir)) continue;
|
|
707
|
+
const destDir = path.join(installedSkillsDir, name);
|
|
708
|
+
const differing = [];
|
|
709
|
+
for (const rel of collectFiles(srcDir, '')) {
|
|
710
|
+
if (!rel.endsWith('.md')) continue;
|
|
711
|
+
const destPath = path.join(destDir, rel);
|
|
712
|
+
if (
|
|
713
|
+
!fs.existsSync(destPath) ||
|
|
714
|
+
Buffer.compare(
|
|
715
|
+
fs.readFileSync(path.join(srcDir, rel)),
|
|
716
|
+
fs.readFileSync(destPath)
|
|
717
|
+
) !== 0
|
|
718
|
+
) {
|
|
719
|
+
differing.push(rel);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
found.push({
|
|
723
|
+
name,
|
|
724
|
+
pkg,
|
|
725
|
+
status: differing.length ? 'stale' : 'unchanged',
|
|
726
|
+
files: differing,
|
|
727
|
+
});
|
|
728
|
+
break; // first matching experimental pkg wins
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
return found;
|
|
732
|
+
}
|
|
733
|
+
|
|
635
734
|
async function upgrade() {
|
|
636
|
-
|
|
735
|
+
// A --json run must be non-interactive end-to-end: a prompt would corrupt the
|
|
736
|
+
// machine-read stream, so --json without --yes is refused before any work.
|
|
737
|
+
if (jsonMode && !yesMode) {
|
|
738
|
+
console.error(' ✖ --json requires --yes (a prompt would corrupt a machine run).\n');
|
|
739
|
+
emitJson({ error: 'yes_required', reason: '--json requires --yes' });
|
|
740
|
+
process.exit(1);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
say('\n Forge Upgrade\n');
|
|
637
744
|
|
|
638
745
|
// Verify Forge is installed
|
|
639
746
|
const settingsPath = path.join(targetDir, SETTINGS_FILE);
|
|
@@ -641,6 +748,12 @@ async function upgrade() {
|
|
|
641
748
|
console.error(
|
|
642
749
|
' Forge is not installed in this directory.\n Run `npx forge-orkes` first to install.\n'
|
|
643
750
|
);
|
|
751
|
+
if (jsonMode) {
|
|
752
|
+
emitJson({
|
|
753
|
+
error: 'not_installed',
|
|
754
|
+
reason: 'Forge is not installed in this directory — run `npx forge-orkes` first',
|
|
755
|
+
});
|
|
756
|
+
}
|
|
644
757
|
process.exit(1);
|
|
645
758
|
}
|
|
646
759
|
|
|
@@ -653,8 +766,8 @@ async function upgrade() {
|
|
|
653
766
|
// proceed with unknown version
|
|
654
767
|
}
|
|
655
768
|
|
|
656
|
-
|
|
657
|
-
|
|
769
|
+
say(` Installed: v${installedVersion}`);
|
|
770
|
+
say(` Available: v${pkgVersion}\n`);
|
|
658
771
|
|
|
659
772
|
// Downgrade guard: refuse to roll backward (overwrites newer framework files
|
|
660
773
|
// with older ones and deletes files newer versions added). Almost always a
|
|
@@ -669,6 +782,12 @@ async function upgrade() {
|
|
|
669
782
|
console.error(` This usually means a stale npx cache served an old forge-orkes.`);
|
|
670
783
|
console.error(` Fix: npx forge-orkes@latest upgrade (or clear the cache: rm -rf ~/.npm/_npx)`);
|
|
671
784
|
console.error(` To downgrade intentionally: forge-orkes upgrade --force\n`);
|
|
785
|
+
if (jsonMode) {
|
|
786
|
+
emitJson({
|
|
787
|
+
error: 'downgrade_refused',
|
|
788
|
+
reason: `installed v${installedVersion} is newer than available v${pkgVersion}`,
|
|
789
|
+
});
|
|
790
|
+
}
|
|
672
791
|
process.exit(1);
|
|
673
792
|
}
|
|
674
793
|
|
|
@@ -720,12 +839,9 @@ async function upgrade() {
|
|
|
720
839
|
// to yes (cleanup is the normal case); auto-confirms when non-interactive
|
|
721
840
|
// (CI / piped stdin) or with --force/--yes, preserving prior behavior.
|
|
722
841
|
if (pendingRemovals.length > 0) {
|
|
723
|
-
const auto =
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
!process.stdin.isTTY;
|
|
727
|
-
console.log(` ${pendingRemovals.length} stale framework file(s) no longer in the template:`);
|
|
728
|
-
for (const c of pendingRemovals) console.log(` ${c.displayPath}`);
|
|
842
|
+
const auto = force || yesMode || !process.stdin.isTTY;
|
|
843
|
+
say(` ${pendingRemovals.length} stale framework file(s) no longer in the template:`);
|
|
844
|
+
for (const c of pendingRemovals) say(` ${c.displayPath}`);
|
|
729
845
|
let go = auto;
|
|
730
846
|
if (!auto) {
|
|
731
847
|
const ans = await prompt(' Remove these stale framework files? [Y/n] ');
|
|
@@ -743,12 +859,12 @@ async function upgrade() {
|
|
|
743
859
|
results.removed.push(c.displayPath);
|
|
744
860
|
}
|
|
745
861
|
} else {
|
|
746
|
-
|
|
862
|
+
say(' Keeping stale files (skipped removal).');
|
|
747
863
|
for (const c of pendingRemovals) {
|
|
748
864
|
results.preserved.push(`${c.displayPath} (stale — kept by choice)`);
|
|
749
865
|
}
|
|
750
866
|
}
|
|
751
|
-
|
|
867
|
+
say();
|
|
752
868
|
}
|
|
753
869
|
|
|
754
870
|
// 2b. Sync the forge .gitignore. Shipped as `gitignore` (npm strips dotfiles
|
|
@@ -773,10 +889,10 @@ async function upgrade() {
|
|
|
773
889
|
const claudeStatus = ensureClaudeMdImport();
|
|
774
890
|
if (claudeStatus === 'migrated') {
|
|
775
891
|
results.updated.push('CLAUDE.md');
|
|
776
|
-
|
|
892
|
+
say(' CLAUDE.md migrated: forge section extracted to .forge/FORGE.md (@import)');
|
|
777
893
|
} else if (claudeStatus === 'appended') {
|
|
778
894
|
results.updated.push('CLAUDE.md');
|
|
779
|
-
|
|
895
|
+
say(' CLAUDE.md: @.forge/FORGE.md import line restored');
|
|
780
896
|
} else if (claudeStatus === 'unchanged') {
|
|
781
897
|
results.unchanged.push('CLAUDE.md');
|
|
782
898
|
} else if (claudeStatus === 'created') {
|
|
@@ -791,64 +907,88 @@ async function upgrade() {
|
|
|
791
907
|
results.unchanged.push(SETTINGS_FILE);
|
|
792
908
|
}
|
|
793
909
|
|
|
910
|
+
// 5. Diff installed experimental skills against their same-tarball sources
|
|
911
|
+
// (detection only — never written; re-sync consent lives in /upgrading).
|
|
912
|
+
const experimental = detectExperimentalStaleness();
|
|
913
|
+
|
|
794
914
|
// Report results
|
|
795
915
|
const totalChanges = results.updated.length + results.added.length;
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
916
|
+
const upToDate = totalChanges === 0 && results.removed.length === 0;
|
|
917
|
+
|
|
918
|
+
if (upToDate) {
|
|
919
|
+
say(' Already up to date.\n');
|
|
920
|
+
// Human mode preserves the historical early exit: an up-to-date project
|
|
921
|
+
// skips the migration checks and notices entirely. --json continues so the
|
|
922
|
+
// report document always carries every contract key.
|
|
923
|
+
if (!jsonMode) return;
|
|
924
|
+
} else {
|
|
925
|
+
if (results.updated.length > 0) {
|
|
926
|
+
say(` Updated (${results.updated.length}):`);
|
|
927
|
+
for (const f of results.updated) {
|
|
928
|
+
say(` ${f}`);
|
|
929
|
+
}
|
|
930
|
+
say();
|
|
806
931
|
}
|
|
807
|
-
console.log();
|
|
808
|
-
}
|
|
809
932
|
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
933
|
+
if (results.added.length > 0) {
|
|
934
|
+
say(` Added (${results.added.length}):`);
|
|
935
|
+
for (const f of results.added) {
|
|
936
|
+
say(` ${f}`);
|
|
937
|
+
}
|
|
938
|
+
say();
|
|
814
939
|
}
|
|
815
|
-
console.log();
|
|
816
|
-
}
|
|
817
940
|
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
941
|
+
if (results.removed.length > 0) {
|
|
942
|
+
say(` Cleaned up (${results.removed.length}):`);
|
|
943
|
+
for (const f of results.removed) {
|
|
944
|
+
say(` ${f} (removed — no longer in framework)`);
|
|
945
|
+
}
|
|
946
|
+
say();
|
|
822
947
|
}
|
|
823
|
-
console.log();
|
|
824
|
-
}
|
|
825
948
|
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
949
|
+
if (results.preserved.length > 0) {
|
|
950
|
+
say(` Preserved (${results.preserved.length}):`);
|
|
951
|
+
for (const f of results.preserved) {
|
|
952
|
+
say(` ${f} (kept — opt-in experimental skill)`);
|
|
953
|
+
}
|
|
954
|
+
for (const e of experimental) {
|
|
955
|
+
say(
|
|
956
|
+
e.status === 'stale'
|
|
957
|
+
? ` ↳ ${e.name} (${e.pkg}): stale — differs from the shipped source: ${e.files.join(', ')}`
|
|
958
|
+
: ` ↳ ${e.name} (${e.pkg}): up to date with the shipped source`
|
|
959
|
+
);
|
|
960
|
+
}
|
|
961
|
+
say(` ↳ re-sync via /upgrading (consent-gated; the bin never overwrites these)`);
|
|
962
|
+
say();
|
|
830
963
|
}
|
|
831
|
-
console.log(
|
|
832
|
-
` ↳ The npm upgrade does not ship experimental sources, so it cannot`
|
|
833
|
-
);
|
|
834
|
-
console.log(
|
|
835
|
-
` re-sync these. To refresh against a Forge clone, run the \`upgrading\``
|
|
836
|
-
);
|
|
837
|
-
console.log(
|
|
838
|
-
` skill (offers a re-sync), or re-run \`experimental/<pkg>/install.sh\`.`
|
|
839
|
-
);
|
|
840
|
-
console.log();
|
|
841
|
-
}
|
|
842
964
|
|
|
843
|
-
|
|
965
|
+
say(` Upgraded to v${pkgVersion}\n`);
|
|
966
|
+
}
|
|
844
967
|
|
|
845
968
|
// Pass the version captured BEFORE upgradeSettings() stamped the new one, so the
|
|
846
969
|
// migration range is (installed, source] against the freshly-synced guides.
|
|
847
|
-
runPostUpgradeMigrationChecks(installedVersion);
|
|
970
|
+
const migrations = runPostUpgradeMigrationChecks(installedVersion);
|
|
848
971
|
|
|
849
972
|
// Presence-gated advisories (no generation, no rebase — notices only).
|
|
850
|
-
noticeCodexAdapter();
|
|
851
|
-
noticeWorktreeLag();
|
|
973
|
+
const codexAdapterPresent = noticeCodexAdapter();
|
|
974
|
+
const worktrees = noticeWorktreeLag();
|
|
975
|
+
|
|
976
|
+
if (jsonMode) {
|
|
977
|
+
emitJson({
|
|
978
|
+
schemaVersion: 1,
|
|
979
|
+
command: 'upgrade',
|
|
980
|
+
installedVersion,
|
|
981
|
+
packageVersion: pkgVersion,
|
|
982
|
+
packageRoot: path.join(__dirname, '..'),
|
|
983
|
+
results,
|
|
984
|
+
claudeMd: claudeStatus,
|
|
985
|
+
settings: settingsStatus,
|
|
986
|
+
experimental,
|
|
987
|
+
migrations,
|
|
988
|
+
codexAdapterPresent,
|
|
989
|
+
worktrees,
|
|
990
|
+
});
|
|
991
|
+
}
|
|
852
992
|
}
|
|
853
993
|
|
|
854
994
|
/**
|
|
@@ -857,17 +997,21 @@ async function upgrade() {
|
|
|
857
997
|
* .claude/ sync and point to the regeneration route (/upgrading Step 4b). It
|
|
858
998
|
* never generates, never writes .agents/, never adds .agents to
|
|
859
999
|
* FRAMEWORK_OWNED_DIRS. Silent when no Codex adapter is present.
|
|
1000
|
+
*
|
|
1001
|
+
* Returns whether a Codex adapter is present — the human notice and the --json
|
|
1002
|
+
* `codexAdapterPresent` key render the same finding.
|
|
860
1003
|
*/
|
|
861
1004
|
function noticeCodexAdapter() {
|
|
862
1005
|
const hasCodex =
|
|
863
1006
|
fs.existsSync(path.join(targetDir, '.agents')) ||
|
|
864
1007
|
fs.existsSync(path.join(targetDir, '.codex'));
|
|
865
|
-
if (!hasCodex) return;
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
1008
|
+
if (!hasCodex) return false;
|
|
1009
|
+
say(' Codex adapter detected (.agents/ / .codex/).');
|
|
1010
|
+
say(' It may now be stale relative to the .claude/ source just synced.');
|
|
1011
|
+
say(' This installer does not regenerate it (no LLM). To refresh it,');
|
|
1012
|
+
say(' run the `/upgrading` skill — its Codex step offers regeneration');
|
|
1013
|
+
say(' from .forge/adapters/codex-generation.md.\n');
|
|
1014
|
+
return true;
|
|
871
1015
|
}
|
|
872
1016
|
|
|
873
1017
|
/**
|
|
@@ -876,6 +1020,9 @@ function noticeCodexAdapter() {
|
|
|
876
1020
|
* branch-point framework files until rebased. The bin only NOTICES — it never
|
|
877
1021
|
* rebases (a rebase can conflict). Silent when there are no live forge worktrees
|
|
878
1022
|
* or this is not a git repo.
|
|
1023
|
+
*
|
|
1024
|
+
* Returns the lagging worktrees as [{path, branch}] — the human notice and the
|
|
1025
|
+
* --json `worktrees` key render the same findings.
|
|
879
1026
|
*/
|
|
880
1027
|
function noticeWorktreeLag() {
|
|
881
1028
|
let out;
|
|
@@ -885,7 +1032,7 @@ function noticeWorktreeLag() {
|
|
|
885
1032
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
886
1033
|
}).toString();
|
|
887
1034
|
} catch {
|
|
888
|
-
return; // not a git repo / git unavailable
|
|
1035
|
+
return []; // not a git repo / git unavailable
|
|
889
1036
|
}
|
|
890
1037
|
const lagging = [];
|
|
891
1038
|
let curPath = null;
|
|
@@ -898,15 +1045,16 @@ function noticeWorktreeLag() {
|
|
|
898
1045
|
}
|
|
899
1046
|
}
|
|
900
1047
|
}
|
|
901
|
-
if (!lagging.length) return;
|
|
902
|
-
|
|
1048
|
+
if (!lagging.length) return lagging;
|
|
1049
|
+
say(` ${lagging.length} live Forge worktree(s) lag this upgrade:`);
|
|
903
1050
|
for (const w of lagging) {
|
|
904
|
-
|
|
1051
|
+
say(` ${w.path} (${w.branch})`);
|
|
905
1052
|
}
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
1053
|
+
say(' They keep their branch-point framework files until rebased.');
|
|
1054
|
+
say(' Pick up this upgrade in each: git -C <path> rebase main');
|
|
1055
|
+
say(' (Gitignored carve-outs like .mcp.json / experimental hooks do not');
|
|
1056
|
+
say(' rebase — re-run the experimental installer if the upgrade changed them.)\n');
|
|
1057
|
+
return lagging;
|
|
910
1058
|
}
|
|
911
1059
|
|
|
912
1060
|
// --- Entry point ---
|
|
@@ -931,18 +1079,21 @@ assertSaneTargetDir();
|
|
|
931
1079
|
|
|
932
1080
|
const subcommand = process.argv[2];
|
|
933
1081
|
|
|
1082
|
+
// Shared failure path for both upgrade entries: human error on stderr; under
|
|
1083
|
+
// --json also emit the {error, reason} document so a machine caller always
|
|
1084
|
+
// gets a parseable stdout even on an unexpected failure.
|
|
1085
|
+
function upgradeFailed(err) {
|
|
1086
|
+
console.error('Error:', err.message);
|
|
1087
|
+
if (jsonMode) emitJson({ error: 'upgrade_failed', reason: err.message });
|
|
1088
|
+
process.exit(1);
|
|
1089
|
+
}
|
|
1090
|
+
|
|
934
1091
|
if (subcommand === 'upgrade') {
|
|
935
|
-
upgrade().catch(
|
|
936
|
-
console.error('Error:', err.message);
|
|
937
|
-
process.exit(1);
|
|
938
|
-
});
|
|
1092
|
+
upgrade().catch(upgradeFailed);
|
|
939
1093
|
} else if (isForgeInstalled()) {
|
|
940
1094
|
// Auto-detect: Forge already installed, run upgrade instead
|
|
941
|
-
|
|
942
|
-
upgrade().catch(
|
|
943
|
-
console.error('Error:', err.message);
|
|
944
|
-
process.exit(1);
|
|
945
|
-
});
|
|
1095
|
+
say(' Forge detected — running upgrade automatically.\n');
|
|
1096
|
+
upgrade().catch(upgradeFailed);
|
|
946
1097
|
} else {
|
|
947
1098
|
install().catch((err) => {
|
|
948
1099
|
console.error('Error:', err.message);
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Forge Conventions — Stack Convention Skills + Rules (Experimental)
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
**Experimental, opt-in.** Per [ADR-018](../../../../docs/decisions/ADR-018-lightweight-policy-layer.md), Forge core is **stack-agnostic** — it ships only the always-on `agent-discipline.md` rule and zero stack-specific conventions. Anything tied to a particular framework (Laravel, Vue, Python) would make core non-portable, so it lives here as an opt-in pack you install **only when your project's stack matches**, exactly like the `m10` orchestration pack.
|
|
6
|
+
|
|
7
|
+
This pack is **not shipped on npm.** The `create-forge` tarball publishes only `bin/` + `template/` (same as `m10`). Convention packs are delivered via the dev repo or the `upgrading` skill's experimental route — clone/pull the Forge dev repo and run the installer against your project.
|
|
8
|
+
|
|
9
|
+
## What it is
|
|
10
|
+
|
|
11
|
+
Opt-in stack convention **skills** (heavy, task-triggered checklists) plus path-scoped **rules** (lightweight, file-path-triggered ambient reminders) for:
|
|
12
|
+
|
|
13
|
+
- **Laravel / PHP** — strict types, thin controllers, Services/Actions, Eloquent, migrations, jobs, security
|
|
14
|
+
- **Python** — type hints, Pydantic v2, async patterns, Django/FastAPI, DI via Protocols, tooling
|
|
15
|
+
- **Vue / Inertia** — `<script setup lang="ts">`, generated-type contracts, `useForm`, accessibility
|
|
16
|
+
- **Universal testing & code quality** — behaviour-not-implementation tests, function-size/magic-value discipline
|
|
17
|
+
|
|
18
|
+
The skills load when a task description matches; the path-scoped rules cost **zero context** until you touch a matching file (`app/Http/**`, `**/*.vue`, `**/*.py`, …). See the rules-layer design in [ADR-018](../../../../docs/decisions/ADR-018-lightweight-policy-layer.md) and `.claude/rules/README.md` in core.
|
|
19
|
+
|
|
20
|
+
These conventions are de-coupled from any single company's setup: multi-tenancy is treated as conditional ("multi-tenant apps") rather than assumed, and the Laravel layout supports both **standard** Laravel (`app/Http/**`, `app/Services/**`) and a nested **module** layout (`app/<project>/<module>/Http/**`) — the rules are dual-globbed so they fire on either.
|
|
21
|
+
|
|
22
|
+
## What it installs
|
|
23
|
+
|
|
24
|
+
Into the **target project's** `.claude/`:
|
|
25
|
+
|
|
26
|
+
### Skills → `.claude/skills/`
|
|
27
|
+
| Skill | Stack |
|
|
28
|
+
|---|---|
|
|
29
|
+
| `laravel-conventions/` | laravel |
|
|
30
|
+
| `python-conventions/` | python |
|
|
31
|
+
| `vue-conventions/` | vue |
|
|
32
|
+
| `testing-standards/` | testing |
|
|
33
|
+
|
|
34
|
+
### Rules → `.claude/rules/`
|
|
35
|
+
| Rule | Stack | Scope (`paths:`) |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| `laravel-http.md` | laravel | `app/Http/**`, `app/*/*/Http/**` |
|
|
38
|
+
| `laravel-services.md` | laravel | `app/Services/**`, `app/Actions/**` (+ module globs) |
|
|
39
|
+
| `laravel-jobs.md` | laravel | `app/Jobs/**`, `app/*/*/Jobs/**` |
|
|
40
|
+
| `laravel-migrations.md` | laravel | `database/migrations/**` (+ module glob) |
|
|
41
|
+
| `vue-inertia.md` | vue | `resources/js/pages/**`, `resources/js/components/**`, `**/*.vue` |
|
|
42
|
+
| `python.md` | python | `**/*.py` |
|
|
43
|
+
| `code-quality.md` | universal | always-on (no `paths:`) |
|
|
44
|
+
| `testing.md` | universal / testing | always-on (no `paths:`) |
|
|
45
|
+
|
|
46
|
+
**Why `code-quality.md` and `testing.md` are included (and additive, not redundant with core):** core ships only `agent-discipline.md`, which covers *agent* failure modes (scope creep, phantom deps, hallucinated APIs). It does **not** cover code *shape* (function/file size, magic values, nesting) or the ambient testing floor (AAA, one-behaviour-per-test, no flaky retries). Both are universal and stack-agnostic, so they ship here as always-on rules that complement core. `agent-discipline.md` itself is **not** re-shipped — it already lives in core, so installing it again would clobber/duplicate.
|
|
47
|
+
|
|
48
|
+
## Install
|
|
49
|
+
|
|
50
|
+
Run from your project root (the directory containing `.forge/project.yml`):
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# All stacks:
|
|
54
|
+
bash experimental/conventions/install.sh
|
|
55
|
+
|
|
56
|
+
# Selectively — only the stacks you use:
|
|
57
|
+
bash experimental/conventions/install.sh laravel vue
|
|
58
|
+
bash experimental/conventions/install.sh python
|
|
59
|
+
bash experimental/conventions/install.sh testing universal
|
|
60
|
+
|
|
61
|
+
# Explicit target path + stacks:
|
|
62
|
+
bash experimental/conventions/install.sh /path/to/your/project laravel
|
|
63
|
+
|
|
64
|
+
# Preview without writing:
|
|
65
|
+
bash experimental/conventions/install.sh --dry-run laravel
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Stacks: `laravel`, `python`, `vue`, `testing`, `universal`. No stack args = install all.
|
|
69
|
+
|
|
70
|
+
**Idempotent and non-clobbering.** If a target skill or rule of the same name already exists in `.claude/`, the installer **skips it and warns** rather than overwriting — this avoids the half-install / stale-skill trap (a same-named skill present from a prior install or your own authoring is never silently replaced). To intentionally refresh a stale file, delete the existing one first, then re-run.
|
|
71
|
+
|
|
72
|
+
## Uninstall
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# Remove everything this pack installed:
|
|
76
|
+
bash experimental/conventions/uninstall.sh
|
|
77
|
+
|
|
78
|
+
# Remove only specific stacks:
|
|
79
|
+
bash experimental/conventions/uninstall.sh laravel
|
|
80
|
+
bash experimental/conventions/uninstall.sh /path/to/your/project vue
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The uninstaller only touches files this pack ships; your own skills/rules are left alone. Note: `testing.md` is shared by the `testing` and `universal` stacks, so removing either stack removes it.
|
|
84
|
+
|
|
85
|
+
## Selective install rationale
|
|
86
|
+
|
|
87
|
+
Install only the stacks a project actually uses. A pure-Python service has no business carrying Laravel HTTP rules; a Vue+Laravel app skips `python`. Path-scoped rules cost zero context until a matching file is touched, but keeping the install scoped to your stack keeps `.claude/` clean and the skill list short.
|