everything-claude-code 1.4.3 → 1.4.4
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/crates/ecc-kernel/src/main.rs +254 -5
- package/docs/ecc-kernel-protocol.md +106 -0
- package/docs/ecc-release.md +82 -0
- package/package.json +3 -1
- package/scripts/ecc/git.js +26 -0
- package/scripts/ecc/install-kernel.js +40 -1
- package/scripts/ecc/kernel-contract.js +63 -0
- package/scripts/ecc/kernel.js +137 -26
- package/scripts/ecc.js +34 -21
|
@@ -7,6 +7,17 @@ use std::process::{Command, ExitCode, Stdio};
|
|
|
7
7
|
use time::format_description::well_known::Rfc3339;
|
|
8
8
|
use time::OffsetDateTime;
|
|
9
9
|
|
|
10
|
+
const PROTOCOL_VERSION: i32 = 1;
|
|
11
|
+
const COMMANDS: &[&str] = &[
|
|
12
|
+
"worktree.ensure",
|
|
13
|
+
"worktree.remove",
|
|
14
|
+
"patch.apply",
|
|
15
|
+
"git.commit_all",
|
|
16
|
+
"verify.run",
|
|
17
|
+
"protocol.version",
|
|
18
|
+
"repo.info",
|
|
19
|
+
];
|
|
20
|
+
|
|
10
21
|
fn now_iso() -> String {
|
|
11
22
|
OffsetDateTime::now_utc()
|
|
12
23
|
.format(&Rfc3339)
|
|
@@ -502,6 +513,127 @@ fn commit_all(input: CommitAllIn) -> Result<CommitAllOut, String> {
|
|
|
502
513
|
})
|
|
503
514
|
}
|
|
504
515
|
|
|
516
|
+
#[derive(Deserialize)]
|
|
517
|
+
struct ProtocolVersionIn {}
|
|
518
|
+
|
|
519
|
+
#[derive(Serialize)]
|
|
520
|
+
struct ProtocolVersionOut {
|
|
521
|
+
version: i32,
|
|
522
|
+
protocol: i32,
|
|
523
|
+
kernelVersion: String,
|
|
524
|
+
commands: Vec<String>,
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
fn protocol_version(_input: ProtocolVersionIn) -> Result<ProtocolVersionOut, String> {
|
|
528
|
+
Ok(ProtocolVersionOut {
|
|
529
|
+
version: 1,
|
|
530
|
+
protocol: PROTOCOL_VERSION,
|
|
531
|
+
kernelVersion: env!("CARGO_PKG_VERSION").to_string(),
|
|
532
|
+
commands: COMMANDS.iter().map(|s| s.to_string()).collect(),
|
|
533
|
+
})
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
#[derive(Deserialize)]
|
|
537
|
+
struct RepoInfoIn {
|
|
538
|
+
cwd: String,
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
#[derive(Serialize)]
|
|
542
|
+
struct RepoInfoOut {
|
|
543
|
+
version: i32,
|
|
544
|
+
repoRoot: Option<String>,
|
|
545
|
+
branch: String,
|
|
546
|
+
sha: String,
|
|
547
|
+
clean: bool,
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
fn repo_info(input: RepoInfoIn) -> Result<RepoInfoOut, String> {
|
|
551
|
+
let cwd = PathBuf::from(input.cwd);
|
|
552
|
+
|
|
553
|
+
let out_root = run_git(
|
|
554
|
+
&vec![
|
|
555
|
+
"-C".to_string(),
|
|
556
|
+
cwd.display().to_string(),
|
|
557
|
+
"rev-parse".to_string(),
|
|
558
|
+
"--show-toplevel".to_string(),
|
|
559
|
+
],
|
|
560
|
+
None,
|
|
561
|
+
)?;
|
|
562
|
+
|
|
563
|
+
if !out_root.ok {
|
|
564
|
+
return Ok(RepoInfoOut {
|
|
565
|
+
version: 1,
|
|
566
|
+
repoRoot: None,
|
|
567
|
+
branch: "".to_string(),
|
|
568
|
+
sha: "".to_string(),
|
|
569
|
+
clean: false,
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
let repo_root = out_root.stdout.trim().to_string();
|
|
574
|
+
|
|
575
|
+
let out_branch = run_git(
|
|
576
|
+
&vec![
|
|
577
|
+
"-C".to_string(),
|
|
578
|
+
repo_root.clone(),
|
|
579
|
+
"rev-parse".to_string(),
|
|
580
|
+
"--abbrev-ref".to_string(),
|
|
581
|
+
"HEAD".to_string(),
|
|
582
|
+
],
|
|
583
|
+
None,
|
|
584
|
+
)?;
|
|
585
|
+
if !out_branch.ok {
|
|
586
|
+
return Err(out_branch
|
|
587
|
+
.stderr
|
|
588
|
+
.is_empty()
|
|
589
|
+
.then(|| "git rev-parse --abbrev-ref HEAD failed".to_string())
|
|
590
|
+
.unwrap_or(out_branch.stderr));
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
let out_sha = run_git(
|
|
594
|
+
&vec![
|
|
595
|
+
"-C".to_string(),
|
|
596
|
+
repo_root.clone(),
|
|
597
|
+
"rev-parse".to_string(),
|
|
598
|
+
"HEAD".to_string(),
|
|
599
|
+
],
|
|
600
|
+
None,
|
|
601
|
+
)?;
|
|
602
|
+
if !out_sha.ok {
|
|
603
|
+
return Err(out_sha
|
|
604
|
+
.stderr
|
|
605
|
+
.is_empty()
|
|
606
|
+
.then(|| "git rev-parse HEAD failed".to_string())
|
|
607
|
+
.unwrap_or(out_sha.stderr));
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
let out_status = run_git(
|
|
611
|
+
&vec![
|
|
612
|
+
"-C".to_string(),
|
|
613
|
+
repo_root.clone(),
|
|
614
|
+
"status".to_string(),
|
|
615
|
+
"--porcelain".to_string(),
|
|
616
|
+
"--untracked-files=no".to_string(),
|
|
617
|
+
],
|
|
618
|
+
None,
|
|
619
|
+
)?;
|
|
620
|
+
if !out_status.ok {
|
|
621
|
+
return Err(out_status
|
|
622
|
+
.stderr
|
|
623
|
+
.is_empty()
|
|
624
|
+
.then(|| "git status --porcelain failed".to_string())
|
|
625
|
+
.unwrap_or(out_status.stderr));
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
Ok(RepoInfoOut {
|
|
629
|
+
version: 1,
|
|
630
|
+
repoRoot: Some(repo_root),
|
|
631
|
+
branch: out_branch.stdout.trim().to_string(),
|
|
632
|
+
sha: out_sha.stdout.trim().to_string(),
|
|
633
|
+
clean: out_status.stdout.trim().is_empty(),
|
|
634
|
+
})
|
|
635
|
+
}
|
|
636
|
+
|
|
505
637
|
#[derive(Deserialize)]
|
|
506
638
|
struct VerifyCmdIn {
|
|
507
639
|
name: String,
|
|
@@ -628,6 +760,7 @@ fn verify_run(input: VerifyRunIn) -> Result<VerifySummaryOut, String> {
|
|
|
628
760
|
}
|
|
629
761
|
|
|
630
762
|
fn usage() {
|
|
763
|
+
let cmds = COMMANDS.join("\n ");
|
|
631
764
|
eprintln!(
|
|
632
765
|
r#"ecc-kernel
|
|
633
766
|
|
|
@@ -635,12 +768,10 @@ Usage:
|
|
|
635
768
|
ecc-kernel <command> (JSON input on stdin; JSON output on stdout)
|
|
636
769
|
|
|
637
770
|
Commands:
|
|
638
|
-
|
|
639
|
-
worktree.remove
|
|
640
|
-
patch.apply
|
|
641
|
-
git.commit_all
|
|
642
|
-
verify.run
|
|
771
|
+
{cmds}
|
|
643
772
|
"#
|
|
773
|
+
,
|
|
774
|
+
cmds = cmds
|
|
644
775
|
);
|
|
645
776
|
}
|
|
646
777
|
|
|
@@ -686,6 +817,16 @@ fn real_main() -> Result<(), String> {
|
|
|
686
817
|
let out = commit_all(input)?;
|
|
687
818
|
write_stdout_json(&out)
|
|
688
819
|
}
|
|
820
|
+
"protocol.version" => {
|
|
821
|
+
let input: ProtocolVersionIn = read_stdin_json()?;
|
|
822
|
+
let out = protocol_version(input)?;
|
|
823
|
+
write_stdout_json(&out)
|
|
824
|
+
}
|
|
825
|
+
"repo.info" => {
|
|
826
|
+
let input: RepoInfoIn = read_stdin_json()?;
|
|
827
|
+
let out = repo_info(input)?;
|
|
828
|
+
write_stdout_json(&out)
|
|
829
|
+
}
|
|
689
830
|
"verify.run" => {
|
|
690
831
|
let input: VerifyRunIn = read_stdin_json()?;
|
|
691
832
|
let out = verify_run(input)?;
|
|
@@ -708,3 +849,111 @@ fn main() -> ExitCode {
|
|
|
708
849
|
}
|
|
709
850
|
}
|
|
710
851
|
}
|
|
852
|
+
|
|
853
|
+
#[cfg(test)]
|
|
854
|
+
mod tests {
|
|
855
|
+
use super::*;
|
|
856
|
+
|
|
857
|
+
fn tmp_dir(prefix: &str) -> PathBuf {
|
|
858
|
+
let mut dir = std::env::temp_dir();
|
|
859
|
+
dir.push(format!(
|
|
860
|
+
"{}-{}-{}",
|
|
861
|
+
prefix,
|
|
862
|
+
std::process::id(),
|
|
863
|
+
OffsetDateTime::now_utc().unix_timestamp_nanos()
|
|
864
|
+
));
|
|
865
|
+
dir
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
fn git(repo: &Path, args: &[&str]) -> Result<CmdOut, String> {
|
|
869
|
+
let mut v: Vec<String> = Vec::new();
|
|
870
|
+
v.push("-C".to_string());
|
|
871
|
+
v.push(repo.display().to_string());
|
|
872
|
+
for a in args {
|
|
873
|
+
v.push(a.to_string());
|
|
874
|
+
}
|
|
875
|
+
run_git(&v, None)
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
fn init_git_repo(repo: &Path) -> Result<(), String> {
|
|
879
|
+
fs::create_dir_all(repo).map_err(|e| format!("mkdir failed: {e}"))?;
|
|
880
|
+
let out = git(repo, &["init"])?;
|
|
881
|
+
if !out.ok {
|
|
882
|
+
return Err(out.stderr);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// Ensure commits work in CI.
|
|
886
|
+
let _ = git(repo, &["config", "user.email", "ecc@example.com"])?;
|
|
887
|
+
let _ = git(repo, &["config", "user.name", "ECC"])?;
|
|
888
|
+
|
|
889
|
+
fs::write(repo.join("base.txt"), "base\n").map_err(|e| format!("write failed: {e}"))?;
|
|
890
|
+
let out_add = git(repo, &["add", "-A"])?;
|
|
891
|
+
if !out_add.ok {
|
|
892
|
+
return Err(out_add.stderr);
|
|
893
|
+
}
|
|
894
|
+
let out_commit = git(repo, &["commit", "-m", "init"])?;
|
|
895
|
+
if !out_commit.ok {
|
|
896
|
+
return Err(out_commit.stderr);
|
|
897
|
+
}
|
|
898
|
+
Ok(())
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
#[test]
|
|
902
|
+
fn protocol_version_has_required_fields() {
|
|
903
|
+
let out = protocol_version(ProtocolVersionIn {}).unwrap();
|
|
904
|
+
assert_eq!(out.version, 1);
|
|
905
|
+
assert_eq!(out.protocol, PROTOCOL_VERSION);
|
|
906
|
+
assert!(!out.kernelVersion.trim().is_empty());
|
|
907
|
+
|
|
908
|
+
let cmds: std::collections::BTreeSet<String> = out.commands.into_iter().collect();
|
|
909
|
+
for c in COMMANDS.iter() {
|
|
910
|
+
assert!(cmds.contains(&c.to_string()), "missing command: {c}");
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
#[test]
|
|
915
|
+
fn repo_info_returns_repo_details_and_cleanliness() {
|
|
916
|
+
let dir = tmp_dir("ecc-kernel-test-repo");
|
|
917
|
+
let res = init_git_repo(&dir);
|
|
918
|
+
assert!(res.is_ok(), "init_git_repo failed: {:?}", res.err());
|
|
919
|
+
|
|
920
|
+
let info = repo_info(RepoInfoIn {
|
|
921
|
+
cwd: dir.display().to_string(),
|
|
922
|
+
})
|
|
923
|
+
.unwrap();
|
|
924
|
+
|
|
925
|
+
assert_eq!(info.version, 1);
|
|
926
|
+
let got_root = info.repoRoot.clone().unwrap();
|
|
927
|
+
let expected_abs = fs::canonicalize(&dir).unwrap();
|
|
928
|
+
let got_abs = fs::canonicalize(std::path::Path::new(&got_root)).unwrap();
|
|
929
|
+
assert_eq!(got_abs, expected_abs);
|
|
930
|
+
assert!(!info.branch.trim().is_empty());
|
|
931
|
+
assert_eq!(info.sha.len(), 40);
|
|
932
|
+
assert_eq!(info.clean, true);
|
|
933
|
+
|
|
934
|
+
// Modifying a tracked file should mark repo as dirty.
|
|
935
|
+
fs::write(dir.join("base.txt"), "changed\n").unwrap();
|
|
936
|
+
let info2 = repo_info(RepoInfoIn {
|
|
937
|
+
cwd: dir.display().to_string(),
|
|
938
|
+
})
|
|
939
|
+
.unwrap();
|
|
940
|
+
assert_eq!(info2.clean, false);
|
|
941
|
+
|
|
942
|
+
let _ = fs::remove_dir_all(&dir);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
#[test]
|
|
946
|
+
fn repo_info_outside_repo_is_null() {
|
|
947
|
+
let dir = tmp_dir("ecc-kernel-test-norepo");
|
|
948
|
+
fs::create_dir_all(&dir).unwrap();
|
|
949
|
+
|
|
950
|
+
let info = repo_info(RepoInfoIn {
|
|
951
|
+
cwd: dir.display().to_string(),
|
|
952
|
+
})
|
|
953
|
+
.unwrap();
|
|
954
|
+
assert_eq!(info.version, 1);
|
|
955
|
+
assert!(info.repoRoot.is_none());
|
|
956
|
+
|
|
957
|
+
let _ = fs::remove_dir_all(&dir);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# ecc-kernel Protocol
|
|
2
|
+
|
|
3
|
+
`ecc-kernel` is a small Rust binary that offloads ECC "kernel" operations:
|
|
4
|
+
- git worktree creation/removal
|
|
5
|
+
- safe patch apply with ownership checks
|
|
6
|
+
- verify command runner (writes evidence files)
|
|
7
|
+
|
|
8
|
+
All commands use:
|
|
9
|
+
- **stdin**: JSON
|
|
10
|
+
- **stdout**: JSON
|
|
11
|
+
- **stderr**: human-readable diagnostics
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Versioning and Compatibility
|
|
16
|
+
|
|
17
|
+
- Node and kernel communicate via a protocol handshake.
|
|
18
|
+
- Node expects `protocol=1` and the required command set.
|
|
19
|
+
- `ECC_KERNEL=auto` falls back to JS if handshake fails.
|
|
20
|
+
- `ECC_KERNEL=rust` fails fast if handshake fails.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Command: `protocol.version`
|
|
25
|
+
|
|
26
|
+
### Input (stdin JSON)
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Output (stdout JSON)
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"version": 1,
|
|
37
|
+
"protocol": 1,
|
|
38
|
+
"kernelVersion": "0.1.0",
|
|
39
|
+
"commands": [
|
|
40
|
+
"worktree.ensure",
|
|
41
|
+
"worktree.remove",
|
|
42
|
+
"patch.apply",
|
|
43
|
+
"git.commit_all",
|
|
44
|
+
"verify.run",
|
|
45
|
+
"protocol.version",
|
|
46
|
+
"repo.info"
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Fields:
|
|
52
|
+
- `version`: output schema version (currently `1`)
|
|
53
|
+
- `protocol`: protocol version (currently `1`)
|
|
54
|
+
- `kernelVersion`: Rust crate version
|
|
55
|
+
- `commands`: supported command identifiers
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Command: `repo.info`
|
|
60
|
+
|
|
61
|
+
Returns git repo information for a given `cwd`.
|
|
62
|
+
|
|
63
|
+
### Input
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{ "cwd": "/path/to/dir" }
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Output (inside a git repo)
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"version": 1,
|
|
74
|
+
"repoRoot": "/path/to/repo",
|
|
75
|
+
"branch": "main",
|
|
76
|
+
"sha": "0123456789abcdef0123456789abcdef01234567",
|
|
77
|
+
"clean": true
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Output (not a git repo)
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{
|
|
85
|
+
"version": 1,
|
|
86
|
+
"repoRoot": null,
|
|
87
|
+
"branch": "",
|
|
88
|
+
"sha": "",
|
|
89
|
+
"clean": false
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Notes:
|
|
94
|
+
- `clean` ignores untracked files (equivalent to `git status --porcelain --untracked-files=no`).
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Other Commands
|
|
99
|
+
|
|
100
|
+
The remaining commands are internal engine plumbing and are documented by source:
|
|
101
|
+
- `worktree.ensure`
|
|
102
|
+
- `worktree.remove`
|
|
103
|
+
- `patch.apply`
|
|
104
|
+
- `git.commit_all`
|
|
105
|
+
- `verify.run`
|
|
106
|
+
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# ECC Release Guide
|
|
2
|
+
|
|
3
|
+
This repo ships ECC as a Node CLI with an optional Rust kernel (`ecc-kernel`).
|
|
4
|
+
|
|
5
|
+
The release chain is designed to make installs reliable:
|
|
6
|
+
- Tag push builds and uploads prebuilt kernels to a GitHub Release
|
|
7
|
+
- npm publish validates those assets exist (so installs can download them)
|
|
8
|
+
- Release smoke tests validate real install + download + run across OSes
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Versioning
|
|
13
|
+
|
|
14
|
+
- `package.json.version` must match the git tag: `v${version}`
|
|
15
|
+
- Use patch bumps by default.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Release Steps (vX.Y.Z)
|
|
20
|
+
|
|
21
|
+
1. Ensure `main` is green (CI passes).
|
|
22
|
+
2. Bump version (on `main`):
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm version patch --no-git-tag-version
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
3. Commit:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
git commit -am "chore(release): vX.Y.Z"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
4. Tag + push:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
git tag vX.Y.Z
|
|
38
|
+
git push origin main
|
|
39
|
+
git push origin vX.Y.Z
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
5. Verify GitHub Release:
|
|
43
|
+
- Workflow: `.github/workflows/release.yml`
|
|
44
|
+
- Assets must include `ecc-kernel-<os>-<arch>[.exe]` plus `.sha256` files.
|
|
45
|
+
|
|
46
|
+
6. Verify Release Smoke:
|
|
47
|
+
- Workflow: `.github/workflows/release-smoke.yml`
|
|
48
|
+
- Runs a real install from a packed tarball and exercises:
|
|
49
|
+
- `ECC_KERNEL_INSTALL=required` + `ECC_KERNEL=rust`
|
|
50
|
+
- `ECC_KERNEL_INSTALL=0` + `ECC_KERNEL=node`
|
|
51
|
+
|
|
52
|
+
7. Publish to npm:
|
|
53
|
+
- Workflow: `.github/workflows/publish-npm.yml`
|
|
54
|
+
- Uses npm Trusted Publishing (OIDC) if configured, with optional `NPM_TOKEN` fallback.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Troubleshooting
|
|
59
|
+
|
|
60
|
+
### Release created but assets missing / 404 during install
|
|
61
|
+
- Kernel upload can be briefly eventual-consistent.
|
|
62
|
+
- Re-run smoke (workflow_dispatch) after a minute:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
gh workflow run "Release Smoke" -f tag=vX.Y.Z
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### npm publish fails (OIDC not configured)
|
|
69
|
+
- Configure npm "Trusted Publisher" for:
|
|
70
|
+
- owner: `sumulige`
|
|
71
|
+
- repo: `everything-claude-code`
|
|
72
|
+
- workflow file: `publish-npm.yml`
|
|
73
|
+
- Re-run publish:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
gh workflow run "Publish (npm)" -f tag=vX.Y.Z
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Rollback
|
|
80
|
+
- If a bad version is published to npm: publish a new patch version.
|
|
81
|
+
- If a GitHub Release is broken: re-run `Release` workflow for the same tag, or create a new tag.
|
|
82
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "everything-claude-code",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.4",
|
|
4
4
|
"description": "Everything Claude Code + ECC (Engineering Change Conveyor) CLI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
"schemas/",
|
|
28
28
|
"prompts/ecc/",
|
|
29
29
|
"docs/ecc.md",
|
|
30
|
+
"docs/ecc-release.md",
|
|
31
|
+
"docs/ecc-kernel-protocol.md",
|
|
30
32
|
"README.md",
|
|
31
33
|
"LICENSE"
|
|
32
34
|
],
|
package/scripts/ecc/git.js
CHANGED
|
@@ -4,6 +4,7 @@ const path = require('path');
|
|
|
4
4
|
const { spawnSync } = require('child_process');
|
|
5
5
|
|
|
6
6
|
const { runKernel } = require('./kernel');
|
|
7
|
+
const { validateRepoInfoOutput } = require('./kernel-contract');
|
|
7
8
|
|
|
8
9
|
function runGit(args, opts = {}) {
|
|
9
10
|
const res = spawnSync('git', args, {
|
|
@@ -123,9 +124,34 @@ function commitAll({ repoRoot, message }) {
|
|
|
123
124
|
return sha;
|
|
124
125
|
}
|
|
125
126
|
|
|
127
|
+
function getRepoInfo(cwd) {
|
|
128
|
+
const kernelOut = runKernel('repo.info', { cwd });
|
|
129
|
+
if (kernelOut !== null) {
|
|
130
|
+
const errors = validateRepoInfoOutput(kernelOut);
|
|
131
|
+
if (errors.length) {
|
|
132
|
+
throw new Error(`ecc-kernel repo.info returned invalid output: ${errors.join('; ')}`);
|
|
133
|
+
}
|
|
134
|
+
return kernelOut;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const repoRoot = getRepoRoot(cwd);
|
|
138
|
+
if (!repoRoot) {
|
|
139
|
+
return { version: 1, repoRoot: null, branch: '', sha: '', clean: false };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
version: 1,
|
|
144
|
+
repoRoot,
|
|
145
|
+
branch: getCurrentBranch(repoRoot),
|
|
146
|
+
sha: getHeadSha(repoRoot),
|
|
147
|
+
clean: isClean(repoRoot)
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
126
151
|
module.exports = {
|
|
127
152
|
runGit,
|
|
128
153
|
getRepoRoot,
|
|
154
|
+
getRepoInfo,
|
|
129
155
|
getHeadSha,
|
|
130
156
|
getCurrentBranch,
|
|
131
157
|
isClean,
|
|
@@ -21,6 +21,8 @@ const crypto = require('crypto');
|
|
|
21
21
|
const { spawnSync } = require('child_process');
|
|
22
22
|
const { URL } = require('url');
|
|
23
23
|
|
|
24
|
+
const { EXPECTED_PROTOCOL, validateProtocolVersionOutput } = require('./kernel-contract');
|
|
25
|
+
|
|
24
26
|
function debugEnabled() {
|
|
25
27
|
return !!(process.env.ECC_KERNEL_DEBUG && String(process.env.ECC_KERNEL_DEBUG).trim());
|
|
26
28
|
}
|
|
@@ -247,6 +249,34 @@ function validateRuns(filePath) {
|
|
|
247
249
|
}
|
|
248
250
|
}
|
|
249
251
|
|
|
252
|
+
function validateProtocol(filePath) {
|
|
253
|
+
const res = spawnSync(filePath, ['protocol.version'], {
|
|
254
|
+
encoding: 'utf8',
|
|
255
|
+
input: '{}',
|
|
256
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
257
|
+
});
|
|
258
|
+
if (res.error) throw new Error(`kernel protocol.version failed: ${res.error.message}`);
|
|
259
|
+
if (res.status !== 0) {
|
|
260
|
+
const stderr = (res.stderr || '').trim();
|
|
261
|
+
throw new Error(`kernel protocol.version failed (exit ${res.status})${stderr ? `: ${stderr}` : ''}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const stdout = (res.stdout || '').trim();
|
|
265
|
+
if (!stdout) throw new Error('kernel protocol.version returned empty output');
|
|
266
|
+
|
|
267
|
+
let obj;
|
|
268
|
+
try {
|
|
269
|
+
obj = JSON.parse(stdout);
|
|
270
|
+
} catch (_err) {
|
|
271
|
+
throw new Error('kernel protocol.version returned non-JSON output');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const errors = validateProtocolVersionOutput(obj, { expectedProtocol: EXPECTED_PROTOCOL });
|
|
275
|
+
if (errors.length) {
|
|
276
|
+
throw new Error(`kernel protocol.version invalid: ${errors.join('; ')}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
250
280
|
async function main() {
|
|
251
281
|
const { mode, envSet } = parseInstallMode();
|
|
252
282
|
if (mode === 'off') return;
|
|
@@ -273,7 +303,15 @@ async function main() {
|
|
|
273
303
|
|
|
274
304
|
if (fs.existsSync(destBin)) {
|
|
275
305
|
logDebug(`kernel already present: ${destBin}`);
|
|
276
|
-
|
|
306
|
+
try {
|
|
307
|
+
ensureExecutable(destBin);
|
|
308
|
+
validateRuns(destBin);
|
|
309
|
+
validateProtocol(destBin);
|
|
310
|
+
return;
|
|
311
|
+
} catch (err) {
|
|
312
|
+
logDebug(`existing kernel invalid/outdated, reinstalling: ${err.message}`);
|
|
313
|
+
try { fs.rmSync(destBin, { force: true }); } catch (_err) { /* ignore */ }
|
|
314
|
+
}
|
|
277
315
|
}
|
|
278
316
|
|
|
279
317
|
const baseUrl = defaultBaseUrl(version, pkg);
|
|
@@ -327,6 +365,7 @@ async function main() {
|
|
|
327
365
|
}
|
|
328
366
|
|
|
329
367
|
validateRuns(destBin);
|
|
368
|
+
validateProtocol(destBin);
|
|
330
369
|
} catch (err) {
|
|
331
370
|
try { fs.rmSync(destBin, { force: true }); } catch (_err) { /* ignore */ }
|
|
332
371
|
if (mode === 'required') throw err;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
function isObj(v) {
|
|
2
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function isInt(v) {
|
|
6
|
+
return Number.isInteger(v);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const EXPECTED_PROTOCOL = 1;
|
|
10
|
+
|
|
11
|
+
// Commands that the Node engine expects the Rust kernel to support.
|
|
12
|
+
const REQUIRED_COMMANDS = [
|
|
13
|
+
'worktree.ensure',
|
|
14
|
+
'worktree.remove',
|
|
15
|
+
'patch.apply',
|
|
16
|
+
'git.commit_all',
|
|
17
|
+
'verify.run',
|
|
18
|
+
'protocol.version',
|
|
19
|
+
'repo.info'
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
function validateProtocolVersionOutput(obj, { expectedProtocol = EXPECTED_PROTOCOL } = {}) {
|
|
23
|
+
const errors = [];
|
|
24
|
+
if (!isObj(obj)) return ['expected object'];
|
|
25
|
+
|
|
26
|
+
if (obj.version !== 1) errors.push('expected version: 1');
|
|
27
|
+
|
|
28
|
+
if (!isInt(obj.protocol)) errors.push('expected protocol: integer');
|
|
29
|
+
else if (obj.protocol !== expectedProtocol) errors.push(`protocol mismatch: expected ${expectedProtocol}, got ${obj.protocol}`);
|
|
30
|
+
|
|
31
|
+
if (typeof obj.kernelVersion !== 'string' || !obj.kernelVersion.trim()) errors.push('expected kernelVersion: non-empty string');
|
|
32
|
+
|
|
33
|
+
if (!Array.isArray(obj.commands)) {
|
|
34
|
+
errors.push('expected commands: array');
|
|
35
|
+
} else {
|
|
36
|
+
const missing = REQUIRED_COMMANDS.filter(c => !obj.commands.includes(c));
|
|
37
|
+
if (missing.length) errors.push(`missing commands: ${missing.join(', ')}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return errors;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function validateRepoInfoOutput(obj) {
|
|
44
|
+
const errors = [];
|
|
45
|
+
if (!isObj(obj)) return ['expected object'];
|
|
46
|
+
|
|
47
|
+
if (obj.version !== 1) errors.push('expected version: 1');
|
|
48
|
+
|
|
49
|
+
if (obj.repoRoot !== null && typeof obj.repoRoot !== 'string') errors.push('expected repoRoot: string|null');
|
|
50
|
+
if (typeof obj.branch !== 'string') errors.push('expected branch: string');
|
|
51
|
+
if (typeof obj.sha !== 'string') errors.push('expected sha: string');
|
|
52
|
+
if (typeof obj.clean !== 'boolean') errors.push('expected clean: boolean');
|
|
53
|
+
|
|
54
|
+
return errors;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = {
|
|
58
|
+
EXPECTED_PROTOCOL,
|
|
59
|
+
REQUIRED_COMMANDS,
|
|
60
|
+
validateProtocolVersionOutput,
|
|
61
|
+
validateRepoInfoOutput
|
|
62
|
+
};
|
|
63
|
+
|
package/scripts/ecc/kernel.js
CHANGED
|
@@ -2,6 +2,8 @@ const fs = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { spawnSync } = require('child_process');
|
|
4
4
|
|
|
5
|
+
const { EXPECTED_PROTOCOL, validateProtocolVersionOutput } = require('./kernel-contract');
|
|
6
|
+
|
|
5
7
|
function isFile(p) {
|
|
6
8
|
try {
|
|
7
9
|
return fs.statSync(p).isFile();
|
|
@@ -47,36 +49,139 @@ function tryKernelFromPath() {
|
|
|
47
49
|
encoding: 'utf8',
|
|
48
50
|
stdio: ['ignore', 'pipe', 'pipe']
|
|
49
51
|
});
|
|
50
|
-
if (res.error)
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
if (res.error) {
|
|
53
|
+
// ENOENT means not on PATH; treat as no candidate.
|
|
54
|
+
if (res.error && res.error.code === 'ENOENT') return null;
|
|
55
|
+
// Other spawn errors still indicate an attemptable candidate (will be probed for a better error).
|
|
56
|
+
return 'ecc-kernel';
|
|
57
|
+
}
|
|
58
|
+
return 'ecc-kernel';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function runKernelJson(bin, command, inputObj) {
|
|
62
|
+
const res = spawnSync(bin, [command], {
|
|
63
|
+
encoding: 'utf8',
|
|
64
|
+
input: JSON.stringify(inputObj || {}),
|
|
65
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (res.error) return { ok: false, error: `spawn failed: ${res.error.message}` };
|
|
69
|
+
const stdout = (res.stdout || '').trim();
|
|
70
|
+
const stderr = (res.stderr || '').trim();
|
|
71
|
+
|
|
72
|
+
if (res.status !== 0) {
|
|
73
|
+
const detail = stderr ? `stderr: ${stderr}` : (stdout ? `stdout: ${stdout}` : '');
|
|
74
|
+
return { ok: false, error: `exit ${res.status}${detail ? ` (${detail})` : ''}` };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!stdout) return { ok: false, error: 'empty stdout' };
|
|
78
|
+
try {
|
|
79
|
+
return { ok: true, value: JSON.parse(stdout) };
|
|
80
|
+
} catch (err) {
|
|
81
|
+
const msg = err && err.message ? err.message : String(err);
|
|
82
|
+
return { ok: false, error: `non-JSON stdout (${msg})` };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function probeKernel(bin) {
|
|
87
|
+
const res = runKernelJson(bin, 'protocol.version', {});
|
|
88
|
+
if (!res.ok) return { ok: false, error: `protocol.version failed: ${res.error}` };
|
|
89
|
+
|
|
90
|
+
const errors = validateProtocolVersionOutput(res.value, { expectedProtocol: EXPECTED_PROTOCOL });
|
|
91
|
+
if (errors.length) {
|
|
92
|
+
return { ok: false, error: `invalid protocol.version output: ${errors.join('; ')}` };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
ok: true,
|
|
97
|
+
protocol: res.value.protocol,
|
|
98
|
+
kernelVersion: res.value.kernelVersion,
|
|
99
|
+
commands: res.value.commands
|
|
100
|
+
};
|
|
53
101
|
}
|
|
54
102
|
|
|
55
|
-
function
|
|
103
|
+
function candidateList() {
|
|
104
|
+
const candidates = [];
|
|
105
|
+
|
|
56
106
|
if (process.env.ECC_KERNEL_PATH) {
|
|
57
|
-
|
|
58
|
-
|
|
107
|
+
candidates.push({
|
|
108
|
+
label: 'ECC_KERNEL_PATH',
|
|
109
|
+
bin: path.resolve(String(process.env.ECC_KERNEL_PATH)),
|
|
110
|
+
requiresFile: true,
|
|
111
|
+
explicit: true
|
|
112
|
+
});
|
|
59
113
|
}
|
|
60
114
|
|
|
61
115
|
// Preferred location for prebuilt binaries installed via postinstall.
|
|
62
116
|
const key = platformArchKey();
|
|
63
117
|
if (key) {
|
|
64
|
-
|
|
65
|
-
|
|
118
|
+
candidates.push({
|
|
119
|
+
label: 'package',
|
|
120
|
+
bin: path.join(__dirname, 'bin', key, binName()),
|
|
121
|
+
requiresFile: true,
|
|
122
|
+
explicit: false
|
|
123
|
+
});
|
|
66
124
|
}
|
|
67
125
|
|
|
126
|
+
// PATH lookup.
|
|
68
127
|
const fromPath = tryKernelFromPath();
|
|
69
|
-
if (fromPath)
|
|
128
|
+
if (fromPath) {
|
|
129
|
+
candidates.push({ label: 'PATH', bin: fromPath, requiresFile: false, explicit: false });
|
|
130
|
+
}
|
|
70
131
|
|
|
132
|
+
// Local dev build (repo).
|
|
71
133
|
const root = path.resolve(__dirname, '..', '..');
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
path.join(root, 'crates', 'ecc-kernel', 'target', '
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
134
|
+
candidates.push({
|
|
135
|
+
label: 'repo-release',
|
|
136
|
+
bin: path.join(root, 'crates', 'ecc-kernel', 'target', 'release', binName()),
|
|
137
|
+
requiresFile: true,
|
|
138
|
+
explicit: false
|
|
139
|
+
});
|
|
140
|
+
candidates.push({
|
|
141
|
+
label: 'repo-debug',
|
|
142
|
+
bin: path.join(root, 'crates', 'ecc-kernel', 'target', 'debug', binName()),
|
|
143
|
+
requiresFile: true,
|
|
144
|
+
explicit: false
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const seen = new Set();
|
|
148
|
+
return candidates.filter(c => {
|
|
149
|
+
if (seen.has(c.bin)) return false;
|
|
150
|
+
seen.add(c.bin);
|
|
151
|
+
return true;
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function selectCompatibleKernel({ mode }) {
|
|
156
|
+
const candidates = candidateList();
|
|
157
|
+
const errors = [];
|
|
158
|
+
|
|
159
|
+
for (const c of candidates) {
|
|
160
|
+
if (c.requiresFile && !isFile(c.bin)) {
|
|
161
|
+
if (c.explicit && mode === 'rust') {
|
|
162
|
+
throw new Error(`ECC kernel required but ECC_KERNEL_PATH is not a file: ${c.bin}`);
|
|
163
|
+
}
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const probe = probeKernel(c.bin);
|
|
168
|
+
if (probe.ok) return { enabled: true, bin: c.bin, ...probe };
|
|
169
|
+
|
|
170
|
+
errors.push(`${c.label}: ${probe.error}`);
|
|
171
|
+
if (c.explicit && mode === 'rust') break;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (mode === 'rust') {
|
|
175
|
+
const detail = errors.length ? `\n\nHandshake errors:\n- ${errors.join('\n- ')}` : '';
|
|
176
|
+
throw new Error(
|
|
177
|
+
'ECC kernel required but not found or incompatible.\n' +
|
|
178
|
+
'Install or build a compatible ecc-kernel, then re-run, or set ECC_KERNEL=node to force JS fallback.' +
|
|
179
|
+
detail
|
|
180
|
+
);
|
|
78
181
|
}
|
|
79
|
-
|
|
182
|
+
|
|
183
|
+
// mode=auto: fall back to JS. If we saw a kernel but it was incompatible, keep the reason for doctor output.
|
|
184
|
+
return { enabled: false, bin: null, reason: errors.length ? errors[0] : null };
|
|
80
185
|
}
|
|
81
186
|
|
|
82
187
|
let _cached = null;
|
|
@@ -86,20 +191,25 @@ function getKernel() {
|
|
|
86
191
|
|
|
87
192
|
const mode = getKernelMode();
|
|
88
193
|
if (mode === 'node') {
|
|
89
|
-
_cached = { mode, enabled: false, bin: null };
|
|
194
|
+
_cached = { mode, enabled: false, bin: null, reason: null };
|
|
90
195
|
return _cached;
|
|
91
196
|
}
|
|
92
197
|
|
|
93
|
-
const
|
|
94
|
-
if (
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
' cargo build --release --manifest-path crates/ecc-kernel/Cargo.toml\n' +
|
|
98
|
-
'Then re-run, or set ECC_KERNEL=node to force JS fallback.'
|
|
99
|
-
);
|
|
198
|
+
const selected = selectCompatibleKernel({ mode });
|
|
199
|
+
if (!selected.enabled) {
|
|
200
|
+
_cached = { mode, enabled: false, bin: null, reason: selected.reason };
|
|
201
|
+
return _cached;
|
|
100
202
|
}
|
|
101
203
|
|
|
102
|
-
_cached = {
|
|
204
|
+
_cached = {
|
|
205
|
+
mode,
|
|
206
|
+
enabled: true,
|
|
207
|
+
bin: selected.bin,
|
|
208
|
+
protocol: selected.protocol,
|
|
209
|
+
kernelVersion: selected.kernelVersion,
|
|
210
|
+
commands: selected.commands,
|
|
211
|
+
reason: null
|
|
212
|
+
};
|
|
103
213
|
return _cached;
|
|
104
214
|
}
|
|
105
215
|
|
|
@@ -140,5 +250,6 @@ function runKernel(command, inputObj) {
|
|
|
140
250
|
|
|
141
251
|
module.exports = {
|
|
142
252
|
getKernel,
|
|
143
|
-
runKernel
|
|
253
|
+
runKernel,
|
|
254
|
+
validateProtocolVersionOutput
|
|
144
255
|
};
|
package/scripts/ecc.js
CHANGED
|
@@ -82,7 +82,8 @@ Environment:
|
|
|
82
82
|
|
|
83
83
|
function resolveProjectRoot(cwd) {
|
|
84
84
|
try {
|
|
85
|
-
|
|
85
|
+
const info = git.getRepoInfo(cwd);
|
|
86
|
+
return info && info.repoRoot ? info.repoRoot : cwd;
|
|
86
87
|
} catch (_err) {
|
|
87
88
|
return cwd;
|
|
88
89
|
}
|
|
@@ -181,8 +182,15 @@ function cmdDoctor() {
|
|
|
181
182
|
|
|
182
183
|
try {
|
|
183
184
|
const k = kernel.getKernel();
|
|
184
|
-
|
|
185
|
-
|
|
185
|
+
if (k.enabled) {
|
|
186
|
+
const proto = k.protocol !== undefined ? ` protocol=${k.protocol}` : '';
|
|
187
|
+
const ver = k.kernelVersion ? ` kernelVersion=${k.kernelVersion}` : '';
|
|
188
|
+
checks.push({ name: 'kernel', ok: true, detail: `rust (${k.bin})${proto}${ver}` });
|
|
189
|
+
} else if (k.reason) {
|
|
190
|
+
checks.push({ name: 'kernel', ok: false, detail: `BAD (${k.reason})` });
|
|
191
|
+
} else {
|
|
192
|
+
checks.push({ name: 'kernel', ok: true, detail: 'js fallback' });
|
|
193
|
+
}
|
|
186
194
|
} catch (err) {
|
|
187
195
|
const msg = err && err.message ? err.message : String(err);
|
|
188
196
|
checks.push({ name: 'kernel', ok: false, detail: msg });
|
|
@@ -191,20 +199,24 @@ function cmdDoctor() {
|
|
|
191
199
|
const gitVer = runCmd('git', ['--version']);
|
|
192
200
|
checks.push({ name: 'git', ok: gitVer.ok, detail: gitVer.ok ? gitVer.stdout : gitVer.stderr });
|
|
193
201
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
if (repoRoot) {
|
|
198
|
-
let clean = false;
|
|
199
|
-
let detail = '';
|
|
202
|
+
if (gitVer.ok) {
|
|
203
|
+
let info = null;
|
|
200
204
|
try {
|
|
201
|
-
|
|
202
|
-
|
|
205
|
+
info = git.getRepoInfo(projectRoot);
|
|
206
|
+
checks.push({
|
|
207
|
+
name: 'repo',
|
|
208
|
+
ok: !!info.repoRoot,
|
|
209
|
+
detail: info.repoRoot ? info.repoRoot : 'not a git repo'
|
|
210
|
+
});
|
|
211
|
+
if (info.repoRoot) {
|
|
212
|
+
checks.push({ name: 'clean', ok: !!info.clean, detail: info.clean ? 'clean' : 'dirty' });
|
|
213
|
+
}
|
|
203
214
|
} catch (err) {
|
|
204
|
-
|
|
205
|
-
|
|
215
|
+
const msg = err && err.message ? err.message : String(err);
|
|
216
|
+
checks.push({ name: 'repo', ok: false, detail: msg });
|
|
206
217
|
}
|
|
207
|
-
|
|
218
|
+
} else {
|
|
219
|
+
checks.push({ name: 'repo', ok: false, detail: 'git not available' });
|
|
208
220
|
}
|
|
209
221
|
|
|
210
222
|
const codex = runCmd('codex', ['--version']);
|
|
@@ -266,9 +278,9 @@ async function cmdPlan(args) {
|
|
|
266
278
|
const runIdBase = requestedRunId || idMod.defaultRunId(intent);
|
|
267
279
|
const runId = requestedRunId ? idMod.ensureUniqueRunId(projectRoot, requestedRunId) : idMod.ensureUniqueRunId(projectRoot, runIdBase);
|
|
268
280
|
|
|
269
|
-
const
|
|
270
|
-
const base = repoRoot
|
|
271
|
-
? { repoRoot, branch:
|
|
281
|
+
const info = git.getRepoInfo(projectRoot);
|
|
282
|
+
const base = info.repoRoot
|
|
283
|
+
? { repoRoot: info.repoRoot, branch: info.branch, sha: info.sha }
|
|
272
284
|
: { repoRoot: projectRoot, branch: '', sha: '' };
|
|
273
285
|
|
|
274
286
|
const { run } = runMod.initRun({
|
|
@@ -381,7 +393,8 @@ function cmdVerify(args) {
|
|
|
381
393
|
);
|
|
382
394
|
}
|
|
383
395
|
|
|
384
|
-
const
|
|
396
|
+
const info = git.getRepoInfo(projectRoot);
|
|
397
|
+
const repoRoot = info.repoRoot;
|
|
385
398
|
if (!repoRoot) throw new Error('verify: requires a git repository');
|
|
386
399
|
|
|
387
400
|
const wt = git.ensureWorktree({
|
|
@@ -427,9 +440,9 @@ async function cmdRun(args) {
|
|
|
427
440
|
const runIdBase = requestedRunId || idMod.defaultRunId(intent);
|
|
428
441
|
const runId = requestedRunId ? idMod.ensureUniqueRunId(projectRoot, requestedRunId) : idMod.ensureUniqueRunId(projectRoot, runIdBase);
|
|
429
442
|
|
|
430
|
-
const
|
|
431
|
-
const base = repoRoot
|
|
432
|
-
? { repoRoot, branch:
|
|
443
|
+
const info = git.getRepoInfo(projectRoot);
|
|
444
|
+
const base = info.repoRoot
|
|
445
|
+
? { repoRoot: info.repoRoot, branch: info.branch, sha: info.sha }
|
|
433
446
|
: { repoRoot: projectRoot, branch: '', sha: '' };
|
|
434
447
|
|
|
435
448
|
const { run } = runMod.initRun({
|