duaer-spec 0.1.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/.cursor/rules/agents-workflow.mdc +50 -0
- package/.cursor/rules/ai-ui-copy.mdc +12 -0
- package/.cursor/rules/duaer-spec.mdc +33 -0
- package/.cursor/skills/duaer-analyze/SKILL.md +259 -0
- package/.cursor/skills/duaer-checklist/SKILL.md +383 -0
- package/.cursor/skills/duaer-clarify/SKILL.md +291 -0
- package/.cursor/skills/duaer-constitution/SKILL.md +177 -0
- package/.cursor/skills/duaer-converge/SKILL.md +277 -0
- package/.cursor/skills/duaer-git-commit/SKILL.md +68 -0
- package/.cursor/skills/duaer-git-feature/SKILL.md +94 -0
- package/.cursor/skills/duaer-git-initialize/SKILL.md +54 -0
- package/.cursor/skills/duaer-git-remote/SKILL.md +50 -0
- package/.cursor/skills/duaer-git-validate/SKILL.md +54 -0
- package/.cursor/skills/duaer-implement/SKILL.md +226 -0
- package/.cursor/skills/duaer-plan/SKILL.md +166 -0
- package/.cursor/skills/duaer-specify/SKILL.md +345 -0
- package/.cursor/skills/duaer-tasks/SKILL.md +214 -0
- package/.cursor/skills/duaer-taskstoissues/SKILL.md +109 -0
- package/.duaer/extensions/.registry +23 -0
- package/.duaer/extensions/git/README.md +119 -0
- package/.duaer/extensions/git/commands/duaer.git.commit.md +63 -0
- package/.duaer/extensions/git/commands/duaer.git.feature.md +82 -0
- package/.duaer/extensions/git/commands/duaer.git.initialize.md +49 -0
- package/.duaer/extensions/git/commands/duaer.git.remote.md +45 -0
- package/.duaer/extensions/git/commands/duaer.git.validate.md +49 -0
- package/.duaer/extensions/git/config-template.yml +79 -0
- package/.duaer/extensions/git/extension.yml +142 -0
- package/.duaer/extensions/git/git-config.yml +79 -0
- package/.duaer/extensions/git/scripts/bash/auto-commit.sh +211 -0
- package/.duaer/extensions/git/scripts/bash/create-new-feature-branch.sh +626 -0
- package/.duaer/extensions/git/scripts/bash/git-common.sh +56 -0
- package/.duaer/extensions/git/scripts/bash/initialize-repo.sh +54 -0
- package/.duaer/extensions/git/scripts/powershell/auto-commit.ps1 +230 -0
- package/.duaer/extensions/git/scripts/powershell/create-new-feature-branch.ps1 +592 -0
- package/.duaer/extensions/git/scripts/powershell/git-common.ps1 +52 -0
- package/.duaer/extensions/git/scripts/powershell/initialize-repo.ps1 +69 -0
- package/.duaer/extensions/git/scripts/python/auto_commit.py +195 -0
- package/.duaer/extensions/git/scripts/python/create_new_feature_branch.py +634 -0
- package/.duaer/extensions/git/scripts/python/git_common.py +81 -0
- package/.duaer/extensions/git/scripts/python/initialize_repo.py +89 -0
- package/.duaer/extensions.yml +167 -0
- package/.duaer/init-options.json +9 -0
- package/.duaer/integration.json +15 -0
- package/.duaer/integrations/cursor-agent.manifest.json +17 -0
- package/.duaer/integrations/duaer.manifest.json +19 -0
- package/.duaer/memory/.constitution-template.json +4 -0
- package/.duaer/memory/constitution.md +37 -0
- package/.duaer/memory/project-context.md +24 -0
- package/.duaer/memory/testing.md +14 -0
- package/.duaer/scripts/bash/check-prerequisites.sh +243 -0
- package/.duaer/scripts/bash/common.sh +926 -0
- package/.duaer/scripts/bash/create-new-feature.sh +407 -0
- package/.duaer/scripts/bash/resolve-template.sh +57 -0
- package/.duaer/scripts/bash/setup-plan.sh +85 -0
- package/.duaer/scripts/bash/setup-tasks.sh +94 -0
- package/.duaer/templates/checklist-template.md +45 -0
- package/.duaer/templates/constitution-template.md +50 -0
- package/.duaer/templates/plan-template.md +113 -0
- package/.duaer/templates/spec-template.md +131 -0
- package/.duaer/templates/tasks-template.md +252 -0
- package/.duaer/workflows/duaer/workflow.yml +78 -0
- package/.duaer/workflows/workflow-registry.json +13 -0
- package/ADOPT.md +75 -0
- package/AGENTS.md +290 -0
- package/CHANGELOG.md +28 -0
- package/DUADER.md +57 -0
- package/LICENSE +21 -0
- package/README.md +74 -0
- package/bin/duaer.mjs +298 -0
- package/docs/agent/README.md +12 -0
- package/docs/agent/change-checklist.md +130 -0
- package/docs/agent/e2e-test-plan.md +33 -0
- package/docs/agent/workflow.md +127 -0
- package/docs/baseline.md +10 -0
- package/package.json +49 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env pwsh
|
|
2
|
+
# Git extension: initialize-repo.ps1
|
|
3
|
+
# Initialize a Git repository with an initial commit.
|
|
4
|
+
# Customizable -- replace this script to add .gitignore templates,
|
|
5
|
+
# default branch config, git-flow, LFS, signing, etc.
|
|
6
|
+
$ErrorActionPreference = 'Stop'
|
|
7
|
+
|
|
8
|
+
# Find project root
|
|
9
|
+
function Find-ProjectRoot {
|
|
10
|
+
param([string]$StartDir)
|
|
11
|
+
$current = Resolve-Path $StartDir
|
|
12
|
+
while ($true) {
|
|
13
|
+
foreach ($marker in @('.duaer', '.git')) {
|
|
14
|
+
if (Test-Path (Join-Path $current $marker)) {
|
|
15
|
+
return $current
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
$parent = Split-Path $current -Parent
|
|
19
|
+
if ($parent -eq $current) { return $null }
|
|
20
|
+
$current = $parent
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
$repoRoot = Find-ProjectRoot -StartDir $PSScriptRoot
|
|
25
|
+
if (-not $repoRoot) { $repoRoot = Get-Location }
|
|
26
|
+
Set-Location $repoRoot
|
|
27
|
+
|
|
28
|
+
# Read commit message from extension config, fall back to default
|
|
29
|
+
$commitMsg = "[Duaer] Initial commit"
|
|
30
|
+
$configFile = Join-Path $repoRoot ".duaer/extensions/git/git-config.yml"
|
|
31
|
+
if (Test-Path $configFile) {
|
|
32
|
+
foreach ($line in Get-Content $configFile) {
|
|
33
|
+
if ($line -match '^init_commit_message:\s*(.+)$') {
|
|
34
|
+
$val = $matches[1].Trim() -replace '^["'']' -replace '["'']$'
|
|
35
|
+
if ($val) { $commitMsg = $val }
|
|
36
|
+
break
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# Check if git is available
|
|
42
|
+
if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
|
|
43
|
+
Write-Warning "[specify] Warning: Git not found; skipped repository initialization"
|
|
44
|
+
exit 0
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# Check if already a git repo
|
|
48
|
+
try {
|
|
49
|
+
git rev-parse --is-inside-work-tree 2>$null | Out-Null
|
|
50
|
+
if ($LASTEXITCODE -eq 0) {
|
|
51
|
+
Write-Warning "[specify] Git repository already initialized; skipping"
|
|
52
|
+
exit 0
|
|
53
|
+
}
|
|
54
|
+
} catch { }
|
|
55
|
+
|
|
56
|
+
# Initialize
|
|
57
|
+
try {
|
|
58
|
+
$out = git init -q 2>&1 | Out-String
|
|
59
|
+
if ($LASTEXITCODE -ne 0) { throw "git init failed: $out" }
|
|
60
|
+
$out = git add . 2>&1 | Out-String
|
|
61
|
+
if ($LASTEXITCODE -ne 0) { throw "git add failed: $out" }
|
|
62
|
+
$out = git commit --allow-empty -q -m $commitMsg 2>&1 | Out-String
|
|
63
|
+
if ($LASTEXITCODE -ne 0) { throw "git commit failed: $out" }
|
|
64
|
+
} catch {
|
|
65
|
+
Write-Warning "[specify] Error: $_"
|
|
66
|
+
exit 1
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
Write-Host "[OK] Git repository initialized"
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Git extension: auto_commit.py
|
|
3
|
+
|
|
4
|
+
Automatically commit changes after a Duaer command completes.
|
|
5
|
+
Python port of ``auto-commit.sh`` / ``auto-commit.ps1``.
|
|
6
|
+
Checks per-command config keys in git-config.yml before committing.
|
|
7
|
+
|
|
8
|
+
Usage: auto_commit.py <event_name>
|
|
9
|
+
e.g.: auto_commit.py after_specify
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import re
|
|
15
|
+
import shutil
|
|
16
|
+
import subprocess
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _find_project_root(start: Path) -> Path | None:
|
|
22
|
+
current = start
|
|
23
|
+
while True:
|
|
24
|
+
if (current / ".duaer").is_dir() or (current / ".git").exists():
|
|
25
|
+
return current
|
|
26
|
+
if current.parent == current:
|
|
27
|
+
return None
|
|
28
|
+
current = current.parent
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _value_after_colon(line: str) -> str:
|
|
32
|
+
return re.sub(r"^[^:]*:\s*", "", line)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _strip_quotes(value: str) -> str:
|
|
36
|
+
"""Strip surrounding whitespace, then one leading quote and all trailing quotes.
|
|
37
|
+
|
|
38
|
+
Trimming first matters when the YAML value has trailing whitespace after a
|
|
39
|
+
closing quote (``message: "Done" ``): stripping quotes anchored to the end
|
|
40
|
+
of string would leave the closing quote dangling (``Done" ``) because the
|
|
41
|
+
quote is no longer at the end. The PowerShell twin ``.Trim()``s before
|
|
42
|
+
stripping, so trim here too to keep all three script variants in parity.
|
|
43
|
+
"""
|
|
44
|
+
value = value.strip()
|
|
45
|
+
value = re.sub(r"^[\"']", "", value)
|
|
46
|
+
return re.sub(r"[\"']*$", "", value)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _parse_auto_commit_config(
|
|
50
|
+
config_file: Path, event_name: str
|
|
51
|
+
) -> tuple[bool, str]:
|
|
52
|
+
"""Parse the auto_commit section for this event, mirroring the bash line parser.
|
|
53
|
+
|
|
54
|
+
Returns (enabled, commit_msg). Looks for auto_commit.<event_name>.enabled
|
|
55
|
+
and .message, with auto_commit.default as fallback.
|
|
56
|
+
"""
|
|
57
|
+
enabled = False
|
|
58
|
+
commit_msg = ""
|
|
59
|
+
default_enabled = False
|
|
60
|
+
in_auto_commit = False
|
|
61
|
+
in_event = False
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
content = config_file.read_text(encoding="utf-8")
|
|
65
|
+
except (OSError, UnicodeDecodeError):
|
|
66
|
+
# Unreadable or non-UTF-8 config is treated like a missing one:
|
|
67
|
+
# auto-commit stays disabled instead of crashing with a traceback.
|
|
68
|
+
return False, ""
|
|
69
|
+
for record in content.splitlines(keepends=True):
|
|
70
|
+
if not record.endswith("\n"):
|
|
71
|
+
break
|
|
72
|
+
line = record[:-1]
|
|
73
|
+
if line.startswith("auto_commit:"):
|
|
74
|
+
in_auto_commit = True
|
|
75
|
+
in_event = False
|
|
76
|
+
continue
|
|
77
|
+
|
|
78
|
+
# Exit auto_commit section on next top-level key
|
|
79
|
+
if in_auto_commit and re.match(r"^[a-z]", line):
|
|
80
|
+
break
|
|
81
|
+
|
|
82
|
+
if not in_auto_commit:
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
if re.match(r"^\s+default:\s", line):
|
|
86
|
+
value = re.sub(r"\s", "", _value_after_colon(line)).lower()
|
|
87
|
+
if value == "true":
|
|
88
|
+
default_enabled = True
|
|
89
|
+
|
|
90
|
+
if re.match(rf"^\s+{re.escape(event_name)}:", line):
|
|
91
|
+
in_event = True
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
if in_event:
|
|
95
|
+
# Exit on next sibling key (same indent level as event name)
|
|
96
|
+
if re.match(r"^\s{2}[a-z]", line) and not re.match(r"^\s{4}", line):
|
|
97
|
+
in_event = False
|
|
98
|
+
continue
|
|
99
|
+
if re.search(r"\s+enabled:", line):
|
|
100
|
+
value = re.sub(r"\s", "", _value_after_colon(line)).lower()
|
|
101
|
+
if value == "true":
|
|
102
|
+
enabled = True
|
|
103
|
+
elif value == "false":
|
|
104
|
+
enabled = False
|
|
105
|
+
if re.search(r"\s+message:", line):
|
|
106
|
+
commit_msg = _strip_quotes(_value_after_colon(line))
|
|
107
|
+
|
|
108
|
+
# If event-specific key not found, use default — but only if the event
|
|
109
|
+
# section didn't exist at all (an explicit false must win).
|
|
110
|
+
if not enabled and default_enabled:
|
|
111
|
+
if not re.search(rf"^\s*{re.escape(event_name)}:", content, re.MULTILINE):
|
|
112
|
+
enabled = True
|
|
113
|
+
|
|
114
|
+
return enabled, commit_msg
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def main(argv: list[str]) -> int:
|
|
118
|
+
event_name = argv[0] if argv else ""
|
|
119
|
+
if not event_name:
|
|
120
|
+
print(f"Usage: {Path(sys.argv[0]).name} <event_name>", file=sys.stderr)
|
|
121
|
+
return 1
|
|
122
|
+
|
|
123
|
+
script_dir = Path(__file__).resolve().parent
|
|
124
|
+
repo_root = _find_project_root(script_dir) or Path.cwd()
|
|
125
|
+
|
|
126
|
+
if shutil.which("git") is None:
|
|
127
|
+
print("[specify] Warning: Git not found; skipped auto-commit", file=sys.stderr)
|
|
128
|
+
return 0
|
|
129
|
+
|
|
130
|
+
probe = subprocess.run(
|
|
131
|
+
["git", "rev-parse", "--is-inside-work-tree"],
|
|
132
|
+
cwd=repo_root,
|
|
133
|
+
capture_output=True,
|
|
134
|
+
text=True,
|
|
135
|
+
)
|
|
136
|
+
if probe.returncode != 0:
|
|
137
|
+
print(
|
|
138
|
+
"[specify] Warning: Not a Git repository; skipped auto-commit",
|
|
139
|
+
file=sys.stderr,
|
|
140
|
+
)
|
|
141
|
+
return 0
|
|
142
|
+
|
|
143
|
+
config_file = repo_root / ".duaer" / "extensions" / "git" / "git-config.yml"
|
|
144
|
+
if not config_file.is_file():
|
|
145
|
+
# No config file — auto-commit disabled by default
|
|
146
|
+
return 0
|
|
147
|
+
|
|
148
|
+
enabled, commit_msg = _parse_auto_commit_config(config_file, event_name)
|
|
149
|
+
if not enabled:
|
|
150
|
+
return 0
|
|
151
|
+
|
|
152
|
+
# Check if there are changes to commit
|
|
153
|
+
def _quiet(*args: str) -> bool:
|
|
154
|
+
return (
|
|
155
|
+
subprocess.run(
|
|
156
|
+
["git", *args], cwd=repo_root, capture_output=True, text=True
|
|
157
|
+
).returncode
|
|
158
|
+
== 0
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
untracked = subprocess.run(
|
|
162
|
+
["git", "ls-files", "--others", "--exclude-standard"],
|
|
163
|
+
cwd=repo_root,
|
|
164
|
+
capture_output=True,
|
|
165
|
+
text=True,
|
|
166
|
+
).stdout.strip()
|
|
167
|
+
if _quiet("diff", "--quiet", "HEAD") and _quiet("diff", "--cached", "--quiet") and not untracked:
|
|
168
|
+
print(f"[specify] No changes to commit after {event_name}", file=sys.stderr)
|
|
169
|
+
return 0
|
|
170
|
+
|
|
171
|
+
# Derive a human-readable command name from the event
|
|
172
|
+
# e.g., after_specify -> specify, before_plan -> plan
|
|
173
|
+
command_name = re.sub(r"^(after_|before_)", "", event_name)
|
|
174
|
+
phase = "before" if event_name.startswith("before_") else "after"
|
|
175
|
+
|
|
176
|
+
if not commit_msg:
|
|
177
|
+
commit_msg = f"[Duaer] Auto-commit {phase} {command_name}"
|
|
178
|
+
|
|
179
|
+
steps = [
|
|
180
|
+
(["git", "add", "."], "git add"),
|
|
181
|
+
(["git", "commit", "-q", "-m", commit_msg], "git commit"),
|
|
182
|
+
]
|
|
183
|
+
for cmd, label in steps:
|
|
184
|
+
result = subprocess.run(cmd, cwd=repo_root, capture_output=True, text=True)
|
|
185
|
+
if result.returncode != 0:
|
|
186
|
+
output = (result.stdout + result.stderr).strip()
|
|
187
|
+
print(f"[specify] Error: {label} failed: {output}", file=sys.stderr)
|
|
188
|
+
return 1
|
|
189
|
+
|
|
190
|
+
print(f"[OK] Changes committed {phase} {command_name}", file=sys.stderr)
|
|
191
|
+
return 0
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
if __name__ == "__main__":
|
|
195
|
+
raise SystemExit(main(sys.argv[1:]))
|