Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion docs/src/content/docs/reference/model-alias-specification.md
Original file line number Diff line number Diff line change
Expand Up @@ -704,7 +704,8 @@ At compile time, an implementation SHOULD:
| Requirement | Test ID | Level | Status |
|-------------|---------|-------|--------|
| Bare identifier parsing | T-MAF-001 | 1 | Required |
| Parameter parsing | T-MAF-002, 004 | 1 | Required |
| Provider/model identifier parsing | T-MAF-003 | 1 | Required |
| Parameter parsing | T-MAF-002, T-MAF-004 | 1 | Required |
| Glob rejection in engine.model | T-MAF-005 | 1 | Required |
| Invalid effort value rejection | T-MAF-006 | 1 | Required |
| Temperature range validation | T-MAF-007 | 1 | Required |
Expand All @@ -714,10 +715,15 @@ At compile time, an implementation SHOULD:
| Transitive alias resolution | T-MAF-021 | 2 | Required |
| Parameter propagation | T-MAF-022 | 2 | Required |
| Caller-wins parameter merge | T-MAF-023 | 2 | Required |
| Builtin-alias composition | T-MAF-024 | 2 | Required |
| Default policy (`""`) | T-MAF-025 | 2 | Required |
| Semver-aware glob selection (latest wins) | T-MAF-026 | 2 | Required |
| Date-suffix tiebreaker | T-MAF-027 | 2 | Required |
| Unversioned model ranking fallback | T-MAF-028 | 2 | Required |
| Main workflow wins merge | T-MAF-030 | 2 | Required |
| First import wins on duplicate key | T-MAF-031 | 2 | Required |
| Main workflow overrides import keys | T-MAF-032 | 2 | Required |
| Builtin-only key retention | T-MAF-033 | 2 | Required |
| Compile-time circular alias detection | T-MAF-040, 041 | 3 | Required |
| Runtime circular alias guard | T-MAF-042 | 3 | Required |
| Unrecognized param warning | T-MAF-043 | 3 | Recommended |
Expand Down Expand Up @@ -852,6 +858,8 @@ Model parameters are compile-time configuration values and are not derived from
- **Enhanced**: Compile-time cycle detection (§8.6.1): expanded from a single sentence to a full DFS algorithm with error-message requirements.
- **Added**: Models payload merge algorithm pseudocode (§10.2) making the three-layer merge semantics explicit.
- **Added**: Merge precedence test T-MAF-033 (builtin-only keys are preserved).
- **Clarified**: `?effort=` parsing/validation semantics now explicitly align with runtime parser behavior (`low|medium|high`, caller-wins merge, compile-time validation).
- **Clarified**: `?temperature=` parsing/validation semantics now explicitly align with runtime parser behavior (numeric range `0.0..2.0`, forwarding and overwrite rules).

### Version 1.0.0 (Draft)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,13 @@ The manifest document MUST be a YAML mapping. Unknown top-level fields MUST be r
| --- | --- | --- | --- |
| `manifest-version` | string | No | Manifest format version. Defaults to `"1"`. |
| `min-version` | string | No | Minimum supported `gh-aw` version. |
| `max-version` | string | No | Maximum supported `gh-aw` version. |
| `name` | string | Yes | Human-readable package name. |
| `emoji` | string | No | Optional package emoji for display in package metadata. |
| `description` | string | No | Human-readable package description. |
| `license` | string | No | SPDX license identifier for package metadata. |
| `tags` | array of strings | No | Package tags (max 10 entries, each max 32 chars). |
| `categories` | array of enum strings | No | Package categories (`automation`, `ci-cd`, `code-review`, `security`, `documentation`, `release`, `triage`, `testing`). |
| `files` | array of strings | No | Explicit installable workflow file list. |

### 4.2 `manifest-version`
Expand All @@ -54,14 +58,18 @@ If omitted, `manifest-version` defaults to `"1"`.

For this version of the format, the only valid value is `"1"`.

### 4.3 `min-version`
### 4.3 Version constraints: `min-version` and `max-version`

If present, `min-version` MUST use the exact `vMAJOR.minor.patch` form, such as:
If present, `min-version` and `max-version` MUST use the exact `vMAJOR.minor.patch` form, such as:

- `v1.2.3`

If the running compiler version is lower than `min-version`, validation MUST fail.

If the running compiler version is higher than `max-version`, validation MUST fail.

If both `min-version` and `max-version` are present, `min-version` MUST be less than or equal to `max-version`.

### 4.4 `name`

`name` MUST be present and MUST be a non-empty string after trimming surrounding whitespace.
Expand All @@ -87,6 +95,29 @@ Each entry MUST be resolved relative to the package root and MUST match one of t

Duplicate entries SHOULD be ignored after normalization.

### 4.8 `license`

If present, `license` MUST be a valid SPDX identifier string.

### 4.9 `tags`

If present, `tags` MUST be an array of strings with at most 10 entries.

Each `tags` entry MUST be 1-32 characters.

### 4.10 `categories`

If present, `categories` MUST be an array of enum values from this set:

- `automation`
- `ci-cd`
- `code-review`
- `security`
- `documentation`
- `release`
- `triage`
- `testing`

## 5. Installable file resolution

Supported installable paths are:
Expand Down Expand Up @@ -131,7 +162,13 @@ Validation MUST fail for at least the following conditions:
- missing or empty `name`;
- unsupported `manifest-version`;
- invalid `min-version`;
- invalid `max-version`;
- current compiler version is lower than `min-version`;
- current compiler version is higher than `max-version`;
- `min-version` greater than `max-version`;
- invalid `license` (non-SPDX);
- more than 10 `tags` entries or any tag longer than 32 characters;
- invalid `categories` value;
- unknown top-level fields, including `docs`; or
- missing required `README.md`; or
- no installable workflow files resolved.
Expand Down Expand Up @@ -196,3 +233,23 @@ Documentation file:
```text
packages/repo-assist/README.md
```

## 10. Package Lifecycle

### 10.1 `gh aw add`

- The implementation MUST fetch and validate `aw.yml` before installing package files.
- The implementation MUST reject installation when manifest validation returns any `manifest_error`.
- The implementation MUST copy raw `.yml` workflow includes verbatim without compilation.

### 10.2 `gh aw update`

- The implementation MUST re-fetch and re-validate `aw.yml` during update.
- The implementation MUST preserve local install targets and only replace files belonging to the package manifest selection.
- The implementation MUST fail the update if a newly resolved package manifest is incompatible with current compiler version constraints.

### 10.3 `gh aw remove`

- The implementation MUST remove only files that were previously installed from the package.
- The implementation MUST NOT delete user-authored files outside the tracked package install set.
- The implementation MUST report missing installed files as warnings (not hard failures) and continue removal for remaining tracked files.
8 changes: 8 additions & 0 deletions docs/src/content/docs/reference/safe-outputs-specification.md
Original file line number Diff line number Diff line change
Expand Up @@ -4227,6 +4227,14 @@ All errors MUST be logged to:

## 10. Execution Guarantees

<!--
SPDD audit note (2026-05-30): §10 MUST/MUST NOT clauses reviewed against pkg/workflow JS handlers.
Current coverage gaps to track:
- Add explicit tests that ordering guarantees (10.2) hold when mixed system/non-system records coexist.
- Add explicit tests that batch partial-failure reporting (10.4) always emits per-type summary counts.
- Add explicit tests that WTD2 conversion paths emit the same caution/label/marker envelope as WTD1.
-->

### 10.1 Atomicity

**Single-Item Operations**: Complete success or complete failure (no partial state).
Expand Down
60 changes: 56 additions & 4 deletions pkg/cli/add_package_manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,13 @@ func loadRepositoryPackageManifestFile(owner, repo, packagePath, ref, host strin
type repositoryPackageManifest struct {
ManifestVersion string
MinVersion string
MaxVersion string
Name string
Emoji string
Description string
License string
Tags []string
Categories []string
Includes []string
Files []string
Skills []string // skill directory paths (e.g. "skills/my-skill")
Expand Down Expand Up @@ -216,16 +220,30 @@ func parseRepositoryPackageManifest(manifestPath string, content []byte) (*repos

if minVersion, ok := stringValue(root["min-version"]); ok {
manifest.MinVersion = strings.TrimSpace(minVersion)
if !isSupportedManifestMinVersion(manifest.MinVersion) {
if !isValidManifestVersionFormat(manifest.MinVersion) {
return nil, nil, fmt.Errorf("invalid Agentic Workflow manifest %q: min-version must use vMAJOR.minor.patch, got %q", manifestPath, minVersion)
}
}
if maxVersion, ok := stringValue(root["max-version"]); ok {
manifest.MaxVersion = strings.TrimSpace(maxVersion)
if !isValidManifestVersionFormat(manifest.MaxVersion) {
return nil, nil, fmt.Errorf("invalid Agentic Workflow manifest %q: max-version must use vMAJOR.minor.patch, got %q", manifestPath, maxVersion)
}
}
if manifest.MinVersion != "" && manifest.MaxVersion != "" && semverutil.Compare(manifest.MinVersion, manifest.MaxVersion) > 0 {
return nil, nil, fmt.Errorf("invalid Agentic Workflow manifest %q: min-version %q cannot be greater than max-version %q", manifestPath, manifest.MinVersion, manifest.MaxVersion)
}
if manifest.MinVersion != "" || manifest.MaxVersion != "" {
currentVersion := GetVersion()
if !semverutil.IsValid(currentVersion) {
return nil, nil, fmt.Errorf("invalid Agentic Workflow manifest %q: min-version validation requires a semantic-versioned compiler, but the current compiler version %q is not a valid semantic version (this indicates a build issue)", manifestPath, currentVersion)
return nil, nil, fmt.Errorf("invalid Agentic Workflow manifest %q: version-range validation requires a semantic-versioned compiler, but the current compiler version %q is not a valid semantic version (this indicates a build issue)", manifestPath, currentVersion)
}
if semverutil.Compare(currentVersion, manifest.MinVersion) < 0 {
if manifest.MinVersion != "" && semverutil.Compare(currentVersion, manifest.MinVersion) < 0 {
return nil, nil, fmt.Errorf("invalid Agentic Workflow manifest %q: min-version %q requires gh-aw %s or newer (current: %s)", manifestPath, manifest.MinVersion, manifest.MinVersion, currentVersion)
}
if manifest.MaxVersion != "" && semverutil.Compare(currentVersion, manifest.MaxVersion) > 0 {
return nil, nil, fmt.Errorf("invalid Agentic Workflow manifest %q: max-version %q requires gh-aw %s or older (current: %s)", manifestPath, manifest.MaxVersion, manifest.MaxVersion, currentVersion)
}
}

if description, ok := stringValue(root["description"]); ok {
Expand All @@ -238,6 +256,15 @@ func parseRepositoryPackageManifest(manifestPath string, content []byte) (*repos
if emoji, ok := stringValue(root["emoji"]); ok {
manifest.Emoji = emoji
}
if license, ok := stringValue(root["license"]); ok {
manifest.License = strings.TrimSpace(license)
}
if tags, ok := stringSliceValue(root["tags"]); ok {
manifest.Tags = tags
}
if categories, ok := stringSliceValue(root["categories"]); ok {
manifest.Categories = categories
}

if includesValue, ok := root["includes"]; ok {
includes, includeWarnings := extractManifestIncludes(includesValue, manifestPath)
Expand Down Expand Up @@ -749,6 +776,29 @@ func stringValue(value any) (string, bool) {
return s, ok
}

// stringSliceValue converts dynamic manifest field values into []string.
// It accepts []any and []string inputs; []string inputs are defensively copied.
// It returns (nil, false) when the input is nil, not a string slice, or contains
// non-string elements.
func stringSliceValue(value any) ([]string, bool) {
switch raw := value.(type) {
case []any:
out := make([]string, 0, len(raw))
for _, entry := range raw {
s, ok := entry.(string)
if !ok {
return nil, false
}
out = append(out, s)
}
return out, true
case []string:
return append([]string(nil), raw...), true
default:
return nil, false
}
}

func isRepositoryFileNotFound(err error) bool {
return errors.Is(err, errRepositoryPackageFileNotFound)
}
Expand All @@ -757,7 +807,9 @@ func isRepositoryPackageManifestNotFound(err error) bool {
return errors.Is(err, errRepositoryPackageManifestNotFound)
}

func isSupportedManifestMinVersion(version string) bool {
// isValidManifestVersionFormat validates the aw.yml version-constraint format:
// exact semantic version tags in vMAJOR.minor.patch form.
func isValidManifestVersionFormat(version string) bool {
const expectedManifestMinVersionDotCount = 2
return semverutil.IsActionVersionTag(version) && strings.Count(strings.TrimPrefix(version, "v"), ".") == expectedManifestMinVersionDotCount
}
Expand Down
Loading