@asm-agent/coding-agent 0.8.4 → 0.8.6

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 (49) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/bundle/{anthropic-CWQBVVHC.js → anthropic-CILMWJ23.js} +1 -1
  3. package/dist/bundle/{azure-openai-responses-7JP64QDL.js → azure-openai-responses-TPNZ54LG.js} +2 -2
  4. package/dist/bundle/{bundled-modules-DZAECPSN.js → bundled-modules-SKYRKITN.js} +3 -3
  5. package/dist/bundle/{chunk-7HN2VZJJ.js → chunk-7QIBGY2R.js} +74 -74
  6. package/dist/bundle/{chunk-O7SB7LML.js → chunk-EFDSSG52.js} +69 -18
  7. package/dist/bundle/{chunk-FCQTXDPX.js → chunk-IYXHHLR6.js} +1 -1
  8. package/dist/bundle/{chunk-4KQVBOJB.js → chunk-YYVQTKC6.js} +2 -2
  9. package/dist/bundle/{cli-main-YXP4FEPB.js → cli-main-O3HK7MNQ.js} +3 -3
  10. package/dist/bundle/cli.js +1 -1
  11. package/dist/bundle/{google-DPYNVS7L.js → google-3TL5FKLN.js} +1 -1
  12. package/dist/bundle/{google-vertex-SKMI6LMM.js → google-vertex-WVFMK6QO.js} +1 -1
  13. package/dist/bundle/{main-DOLQ46ZM.js → main-4QYKME2K.js} +3 -3
  14. package/dist/bundle/{mistral-DUU7KDZ3.js → mistral-QV2OSBWW.js} +1 -1
  15. package/dist/bundle/{openai-codex-responses-D7FCP7DO.js → openai-codex-responses-J2GJ2FAO.js} +2 -2
  16. package/dist/bundle/{openai-completions-AH6TQF5C.js → openai-completions-W622H2EL.js} +1 -1
  17. package/dist/bundle/{openai-responses-DDC3EYTG.js → openai-responses-V75QG5T7.js} +2 -2
  18. package/dist/core/always-applied-skills.d.ts +3 -0
  19. package/dist/core/always-applied-skills.d.ts.map +1 -0
  20. package/dist/core/always-applied-skills.js +47 -0
  21. package/dist/core/always-applied-skills.js.map +1 -0
  22. package/dist/core/system-prompt.d.ts.map +1 -1
  23. package/dist/core/system-prompt.js +9 -0
  24. package/dist/core/system-prompt.js.map +1 -1
  25. package/dist/skills/final-response-summary/SKILL.md +39 -0
  26. package/dist/skills/request-report/SKILL.md +46 -0
  27. package/dist/skills/request-report/pyproject.toml +16 -0
  28. package/dist/skills/request-report/src/request_report/__init__.py +160 -0
  29. package/dist/skills/skill-creator/SKILL.md +2 -1
  30. package/dist/skills/solid-source-modularity/SKILL.md +48 -0
  31. package/dist/skills/solid-source-modularity/pyproject.toml +16 -0
  32. package/dist/skills/solid-source-modularity/src/solid_source_modularity/__init__.py +154 -0
  33. package/docs/skills.md +8 -3
  34. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  35. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  36. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  37. package/examples/extensions/sandbox/package-lock.json +2 -2
  38. package/examples/extensions/sandbox/package.json +1 -1
  39. package/examples/extensions/with-deps/package-lock.json +2 -2
  40. package/examples/extensions/with-deps/package.json +1 -1
  41. package/package.json +5 -5
  42. package/skills/final-response-summary/SKILL.md +39 -0
  43. package/skills/request-report/SKILL.md +46 -0
  44. package/skills/request-report/pyproject.toml +16 -0
  45. package/skills/request-report/src/request_report/__init__.py +160 -0
  46. package/skills/skill-creator/SKILL.md +2 -1
  47. package/skills/solid-source-modularity/SKILL.md +48 -0
  48. package/skills/solid-source-modularity/pyproject.toml +16 -0
  49. package/skills/solid-source-modularity/src/solid_source_modularity/__init__.py +154 -0
@@ -0,0 +1,48 @@
1
+ ---
2
+ name: solid-source-modularity
3
+ always-apply: true
4
+ description: Enforce SOLID design, cohesive source modules, staged refactoring, and source-file size limits. MUST be used whenever the agent creates, modifies, reviews, or refactors authored source code in any project.
5
+ ---
6
+
7
+ # SOLID Source Modularity
8
+
9
+ Apply this policy to all authored source-code work.
10
+
11
+ ## Required policy
12
+
13
+ - Follow SOLID and give each module one cohesive responsibility.
14
+ - Separate orchestration, domain logic, I/O, rendering, and platform adapters.
15
+ - Prefer small contracts and explicit dependencies. Avoid generic catch-all modules.
16
+ - Target at most 300 physical lines per authored source file.
17
+ - Allow 301–500 lines only as a documented exception when splitting would reduce
18
+ cohesion or create artificial coupling.
19
+ - More than 500 lines is always a violation and requires modularization.
20
+ - Generated files and lockfiles are exempt.
21
+
22
+ ## Required refactoring sequence
23
+
24
+ 1. Inventory files and responsibilities.
25
+ 2. Define boundaries, contracts, and dependency direction.
26
+ 3. Create all destination modules before replacing the original implementation.
27
+ 4. Validate new modules in isolation when possible.
28
+ 5. Replace the original code with imports, delegation, and orchestration only
29
+ after the destination modules exist.
30
+ 6. Organize tests by responsibility and run the project's required checks.
31
+ 7. Run the source audit and document each 301–500-line exception.
32
+
33
+ Keep structural refactoring separate from new behavior. Preserve behavior during
34
+ structural changes. Never create a commit without explicit user authorization.
35
+
36
+ ## Audit
37
+
38
+ Call the prepared Python skill from the repository root:
39
+
40
+ ```python
41
+ report = await solid_source_modularity(
42
+ root=".",
43
+ extensions=[".ts", ".tsx", ".py"],
44
+ )
45
+ ```
46
+
47
+ `report` separates `compliant`, `exceptions`, `violations`, and `skipped` files.
48
+ Do not raise `target_lines` above 300 or `hard_limit` above 500.
@@ -0,0 +1,16 @@
1
+ [project]
2
+ name = "solid-source-modularity"
3
+ version = "0.1.0"
4
+ description = "Audit authored source files against ASM Agent modularity limits."
5
+ requires-python = ">=3.10"
6
+ dependencies = []
7
+
8
+ [project.scripts]
9
+ solid_source_modularity = "rlm.skill:cli"
10
+
11
+ [build-system]
12
+ requires = ["hatchling"]
13
+ build-backend = "hatchling.build"
14
+
15
+ [tool.hatch.build.targets.wheel]
16
+ packages = ["src/solid_source_modularity"]
@@ -0,0 +1,154 @@
1
+ """Audit authored source files against ASM Agent modularity limits."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+ from typing import Iterable
8
+
9
+ _DEFAULT_EXTENSIONS = (
10
+ ".c",
11
+ ".cpp",
12
+ ".cs",
13
+ ".go",
14
+ ".h",
15
+ ".hpp",
16
+ ".java",
17
+ ".js",
18
+ ".jsx",
19
+ ".py",
20
+ ".rs",
21
+ ".ts",
22
+ ".tsx",
23
+ )
24
+ _DEFAULT_EXCLUDED_NAMES = (
25
+ "Cargo.lock",
26
+ "package-lock.json",
27
+ "pnpm-lock.yaml",
28
+ "yarn.lock",
29
+ )
30
+ _EXCLUDED_DIRECTORIES = frozenset(
31
+ {
32
+ ".git",
33
+ ".mypy_cache",
34
+ ".next",
35
+ ".pytest_cache",
36
+ ".tox",
37
+ ".venv",
38
+ "build",
39
+ "coverage",
40
+ "dist",
41
+ "node_modules",
42
+ "target",
43
+ "vendor",
44
+ }
45
+ )
46
+ _GENERATED_NAME_MARKERS = (".generated.", ".gen.")
47
+
48
+
49
+ def _normalize_extensions(extensions: Iterable[str]) -> frozenset[str]:
50
+ normalized = set()
51
+ for extension in extensions:
52
+ if not isinstance(extension, str) or not extension.strip():
53
+ raise TypeError("extensions must contain non-empty strings")
54
+ value = extension.strip().lower()
55
+ normalized.add(value if value.startswith(".") else f".{value}")
56
+ if not normalized:
57
+ raise ValueError("extensions must not be empty")
58
+ return frozenset(normalized)
59
+
60
+
61
+ def _validate_limits(target_lines: int, hard_limit: int) -> None:
62
+ if not isinstance(target_lines, int) or isinstance(target_lines, bool):
63
+ raise TypeError("target_lines must be an integer")
64
+ if not isinstance(hard_limit, int) or isinstance(hard_limit, bool):
65
+ raise TypeError("hard_limit must be an integer")
66
+ if target_lines <= 0 or target_lines > 300:
67
+ raise ValueError("target_lines must be between 1 and 300")
68
+ if hard_limit <= target_lines or hard_limit > 500:
69
+ raise ValueError("hard_limit must be greater than target_lines and at most 500")
70
+
71
+
72
+ def _is_generated_name(name: str) -> bool:
73
+ lowered = name.lower()
74
+ return any(marker in lowered for marker in _GENERATED_NAME_MARKERS)
75
+
76
+
77
+ def _entry(path: Path, root: Path, lines: int) -> dict[str, object]:
78
+ return {"path": path.relative_to(root).as_posix(), "lines": lines}
79
+
80
+
81
+ async def audit_source_files(
82
+ root: str,
83
+ extensions: list[str] | None = None,
84
+ target_lines: int = 300,
85
+ hard_limit: int = 500,
86
+ excluded_names: list[str] | None = None,
87
+ ) -> dict[str, object]:
88
+ """Audit authored source files and classify their physical line counts."""
89
+ _validate_limits(target_lines, hard_limit)
90
+ root_path = Path(root).expanduser().resolve()
91
+ if not root_path.is_dir():
92
+ raise NotADirectoryError(f"{root} is not an existing directory")
93
+
94
+ included_extensions = _normalize_extensions(extensions or list(_DEFAULT_EXTENSIONS))
95
+ excluded = set(excluded_names or list(_DEFAULT_EXCLUDED_NAMES))
96
+ compliant: list[dict[str, object]] = []
97
+ exceptions: list[dict[str, object]] = []
98
+ violations: list[dict[str, object]] = []
99
+ skipped: list[dict[str, str]] = []
100
+
101
+ for current_root, directories, filenames in os.walk(root_path):
102
+ directories[:] = sorted(name for name in directories if name not in _EXCLUDED_DIRECTORIES)
103
+ current_path = Path(current_root)
104
+ for filename in sorted(filenames):
105
+ path = current_path / filename
106
+ if path.suffix.lower() not in included_extensions:
107
+ continue
108
+ relative = path.relative_to(root_path).as_posix()
109
+ if filename in excluded or _is_generated_name(filename):
110
+ skipped.append({"path": relative, "reason": "excluded or generated"})
111
+ continue
112
+ try:
113
+ line_count = len(path.read_text(encoding="utf-8").splitlines())
114
+ except (OSError, UnicodeError) as error:
115
+ skipped.append({"path": relative, "reason": str(error)})
116
+ continue
117
+ entry = _entry(path, root_path, line_count)
118
+ if line_count <= target_lines:
119
+ compliant.append(entry)
120
+ elif line_count <= hard_limit:
121
+ exceptions.append(entry)
122
+ else:
123
+ violations.append(entry)
124
+
125
+ return {
126
+ "root": str(root_path),
127
+ "target_lines": target_lines,
128
+ "hard_limit": hard_limit,
129
+ "scanned_files": len(compliant) + len(exceptions) + len(violations),
130
+ "compliant": compliant,
131
+ "exceptions": exceptions,
132
+ "violations": violations,
133
+ "skipped": skipped,
134
+ }
135
+
136
+
137
+ async def run(
138
+ root: str,
139
+ extensions: list[str] | None = None,
140
+ target_lines: int = 300,
141
+ hard_limit: int = 500,
142
+ excluded_names: list[str] | None = None,
143
+ ) -> dict[str, object]:
144
+ """Audit source files under ``root`` against the 300/500-line policy."""
145
+ return await audit_source_files(
146
+ root,
147
+ extensions=extensions,
148
+ target_lines=target_lines,
149
+ hard_limit=hard_limit,
150
+ excluded_names=excluded_names,
151
+ )
152
+
153
+
154
+ __all__ = ["audit_source_files", "run"]
package/docs/skills.md CHANGED
@@ -50,6 +50,9 @@ ASM Agent ships with built-in skills that load by default:
50
50
 
51
51
  - `prime-intellect` - Prime Intellect products and workflows via the prime CLI: verifiers environments and the Environments Hub, evaluations (local and hosted), Hosted Training and prime-rl, sandboxes, tunnels, Prime Inference, GPU compute, and storage. Reference docs for each area load on demand from the skill's `references/` directory.
52
52
  - `skill-creator` - teaches the agent to create new skills: markdown skill layout, frontmatter rules, placement and precedence, and the full Python-backed skill contract (package layout, `run()` convention, optional CLI, kernel venv behavior) with a working template in `references/python-skills.md`.
53
+ - `solid-source-modularity` - an always-applied SOLID and source modularity policy with a Python source-size auditor.
54
+ - `final-response-summary` - an always-applied root-agent completion and final-summary policy.
55
+ - `request-report` - an always-applied report policy with a Python writer for `docs/report/<number>_<feature>_<date>.md`.
53
56
  - `websearch` - a Python-backed Google search skill using the [Serper](https://serper.dev) API.
54
57
 
55
58
  Built-in skills behave like any other skill but have the lowest precedence: a user, project, package, or `--skill` skill with the same name overrides the built-in one.
@@ -132,9 +135,10 @@ For project-level Claude Code skills, add to `.asm-agent/settings.json`:
132
135
  1. At startup, ASM Agent scans skill locations and extracts names, descriptions, type, and file locations
133
136
  2. The system prompt includes visible skills in XML format per the [specification](https://agentskills.io/integrate-skills)
134
137
  3. When a task matches, the agent uses `ipython` to load the full `SKILL.md` (models don't always do this; use prompting or `/skill:name` to force it)
135
- 4. The agent follows the instructions, using relative paths to reference scripts and assets
138
+ 4. Skills with `always-apply: true` have their full instructions injected into the system prompt without an explicit invocation
139
+ 5. The agent follows the instructions, using relative paths to reference scripts and assets
136
140
 
137
- This is progressive disclosure: only descriptions are always in context, full instructions load on-demand.
141
+ This normally uses progressive disclosure: descriptions are always in context and full instructions load on demand. Reserve `always-apply` for concise universal policies because their bodies consume context in every session.
138
142
 
139
143
  Skills with `disable-model-invocation: true` are hidden from the startup skill list. They can still be invoked explicitly with `/skill:name`.
140
144
 
@@ -306,6 +310,7 @@ Per the [Agent Skills specification](https://agentskills.io/specification#frontm
306
310
  | `compatibility` | No | Max 500 chars. Environment requirements. |
307
311
  | `metadata` | No | Arbitrary key-value mapping. |
308
312
  | `allowed-tools` | No | Space-delimited list of pre-approved tools (experimental). |
313
+ | `always-apply` | No | ASM Agent extension. When `true`, injects the full skill instructions into every system prompt. |
309
314
  | `disable-model-invocation` | No | When `true`, skill is hidden from system prompt. Users must use `/skill:name`. |
310
315
 
311
316
  ### Name Rules
@@ -342,7 +347,7 @@ ASM Agent validates skills against the Agent Skills standard. Most issues produc
342
347
  - Name starts/ends with hyphen or has consecutive hyphens
343
348
  - Description exceeds 1024 characters
344
349
 
345
- Unknown frontmatter fields are ignored.
350
+ Unknown frontmatter fields are ignored. `always-apply` is an ASM Agent extension to the standard.
346
351
 
347
352
  **Exception:** Skills with missing description are not loaded.
348
353
 
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pi-extension-custom-provider",
3
- "version": "0.0.4",
3
+ "version": "0.8.6",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pi-extension-custom-provider",
9
- "version": "0.0.4",
9
+ "version": "0.8.6",
10
10
  "dependencies": {
11
11
  "@anthropic-ai/sdk": "^0.52.0"
12
12
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-extension-custom-provider-anthropic",
3
3
  "private": true,
4
- "version": "0.0.4",
4
+ "version": "0.8.6",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "clean": "echo 'nothing to clean'",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-extension-custom-provider-gitlab-duo",
3
3
  "private": true,
4
- "version": "0.0.4",
4
+ "version": "0.8.6",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "clean": "echo 'nothing to clean'",
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pi-extension-sandbox",
3
- "version": "1.4.3",
3
+ "version": "0.8.6",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pi-extension-sandbox",
9
- "version": "1.4.3",
9
+ "version": "0.8.6",
10
10
  "dependencies": {
11
11
  "@anthropic-ai/sandbox-runtime": "^0.0.55"
12
12
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-extension-sandbox",
3
3
  "private": true,
4
- "version": "1.4.3",
4
+ "version": "0.8.6",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "clean": "echo 'nothing to clean'",
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pi-extension-with-deps",
3
- "version": "0.0.4",
3
+ "version": "0.8.6",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pi-extension-with-deps",
9
- "version": "0.0.4",
9
+ "version": "0.8.6",
10
10
  "dependencies": {
11
11
  "ms": "^2.1.3"
12
12
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-extension-with-deps",
3
3
  "private": true,
4
- "version": "0.0.4",
4
+ "version": "0.8.6",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "clean": "echo 'nothing to clean'",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asm-agent/coding-agent",
3
- "version": "0.8.4",
3
+ "version": "0.8.6",
4
4
  "description": "Coding agent CLI with a persistent Python REPL kernel and session management",
5
5
  "type": "module",
6
6
  "asmAgentConfig": {
@@ -48,10 +48,10 @@
48
48
  },
49
49
  "dependencies": {
50
50
  "@agentclientprotocol/sdk": "^1.3.0",
51
- "@asm-agent/agent": "^0.8.4",
52
- "@asm-agent/ai": "^0.8.4",
53
- "@asm-agent/postgres": "^0.8.4",
54
- "@asm-agent/tui": "^0.8.4",
51
+ "@asm-agent/agent": "^0.8.6",
52
+ "@asm-agent/ai": "^0.8.6",
53
+ "@asm-agent/postgres": "^0.8.6",
54
+ "@asm-agent/tui": "^0.8.6",
55
55
  "@silvia-odwyer/photon-node": "^0.3.4",
56
56
  "chalk": "^5.5.0",
57
57
  "cli-highlight": "^2.1.11",
@@ -0,0 +1,39 @@
1
+ ---
2
+ name: final-response-summary
3
+ always-apply: true
4
+ description: Ensure the root ASM Agent sends one concise final response that summarizes the complete user request after all delegated agents, agent follow-ups, validations, and other work have settled. MUST be used for every root user request that reaches completion.
5
+ ---
6
+
7
+ # Final Response Summary
8
+
9
+ Apply this workflow only to the root, user-facing agent. Child agents must report
10
+ their results to their parent instead of producing the user's final summary.
11
+
12
+ ## Completion gate
13
+
14
+ Before sending a final answer:
15
+
16
+ 1. Re-read the user's original request and all later user changes.
17
+ 2. Collect every required reply from delegated agents and their follow-ups.
18
+ 3. Confirm no relevant child is still running, queued, or expected to reply.
19
+ 4. Complete required edits, checks, tests, and the request report.
20
+ 5. Separate verified results from blockers or unverified claims.
21
+
22
+ If relevant delegated work is still active, send only a short progress update and
23
+ end the turn. Do not present that update as the final result. Resume when replies
24
+ arrive.
25
+
26
+ ## Final response
27
+
28
+ Send one concise response that covers:
29
+
30
+ - what the user requested;
31
+ - what was completed;
32
+ - important files or behavior changed;
33
+ - checks and tests run;
34
+ - blockers, remaining work, or next steps, when applicable;
35
+ - the generated `docs/report/...` path when the request-report skill applies.
36
+
37
+ Do not repeat the full transcript. Do not invent completion, test results, or
38
+ agent findings. Keep simple requests short and use sections only when they make
39
+ the result clearer.
@@ -0,0 +1,46 @@
1
+ ---
2
+ name: request-report
3
+ always-apply: true
4
+ description: Create one numbered Markdown report at docs/report/<number_mil>_<feature>_<date>.md for the latest user request. MUST be used by the root ASM Agent for every completed project task or user request in a writable workspace, after all work and validation finish and before the final response.
5
+ ---
6
+
7
+ # Request Report
8
+
9
+ The root, user-facing agent must create exactly one report for each completed
10
+ user request in a writable project workspace. Child agents must send findings
11
+ to their parent and must not create request reports.
12
+
13
+ Create the report after edits and validation finish, but before the final user
14
+ response. Call the prepared Python skill:
15
+
16
+ ```python
17
+ path = await request_report(
18
+ root=".",
19
+ feature="short feature name",
20
+ request="The user's complete request, including later changes.",
21
+ summary="What was completed and the resulting behavior.",
22
+ modified_files=["packages/example/src/file.ts", "docs/example.md"],
23
+ changes=["Changed ...", "Added ..."],
24
+ validation=["npm run check — passed"],
25
+ status="completed",
26
+ )
27
+ ```
28
+
29
+ The skill:
30
+
31
+ - creates `docs/report/` when needed;
32
+ - allocates sequential numbers with at least four digits (`0001`, `0002`, ...);
33
+ - converts `feature` to a safe kebab-case slug;
34
+ - uses an ISO date (`YYYY-MM-DD`);
35
+ - requires `modified_files` and renders every path as a relative Markdown link;
36
+ - always links the generated report itself in the modified-files list;
37
+ - rejects modified-file paths outside the project root;
38
+ - creates the file atomically without overwriting an existing report;
39
+ - returns the created path.
40
+
41
+ Pass every created, modified, or deleted project file in `modified_files`. Use
42
+ paths relative to `root` when possible. Use `status="partial"` or
43
+ `status="blocked"` when appropriate. Record only checks
44
+ that actually ran. If the workspace is read-only or the request does not have a
45
+ project filesystem, state that limitation in the final response instead of
46
+ claiming a report was created.
@@ -0,0 +1,16 @@
1
+ [project]
2
+ name = "request-report"
3
+ version = "0.1.0"
4
+ description = "Create numbered Markdown reports for completed ASM Agent requests."
5
+ requires-python = ">=3.10"
6
+ dependencies = []
7
+
8
+ [project.scripts]
9
+ request_report = "rlm.skill:cli"
10
+
11
+ [build-system]
12
+ requires = ["hatchling"]
13
+ build-backend = "hatchling.build"
14
+
15
+ [tool.hatch.build.targets.wheel]
16
+ packages = ["src/request_report"]
@@ -0,0 +1,160 @@
1
+ """Create numbered Markdown reports for completed user requests."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import re
7
+ import unicodedata
8
+ from datetime import date as date_type
9
+ from datetime import datetime, timezone
10
+ from pathlib import Path
11
+ from typing import Iterable
12
+ from urllib.parse import quote
13
+
14
+ _REPORT_NUMBER_WIDTH = 4
15
+ _REPORT_NAME_PATTERN = re.compile(r"^(\d{4,})_.*\.md$")
16
+ _VALID_STATUSES = frozenset({"blocked", "completed", "partial"})
17
+
18
+
19
+ def _slugify(value: str) -> str:
20
+ normalized = unicodedata.normalize("NFKD", value)
21
+ ascii_value = normalized.encode("ascii", "ignore").decode("ascii").lower()
22
+ slug = re.sub(r"[^a-z0-9]+", "-", ascii_value).strip("-")
23
+ return (slug or "request")[:60].rstrip("-")
24
+
25
+
26
+ def _validate_date(value: str | None) -> str:
27
+ if value is None:
28
+ return datetime.now(timezone.utc).date().isoformat()
29
+ try:
30
+ return date_type.fromisoformat(value).isoformat()
31
+ except ValueError as error:
32
+ raise ValueError("date must use YYYY-MM-DD format") from error
33
+
34
+
35
+ def _next_number(report_dir: Path) -> int:
36
+ highest = 0
37
+ for path in report_dir.glob("*.md"):
38
+ match = _REPORT_NAME_PATTERN.match(path.name)
39
+ if match:
40
+ highest = max(highest, int(match.group(1)))
41
+ return highest + 1
42
+
43
+
44
+ def _section(title: str, content: str) -> list[str]:
45
+ return [f"## {title}", "", content.strip() or "Not provided.", ""]
46
+
47
+
48
+ def _bullet_section(title: str, values: Iterable[str] | None) -> list[str]:
49
+ items = [value.strip() for value in (values or []) if value.strip()]
50
+ content = "\n".join(f"- {item}" for item in items) or "- None recorded."
51
+ return _section(title, content)
52
+
53
+
54
+ def _normalize_modified_files(root: Path, values: list[str]) -> list[str]:
55
+ if not isinstance(values, list):
56
+ raise TypeError("modified_files must be a list of project file paths")
57
+ normalized: list[str] = []
58
+ for value in values:
59
+ if not isinstance(value, str) or not value.strip():
60
+ raise ValueError("modified_files must contain non-empty strings")
61
+ candidate = Path(value).expanduser()
62
+ resolved = candidate.resolve() if candidate.is_absolute() else (root / candidate).resolve()
63
+ try:
64
+ relative = resolved.relative_to(root).as_posix()
65
+ except ValueError as error:
66
+ raise ValueError(f"modified file path is outside the project root: {value}") from error
67
+ if relative not in normalized:
68
+ normalized.append(relative)
69
+ return normalized
70
+
71
+
72
+ def _escape_link_label(value: str) -> str:
73
+ return value.replace("\\", "\\\\").replace("[", "\\[").replace("]", "\\]")
74
+
75
+
76
+ def _modified_files_section(root: Path, report_path: Path, modified_files: list[str]) -> list[str]:
77
+ report_relative = report_path.relative_to(root).as_posix()
78
+ paths = [*modified_files]
79
+ if report_relative not in paths:
80
+ paths.append(report_relative)
81
+ links = []
82
+ for path in paths:
83
+ target = Path(os.path.relpath(root / path, report_path.parent)).as_posix()
84
+ links.append(f"- [{_escape_link_label(path)}]({quote(target, safe='/-._')})")
85
+ return _section("Modified files", "\n".join(links))
86
+
87
+
88
+ def _render_report(
89
+ root: Path,
90
+ report_path: Path,
91
+ request: str,
92
+ summary: str,
93
+ modified_files: list[str],
94
+ changes: list[str] | None,
95
+ validation: list[str] | None,
96
+ status: str,
97
+ generated_date: str,
98
+ ) -> str:
99
+ lines = ["# Request Report", "", f"- Status: {status}", f"- Date: {generated_date}", ""]
100
+ lines.extend(_section("User request", request))
101
+ lines.extend(_section("Summary", summary))
102
+ lines.extend(_modified_files_section(root, report_path, modified_files))
103
+ lines.extend(_bullet_section("Changes", changes))
104
+ lines.extend(_bullet_section("Validation", validation))
105
+ return "\n".join(lines).rstrip() + "\n"
106
+
107
+
108
+ async def run(
109
+ root: str,
110
+ feature: str,
111
+ request: str,
112
+ summary: str,
113
+ modified_files: list[str],
114
+ changes: list[str] | None = None,
115
+ validation: list[str] | None = None,
116
+ status: str = "completed",
117
+ date: str | None = None,
118
+ ) -> str:
119
+ """Create and return one atomic numbered report for a completed request."""
120
+ root_path = Path(root).expanduser().resolve()
121
+ if not root_path.is_dir():
122
+ raise NotADirectoryError(f"{root} is not an existing directory")
123
+ if not isinstance(feature, str) or not feature.strip():
124
+ raise ValueError("feature must be a non-empty string")
125
+ if not isinstance(request, str) or not request.strip():
126
+ raise ValueError("request must be a non-empty string")
127
+ if not isinstance(summary, str) or not summary.strip():
128
+ raise ValueError("summary must be a non-empty string")
129
+ if status not in _VALID_STATUSES:
130
+ raise ValueError(f"status must be one of: {', '.join(sorted(_VALID_STATUSES))}")
131
+
132
+ generated_date = _validate_date(date)
133
+ normalized_modified_files = _normalize_modified_files(root_path, modified_files)
134
+ report_dir = root_path / "docs" / "report"
135
+ report_dir.mkdir(parents=True, exist_ok=True)
136
+ slug = _slugify(feature)
137
+ number = _next_number(report_dir)
138
+
139
+ while True:
140
+ report_path = report_dir / f"{number:0{_REPORT_NUMBER_WIDTH}d}_{slug}_{generated_date}.md"
141
+ content = _render_report(
142
+ root_path,
143
+ report_path,
144
+ request,
145
+ summary,
146
+ normalized_modified_files,
147
+ changes,
148
+ validation,
149
+ status,
150
+ generated_date,
151
+ )
152
+ try:
153
+ with report_path.open("x", encoding="utf-8") as report_file:
154
+ report_file.write(content)
155
+ return str(report_path)
156
+ except FileExistsError:
157
+ number += 1
158
+
159
+
160
+ __all__ = ["run"]
@@ -5,7 +5,7 @@ description: Create, validate, and install ASM Agent skills - both markdown skil
5
5
 
6
6
  # Skill Creator
7
7
 
8
- A skill is a directory with a `SKILL.md` file (YAML frontmatter + markdown instructions). At startup ASM Agent reads only each skill's name and description into the system prompt; the full file loads on demand when a task matches. ASM Agent follows the [Agent Skills standard](https://agentskills.io/specification) and extends it with Python-backed skills.
8
+ A skill is a directory with a `SKILL.md` file (YAML frontmatter + markdown instructions). At startup ASM Agent reads each skill's name and description into the system prompt. The full file normally loads on demand when a task matches. Skills marked `always-apply: true` are an ASM Agent extension whose full instructions are injected at startup. ASM Agent follows the [Agent Skills standard](https://agentskills.io/specification) and extends it with Python-backed and always-applied skills.
9
9
 
10
10
  | Kind | What it is | When to use |
11
11
  |---|---|---|
@@ -51,6 +51,7 @@ description: What this skill does and when to use it. Be specific.
51
51
  |---|---|---|
52
52
  | `name` | Yes | Max 64 chars. Lowercase a-z, 0-9, hyphens. No leading/trailing/consecutive hyphens. Must match the parent directory name. |
53
53
  | `description` | Yes | Max 1024 chars. A skill with a missing or empty description is **silently not loaded**. |
54
+ | `always-apply` | No | `true` injects the full skill instructions into every system prompt. Reserve this for small, universal policies. |
54
55
  | `disable-model-invocation` | No | `true` hides the skill from the system prompt; only explicit `/skill:<name>` invokes it. |
55
56
  | `license` | No | License name or reference to a bundled file. |
56
57
  | `compatibility` | No | Max 500 chars. Environment requirements. |