compass-st 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/README.md +105 -0
  2. package/VERSION +1 -0
  3. package/bin/install +174 -0
  4. package/bootstrap.sh +95 -0
  5. package/cli/Cargo.lock +270 -0
  6. package/cli/Cargo.toml +24 -0
  7. package/cli/src/cmd/context.rs +59 -0
  8. package/cli/src/cmd/dag.rs +133 -0
  9. package/cli/src/cmd/git.rs +148 -0
  10. package/cli/src/cmd/hook.rs +51 -0
  11. package/cli/src/cmd/index.rs +363 -0
  12. package/cli/src/cmd/manifest.rs +34 -0
  13. package/cli/src/cmd/memory.rs +680 -0
  14. package/cli/src/cmd/migrate.rs +790 -0
  15. package/cli/src/cmd/mod.rs +14 -0
  16. package/cli/src/cmd/progress.rs +107 -0
  17. package/cli/src/cmd/project.rs +1700 -0
  18. package/cli/src/cmd/session.rs +64 -0
  19. package/cli/src/cmd/state.rs +317 -0
  20. package/cli/src/cmd/validate/mod.rs +506 -0
  21. package/cli/src/cmd/validate/prd.rs +472 -0
  22. package/cli/src/cmd/version.rs +89 -0
  23. package/cli/src/helpers.rs +40 -0
  24. package/cli/src/main.rs +75 -0
  25. package/cli/tests/fixtures/plan_empty_pointers.json +60 -0
  26. package/cli/tests/fixtures/plan_missing_pointers.json +59 -0
  27. package/cli/tests/fixtures/plan_too_many_pointers.json +92 -0
  28. package/cli/tests/fixtures/plan_v1_valid.json +64 -0
  29. package/cli/tests/fixtures/prd_bad_flow_bullet.md +37 -0
  30. package/cli/tests/fixtures/prd_bad_flow_prose.md +33 -0
  31. package/cli/tests/fixtures/prd_good_flow.md +41 -0
  32. package/cli/tests/fixtures/prd_xref_dangling.md +38 -0
  33. package/cli/tests/fixtures/prd_xref_valid.md +53 -0
  34. package/cli/tests/fixtures/projects/proj_a/.compass/.state/config.json +12 -0
  35. package/cli/tests/fixtures/projects/proj_b/.compass/.state/config.json +12 -0
  36. package/cli/tests/fixtures/projects/proj_c/.compass/.state/config.json +12 -0
  37. package/cli/tests/fixtures/registry/all_dead.json +18 -0
  38. package/cli/tests/fixtures/registry/corrupt.json +1 -0
  39. package/cli/tests/fixtures/registry/empty.json +1 -0
  40. package/cli/tests/fixtures/registry/last_active_dead.json +24 -0
  41. package/cli/tests/fixtures/registry/multi_alive.json +24 -0
  42. package/cli/tests/fixtures/registry/one_alive.json +12 -0
  43. package/cli/tests/fixtures/v0_project/.compass/.state/config.json +5 -0
  44. package/cli/tests/fixtures/v0_project/.compass/.state/sessions/onboarding-redesign/plan.json +29 -0
  45. package/cli/tests/fixtures/v0_project/.compass/.state/sessions/sample-feature/context.json +11 -0
  46. package/cli/tests/fixtures/v0_project/.compass/.state/sessions/sample-feature/plan.json +49 -0
  47. package/core/colleagues/base-rules.md +112 -0
  48. package/core/colleagues/manifest.json +85 -0
  49. package/core/colleagues/market-analyst.md +50 -0
  50. package/core/colleagues/prioritizer.md +53 -0
  51. package/core/colleagues/researcher.md +54 -0
  52. package/core/colleagues/reviewer.md +55 -0
  53. package/core/colleagues/stakeholder-comm.md +59 -0
  54. package/core/colleagues/story-breaker.md +57 -0
  55. package/core/colleagues/ux-reviewer.md +54 -0
  56. package/core/colleagues/writer.md +55 -0
  57. package/core/commands/compass/brief.md +28 -0
  58. package/core/commands/compass/check.md +27 -0
  59. package/core/commands/compass/epic.md +32 -0
  60. package/core/commands/compass/feedback.md +32 -0
  61. package/core/commands/compass/help.md +24 -0
  62. package/core/commands/compass/ideate.md +32 -0
  63. package/core/commands/compass/init.md +30 -0
  64. package/core/commands/compass/plan.md +27 -0
  65. package/core/commands/compass/prd.md +39 -0
  66. package/core/commands/compass/prioritize.md +36 -0
  67. package/core/commands/compass/prototype.md +28 -0
  68. package/core/commands/compass/release.md +32 -0
  69. package/core/commands/compass/research.md +31 -0
  70. package/core/commands/compass/roadmap.md +32 -0
  71. package/core/commands/compass/run.md +28 -0
  72. package/core/commands/compass/setup.md +32 -0
  73. package/core/commands/compass/sprint.md +32 -0
  74. package/core/commands/compass/status.md +32 -0
  75. package/core/commands/compass/story.md +37 -0
  76. package/core/commands/compass/undo.md +33 -0
  77. package/core/commands/compass/update.md +29 -0
  78. package/core/hooks/context-monitor.sh +5 -0
  79. package/core/hooks/manifest-tracker.sh +62 -0
  80. package/core/hooks/statusline.sh +12 -0
  81. package/core/hooks/update-checker.sh +24 -0
  82. package/core/integrations/confluence.md +267 -0
  83. package/core/integrations/figma.md +277 -0
  84. package/core/integrations/jira.md +436 -0
  85. package/core/integrations/vercel.md +170 -0
  86. package/core/manifest.json +172 -0
  87. package/core/shared/SCHEMAS-v1.md +404 -0
  88. package/core/shared/progress.md +145 -0
  89. package/core/shared/project-scan.md +293 -0
  90. package/core/shared/resolve-project.md +136 -0
  91. package/core/shared/ux-rules.md +52 -0
  92. package/core/shared/version-backup.md +38 -0
  93. package/core/templates/prd-template.md +145 -0
  94. package/core/templates/story-template.md +99 -0
  95. package/core/workflows/brief.md +184 -0
  96. package/core/workflows/check.md +436 -0
  97. package/core/workflows/epic.md +177 -0
  98. package/core/workflows/feedback.md +164 -0
  99. package/core/workflows/help.md +79 -0
  100. package/core/workflows/ideate.md +320 -0
  101. package/core/workflows/init.md +524 -0
  102. package/core/workflows/migrate.md +136 -0
  103. package/core/workflows/plan.md +320 -0
  104. package/core/workflows/prd.md +632 -0
  105. package/core/workflows/prioritize.md +301 -0
  106. package/core/workflows/project.md +177 -0
  107. package/core/workflows/prototype.md +174 -0
  108. package/core/workflows/release.md +179 -0
  109. package/core/workflows/research.md +613 -0
  110. package/core/workflows/roadmap.md +152 -0
  111. package/core/workflows/run.md +367 -0
  112. package/core/workflows/setup.md +294 -0
  113. package/core/workflows/sprint.md +187 -0
  114. package/core/workflows/status.md +185 -0
  115. package/core/workflows/story.md +477 -0
  116. package/core/workflows/undo.md +42 -0
  117. package/core/workflows/update.md +127 -0
  118. package/package.json +37 -0
@@ -0,0 +1,59 @@
1
+ use crate::helpers;
2
+ use serde_json::json;
3
+ use std::path::Path;
4
+
5
+ pub fn run(args: &[String]) -> Result<String, String> {
6
+ if args.len() < 3 { return Err("Usage: compass-cli context pack <session_dir> <task_id>".into()); }
7
+ if args[0] != "pack" && args[0] != "get" {
8
+ return Err(format!("Unknown context command: {}", args[0]));
9
+ }
10
+
11
+ let session_dir = Path::new(&args[1]);
12
+ let task_id = &args[2];
13
+ let plan_path = session_dir.join("plan.json");
14
+
15
+ if !plan_path.exists() { return Err("plan.json not found in session dir".into()); }
16
+ let plan = helpers::read_json(&plan_path)?;
17
+
18
+ let tasks_key = if plan.get("colleagues").is_some() { "colleagues" } else { "tasks" };
19
+ let tasks = plan.get(tasks_key).and_then(|t| t.as_array())
20
+ .ok_or("No tasks array in plan")?;
21
+
22
+ let task = tasks.iter()
23
+ .find(|t| t["id"].as_str() == Some(task_id))
24
+ .ok_or(format!("Task {} not found", task_id))?;
25
+
26
+ let mut files: serde_json::Map<String, serde_json::Value> = serde_json::Map::new();
27
+
28
+ if let Some(pointers) = task.get("context_pointers").and_then(|p| p.as_array()) {
29
+ for ptr in pointers {
30
+ if let Some(ptr_str) = ptr.as_str() {
31
+ // Parse "path:start-end" or just "path"
32
+ let (file_path, _range) = if let Some(colon_pos) = ptr_str.rfind(':') {
33
+ let maybe_range = &ptr_str[colon_pos + 1..];
34
+ if maybe_range.contains('-') && maybe_range.chars().all(|c| c.is_ascii_digit() || c == '-') {
35
+ (&ptr_str[..colon_pos], Some(maybe_range))
36
+ } else {
37
+ (ptr_str, None)
38
+ }
39
+ } else {
40
+ (ptr_str, None)
41
+ };
42
+
43
+ if let Ok(content) = std::fs::read_to_string(file_path) {
44
+ files.insert(file_path.to_string(), json!(content));
45
+ }
46
+ }
47
+ }
48
+ }
49
+
50
+ let output = json!({"task_id": task_id, "files": files});
51
+
52
+ // Write context pack file
53
+ if args[0] == "pack" {
54
+ let out_path = session_dir.join(format!("{}.context.json", task_id));
55
+ helpers::write_json(&out_path, &output)?;
56
+ }
57
+
58
+ Ok(serde_json::to_string_pretty(&output).unwrap())
59
+ }
@@ -0,0 +1,133 @@
1
+ use crate::helpers;
2
+ use serde_json::json;
3
+ use std::collections::{HashMap, HashSet, VecDeque};
4
+ use std::path::Path;
5
+
6
+ pub fn run(args: &[String]) -> Result<String, String> {
7
+ if args.len() < 2 { return Err("Usage: compass-cli dag <check|waves> <path>".into()); }
8
+ let data = helpers::read_json(Path::new(&args[1]))?;
9
+ let tasks_key = if data.get("colleagues").is_some() { "colleagues" } else { "tasks" };
10
+ let tasks = data.get(tasks_key).and_then(|t| t.as_array())
11
+ .ok_or("Missing tasks/colleagues array")?;
12
+
13
+ match args[0].as_str() {
14
+ "check" => dag_check(tasks),
15
+ "waves" => dag_waves(tasks),
16
+ _ => Err(format!("Unknown dag command: {}", args[0])),
17
+ }
18
+ }
19
+
20
+ fn dag_check(tasks: &[serde_json::Value]) -> Result<String, String> {
21
+ let ids: HashSet<&str> = tasks.iter().filter_map(|t| t["id"].as_str()).collect();
22
+ let mut dangling = vec![];
23
+ let mut adj: HashMap<&str, Vec<&str>> = HashMap::new();
24
+
25
+ for task in tasks {
26
+ let id = task["id"].as_str().unwrap_or("");
27
+ adj.entry(id).or_default();
28
+ if let Some(deps) = task.get("depends_on").and_then(|d| d.as_array()) {
29
+ for dep in deps {
30
+ if let Some(dep_id) = dep.as_str() {
31
+ if !ids.contains(dep_id) { dangling.push(format!("{} -> {}", id, dep_id)); }
32
+ adj.entry(dep_id).or_default().push(id);
33
+ }
34
+ }
35
+ }
36
+ }
37
+
38
+ // Cycle detection via DFS
39
+ let mut visited: HashSet<&str> = HashSet::new();
40
+ let mut stack: HashSet<&str> = HashSet::new();
41
+ let mut cycles = vec![];
42
+
43
+ fn dfs<'a>(
44
+ node: &'a str,
45
+ adj: &HashMap<&'a str, Vec<&'a str>>,
46
+ visited: &mut HashSet<&'a str>,
47
+ stack: &mut HashSet<&'a str>,
48
+ cycles: &mut Vec<String>,
49
+ ) {
50
+ visited.insert(node);
51
+ stack.insert(node);
52
+ if let Some(neighbors) = adj.get(node) {
53
+ for &next in neighbors {
54
+ if !visited.contains(next) {
55
+ dfs(next, adj, visited, stack, cycles);
56
+ } else if stack.contains(next) {
57
+ cycles.push(format!("{} -> {}", node, next));
58
+ }
59
+ }
60
+ }
61
+ stack.remove(node);
62
+ }
63
+
64
+ for &id in &ids {
65
+ if !visited.contains(id) {
66
+ dfs(id, &adj, &mut visited, &mut stack, &mut cycles);
67
+ }
68
+ }
69
+
70
+ Ok(serde_json::to_string_pretty(&json!({
71
+ "valid": cycles.is_empty() && dangling.is_empty(),
72
+ "cycles": cycles,
73
+ "dangling": dangling,
74
+ })).unwrap())
75
+ }
76
+
77
+ fn dag_waves(tasks: &[serde_json::Value]) -> Result<String, String> {
78
+ let mut in_degree: HashMap<String, usize> = HashMap::new();
79
+ let mut adj: HashMap<String, Vec<String>> = HashMap::new();
80
+ let mut task_map: HashMap<String, &serde_json::Value> = HashMap::new();
81
+
82
+ for task in tasks {
83
+ let id = task["id"].as_str().unwrap_or("").to_string();
84
+ in_degree.entry(id.clone()).or_insert(0);
85
+ adj.entry(id.clone()).or_default();
86
+ task_map.insert(id.clone(), task);
87
+
88
+ if let Some(deps) = task.get("depends_on").and_then(|d| d.as_array()) {
89
+ for dep in deps {
90
+ if let Some(dep_id) = dep.as_str() {
91
+ adj.entry(dep_id.to_string()).or_default().push(id.clone());
92
+ *in_degree.entry(id.clone()).or_insert(0) += 1;
93
+ }
94
+ }
95
+ }
96
+ }
97
+
98
+ let mut waves: Vec<Vec<serde_json::Value>> = vec![];
99
+ let mut queue: VecDeque<String> = in_degree.iter()
100
+ .filter(|(_, &deg)| deg == 0)
101
+ .map(|(id, _)| id.clone())
102
+ .collect();
103
+
104
+ while !queue.is_empty() {
105
+ let mut wave = vec![];
106
+ let mut next_queue = VecDeque::new();
107
+ while let Some(id) = queue.pop_front() {
108
+ if let Some(task) = task_map.get(&id) {
109
+ wave.push(json!({
110
+ "id": id,
111
+ "name": task["name"].as_str().unwrap_or(""),
112
+ "complexity": task["complexity"].as_str().unwrap_or(""),
113
+ "budget_tokens": task["budget_tokens"].as_u64().unwrap_or(0),
114
+ }));
115
+ }
116
+ if let Some(neighbors) = adj.get(&id) {
117
+ for next in neighbors {
118
+ if let Some(deg) = in_degree.get_mut(next) {
119
+ *deg -= 1;
120
+ if *deg == 0 { next_queue.push_back(next.clone()); }
121
+ }
122
+ }
123
+ }
124
+ }
125
+ if !wave.is_empty() { waves.push(wave); }
126
+ queue = next_queue;
127
+ }
128
+
129
+ Ok(serde_json::to_string_pretty(&json!({
130
+ "wave_count": waves.len(),
131
+ "waves": waves,
132
+ })).unwrap())
133
+ }
@@ -0,0 +1,148 @@
1
+ use serde_json::json;
2
+ use std::process::Command;
3
+
4
+ const DOC_DIRS: &[&str] = &["prd", "epics", "research", "technical", "wiki", "compass"];
5
+
6
+ fn git_available() -> bool {
7
+ Command::new("git")
8
+ .arg("--version")
9
+ .output()
10
+ .map(|o| o.status.success())
11
+ .unwrap_or(false)
12
+ }
13
+
14
+ fn run_git(args: &[&str]) -> Result<std::process::Output, String> {
15
+ Command::new("git")
16
+ .args(args)
17
+ .output()
18
+ .map_err(|e| e.to_string())
19
+ }
20
+
21
+ pub fn run(args: &[String]) -> Result<String, String> {
22
+ if !git_available() {
23
+ return Ok(json!({"available": false}).to_string());
24
+ }
25
+
26
+ if args.is_empty() {
27
+ return Err("Usage: compass-cli git <branch|commit|status> [args...]".into());
28
+ }
29
+
30
+ match args[0].as_str() {
31
+ "branch" => cmd_branch(args),
32
+ "commit" => cmd_commit(args),
33
+ "status" => cmd_status(),
34
+ _ => Err(format!("Unknown git command: {}", args[0])),
35
+ }
36
+ }
37
+
38
+ fn cmd_branch(args: &[String]) -> Result<String, String> {
39
+ if args.len() < 2 {
40
+ return Err("Usage: compass-cli git branch <name>".into());
41
+ }
42
+ let name = &args[1];
43
+ let branch = format!("docs/{}", name);
44
+
45
+ let out = run_git(&["checkout", "-b", &branch])
46
+ .map_err(|e| e.to_string())?;
47
+
48
+ if out.status.success() {
49
+ Ok(json!({
50
+ "success": true,
51
+ "branch": branch,
52
+ "action": "created_and_checked_out"
53
+ })
54
+ .to_string())
55
+ } else {
56
+ // Branch may already exist — try just checking out
57
+ let out2 = run_git(&["checkout", &branch]).map_err(|e| e.to_string())?;
58
+ if out2.status.success() {
59
+ Ok(json!({
60
+ "success": true,
61
+ "branch": branch,
62
+ "action": "checked_out_existing"
63
+ })
64
+ .to_string())
65
+ } else {
66
+ let stderr = String::from_utf8_lossy(&out.stderr).to_string();
67
+ Err(format!("git checkout failed: {}", stderr))
68
+ }
69
+ }
70
+ }
71
+
72
+ fn cmd_commit(args: &[String]) -> Result<String, String> {
73
+ if args.len() < 2 {
74
+ return Err("Usage: compass-cli git commit <message>".into());
75
+ }
76
+ let message = &args[1];
77
+
78
+ // Stage only files inside designated doc dirs
79
+ let mut staged_count = 0usize;
80
+ for dir in DOC_DIRS {
81
+ let out = run_git(&["add", "--", dir]).map_err(|e| e.to_string())?;
82
+ if out.status.success() {
83
+ staged_count += 1;
84
+ }
85
+ }
86
+
87
+ // Check if there is anything staged
88
+ let status_out = run_git(&["diff", "--cached", "--name-only"]).map_err(|e| e.to_string())?;
89
+ let staged_files: Vec<String> = String::from_utf8_lossy(&status_out.stdout)
90
+ .lines()
91
+ .map(|l| l.to_string())
92
+ .filter(|l| !l.is_empty())
93
+ .collect();
94
+
95
+ if staged_files.is_empty() {
96
+ return Ok(json!({
97
+ "success": false,
98
+ "reason": "nothing_to_commit",
99
+ "staged": staged_count
100
+ })
101
+ .to_string());
102
+ }
103
+
104
+ let commit_out = run_git(&["commit", "-m", message]).map_err(|e| e.to_string())?;
105
+
106
+ if commit_out.status.success() {
107
+ Ok(json!({
108
+ "success": true,
109
+ "message": message,
110
+ "files": staged_files
111
+ })
112
+ .to_string())
113
+ } else {
114
+ let stderr = String::from_utf8_lossy(&commit_out.stderr).to_string();
115
+ Err(format!("git commit failed: {}", stderr))
116
+ }
117
+ }
118
+
119
+ fn cmd_status() -> Result<String, String> {
120
+ // Current branch
121
+ let branch_out = run_git(&["rev-parse", "--abbrev-ref", "HEAD"]).map_err(|e| e.to_string())?;
122
+ let branch = String::from_utf8_lossy(&branch_out.stdout)
123
+ .trim()
124
+ .to_string();
125
+
126
+ // Changed files in doc dirs only
127
+ let status_out = run_git(&["status", "--porcelain"]).map_err(|e| e.to_string())?;
128
+ let changed: Vec<String> = String::from_utf8_lossy(&status_out.stdout)
129
+ .lines()
130
+ .filter_map(|line| {
131
+ let path = line.get(3..).unwrap_or("").trim().to_string();
132
+ let in_doc_dir = DOC_DIRS.iter().any(|d| path.starts_with(d));
133
+ if in_doc_dir {
134
+ Some(path)
135
+ } else {
136
+ None
137
+ }
138
+ })
139
+ .collect();
140
+
141
+ Ok(json!({
142
+ "available": true,
143
+ "branch": branch,
144
+ "changed_files": changed,
145
+ "changed_count": changed.len()
146
+ })
147
+ .to_string())
148
+ }
@@ -0,0 +1,51 @@
1
+ use std::path::Path;
2
+
3
+ pub fn run(args: &[String]) -> Result<String, String> {
4
+ if args.is_empty() { return Err("Usage: compass-cli hook <statusline|update-checker>".into()); }
5
+ match args[0].as_str() {
6
+ "statusline" => statusline(),
7
+ "update-checker" | "check-update" => update_checker(),
8
+ "context-monitor" => Ok(String::new()), // placeholder
9
+ _ => Err(format!("Unknown hook: {}", args[0])),
10
+ }
11
+ }
12
+
13
+ fn statusline() -> Result<String, String> {
14
+ let config_path = Path::new(".compass/.state/config.json");
15
+ if !config_path.exists() { return Ok("Compass: not initialized".to_string()); }
16
+
17
+ let content = std::fs::read_to_string(config_path).map_err(|e| e.to_string())?;
18
+ let data: serde_json::Value = serde_json::from_str(&content).map_err(|e| e.to_string())?;
19
+
20
+ let project = data.pointer("/project/name").and_then(|v| v.as_str()).unwrap_or("?");
21
+ let prefix = data.get("prefix").and_then(|v| v.as_str()).unwrap_or("?");
22
+ let mode = data.get("mode").and_then(|v| v.as_str()).unwrap_or("?");
23
+
24
+ Ok(format!("Compass: {} ({}) | {}", project, prefix, mode))
25
+ }
26
+
27
+ fn update_checker() -> Result<String, String> {
28
+ // Same logic as shell hook but in Rust — check once per day
29
+ let home = std::env::var("HOME").unwrap_or_default();
30
+ let cache_file = Path::new(&home).join(".compass").join(".update-check-cache");
31
+
32
+ if cache_file.exists() {
33
+ if let Ok(content) = std::fs::read_to_string(&cache_file) {
34
+ if let Ok(ts) = content.trim().parse::<u64>() {
35
+ let now = std::time::SystemTime::now()
36
+ .duration_since(std::time::UNIX_EPOCH)
37
+ .unwrap().as_secs();
38
+ if now - ts < 86400 { return Ok(String::new()); }
39
+ }
40
+ }
41
+ }
42
+
43
+ // Write cache timestamp
44
+ let now = std::time::SystemTime::now()
45
+ .duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
46
+ let _ = std::fs::write(&cache_file, now.to_string());
47
+
48
+ // We can't easily do HTTP in pure Rust without dependencies, so just output empty
49
+ // The shell hook handles the actual GitHub API call
50
+ Ok(String::new())
51
+ }