diff --git a/.github/agents/architect.agent.md b/.github/agents/architect.agent.md index c7012153..6c1a5ef0 100644 --- a/.github/agents/architect.agent.md +++ b/.github/agents/architect.agent.md @@ -16,8 +16,14 @@ tools: - todo - agent agents: - - '*' + - product + - architect + - designer + - engineer + - tester + - release model: + - auto - Claude Sonnet 4.6 (copilot) - GPT-5.3-Codex (copilot) - Claude Opus 4.7 (copilot) @@ -102,7 +108,18 @@ Handoffs you own: Planner-coordinated mode (`@planner` invokes this role as a subagent): - Execute architect-stage scope only; do not invoke downstream roles unless explicitly asked. -- End with a stage report containing: `status`, `changes_made`, `updated_items`, `blockers`, and `next_handoff_summary`. +- End with a structured stage report using this schema: + +Use this exact stage report schema at the end of your response: + +- `status`: `ready` or `blocked` +- `changes_made`: `yes` or `no` +- `updated_items`: list of paths (or `none`) +- `blockers`: list (or `none`) +- `next_handoff_summary`: one short paragraph +- `planner_run_id`: value received in `PLANNER_RUN_ID` (or `none` when not provided) +- `model_used`: model identifier used for this stage (or `unknown`) +- `subagents_invoked`: list of delegated subagents called during this stage (or `none`) ## assess current state @@ -190,4 +207,4 @@ that requires changes to upstream items, flag it and trigger a reverse handoff. - `@#gdpr` — privacy by design and data processing architecture review - + diff --git a/.github/agents/designer.agent.md b/.github/agents/designer.agent.md index a1474018..5314478b 100644 --- a/.github/agents/designer.agent.md +++ b/.github/agents/designer.agent.md @@ -15,8 +15,14 @@ tools: - todo - agent agents: - - '*' + - product + - architect + - designer + - engineer + - tester + - release model: + - auto - Claude Sonnet 4.6 (copilot) - GPT-5.3-Codex (copilot) user-invocable: true @@ -115,7 +121,18 @@ Handoffs you own: Planner-coordinated mode (`@planner` invokes this role as a subagent): - Execute designer-stage scope only; do not invoke downstream roles unless explicitly asked. -- End with a stage report containing: `status`, `changes_made`, `updated_items`, `blockers`, and `next_handoff_summary`. +- End with a structured stage report using this schema: + +Use this exact stage report schema at the end of your response: + +- `status`: `ready` or `blocked` +- `changes_made`: `yes` or `no` +- `updated_items`: list of paths (or `none`) +- `blockers`: list (or `none`) +- `next_handoff_summary`: one short paragraph +- `planner_run_id`: value received in `PLANNER_RUN_ID` (or `none` when not provided) +- `model_used`: model identifier used for this stage (or `unknown`) +- `subagents_invoked`: list of delegated subagents called during this stage (or `none`) ## assess current state @@ -203,4 +220,4 @@ that requires changes to upstream items, flag it and trigger a reverse handoff. - `@#openapi` — OpenAPI 3.1 spec writing and review - + diff --git a/.github/agents/engineer.agent.md b/.github/agents/engineer.agent.md index 8ad86118..e766781a 100644 --- a/.github/agents/engineer.agent.md +++ b/.github/agents/engineer.agent.md @@ -15,8 +15,14 @@ tools: - todo - agent agents: - - '*' + - product + - architect + - designer + - engineer + - tester + - release model: + - auto - GPT-5.3-Codex (copilot) - Claude Sonnet 4.6 (copilot) user-invocable: true @@ -92,7 +98,18 @@ Handoffs you own: Planner-coordinated mode (`@planner` invokes this role as a subagent): - Execute engineer-stage scope only; do not invoke downstream roles unless explicitly asked. -- End with a stage report containing: `status`, `changes_made`, `updated_items`, `blockers`, and `next_handoff_summary`. +- End with a structured stage report using this schema: + +Use this exact stage report schema at the end of your response: + +- `status`: `ready` or `blocked` +- `changes_made`: `yes` or `no` +- `updated_items`: list of paths (or `none`) +- `blockers`: list (or `none`) +- `next_handoff_summary`: one short paragraph +- `planner_run_id`: value received in `PLANNER_RUN_ID` (or `none` when not provided) +- `model_used`: model identifier used for this stage (or `unknown`) +- `subagents_invoked`: list of delegated subagents called during this stage (or `none`) ## parallel delegation @@ -204,4 +221,4 @@ that requires changes to upstream items, flag it and trigger a reverse handoff. - `@#rancher` — Rancher and Fleet multi-cluster operations and governance - + diff --git a/.github/agents/planner.agent.md b/.github/agents/planner.agent.md index fac9f062..7289bccc 100644 --- a/.github/agents/planner.agent.md +++ b/.github/agents/planner.agent.md @@ -17,6 +17,7 @@ agents: - tester - release model: + - auto - GPT-5.3-Codex (copilot) - Claude Sonnet 4.6 (copilot) user-invocable: true @@ -26,13 +27,16 @@ target: vscode ## identity and purpose -You are the **vstack orchestration planner**. You coordinate stage execution by -invoking role agents as subagents and enforcing explicit gate progression. +You are the **vstack orchestration planner**. Your role is to **plan and delegate — not to execute**. + +You coordinate stage execution by invoking the right role agent for each stage and enforcing +explicit gate progression. Every piece of substantive work belongs to a worker agent. The planner +never does that work itself — it assigns, tracks, and advances. ## responsibilities - Read the configured workflow stages and evaluate `depends_on` to determine execution order. -- Invoke the correct role agent for each stage when all its predecessors are complete. +- Invoke the designated worker agent for each ready stage and collect its stage report; never perform the stage work yourself. - Run independent branches in parallel when their `depends_on` sets do not overlap. - Apply gate and human-in-the-loop policy at each transition. - Keep a concise execution log: completed, skipped, blocked, and pending stages. @@ -47,18 +51,58 @@ invoking role agents as subagents and enforcing explicit gate progression. ## scope and boundaries -- Planner owns orchestration and progression logic. -- Worker role agents own domain decisions and artifact updates. -- Planner does not replace role-specific analysis, coding, testing, or release work. +- Planner owns **orchestration only**: dependency evaluation, agent invocation, gate enforcement, and execution tracking. +- Planner produces **no work product of its own**: no code, no architecture decisions, no API contracts, no test results, no release artifacts. All of that belongs to the worker agents. +- When a task or question surfaces, the default answer is: **which worker agent owns this?** Route it. Do not answer it yourself. +- Only coordination tasks with no worker-agent owner (dependency evaluation, gate checks, execution logging, status reporting) stay with the planner. ## limitations and do not do -- Do not perform role-specific work that belongs to worker agents. +The planner does not execute work. It delegates. + +Every work type has a designated worker agent. Route to the right one immediately: + +| Work type | Delegate to | +| --------------------------------------------------- | ------------ | +| Code implementation, review, debugging, refactoring | `@engineer` | +| Architecture decisions, ADRs, service decomposition | `@architect` | +| API contracts, schemas, service interaction flows | `@designer` | +| Requirements, user stories, product specifications | `@product` | +| Verification, security audits, performance analysis | `@tester` | +| Release notes, changelogs, PR preparation | `@release` | + +If you find yourself writing code, drafting an architecture decision, reviewing an API contract, or producing any other domain artifact — stop. That is a worker agent's job. Delegate it. + +Additional constraints: + - Do not auto-advance a blocked stage without explicit user approval. - Do not skip required stages without a clear policy reason. +## request classification — do this first, before starting the pipeline + +Before doing anything else, classify the incoming request into one of three types: + +| Type | Description | Action | +| ----------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ | +| **Full pipeline** | Delivering a feature, fix, or release that spans multiple roles (product → architect → … → release) | Start the stage pipeline | +| **Focused task** | A clearly scoped task owned by one role (e.g. "update the architecture docs", "write an ADR", "fix this bug", "run the tests") | Route directly to the single owning specialist — do not start the pipeline | +| **Query** | A question about the system, status, or plan | Answer from context, or route to the owning specialist if domain expertise is needed | + +**Focused task routing is the most common case for day-to-day work.** When a request maps cleanly to a single role's domain (see the routing table above), invoke only that specialist — not the full pipeline. The pipeline exists for coordinated multi-role delivery, not for every individual task. + +Signs a request is a focused task (not a pipeline run): + +- It names a specific artifact: "update the ADR", "fix the failing test", "write the release notes" +- It targets a single domain: architecture, design, verification, or release — not all of them +- It does not require cross-role handoffs to produce a meaningful result +- It is a maintenance task: documentation update, report refresh, dependency bump + +When in doubt, ask: "Does this need more than one role to complete?" If not, route directly. + ## working principles +- **Classify before orchestrating.** Determine whether the request is a full pipeline run or a focused task before starting any stage. Starting the pipeline for a focused task is overhead without benefit. +- **Delegate always.** The planner does not perform substantive work — it assigns it to the right worker agent and relays the outcome. This is not a fallback strategy; it is the primary operating mode. - Use the configured workflow contract as source of truth. - Evaluate `depends_on` before each stage: a stage is **ready** when all its listed predecessors have status `ready` or `skipped`. A stage without `depends_on` implicitly depends on the @@ -68,6 +112,20 @@ invoking role agents as subagents and enforcing explicit gate progression. - Prefer explicit user confirmation at gate boundaries. - Keep summaries short, factual, and stage-oriented. +## how to delegate + +For every ready stage or domain question: + +1. **Check for a specialist first.** Identify which worker agent owns this type of work (see specialist routing table above). +1. **Compose a focused context prompt:** include the stage goal, relevant predecessor outputs, and changed scope. +1. **Ensure planner correlation is set:** generate one `PLANNER_RUN_ID` at the start of the orchestration run and reuse it for every delegated stage. +1. **Invoke the worker agent:** `@ ` and include `PLANNER_RUN_ID=` in the delegated prompt. +1. **Wait** for the structured stage report or answer from the worker agent. +1. **Relay the output** to the user or the next stage; do not redo, second-guess, or supplement the agent's work. +1. **Evaluate gate and hitl policy** before advancing to the next stage. + +If a domain question surfaces mid-orchestration that no stage report has answered, route it to the relevant specialist instead of answering it yourself. + ## decision guidelines - If workflow config is missing or invalid, stop and report exactly what is wrong. @@ -103,20 +161,31 @@ Execution model: - `hybrid`: orchestrate when explicitly requested; otherwise allow manual flow. 1. Repeat until the graph is fully resolved or a blocker stops progression: a. Identify all stages whose `depends_on` predecessors are all `ready` or `skipped`. - These are the **ready set**. + These are the **ready set**. b. Invoke all stages in the ready set. Stages with no unresolved predecessors may run - in parallel. + in parallel. c. Collect stage reports and mark each stage `ready`, `skipped`, or `blocked`. d. Evaluate gate and hitl policy. Pause for user approval where required before continuing. 1. Continue until the release stage completes or a blocker stops progression. +Planner run correlation: + +- At run start, create one stable `PLANNER_RUN_ID` (for example, UTC timestamp + short suffix). +- Pass the same `PLANNER_RUN_ID` to every delegated worker stage. +- Require each worker stage report to echo the same value in `planner_run_id`. + When invoking a worker stage, require this structured stage report at the end: +Use this exact stage report schema at the end of your response: + - `status`: `ready` or `blocked` - `changes_made`: `yes` or `no` -- `updated_items`: list of paths +- `updated_items`: list of paths (or `none`) - `blockers`: list (or `none`) - `next_handoff_summary`: one short paragraph +- `planner_run_id`: value received in `PLANNER_RUN_ID` (or `none` when not provided) +- `model_used`: model identifier used for this stage (or `unknown`) +- `subagents_invoked`: list of delegated subagents called during this stage (or `none`) ## success criteria @@ -162,4 +231,4 @@ that requires changes to upstream items, flag it and trigger a reverse handoff. - `@#analyse` - assess stage impact, skip rationale, and trade-offs - + diff --git a/.github/agents/product.agent.md b/.github/agents/product.agent.md index 4fd1fbbe..16513d4f 100644 --- a/.github/agents/product.agent.md +++ b/.github/agents/product.agent.md @@ -15,8 +15,14 @@ tools: - todo - agent agents: - - '*' + - product + - architect + - designer + - engineer + - tester + - release model: + - auto - Claude Sonnet 4.6 (copilot) - GPT-5.3-Codex (copilot) - Claude Opus 4.7 (copilot) @@ -103,7 +109,18 @@ Handoffs you own: Planner-coordinated mode (`@planner` invokes this role as a subagent): - Execute product-stage scope only; do not invoke downstream roles unless explicitly asked. -- End with a stage report containing: `status`, `changes_made`, `updated_items`, `blockers`, and `next_handoff_summary`. +- End with a structured stage report using this schema: + +Use this exact stage report schema at the end of your response: + +- `status`: `ready` or `blocked` +- `changes_made`: `yes` or `no` +- `updated_items`: list of paths (or `none`) +- `blockers`: list (or `none`) +- `next_handoff_summary`: one short paragraph +- `planner_run_id`: value received in `PLANNER_RUN_ID` (or `none` when not provided) +- `model_used`: model identifier used for this stage (or `unknown`) +- `subagents_invoked`: list of delegated subagents called during this stage (or `none`) ## how you work @@ -171,7 +188,8 @@ that requires changes to upstream items, flag it and trigger a reverse handoff. - `@#analyse` — impact analysis, tradeoffs, feasibility - `@#adr` — architecture decision record writing (if significant decisions) - `@#onboard` — contributor onboarding guide generation +- `@#space-setup` — set up and maintain Copilot Spaces for project context curation - `@#gh-issues` — create and manage GitHub Issues for requirements, tasks, and user stories - + diff --git a/.github/agents/release.agent.md b/.github/agents/release.agent.md index ddf3034d..72a03f3d 100644 --- a/.github/agents/release.agent.md +++ b/.github/agents/release.agent.md @@ -16,8 +16,14 @@ tools: - todo - agent agents: - - '*' + - product + - architect + - designer + - engineer + - tester + - release model: + - auto - Claude Sonnet 4.6 (copilot) - GPT-5.3-Codex (copilot) user-invocable: true @@ -99,7 +105,18 @@ and wait for explicit user routing decisions. Planner-coordinated mode (`@planner` invokes this role as a subagent): - Execute release-stage scope only. -- End with a stage report containing: `status`, `changes_made`, `updated_items`, `blockers`, and `next_handoff_summary`. +- End with a structured stage report using this schema: + +Use this exact stage report schema at the end of your response: + +- `status`: `ready` or `blocked` +- `changes_made`: `yes` or `no` +- `updated_items`: list of paths (or `none`) +- `blockers`: list (or `none`) +- `next_handoff_summary`: one short paragraph +- `planner_run_id`: value received in `PLANNER_RUN_ID` (or `none` when not provided) +- `model_used`: model identifier used for this stage (or `unknown`) +- `subagents_invoked`: list of delegated subagents called during this stage (or `none`) ## how you work @@ -162,6 +179,7 @@ that requires changes to upstream items, flag it and trigger a reverse handoff. - `@#explore` — codebase discovery and mapping - `@#code-review` — final review before PR is opened - `@#gh-issues` — create and manage GitHub Issues for tracking work and bug reports +- `@#copilot-ops` — operate Copilot governance settings with audit-first change control - + diff --git a/.github/agents/tester.agent.md b/.github/agents/tester.agent.md index b28bbd65..0b28761a 100644 --- a/.github/agents/tester.agent.md +++ b/.github/agents/tester.agent.md @@ -15,8 +15,14 @@ tools: - todo - agent agents: - - '*' + - product + - architect + - designer + - engineer + - tester + - release model: + - auto - Claude Sonnet 4.6 (copilot) - GPT-5.3-Codex (copilot) user-invocable: true @@ -100,7 +106,18 @@ Handoffs you own: Planner-coordinated mode (`@planner` invokes this role as a subagent): - Execute tester-stage scope only; do not invoke downstream roles unless explicitly asked. -- End with a stage report containing: `status`, `changes_made`, `updated_items`, `blockers`, and `next_handoff_summary`. +- End with a structured stage report using this schema: + +Use this exact stage report schema at the end of your response: + +- `status`: `ready` or `blocked` +- `changes_made`: `yes` or `no` +- `updated_items`: list of paths (or `none`) +- `blockers`: list (or `none`) +- `next_handoff_summary`: one short paragraph +- `planner_run_id`: value received in `PLANNER_RUN_ID` (or `none` when not provided) +- `model_used`: model identifier used for this stage (or `unknown`) +- `subagents_invoked`: list of delegated subagents called during this stage (or `none`) ## assess current state @@ -190,4 +207,4 @@ that requires changes to upstream items, flag it and trigger a reverse handoff. - `@#rancher` — Rancher/Fleet configuration and multi-cluster governance review - + diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 95759d18..18c4f9c6 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -12,6 +12,16 @@ vstack provides structured skills for backend/microservice development, executab - **VS Code native.** Skills run in Copilot Chat / Agent Mode. No assumptions about Claude Code or CLI-only flows. - **Backend first.** Prioritize API correctness, reliability, observability, CI/CD, contracts, performance, and security. Browser automation is optional and pluggable. +## Artifact Choice Policy + +- Agents are for roles and handoffs. +- Skills are for reusable procedures. +- Instructions are for always-on policies. +- Prompts are for one-shot task framing. +- Hooks are for repository-level automation. + +When proposals span multiple artifact types, split them into small independent changes. + ## System Structure ``` diff --git a/.github/hooks/agent-call-audit.json b/.github/hooks/agent-call-audit.json new file mode 100644 index 00000000..a378226d --- /dev/null +++ b/.github/hooks/agent-call-audit.json @@ -0,0 +1,45 @@ +{ + "version": 1, + "hooks": { + "sessionStart": [ + { + "type": "command", + "description": "Log session start with best-effort actor identity and model metadata.\n", + "bash": "script_start_ms=\"$(date +%s%3N 2>/dev/null || true)\"\nif [ -z \"$script_start_ms\" ]; then\n script_start_ms=\"$(( $(date +%s) * 1000 ))\"\nfi\ninput=\"$(cat)\"\nlog_level=\"${VSTACK_HOOKS_LOG_LEVEL:-minimal}\"\nlog_root=\"${VSTACK_HOOK_LOG_DIR:-.vstack/logs}\"\nretention_days=\"${VSTACK_HOOK_RETENTION_DAYS:-7}\"\nif ! printf '%s' \"$retention_days\" | grep -Eq '^[1-9][0-9]*$'; then\n retention_days=7\nfi\nif [ -d \"$log_root\" ]; then\n cutoff_day=\"$(date -u -d \"$retention_days days ago\" +%Y%m%d 2>/dev/null || true)\"\n if [ -n \"$cutoff_day\" ]; then\n for day_dir in \"$log_root\"/*; do\n [ -d \"$day_dir\" ] || continue\n day_name=\"$(basename \"$day_dir\")\"\n case \"$day_name\" in\n [0-9][0-9][0-9][0-9][0-1][0-9][0-3][0-9]) ;;\n *) continue ;;\n esac\n if [ \"$day_name\" -lt \"$cutoff_day\" ]; then\n rm -rf \"$day_dir\" || true\n fi\n done\n fi\nfi\nlog_dir=\"$log_root/$(date -u +%Y%m%d)\"\nmkdir -p \"$log_dir\"\nif [ \"$log_level\" = \"off\" ]; then\n exit 0\nfi\nif [ \"$log_level\" = \"verbose\" ]; then\n printf '%s\\n' \"$input\" >> \"$log_dir/hook-agent-call.log\"\n exit 0\nfi\n\nts=\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"\nunknown_events_file=\"$log_dir/hook-agent-call-unknown-events.tsv\"\n\nextract_json_string() {\n key=\"$1\"\n printf '%s' \"$input\" | sed -n \"s/.*\\\"${key}\\\"[[:space:]]*:[[:space:]]*\\\"\\\\([^\\\"]*\\\\)\\\".*/\\\\1/p\" | head -n1\n}\n\nincrement_unknown_event() {\n sid=\"$1\"\n reason=\"$2\"\n tmp_file=\"$unknown_events_file.tmp.$$\"\n if [ -f \"$unknown_events_file\" ]; then\n awk -F '\\t' -v sid=\"$sid\" -v reason=\"$reason\" 'BEGIN { OFS=\"\\t\" }\n NF >= 2 {\n key = $1 SUBSEP $2\n count = 1\n if (NF >= 3 && $3 ~ /^[0-9]+$/) {\n count = $3 + 0\n }\n counts[key] += count\n }\n END {\n target = sid SUBSEP reason\n counts[target] += 1\n for (k in counts) {\n split(k, parts, SUBSEP)\n print parts[1], parts[2], counts[k]\n }\n }' \"$unknown_events_file\" > \"$tmp_file\" && mv \"$tmp_file\" \"$unknown_events_file\"\n else\n printf '%s\\t%s\\t1\\n' \"$sid\" \"$reason\" > \"$unknown_events_file\"\n fi\n}\n\nsession_id=\"$(extract_json_string sessionId)\"\n[ -z \"$session_id\" ] && session_id=\"$(extract_json_string session_id)\"\n[ -z \"$session_id\" ] && session_id=\"$(extract_json_string conversationId)\"\n[ -z \"$session_id\" ] && session_id=\"$(extract_json_string conversation_id)\"\n[ -z \"$session_id\" ] && session_id=\"$(extract_json_string chatId)\"\n[ -z \"$session_id\" ] && session_id=\"$(extract_json_string chat_id)\"\n[ -z \"$session_id\" ] && session_id=\"unknown-session\"\n\nactor_name=\"$(extract_json_string actorName)\"\n[ -z \"$actor_name\" ] && actor_name=\"$(extract_json_string actor_name)\"\n[ -z \"$actor_name\" ] && actor_name=\"$(extract_json_string subagentName)\"\n[ -z \"$actor_name\" ] && actor_name=\"$(extract_json_string subagent_name)\"\n[ -z \"$actor_name\" ] && actor_name=\"$(extract_json_string agentName)\"\n[ -z \"$actor_name\" ] && actor_name=\"$(extract_json_string agent_name)\"\n[ -z \"$actor_name\" ] && actor_name=\"unknown\"\n\nactor_type=\"$(extract_json_string actorType)\"\n[ -z \"$actor_type\" ] && actor_type=\"$(extract_json_string actor_type)\"\nif [ -z \"$actor_type\" ]; then\n if printf '%s' \"$actor_name\" | grep -qi 'subagent'; then\n actor_type=\"subagent\"\n elif [ \"$actor_name\" != \"unknown\" ]; then\n actor_type=\"agent\"\n else\n actor_type=\"unknown\"\n fi\nfi\n\nmodel_used=\"$(extract_json_string model)\"\n[ -z \"$model_used\" ] && model_used=\"$(extract_json_string modelName)\"\n[ -z \"$model_used\" ] && model_used=\"$(extract_json_string model_name)\"\n[ -z \"$model_used\" ] && model_used=\"$(extract_json_string toolModel)\"\n[ -z \"$model_used\" ] && model_used=\"$(extract_json_string tool_model)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"toolCall\"[[:space:]]*:[[:space:]]*{[^}]*\"model\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"arguments\"[[:space:]]*:[[:space:]]*{[^}]*\"model\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"arguments\"[[:space:]]*:[[:space:]]*{[^}]*\"modelName\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"arguments\"[[:space:]]*:[[:space:]]*{[^}]*\"model_name\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"args\"[[:space:]]*:[[:space:]]*{[^}]*\"model\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"args\"[[:space:]]*:[[:space:]]*{[^}]*\"modelName\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"args\"[[:space:]]*:[[:space:]]*{[^}]*\"model_name\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"tool\"[[:space:]]*:[[:space:]]*{[^}]*\"arguments\"[[:space:]]*:[[:space:]]*{[^}]*\"model\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"tool\"[[:space:]]*:[[:space:]]*{[^}]*\"arguments\"[[:space:]]*:[[:space:]]*{[^}]*\"modelName\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"tool\"[[:space:]]*:[[:space:]]*{[^}]*\"arguments\"[[:space:]]*:[[:space:]]*{[^}]*\"model_name\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"toolCall\"[[:space:]]*:[[:space:]]*{[^}]*\"arguments\"[[:space:]]*:[[:space:]]*{[^}]*\"model\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"toolCall\"[[:space:]]*:[[:space:]]*{[^}]*\"arguments\"[[:space:]]*:[[:space:]]*{[^}]*\"modelName\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"toolCall\"[[:space:]]*:[[:space:]]*{[^}]*\"arguments\"[[:space:]]*:[[:space:]]*{[^}]*\"model_name\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"tool_call\"[[:space:]]*:[[:space:]]*{[^}]*\"arguments\"[[:space:]]*:[[:space:]]*{[^}]*\"model\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"tool_call\"[[:space:]]*:[[:space:]]*{[^}]*\"arguments\"[[:space:]]*:[[:space:]]*{[^}]*\"modelName\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"tool_call\"[[:space:]]*:[[:space:]]*{[^}]*\"arguments\"[[:space:]]*:[[:space:]]*{[^}]*\"model_name\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"unknown\"\n\nif [ \"$actor_name\" = \"unknown\" ]; then\n increment_unknown_event \"$session_id\" \"sessionStart_unknown_actor\"\n exit 0\nfi\n\nbytes=\"$(printf '%s' \"$input\" | wc -c | tr -d ' ')\"\nest_tokens=\"$(( (bytes + 3) / 4 ))\"\nscript_end_ms=\"$(date +%s%3N 2>/dev/null || true)\"\nif [ -z \"$script_end_ms\" ]; then\n script_end_ms=\"$(( $(date +%s) * 1000 ))\"\nfi\nhook_execution_ms=\"$((script_end_ms - script_start_ms))\"\n\nprintf '{\"timestamp\":\"%s\",\"event\":\"sessionStart\",\"session_id\":\"%s\",\"actor_name\":\"%s\",\"actor_type\":\"%s\",\"model_used\":\"%s\",\"size_bytes\":%s,\"estimated_tokens\":%s,\"hook_execution_ms\":%s}\\n' \\\n \"$ts\" \"$session_id\" \"$actor_name\" \"$actor_type\" \"$model_used\" \"$bytes\" \"$est_tokens\" \"$hook_execution_ms\" >> \"$log_dir/hook-agent-call.log\"\n", + "powershell": "$inputText = [Console]::In.ReadToEnd()\n$logLevel = if ($env:VSTACK_HOOKS_LOG_LEVEL) { $env:VSTACK_HOOKS_LOG_LEVEL } else { 'minimal' }\n$logRoot = if ($env:VSTACK_HOOK_LOG_DIR) { $env:VSTACK_HOOK_LOG_DIR } else { '.vstack/logs' }\n$retentionDaysRaw = if ($env:VSTACK_HOOK_RETENTION_DAYS) { $env:VSTACK_HOOK_RETENTION_DAYS } else { '7' }\n$retentionDays = 7\nif (-not ([int]::TryParse([string]$retentionDaysRaw, [ref]$retentionDays) -and $retentionDays -gt 0)) {\n $retentionDays = 7\n}\ntry {\n if (Test-Path -LiteralPath $logRoot) {\n $cutoffDay = (Get-Date).ToUniversalTime().AddDays(-$retentionDays).ToString('yyyyMMdd')\n Get-ChildItem -LiteralPath $logRoot -Directory -ErrorAction Stop |\n Where-Object { $_.Name -match '^\\d{8}$' -and $_.Name -lt $cutoffDay } |\n ForEach-Object { Remove-Item -LiteralPath $_.FullName -Recurse -Force -ErrorAction SilentlyContinue }\n }\n} catch {}\n$logDir = Join-Path $logRoot (Get-Date).ToUniversalTime().ToString('yyyyMMdd')\nNew-Item -ItemType Directory -Force -Path $logDir | Out-Null\nif ($logLevel -eq 'off') { exit 0 }\nif ($logLevel -eq 'verbose') {\n Add-Content -Path (Join-Path $logDir 'hook-agent-call.log') -Value $inputText\n exit 0\n}\n\n$scriptStart = Get-Date\n$unknownEventsPath = Join-Path $logDir 'hook-agent-call-unknown-events.tsv'\n\nfunction Get-PathValue {\n param([object]$Object, [string]$Path)\n $current = $Object\n foreach ($part in $Path.Split('.')) {\n if ($null -eq $current) { return $null }\n $prop = $current.PSObject.Properties[$part]\n if ($null -eq $prop) { return $null }\n $current = $prop.Value\n }\n return $current\n}\n\nfunction Get-FirstString {\n param([object]$Object, [string[]]$Paths)\n foreach ($path in $Paths) {\n $value = Get-PathValue -Object $Object -Path $path\n if ($null -ne $value -and [string]$value -ne '') { return [string]$value }\n }\n return $null\n}\n\nfunction Add-UnknownEventCounter {\n param(\n [string]$Path,\n [string]$SessionId,\n [string]$Reason\n )\n\n $counts = @{}\n if (Test-Path $Path) {\n foreach ($line in (Get-Content -Path $Path)) {\n if ([string]::IsNullOrWhiteSpace($line)) { continue }\n $parts = $line -split \"`t\"\n if ($parts.Count -lt 2) { continue }\n\n $sid = [string]$parts[0]\n $why = [string]$parts[1]\n $count = 1\n if ($parts.Count -ge 3) {\n $parsedCount = 0\n if ([int]::TryParse([string]$parts[2], [ref]$parsedCount)) {\n $count = [int][Math]::Max(0, $parsedCount)\n }\n }\n\n $key = \"$sid`t$why\"\n if (-not $counts.ContainsKey($key)) { $counts[$key] = 0 }\n $counts[$key] += $count\n }\n }\n\n $target = \"$SessionId`t$Reason\"\n if (-not $counts.ContainsKey($target)) { $counts[$target] = 0 }\n $counts[$target] += 1\n\n $rows = foreach ($entry in ($counts.GetEnumerator() | Sort-Object Name)) {\n \"{0}`t{1}\" -f $entry.Name, $entry.Value\n }\n Set-Content -Path $Path -Value $rows\n}\n\n$payload = $null\ntry { $payload = $inputText | ConvertFrom-Json -ErrorAction Stop } catch {}\n\n$sessionId = 'unknown-session'\n$actorName = 'unknown'\n$actorType = 'unknown'\n$modelUsed = 'unknown'\n\nif ($null -ne $payload) {\n $sessionIdCandidate = Get-FirstString -Object $payload -Paths @('sessionId', 'session_id', 'conversationId', 'conversation_id', 'chatId', 'chat_id')\n if ($sessionIdCandidate) { $sessionId = $sessionIdCandidate }\n\n $actorNameCandidate = Get-FirstString -Object $payload -Paths @('actorName', 'actor_name', 'subagentName', 'subagent_name', 'agentName', 'agent_name')\n if ($actorNameCandidate) { $actorName = $actorNameCandidate }\n\n $actorTypeCandidate = Get-FirstString -Object $payload -Paths @('actorType', 'actor_type')\n if ($actorTypeCandidate) { $actorType = $actorTypeCandidate }\n\n $modelCandidate = Get-FirstString -Object $payload -Paths @('model', 'modelName', 'model_name', 'toolModel', 'tool_model', 'toolCall.model', 'tool_call.model', 'arguments.model', 'arguments.modelName', 'arguments.model_name', 'args.model', 'args.modelName', 'args.model_name', 'tool.arguments.model', 'tool.arguments.modelName', 'tool.arguments.model_name', 'toolCall.arguments.model', 'toolCall.arguments.modelName', 'toolCall.arguments.model_name', 'tool_call.arguments.model', 'tool_call.arguments.modelName', 'tool_call.arguments.model_name')\n if ($modelCandidate) { $modelUsed = $modelCandidate }\n}\n\nif ($actorType -eq 'unknown') {\n if ($actorName -match 'subagent') {\n $actorType = 'subagent'\n } elseif ($actorName -ne 'unknown') {\n $actorType = 'agent'\n }\n}\n\nif ($actorName -eq 'unknown') {\n Add-UnknownEventCounter -Path $unknownEventsPath -SessionId $sessionId -Reason 'sessionStart_unknown_actor'\n exit 0\n}\n\n$sizeBytes = [System.Text.Encoding]::UTF8.GetByteCount($inputText)\n$hookExecutionMs = [int][Math]::Max(0, ((Get-Date) - $scriptStart).TotalMilliseconds)\n$record = @{\n timestamp = (Get-Date).ToUniversalTime().ToString('o')\n event = 'sessionStart'\n session_id = $sessionId\n actor_name = $actorName\n actor_type = $actorType\n model_used = $modelUsed\n size_bytes = $sizeBytes\n estimated_tokens = [int][math]::Ceiling($sizeBytes / 4.0)\n hook_execution_ms = $hookExecutionMs\n} | ConvertTo-Json -Compress\nAdd-Content -Path (Join-Path $logDir 'hook-agent-call.log') -Value $record\n", + "cwd": ".", + "timeoutSec": 5 + } + ], + "sessionEnd": [ + { + "type": "command", + "description": "Log session end with best-effort actor identity and unknown-summary counters.\n", + "bash": "script_start_ms=\"$(date +%s%3N 2>/dev/null || true)\"\nif [ -z \"$script_start_ms\" ]; then\n script_start_ms=\"$(( $(date +%s) * 1000 ))\"\nfi\ninput=\"$(cat)\"\nlog_level=\"${VSTACK_HOOKS_LOG_LEVEL:-minimal}\"\nlog_root=\"${VSTACK_HOOK_LOG_DIR:-.vstack/logs}\"\nretention_days=\"${VSTACK_HOOK_RETENTION_DAYS:-7}\"\nif ! printf '%s' \"$retention_days\" | grep -Eq '^[1-9][0-9]*$'; then\n retention_days=7\nfi\nif [ -d \"$log_root\" ]; then\n cutoff_day=\"$(date -u -d \"$retention_days days ago\" +%Y%m%d 2>/dev/null || true)\"\n if [ -n \"$cutoff_day\" ]; then\n for day_dir in \"$log_root\"/*; do\n [ -d \"$day_dir\" ] || continue\n day_name=\"$(basename \"$day_dir\")\"\n case \"$day_name\" in\n [0-9][0-9][0-9][0-9][0-1][0-9][0-3][0-9]) ;;\n *) continue ;;\n esac\n if [ \"$day_name\" -lt \"$cutoff_day\" ]; then\n rm -rf \"$day_dir\" || true\n fi\n done\n fi\nfi\nlog_dir=\"$log_root/$(date -u +%Y%m%d)\"\nmkdir -p \"$log_dir\"\nif [ \"$log_level\" = \"off\" ]; then\n exit 0\nfi\nif [ \"$log_level\" = \"verbose\" ]; then\n printf '%s\\n' \"$input\" >> \"$log_dir/hook-agent-call.log\"\n exit 0\nfi\n\nts=\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"\nunknown_events_file=\"$log_dir/hook-agent-call-unknown-events.tsv\"\n\nextract_json_string() {\n key=\"$1\"\n printf '%s' \"$input\" | sed -n \"s/.*\\\"${key}\\\"[[:space:]]*:[[:space:]]*\\\"\\\\([^\\\"]*\\\\)\\\".*/\\\\1/p\" | head -n1\n}\n\nincrement_unknown_event() {\n sid=\"$1\"\n reason=\"$2\"\n tmp_file=\"$unknown_events_file.tmp.$$\"\n if [ -f \"$unknown_events_file\" ]; then\n awk -F '\\t' -v sid=\"$sid\" -v reason=\"$reason\" 'BEGIN { OFS=\"\\t\" }\n NF >= 2 {\n key = $1 SUBSEP $2\n count = 1\n if (NF >= 3 && $3 ~ /^[0-9]+$/) {\n count = $3 + 0\n }\n counts[key] += count\n }\n END {\n target = sid SUBSEP reason\n counts[target] += 1\n for (k in counts) {\n split(k, parts, SUBSEP)\n print parts[1], parts[2], counts[k]\n }\n }' \"$unknown_events_file\" > \"$tmp_file\" && mv \"$tmp_file\" \"$unknown_events_file\"\n else\n printf '%s\\t%s\\t1\\n' \"$sid\" \"$reason\" > \"$unknown_events_file\"\n fi\n}\n\nsession_id=\"$(extract_json_string sessionId)\"\n[ -z \"$session_id\" ] && session_id=\"$(extract_json_string session_id)\"\n[ -z \"$session_id\" ] && session_id=\"$(extract_json_string conversationId)\"\n[ -z \"$session_id\" ] && session_id=\"$(extract_json_string conversation_id)\"\n[ -z \"$session_id\" ] && session_id=\"$(extract_json_string chatId)\"\n[ -z \"$session_id\" ] && session_id=\"$(extract_json_string chat_id)\"\n[ -z \"$session_id\" ] && session_id=\"unknown-session\"\n\nactor_name=\"$(extract_json_string actorName)\"\n[ -z \"$actor_name\" ] && actor_name=\"$(extract_json_string actor_name)\"\n[ -z \"$actor_name\" ] && actor_name=\"$(extract_json_string subagentName)\"\n[ -z \"$actor_name\" ] && actor_name=\"$(extract_json_string subagent_name)\"\n[ -z \"$actor_name\" ] && actor_name=\"$(extract_json_string agentName)\"\n[ -z \"$actor_name\" ] && actor_name=\"$(extract_json_string agent_name)\"\n[ -z \"$actor_name\" ] && actor_name=\"unknown\"\n\nactor_type=\"$(extract_json_string actorType)\"\n[ -z \"$actor_type\" ] && actor_type=\"$(extract_json_string actor_type)\"\nif [ -z \"$actor_type\" ]; then\n if printf '%s' \"$actor_name\" | grep -qi 'subagent'; then\n actor_type=\"subagent\"\n elif [ \"$actor_name\" != \"unknown\" ]; then\n actor_type=\"agent\"\n else\n actor_type=\"unknown\"\n fi\nfi\n\nmodel_used=\"$(extract_json_string model)\"\n[ -z \"$model_used\" ] && model_used=\"$(extract_json_string modelName)\"\n[ -z \"$model_used\" ] && model_used=\"$(extract_json_string model_name)\"\n[ -z \"$model_used\" ] && model_used=\"$(extract_json_string toolModel)\"\n[ -z \"$model_used\" ] && model_used=\"$(extract_json_string tool_model)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"toolCall\"[[:space:]]*:[[:space:]]*{[^}]*\"model\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"arguments\"[[:space:]]*:[[:space:]]*{[^}]*\"model\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"arguments\"[[:space:]]*:[[:space:]]*{[^}]*\"modelName\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"arguments\"[[:space:]]*:[[:space:]]*{[^}]*\"model_name\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"args\"[[:space:]]*:[[:space:]]*{[^}]*\"model\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"args\"[[:space:]]*:[[:space:]]*{[^}]*\"modelName\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"args\"[[:space:]]*:[[:space:]]*{[^}]*\"model_name\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"tool\"[[:space:]]*:[[:space:]]*{[^}]*\"arguments\"[[:space:]]*:[[:space:]]*{[^}]*\"model\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"tool\"[[:space:]]*:[[:space:]]*{[^}]*\"arguments\"[[:space:]]*:[[:space:]]*{[^}]*\"modelName\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"tool\"[[:space:]]*:[[:space:]]*{[^}]*\"arguments\"[[:space:]]*:[[:space:]]*{[^}]*\"model_name\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"toolCall\"[[:space:]]*:[[:space:]]*{[^}]*\"arguments\"[[:space:]]*:[[:space:]]*{[^}]*\"model\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"toolCall\"[[:space:]]*:[[:space:]]*{[^}]*\"arguments\"[[:space:]]*:[[:space:]]*{[^}]*\"modelName\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"toolCall\"[[:space:]]*:[[:space:]]*{[^}]*\"arguments\"[[:space:]]*:[[:space:]]*{[^}]*\"model_name\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"tool_call\"[[:space:]]*:[[:space:]]*{[^}]*\"arguments\"[[:space:]]*:[[:space:]]*{[^}]*\"model\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"tool_call\"[[:space:]]*:[[:space:]]*{[^}]*\"arguments\"[[:space:]]*:[[:space:]]*{[^}]*\"modelName\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"tool_call\"[[:space:]]*:[[:space:]]*{[^}]*\"arguments\"[[:space:]]*:[[:space:]]*{[^}]*\"model_name\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"unknown\"\n\nif [ \"$actor_name\" = \"unknown\" ]; then\n increment_unknown_event \"$session_id\" \"sessionEnd_unknown_actor\"\nelse\n bytes=\"$(printf '%s' \"$input\" | wc -c | tr -d ' ')\"\n est_tokens=\"$(( (bytes + 3) / 4 ))\"\n script_end_ms=\"$(date +%s%3N 2>/dev/null || true)\"\n if [ -z \"$script_end_ms\" ]; then\n script_end_ms=\"$(( $(date +%s) * 1000 ))\"\n fi\n hook_execution_ms=\"$((script_end_ms - script_start_ms))\"\n\n printf '{\"timestamp\":\"%s\",\"event\":\"sessionEnd\",\"session_id\":\"%s\",\"actor_name\":\"%s\",\"actor_type\":\"%s\",\"model_used\":\"%s\",\"size_bytes\":%s,\"estimated_tokens\":%s,\"hook_execution_ms\":%s}\\n' \\\n \"$ts\" \"$session_id\" \"$actor_name\" \"$actor_type\" \"$model_used\" \"$bytes\" \"$est_tokens\" \"$hook_execution_ms\" >> \"$log_dir/hook-agent-call.log\"\nfi\n\nif [ -f \"$unknown_events_file\" ]; then\n unknown_total=\"$(awk -F '\\t' -v sid=\"$session_id\" '$1==sid {count=1; if (NF>=3 && $3 ~ /^[0-9]+$/) count=$3+0; total+=count} END {print total+0}' \"$unknown_events_file\")\"\n if [ \"$unknown_total\" -gt 0 ]; then\n unknown_breakdown=\"$(awk -F '\\t' -v sid=\"$session_id\" '$1==sid {count=1; if (NF>=3 && $3 ~ /^[0-9]+$/) count=$3+0; counts[$2]+=count} END {for (k in counts) {if (out != \"\") out=out \";\"; out=out k \"=\" counts[k]} print out}' \"$unknown_events_file\")\"\n printf '{\"timestamp\":\"%s\",\"event\":\"unknownSummary\",\"session_id\":\"%s\",\"unknown_total\":%s,\"unknown_breakdown\":\"%s\",\"model_used\":\"%s\"}\\n' \\\n \"$ts\" \"$session_id\" \"$unknown_total\" \"$unknown_breakdown\" \"$model_used\" >> \"$log_dir/hook-agent-call.log\"\n fi\n tmp_file=\"$unknown_events_file.tmp.$$\"\n awk -F '\\t' -v sid=\"$session_id\" 'BEGIN { OFS=\"\\t\" } $1!=sid && NF>=2 {count=1; if (NF>=3 && $3 ~ /^[0-9]+$/) count=$3+0; print $1, $2, count}' \"$unknown_events_file\" > \"$tmp_file\" && mv \"$tmp_file\" \"$unknown_events_file\"\nfi\n", + "powershell": "$inputText = [Console]::In.ReadToEnd()\n$logLevel = if ($env:VSTACK_HOOKS_LOG_LEVEL) { $env:VSTACK_HOOKS_LOG_LEVEL } else { 'minimal' }\n$logRoot = if ($env:VSTACK_HOOK_LOG_DIR) { $env:VSTACK_HOOK_LOG_DIR } else { '.vstack/logs' }\n$retentionDaysRaw = if ($env:VSTACK_HOOK_RETENTION_DAYS) { $env:VSTACK_HOOK_RETENTION_DAYS } else { '7' }\n$retentionDays = 7\nif (-not ([int]::TryParse([string]$retentionDaysRaw, [ref]$retentionDays) -and $retentionDays -gt 0)) {\n $retentionDays = 7\n}\ntry {\n if (Test-Path -LiteralPath $logRoot) {\n $cutoffDay = (Get-Date).ToUniversalTime().AddDays(-$retentionDays).ToString('yyyyMMdd')\n Get-ChildItem -LiteralPath $logRoot -Directory -ErrorAction Stop |\n Where-Object { $_.Name -match '^\\d{8}$' -and $_.Name -lt $cutoffDay } |\n ForEach-Object { Remove-Item -LiteralPath $_.FullName -Recurse -Force -ErrorAction SilentlyContinue }\n }\n} catch {}\n$logDir = Join-Path $logRoot (Get-Date).ToUniversalTime().ToString('yyyyMMdd')\nNew-Item -ItemType Directory -Force -Path $logDir | Out-Null\nif ($logLevel -eq 'off') { exit 0 }\nif ($logLevel -eq 'verbose') {\n Add-Content -Path (Join-Path $logDir 'hook-agent-call.log') -Value $inputText\n exit 0\n}\n\n$scriptStart = Get-Date\n$unknownEventsPath = Join-Path $logDir 'hook-agent-call-unknown-events.tsv'\n\nfunction Get-PathValue {\n param([object]$Object, [string]$Path)\n $current = $Object\n foreach ($part in $Path.Split('.')) {\n if ($null -eq $current) { return $null }\n $prop = $current.PSObject.Properties[$part]\n if ($null -eq $prop) { return $null }\n $current = $prop.Value\n }\n return $current\n}\n\nfunction Get-FirstString {\n param([object]$Object, [string[]]$Paths)\n foreach ($path in $Paths) {\n $value = Get-PathValue -Object $Object -Path $path\n if ($null -ne $value -and [string]$value -ne '') { return [string]$value }\n }\n return $null\n}\n\nfunction Add-UnknownEventCounter {\n param(\n [string]$Path,\n [string]$SessionId,\n [string]$Reason\n )\n\n $counts = @{}\n if (Test-Path $Path) {\n foreach ($line in (Get-Content -Path $Path)) {\n if ([string]::IsNullOrWhiteSpace($line)) { continue }\n $parts = $line -split \"`t\"\n if ($parts.Count -lt 2) { continue }\n\n $sid = [string]$parts[0]\n $why = [string]$parts[1]\n $count = 1\n if ($parts.Count -ge 3) {\n $parsedCount = 0\n if ([int]::TryParse([string]$parts[2], [ref]$parsedCount)) {\n $count = [int][Math]::Max(0, $parsedCount)\n }\n }\n\n $key = \"$sid`t$why\"\n if (-not $counts.ContainsKey($key)) { $counts[$key] = 0 }\n $counts[$key] += $count\n }\n }\n\n $target = \"$SessionId`t$Reason\"\n if (-not $counts.ContainsKey($target)) { $counts[$target] = 0 }\n $counts[$target] += 1\n\n $rows = foreach ($entry in ($counts.GetEnumerator() | Sort-Object Name)) {\n \"{0}`t{1}\" -f $entry.Name, $entry.Value\n }\n Set-Content -Path $Path -Value $rows\n}\n\n$payload = $null\ntry { $payload = $inputText | ConvertFrom-Json -ErrorAction Stop } catch {}\n\n$sessionId = 'unknown-session'\n$actorName = 'unknown'\n$actorType = 'unknown'\n$modelUsed = 'unknown'\n\nif ($null -ne $payload) {\n $sessionIdCandidate = Get-FirstString -Object $payload -Paths @('sessionId', 'session_id', 'conversationId', 'conversation_id', 'chatId', 'chat_id')\n if ($sessionIdCandidate) { $sessionId = $sessionIdCandidate }\n\n $actorNameCandidate = Get-FirstString -Object $payload -Paths @('actorName', 'actor_name', 'subagentName', 'subagent_name', 'agentName', 'agent_name')\n if ($actorNameCandidate) { $actorName = $actorNameCandidate }\n\n $actorTypeCandidate = Get-FirstString -Object $payload -Paths @('actorType', 'actor_type')\n if ($actorTypeCandidate) { $actorType = $actorTypeCandidate }\n\n $modelCandidate = Get-FirstString -Object $payload -Paths @('model', 'modelName', 'model_name', 'toolModel', 'tool_model', 'toolCall.model', 'tool_call.model', 'arguments.model', 'arguments.modelName', 'arguments.model_name', 'args.model', 'args.modelName', 'args.model_name', 'tool.arguments.model', 'tool.arguments.modelName', 'tool.arguments.model_name', 'toolCall.arguments.model', 'toolCall.arguments.modelName', 'toolCall.arguments.model_name', 'tool_call.arguments.model', 'tool_call.arguments.modelName', 'tool_call.arguments.model_name')\n if ($modelCandidate) { $modelUsed = $modelCandidate }\n}\n\nif ($actorType -eq 'unknown') {\n if ($actorName -match 'subagent') {\n $actorType = 'subagent'\n } elseif ($actorName -ne 'unknown') {\n $actorType = 'agent'\n }\n}\n\nif ($actorName -eq 'unknown') {\n Add-UnknownEventCounter -Path $unknownEventsPath -SessionId $sessionId -Reason 'sessionEnd_unknown_actor'\n} else {\n $sizeBytes = [System.Text.Encoding]::UTF8.GetByteCount($inputText)\n $hookExecutionMs = [int][Math]::Max(0, ((Get-Date) - $scriptStart).TotalMilliseconds)\n $record = @{\n timestamp = (Get-Date).ToUniversalTime().ToString('o')\n event = 'sessionEnd'\n session_id = $sessionId\n actor_name = $actorName\n actor_type = $actorType\n model_used = $modelUsed\n size_bytes = $sizeBytes\n estimated_tokens = [int][math]::Ceiling($sizeBytes / 4.0)\n hook_execution_ms = $hookExecutionMs\n } | ConvertTo-Json -Compress\n Add-Content -Path (Join-Path $logDir 'hook-agent-call.log') -Value $record\n}\n\nif (Test-Path $unknownEventsPath) {\n $matchingCounts = @{}\n $remainingCounts = @{}\n foreach ($line in (Get-Content -Path $unknownEventsPath)) {\n if ([string]::IsNullOrWhiteSpace($line)) { continue }\n $parts = $line -split \"`t\"\n if ($parts.Count -lt 2) { continue }\n\n $lineSessionId = [string]$parts[0]\n $reason = [string]$parts[1]\n $count = 1\n if ($parts.Count -ge 3) {\n $parsedCount = 0\n if ([int]::TryParse([string]$parts[2], [ref]$parsedCount)) {\n $count = [int][Math]::Max(0, $parsedCount)\n }\n }\n\n if ($lineSessionId -eq $sessionId) {\n if (-not $matchingCounts.ContainsKey($reason)) { $matchingCounts[$reason] = 0 }\n $matchingCounts[$reason] += $count\n } else {\n $remainingKey = \"$lineSessionId`t$reason\"\n if (-not $remainingCounts.ContainsKey($remainingKey)) { $remainingCounts[$remainingKey] = 0 }\n $remainingCounts[$remainingKey] += $count\n }\n }\n\n if ($matchingCounts.Count -gt 0) {\n $unknownTotal = 0\n $summaryPairs = @()\n foreach ($entry in ($matchingCounts.GetEnumerator() | Sort-Object Name)) {\n $unknownTotal += [int]$entry.Value\n $summaryPairs += (\"{0}={1}\" -f $entry.Name, $entry.Value)\n }\n $summary = $summaryPairs -join ';'\n $summaryRecord = @{\n timestamp = (Get-Date).ToUniversalTime().ToString('o')\n event = 'unknownSummary'\n session_id = $sessionId\n unknown_total = $unknownTotal\n unknown_breakdown = $summary\n model_used = $modelUsed\n } | ConvertTo-Json -Compress\n Add-Content -Path (Join-Path $logDir 'hook-agent-call.log') -Value $summaryRecord\n }\n\n $remaining = @()\n foreach ($entry in ($remainingCounts.GetEnumerator() | Sort-Object Name)) {\n $remaining += (\"{0}`t{1}\" -f $entry.Name, $entry.Value)\n }\n Set-Content -Path $unknownEventsPath -Value $remaining\n}\n", + "cwd": ".", + "timeoutSec": 5 + } + ], + "preToolUse": [ + { + "type": "command", + "description": "Log tool invocation requests with best-effort actor, delegation, and model metadata.\n", + "bash": "script_start_ms=\"$(date +%s%3N 2>/dev/null || true)\"\nif [ -z \"$script_start_ms\" ]; then\n script_start_ms=\"$(( $(date +%s) * 1000 ))\"\nfi\ninput=\"$(cat)\"\nlog_level=\"${VSTACK_HOOKS_LOG_LEVEL:-minimal}\"\nlog_root=\"${VSTACK_HOOK_LOG_DIR:-.vstack/logs}\"\nretention_days=\"${VSTACK_HOOK_RETENTION_DAYS:-7}\"\nif ! printf '%s' \"$retention_days\" | grep -Eq '^[1-9][0-9]*$'; then\n retention_days=7\nfi\nif [ -d \"$log_root\" ]; then\n cutoff_day=\"$(date -u -d \"$retention_days days ago\" +%Y%m%d 2>/dev/null || true)\"\n if [ -n \"$cutoff_day\" ]; then\n for day_dir in \"$log_root\"/*; do\n [ -d \"$day_dir\" ] || continue\n day_name=\"$(basename \"$day_dir\")\"\n case \"$day_name\" in\n [0-9][0-9][0-9][0-9][0-1][0-9][0-3][0-9]) ;;\n *) continue ;;\n esac\n if [ \"$day_name\" -lt \"$cutoff_day\" ]; then\n rm -rf \"$day_dir\" || true\n fi\n done\n fi\nfi\nlog_dir=\"$log_root/$(date -u +%Y%m%d)\"\nmkdir -p \"$log_dir\"\nif [ \"$log_level\" = \"off\" ]; then\n exit 0\nfi\nif [ \"$log_level\" = \"verbose\" ]; then\n printf '%s\\n' \"$input\" >> \"$log_dir/hook-agent-call.log\"\n exit 0\nfi\n\nts=\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"\nunknown_events_file=\"$log_dir/hook-agent-call-unknown-events.tsv\"\n\nextract_json_string() {\n key=\"$1\"\n printf '%s' \"$input\" | sed -n \"s/.*\\\"${key}\\\"[[:space:]]*:[[:space:]]*\\\"\\\\([^\\\"]*\\\\)\\\".*/\\\\1/p\" | head -n1\n}\n\nincrement_unknown_event() {\n sid=\"$1\"\n reason=\"$2\"\n tmp_file=\"$unknown_events_file.tmp.$$\"\n if [ -f \"$unknown_events_file\" ]; then\n awk -F '\\t' -v sid=\"$sid\" -v reason=\"$reason\" 'BEGIN { OFS=\"\\t\" }\n NF >= 2 {\n key = $1 SUBSEP $2\n count = 1\n if (NF >= 3 && $3 ~ /^[0-9]+$/) {\n count = $3 + 0\n }\n counts[key] += count\n }\n END {\n target = sid SUBSEP reason\n counts[target] += 1\n for (k in counts) {\n split(k, parts, SUBSEP)\n print parts[1], parts[2], counts[k]\n }\n }' \"$unknown_events_file\" > \"$tmp_file\" && mv \"$tmp_file\" \"$unknown_events_file\"\n else\n printf '%s\\t%s\\t1\\n' \"$sid\" \"$reason\" > \"$unknown_events_file\"\n fi\n}\n\nsession_id=\"$(extract_json_string sessionId)\"\n[ -z \"$session_id\" ] && session_id=\"$(extract_json_string session_id)\"\n[ -z \"$session_id\" ] && session_id=\"$(extract_json_string conversationId)\"\n[ -z \"$session_id\" ] && session_id=\"$(extract_json_string conversation_id)\"\n[ -z \"$session_id\" ] && session_id=\"$(extract_json_string chatId)\"\n[ -z \"$session_id\" ] && session_id=\"$(extract_json_string chat_id)\"\n[ -z \"$session_id\" ] && session_id=\"unknown-session\"\n\nactor_name=\"$(extract_json_string actorName)\"\n[ -z \"$actor_name\" ] && actor_name=\"$(extract_json_string actor_name)\"\n[ -z \"$actor_name\" ] && actor_name=\"$(extract_json_string subagentName)\"\n[ -z \"$actor_name\" ] && actor_name=\"$(extract_json_string subagent_name)\"\n[ -z \"$actor_name\" ] && actor_name=\"$(extract_json_string agentName)\"\n[ -z \"$actor_name\" ] && actor_name=\"$(extract_json_string agent_name)\"\n[ -z \"$actor_name\" ] && actor_name=\"unknown\"\n\nactor_type=\"$(extract_json_string actorType)\"\n[ -z \"$actor_type\" ] && actor_type=\"$(extract_json_string actor_type)\"\nif [ -z \"$actor_type\" ]; then\n if printf '%s' \"$actor_name\" | grep -qi 'subagent'; then\n actor_type=\"subagent\"\n elif [ \"$actor_name\" != \"unknown\" ]; then\n actor_type=\"agent\"\n else\n actor_type=\"unknown\"\n fi\nfi\n\ntool_name=\"$(extract_json_string toolName)\"\n[ -z \"$tool_name\" ] && tool_name=\"$(extract_json_string tool_name)\"\n[ -z \"$tool_name\" ] && tool_name=\"$(extract_json_string recipient_name)\"\n[ -z \"$tool_name\" ] && tool_name=\"$(printf '%s' \"$input\" | sed -n 's/.*\"tool\"[[:space:]]*:[[:space:]]*{[^}]*\"name\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$tool_name\" ] && tool_name=\"$(printf '%s' \"$input\" | sed -n 's/.*\"toolCall\"[[:space:]]*:[[:space:]]*{[^}]*\"name\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$tool_name\" ] && tool_name=\"$(printf '%s' \"$input\" | sed -n 's/.*\"toolCall\"[[:space:]]*:[[:space:]]*{[^}]*\"toolName\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$tool_name\" ] && tool_name=\"$(printf '%s' \"$input\" | sed -n 's/.*\"tool_call\"[[:space:]]*:[[:space:]]*{[^}]*\"name\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$tool_name\" ] && tool_name=\"$(printf '%s' \"$input\" | sed -n 's/.*\"tool_call\"[[:space:]]*:[[:space:]]*{[^}]*\"tool_name\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$tool_name\" ] && tool_name=\"$(printf '%s' \"$input\" | grep -Eo '[A-Za-z_][A-Za-z0-9_]*\\.[A-Za-z_][A-Za-z0-9_]*' | head -n1)\"\n[ -z \"$tool_name\" ] && tool_name=\"unknown\"\n\ndelegated_agent_name=\"unknown\"\nif [ \"$tool_name\" = \"runSubagent\" ]; then\n delegated_agent_name=\"$(extract_json_string delegatedAgentName)\"\n [ -z \"$delegated_agent_name\" ] && delegated_agent_name=\"$(extract_json_string delegated_agent_name)\"\n [ -z \"$delegated_agent_name\" ] && delegated_agent_name=\"$(extract_json_string agentName)\"\n [ -z \"$delegated_agent_name\" ] && delegated_agent_name=\"$(extract_json_string agent_name)\"\n [ -z \"$delegated_agent_name\" ] && delegated_agent_name=\"$(printf '%s' \"$input\" | sed -n 's/.*\"runSubagent\"[[:space:]]*:[[:space:]]*{[^}]*\"agentName\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n [ -z \"$delegated_agent_name\" ] && delegated_agent_name=\"$(printf '%s' \"$input\" | sed -n 's/.*\"runSubagent\"[[:space:]]*:[[:space:]]*{[^}]*\"agent_name\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n [ -z \"$delegated_agent_name\" ] && delegated_agent_name=\"$(printf '%s' \"$input\" | sed -n 's/.*\"arguments\"[[:space:]]*:[[:space:]]*{[^}]*\"agentName\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n [ -z \"$delegated_agent_name\" ] && delegated_agent_name=\"$(printf '%s' \"$input\" | sed -n 's/.*\"arguments\"[[:space:]]*:[[:space:]]*{[^}]*\"agent_name\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n [ -z \"$delegated_agent_name\" ] && delegated_agent_name=\"unknown\"\nfi\n\ntool_call_id=\"$(extract_json_string toolCallId)\"\n[ -z \"$tool_call_id\" ] && tool_call_id=\"$(extract_json_string tool_call_id)\"\n[ -z \"$tool_call_id\" ] && tool_call_id=\"$(extract_json_string callId)\"\n[ -z \"$tool_call_id\" ] && tool_call_id=\"$(extract_json_string call_id)\"\n[ -z \"$tool_call_id\" ] && tool_call_id=\"$(printf '%s' \"$input\" | sed -n 's/.*\"toolCall\"[[:space:]]*:[[:space:]]*{[^}]*\"id\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$tool_call_id\" ] && tool_call_id=\"$(printf '%s' \"$input\" | sed -n 's/.*\"tool_call\"[[:space:]]*:[[:space:]]*{[^}]*\"id\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$tool_call_id\" ] && tool_call_id=\"unknown\"\n\nmodel_used=\"$(extract_json_string model)\"\n[ -z \"$model_used\" ] && model_used=\"$(extract_json_string modelName)\"\n[ -z \"$model_used\" ] && model_used=\"$(extract_json_string model_name)\"\n[ -z \"$model_used\" ] && model_used=\"$(extract_json_string toolModel)\"\n[ -z \"$model_used\" ] && model_used=\"$(extract_json_string tool_model)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"toolCall\"[[:space:]]*:[[:space:]]*{[^}]*\"model\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"arguments\"[[:space:]]*:[[:space:]]*{[^}]*\"model\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"arguments\"[[:space:]]*:[[:space:]]*{[^}]*\"modelName\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"arguments\"[[:space:]]*:[[:space:]]*{[^}]*\"model_name\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"args\"[[:space:]]*:[[:space:]]*{[^}]*\"model\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"args\"[[:space:]]*:[[:space:]]*{[^}]*\"modelName\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"args\"[[:space:]]*:[[:space:]]*{[^}]*\"model_name\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"tool\"[[:space:]]*:[[:space:]]*{[^}]*\"arguments\"[[:space:]]*:[[:space:]]*{[^}]*\"model\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"tool\"[[:space:]]*:[[:space:]]*{[^}]*\"arguments\"[[:space:]]*:[[:space:]]*{[^}]*\"modelName\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"tool\"[[:space:]]*:[[:space:]]*{[^}]*\"arguments\"[[:space:]]*:[[:space:]]*{[^}]*\"model_name\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"toolCall\"[[:space:]]*:[[:space:]]*{[^}]*\"arguments\"[[:space:]]*:[[:space:]]*{[^}]*\"model\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"toolCall\"[[:space:]]*:[[:space:]]*{[^}]*\"arguments\"[[:space:]]*:[[:space:]]*{[^}]*\"modelName\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"toolCall\"[[:space:]]*:[[:space:]]*{[^}]*\"arguments\"[[:space:]]*:[[:space:]]*{[^}]*\"model_name\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"tool_call\"[[:space:]]*:[[:space:]]*{[^}]*\"arguments\"[[:space:]]*:[[:space:]]*{[^}]*\"model\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"tool_call\"[[:space:]]*:[[:space:]]*{[^}]*\"arguments\"[[:space:]]*:[[:space:]]*{[^}]*\"modelName\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"tool_call\"[[:space:]]*:[[:space:]]*{[^}]*\"arguments\"[[:space:]]*:[[:space:]]*{[^}]*\"model_name\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"unknown\"\n\nif [ \"$actor_name\" = \"unknown\" ] && [ \"$delegated_agent_name\" = \"unknown\" ] && [ \"$tool_name\" != \"runSubagent\" ]; then\n increment_unknown_event \"$session_id\" \"preToolUse_unknown_context\"\n exit 0\nfi\n\nevent_name=\"preToolUse\"\nif [ \"$tool_name\" = \"runSubagent\" ]; then\n event_name=\"delegationStart\"\nfi\n\nbytes=\"$(printf '%s' \"$input\" | wc -c | tr -d ' ')\"\nest_tokens=\"$(( (bytes + 3) / 4 ))\"\nscript_end_ms=\"$(date +%s%3N 2>/dev/null || true)\"\nif [ -z \"$script_end_ms\" ]; then\n script_end_ms=\"$(( $(date +%s) * 1000 ))\"\nfi\nhook_execution_ms=\"$((script_end_ms - script_start_ms))\"\n\nprintf '{\"timestamp\":\"%s\",\"event\":\"%s\",\"session_id\":\"%s\",\"actor_name\":\"%s\",\"actor_type\":\"%s\",\"tool_name\":\"%s\",\"delegated_agent_name\":\"%s\",\"model_used\":\"%s\",\"size_bytes\":%s,\"estimated_tokens\":%s,\"hook_execution_ms\":%s}\\n' \\\n \"$ts\" \"$event_name\" \"$session_id\" \"$actor_name\" \"$actor_type\" \"$tool_name\" \"$delegated_agent_name\" \"$model_used\" \"$bytes\" \"$est_tokens\" \"$hook_execution_ms\" >> \"$log_dir/hook-agent-call.log\"\n", + "powershell": "$inputText = [Console]::In.ReadToEnd()\n$logLevel = if ($env:VSTACK_HOOKS_LOG_LEVEL) { $env:VSTACK_HOOKS_LOG_LEVEL } else { 'minimal' }\n$logRoot = if ($env:VSTACK_HOOK_LOG_DIR) { $env:VSTACK_HOOK_LOG_DIR } else { '.vstack/logs' }\n$retentionDaysRaw = if ($env:VSTACK_HOOK_RETENTION_DAYS) { $env:VSTACK_HOOK_RETENTION_DAYS } else { '7' }\n$retentionDays = 7\nif (-not ([int]::TryParse([string]$retentionDaysRaw, [ref]$retentionDays) -and $retentionDays -gt 0)) {\n $retentionDays = 7\n}\ntry {\n if (Test-Path -LiteralPath $logRoot) {\n $cutoffDay = (Get-Date).ToUniversalTime().AddDays(-$retentionDays).ToString('yyyyMMdd')\n Get-ChildItem -LiteralPath $logRoot -Directory -ErrorAction Stop |\n Where-Object { $_.Name -match '^\\d{8}$' -and $_.Name -lt $cutoffDay } |\n ForEach-Object { Remove-Item -LiteralPath $_.FullName -Recurse -Force -ErrorAction SilentlyContinue }\n }\n} catch {}\n$logDir = Join-Path $logRoot (Get-Date).ToUniversalTime().ToString('yyyyMMdd')\nNew-Item -ItemType Directory -Force -Path $logDir | Out-Null\nif ($logLevel -eq 'off') { exit 0 }\nif ($logLevel -eq 'verbose') {\n Add-Content -Path (Join-Path $logDir 'hook-agent-call.log') -Value $inputText\n exit 0\n}\n\n$scriptStart = Get-Date\n$unknownEventsPath = Join-Path $logDir 'hook-agent-call-unknown-events.tsv'\n\nfunction Get-PathValue {\n param([object]$Object, [string]$Path)\n $current = $Object\n foreach ($part in $Path.Split('.')) {\n if ($null -eq $current) { return $null }\n $prop = $current.PSObject.Properties[$part]\n if ($null -eq $prop) { return $null }\n $current = $prop.Value\n }\n return $current\n}\n\nfunction Get-FirstString {\n param([object]$Object, [string[]]$Paths)\n foreach ($path in $Paths) {\n $value = Get-PathValue -Object $Object -Path $path\n if ($null -ne $value -and [string]$value -ne '') { return [string]$value }\n }\n return $null\n}\n\nfunction Add-UnknownEventCounter {\n param(\n [string]$Path,\n [string]$SessionId,\n [string]$Reason\n )\n\n $counts = @{}\n if (Test-Path $Path) {\n foreach ($line in (Get-Content -Path $Path)) {\n if ([string]::IsNullOrWhiteSpace($line)) { continue }\n $parts = $line -split \"`t\"\n if ($parts.Count -lt 2) { continue }\n\n $sid = [string]$parts[0]\n $why = [string]$parts[1]\n $count = 1\n if ($parts.Count -ge 3) {\n $parsedCount = 0\n if ([int]::TryParse([string]$parts[2], [ref]$parsedCount)) {\n $count = [int][Math]::Max(0, $parsedCount)\n }\n }\n\n $key = \"$sid`t$why\"\n if (-not $counts.ContainsKey($key)) { $counts[$key] = 0 }\n $counts[$key] += $count\n }\n }\n\n $target = \"$SessionId`t$Reason\"\n if (-not $counts.ContainsKey($target)) { $counts[$target] = 0 }\n $counts[$target] += 1\n\n $rows = foreach ($entry in ($counts.GetEnumerator() | Sort-Object Name)) {\n \"{0}`t{1}\" -f $entry.Name, $entry.Value\n }\n Set-Content -Path $Path -Value $rows\n}\n\n$payload = $null\ntry { $payload = $inputText | ConvertFrom-Json -ErrorAction Stop } catch {}\n\n$sessionId = 'unknown-session'\n$actorName = 'unknown'\n$actorType = 'unknown'\n$toolName = 'unknown'\n$toolCallId = 'unknown'\n$delegatedAgentName = 'unknown'\n$modelUsed = 'unknown'\n\nif ($null -ne $payload) {\n $sessionIdCandidate = Get-FirstString -Object $payload -Paths @('sessionId', 'session_id', 'conversationId', 'conversation_id', 'chatId', 'chat_id')\n if ($sessionIdCandidate) { $sessionId = $sessionIdCandidate }\n\n $actorNameCandidate = Get-FirstString -Object $payload -Paths @('actorName', 'actor_name', 'subagentName', 'subagent_name', 'agentName', 'agent_name')\n if ($actorNameCandidate) { $actorName = $actorNameCandidate }\n\n $actorTypeCandidate = Get-FirstString -Object $payload -Paths @('actorType', 'actor_type')\n if ($actorTypeCandidate) { $actorType = $actorTypeCandidate }\n\n $toolNameCandidate = Get-FirstString -Object $payload -Paths @('toolName', 'tool_name', 'recipient_name', 'tool.name', 'toolCall.name', 'toolCall.toolName', 'tool_call.name', 'tool_call.tool_name')\n if ($toolNameCandidate) { $toolName = $toolNameCandidate }\n\n $toolCallIdCandidate = Get-FirstString -Object $payload -Paths @('toolCallId', 'tool_call_id', 'callId', 'call_id', 'toolCall.id', 'tool_call.id')\n if ($toolCallIdCandidate) { $toolCallId = $toolCallIdCandidate }\n\n $delegatedCandidate = Get-FirstString -Object $payload -Paths @('delegatedAgentName', 'delegated_agent_name', 'runSubagent.agentName', 'runSubagent.agent_name', 'arguments.agentName', 'arguments.agent_name', 'args.agentName', 'args.agent_name', 'tool.arguments.agentName', 'tool.arguments.agent_name', 'toolCall.arguments.agentName', 'toolCall.arguments.agent_name', 'tool_call.arguments.agentName', 'tool_call.arguments.agent_name')\n if ($delegatedCandidate) { $delegatedAgentName = $delegatedCandidate }\n\n $modelCandidate = Get-FirstString -Object $payload -Paths @('model', 'modelName', 'model_name', 'toolModel', 'tool_model', 'toolCall.model', 'tool_call.model', 'arguments.model', 'arguments.modelName', 'arguments.model_name', 'args.model', 'args.modelName', 'args.model_name', 'tool.arguments.model', 'tool.arguments.modelName', 'tool.arguments.model_name', 'toolCall.arguments.model', 'toolCall.arguments.modelName', 'toolCall.arguments.model_name', 'tool_call.arguments.model', 'tool_call.arguments.modelName', 'tool_call.arguments.model_name')\n if ($modelCandidate) { $modelUsed = $modelCandidate }\n}\n\nif ($actorType -eq 'unknown') {\n if ($actorName -match 'subagent') {\n $actorType = 'subagent'\n } elseif ($actorName -ne 'unknown') {\n $actorType = 'agent'\n }\n}\n\nif ($toolName -eq 'unknown') {\n $toolNameMatch = [regex]::Match($inputText, '[A-Za-z_][A-Za-z0-9_]*\\.[A-Za-z_][A-Za-z0-9_]*')\n if ($toolNameMatch.Success) { $toolName = $toolNameMatch.Value }\n}\n\nif ($toolName -eq 'runSubagent' -and $delegatedAgentName -eq 'unknown' -and $null -ne $payload) {\n $delegatedFallback = Get-FirstString -Object $payload -Paths @('agentName', 'agent_name')\n if ($delegatedFallback) { $delegatedAgentName = $delegatedFallback }\n}\n\nif ($actorName -eq 'unknown' -and $delegatedAgentName -eq 'unknown' -and $toolName -ne 'runSubagent') {\n Add-UnknownEventCounter -Path $unknownEventsPath -SessionId $sessionId -Reason 'preToolUse_unknown_context'\n exit 0\n}\n\n$eventName = if ($toolName -eq 'runSubagent') { 'delegationStart' } else { 'preToolUse' }\n\n$sizeBytes = [System.Text.Encoding]::UTF8.GetByteCount($inputText)\n$hookExecutionMs = [int][Math]::Max(0, ((Get-Date) - $scriptStart).TotalMilliseconds)\n$record = @{\n timestamp = (Get-Date).ToUniversalTime().ToString('o')\n event = $eventName\n session_id = $sessionId\n actor_name = $actorName\n actor_type = $actorType\n tool_name = $toolName\n delegated_agent_name = $delegatedAgentName\n model_used = $modelUsed\n size_bytes = $sizeBytes\n estimated_tokens = [int][math]::Ceiling($sizeBytes / 4.0)\n hook_execution_ms = $hookExecutionMs\n} | ConvertTo-Json -Compress\nAdd-Content -Path (Join-Path $logDir 'hook-agent-call.log') -Value $record\n", + "cwd": ".", + "timeoutSec": 5 + } + ], + "postToolUse": [ + { + "type": "command", + "description": "Log tool result events with best-effort actor, delegation, and model metadata.\n", + "bash": "script_start_ms=\"$(date +%s%3N 2>/dev/null || true)\"\nif [ -z \"$script_start_ms\" ]; then\n script_start_ms=\"$(( $(date +%s) * 1000 ))\"\nfi\ninput=\"$(cat)\"\nlog_level=\"${VSTACK_HOOKS_LOG_LEVEL:-minimal}\"\nlog_root=\"${VSTACK_HOOK_LOG_DIR:-.vstack/logs}\"\nretention_days=\"${VSTACK_HOOK_RETENTION_DAYS:-7}\"\nif ! printf '%s' \"$retention_days\" | grep -Eq '^[1-9][0-9]*$'; then\n retention_days=7\nfi\nif [ -d \"$log_root\" ]; then\n cutoff_day=\"$(date -u -d \"$retention_days days ago\" +%Y%m%d 2>/dev/null || true)\"\n if [ -n \"$cutoff_day\" ]; then\n for day_dir in \"$log_root\"/*; do\n [ -d \"$day_dir\" ] || continue\n day_name=\"$(basename \"$day_dir\")\"\n case \"$day_name\" in\n [0-9][0-9][0-9][0-9][0-1][0-9][0-3][0-9]) ;;\n *) continue ;;\n esac\n if [ \"$day_name\" -lt \"$cutoff_day\" ]; then\n rm -rf \"$day_dir\" || true\n fi\n done\n fi\nfi\nlog_dir=\"$log_root/$(date -u +%Y%m%d)\"\nmkdir -p \"$log_dir\"\nif [ \"$log_level\" = \"off\" ]; then\n exit 0\nfi\nif [ \"$log_level\" = \"verbose\" ]; then\n printf '%s\\n' \"$input\" >> \"$log_dir/hook-agent-call.log\"\n exit 0\nfi\n\nts=\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"\nunknown_events_file=\"$log_dir/hook-agent-call-unknown-events.tsv\"\n\nextract_json_string() {\n key=\"$1\"\n printf '%s' \"$input\" | sed -n \"s/.*\\\"${key}\\\"[[:space:]]*:[[:space:]]*\\\"\\\\([^\\\"]*\\\\)\\\".*/\\\\1/p\" | head -n1\n}\n\nincrement_unknown_event() {\n sid=\"$1\"\n reason=\"$2\"\n tmp_file=\"$unknown_events_file.tmp.$$\"\n if [ -f \"$unknown_events_file\" ]; then\n awk -F '\\t' -v sid=\"$sid\" -v reason=\"$reason\" 'BEGIN { OFS=\"\\t\" }\n NF >= 2 {\n key = $1 SUBSEP $2\n count = 1\n if (NF >= 3 && $3 ~ /^[0-9]+$/) {\n count = $3 + 0\n }\n counts[key] += count\n }\n END {\n target = sid SUBSEP reason\n counts[target] += 1\n for (k in counts) {\n split(k, parts, SUBSEP)\n print parts[1], parts[2], counts[k]\n }\n }' \"$unknown_events_file\" > \"$tmp_file\" && mv \"$tmp_file\" \"$unknown_events_file\"\n else\n printf '%s\\t%s\\t1\\n' \"$sid\" \"$reason\" > \"$unknown_events_file\"\n fi\n}\n\nsession_id=\"$(extract_json_string sessionId)\"\n[ -z \"$session_id\" ] && session_id=\"$(extract_json_string session_id)\"\n[ -z \"$session_id\" ] && session_id=\"$(extract_json_string conversationId)\"\n[ -z \"$session_id\" ] && session_id=\"$(extract_json_string conversation_id)\"\n[ -z \"$session_id\" ] && session_id=\"$(extract_json_string chatId)\"\n[ -z \"$session_id\" ] && session_id=\"$(extract_json_string chat_id)\"\n[ -z \"$session_id\" ] && session_id=\"unknown-session\"\n\nactor_name=\"$(extract_json_string actorName)\"\n[ -z \"$actor_name\" ] && actor_name=\"$(extract_json_string actor_name)\"\n[ -z \"$actor_name\" ] && actor_name=\"$(extract_json_string subagentName)\"\n[ -z \"$actor_name\" ] && actor_name=\"$(extract_json_string subagent_name)\"\n[ -z \"$actor_name\" ] && actor_name=\"$(extract_json_string agentName)\"\n[ -z \"$actor_name\" ] && actor_name=\"$(extract_json_string agent_name)\"\n[ -z \"$actor_name\" ] && actor_name=\"unknown\"\n\nactor_type=\"$(extract_json_string actorType)\"\n[ -z \"$actor_type\" ] && actor_type=\"$(extract_json_string actor_type)\"\nif [ -z \"$actor_type\" ]; then\n if printf '%s' \"$actor_name\" | grep -qi 'subagent'; then\n actor_type=\"subagent\"\n elif [ \"$actor_name\" != \"unknown\" ]; then\n actor_type=\"agent\"\n else\n actor_type=\"unknown\"\n fi\nfi\n\ntool_name=\"$(extract_json_string toolName)\"\n[ -z \"$tool_name\" ] && tool_name=\"$(extract_json_string tool_name)\"\n[ -z \"$tool_name\" ] && tool_name=\"$(extract_json_string recipient_name)\"\n[ -z \"$tool_name\" ] && tool_name=\"$(printf '%s' \"$input\" | sed -n 's/.*\"tool\"[[:space:]]*:[[:space:]]*{[^}]*\"name\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$tool_name\" ] && tool_name=\"$(printf '%s' \"$input\" | sed -n 's/.*\"toolCall\"[[:space:]]*:[[:space:]]*{[^}]*\"name\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$tool_name\" ] && tool_name=\"$(printf '%s' \"$input\" | sed -n 's/.*\"toolCall\"[[:space:]]*:[[:space:]]*{[^}]*\"toolName\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$tool_name\" ] && tool_name=\"$(printf '%s' \"$input\" | sed -n 's/.*\"tool_call\"[[:space:]]*:[[:space:]]*{[^}]*\"name\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$tool_name\" ] && tool_name=\"$(printf '%s' \"$input\" | sed -n 's/.*\"tool_call\"[[:space:]]*:[[:space:]]*{[^}]*\"tool_name\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$tool_name\" ] && tool_name=\"$(printf '%s' \"$input\" | grep -Eo '[A-Za-z_][A-Za-z0-9_]*\\.[A-Za-z_][A-Za-z0-9_]*' | head -n1)\"\n[ -z \"$tool_name\" ] && tool_name=\"unknown\"\n\ndelegated_agent_name=\"unknown\"\nif [ \"$tool_name\" = \"runSubagent\" ]; then\n delegated_agent_name=\"$(extract_json_string delegatedAgentName)\"\n [ -z \"$delegated_agent_name\" ] && delegated_agent_name=\"$(extract_json_string delegated_agent_name)\"\n [ -z \"$delegated_agent_name\" ] && delegated_agent_name=\"$(extract_json_string agentName)\"\n [ -z \"$delegated_agent_name\" ] && delegated_agent_name=\"$(extract_json_string agent_name)\"\n [ -z \"$delegated_agent_name\" ] && delegated_agent_name=\"$(printf '%s' \"$input\" | sed -n 's/.*\"runSubagent\"[[:space:]]*:[[:space:]]*{[^}]*\"agentName\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n [ -z \"$delegated_agent_name\" ] && delegated_agent_name=\"$(printf '%s' \"$input\" | sed -n 's/.*\"runSubagent\"[[:space:]]*:[[:space:]]*{[^}]*\"agent_name\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n [ -z \"$delegated_agent_name\" ] && delegated_agent_name=\"$(printf '%s' \"$input\" | sed -n 's/.*\"arguments\"[[:space:]]*:[[:space:]]*{[^}]*\"agentName\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n [ -z \"$delegated_agent_name\" ] && delegated_agent_name=\"$(printf '%s' \"$input\" | sed -n 's/.*\"arguments\"[[:space:]]*:[[:space:]]*{[^}]*\"agent_name\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n [ -z \"$delegated_agent_name\" ] && delegated_agent_name=\"unknown\"\nfi\n\ntool_call_id=\"$(extract_json_string toolCallId)\"\n[ -z \"$tool_call_id\" ] && tool_call_id=\"$(extract_json_string tool_call_id)\"\n[ -z \"$tool_call_id\" ] && tool_call_id=\"$(extract_json_string callId)\"\n[ -z \"$tool_call_id\" ] && tool_call_id=\"$(extract_json_string call_id)\"\n[ -z \"$tool_call_id\" ] && tool_call_id=\"$(printf '%s' \"$input\" | sed -n 's/.*\"toolCall\"[[:space:]]*:[[:space:]]*{[^}]*\"id\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$tool_call_id\" ] && tool_call_id=\"$(printf '%s' \"$input\" | sed -n 's/.*\"tool_call\"[[:space:]]*:[[:space:]]*{[^}]*\"id\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$tool_call_id\" ] && tool_call_id=\"unknown\"\n\nmodel_used=\"$(extract_json_string model)\"\n[ -z \"$model_used\" ] && model_used=\"$(extract_json_string modelName)\"\n[ -z \"$model_used\" ] && model_used=\"$(extract_json_string model_name)\"\n[ -z \"$model_used\" ] && model_used=\"$(extract_json_string toolModel)\"\n[ -z \"$model_used\" ] && model_used=\"$(extract_json_string tool_model)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"toolCall\"[[:space:]]*:[[:space:]]*{[^}]*\"model\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"arguments\"[[:space:]]*:[[:space:]]*{[^}]*\"model\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"arguments\"[[:space:]]*:[[:space:]]*{[^}]*\"modelName\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"arguments\"[[:space:]]*:[[:space:]]*{[^}]*\"model_name\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"args\"[[:space:]]*:[[:space:]]*{[^}]*\"model\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"args\"[[:space:]]*:[[:space:]]*{[^}]*\"modelName\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"args\"[[:space:]]*:[[:space:]]*{[^}]*\"model_name\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"tool\"[[:space:]]*:[[:space:]]*{[^}]*\"arguments\"[[:space:]]*:[[:space:]]*{[^}]*\"model\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"tool\"[[:space:]]*:[[:space:]]*{[^}]*\"arguments\"[[:space:]]*:[[:space:]]*{[^}]*\"modelName\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"tool\"[[:space:]]*:[[:space:]]*{[^}]*\"arguments\"[[:space:]]*:[[:space:]]*{[^}]*\"model_name\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"toolCall\"[[:space:]]*:[[:space:]]*{[^}]*\"arguments\"[[:space:]]*:[[:space:]]*{[^}]*\"model\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"toolCall\"[[:space:]]*:[[:space:]]*{[^}]*\"arguments\"[[:space:]]*:[[:space:]]*{[^}]*\"modelName\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"toolCall\"[[:space:]]*:[[:space:]]*{[^}]*\"arguments\"[[:space:]]*:[[:space:]]*{[^}]*\"model_name\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"tool_call\"[[:space:]]*:[[:space:]]*{[^}]*\"arguments\"[[:space:]]*:[[:space:]]*{[^}]*\"model\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"tool_call\"[[:space:]]*:[[:space:]]*{[^}]*\"arguments\"[[:space:]]*:[[:space:]]*{[^}]*\"modelName\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"$(printf '%s' \"$input\" | sed -n 's/.*\"tool_call\"[[:space:]]*:[[:space:]]*{[^}]*\"arguments\"[[:space:]]*:[[:space:]]*{[^}]*\"model_name\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\n[ -z \"$model_used\" ] && model_used=\"unknown\"\n\nif [ \"$actor_name\" = \"unknown\" ] && [ \"$delegated_agent_name\" = \"unknown\" ] && [ \"$tool_name\" != \"runSubagent\" ]; then\n increment_unknown_event \"$session_id\" \"postToolUse_unknown_context\"\n exit 0\nfi\n\nevent_name=\"postToolUse\"\nif [ \"$tool_name\" = \"runSubagent\" ]; then\n event_name=\"delegationEnd\"\nfi\n\nbytes=\"$(printf '%s' \"$input\" | wc -c | tr -d ' ')\"\nest_tokens=\"$(( (bytes + 3) / 4 ))\"\nscript_end_ms=\"$(date +%s%3N 2>/dev/null || true)\"\nif [ -z \"$script_end_ms\" ]; then\n script_end_ms=\"$(( $(date +%s) * 1000 ))\"\nfi\nhook_execution_ms=\"$((script_end_ms - script_start_ms))\"\n\nprintf '{\"timestamp\":\"%s\",\"event\":\"%s\",\"session_id\":\"%s\",\"actor_name\":\"%s\",\"actor_type\":\"%s\",\"tool_name\":\"%s\",\"delegated_agent_name\":\"%s\",\"model_used\":\"%s\",\"size_bytes\":%s,\"estimated_tokens\":%s,\"hook_execution_ms\":%s}\\n' \\\n \"$ts\" \"$event_name\" \"$session_id\" \"$actor_name\" \"$actor_type\" \"$tool_name\" \"$delegated_agent_name\" \"$model_used\" \"$bytes\" \"$est_tokens\" \"$hook_execution_ms\" >> \"$log_dir/hook-agent-call.log\"\n", + "powershell": "$inputText = [Console]::In.ReadToEnd()\n$logLevel = if ($env:VSTACK_HOOKS_LOG_LEVEL) { $env:VSTACK_HOOKS_LOG_LEVEL } else { 'minimal' }\n$logRoot = if ($env:VSTACK_HOOK_LOG_DIR) { $env:VSTACK_HOOK_LOG_DIR } else { '.vstack/logs' }\n$retentionDaysRaw = if ($env:VSTACK_HOOK_RETENTION_DAYS) { $env:VSTACK_HOOK_RETENTION_DAYS } else { '7' }\n$retentionDays = 7\nif (-not ([int]::TryParse([string]$retentionDaysRaw, [ref]$retentionDays) -and $retentionDays -gt 0)) {\n $retentionDays = 7\n}\ntry {\n if (Test-Path -LiteralPath $logRoot) {\n $cutoffDay = (Get-Date).ToUniversalTime().AddDays(-$retentionDays).ToString('yyyyMMdd')\n Get-ChildItem -LiteralPath $logRoot -Directory -ErrorAction Stop |\n Where-Object { $_.Name -match '^\\d{8}$' -and $_.Name -lt $cutoffDay } |\n ForEach-Object { Remove-Item -LiteralPath $_.FullName -Recurse -Force -ErrorAction SilentlyContinue }\n }\n} catch {}\n$logDir = Join-Path $logRoot (Get-Date).ToUniversalTime().ToString('yyyyMMdd')\nNew-Item -ItemType Directory -Force -Path $logDir | Out-Null\nif ($logLevel -eq 'off') { exit 0 }\nif ($logLevel -eq 'verbose') {\n Add-Content -Path (Join-Path $logDir 'hook-agent-call.log') -Value $inputText\n exit 0\n}\n\n$scriptStart = Get-Date\n$unknownEventsPath = Join-Path $logDir 'hook-agent-call-unknown-events.tsv'\n\nfunction Get-PathValue {\n param([object]$Object, [string]$Path)\n $current = $Object\n foreach ($part in $Path.Split('.')) {\n if ($null -eq $current) { return $null }\n $prop = $current.PSObject.Properties[$part]\n if ($null -eq $prop) { return $null }\n $current = $prop.Value\n }\n return $current\n}\n\nfunction Get-FirstString {\n param([object]$Object, [string[]]$Paths)\n foreach ($path in $Paths) {\n $value = Get-PathValue -Object $Object -Path $path\n if ($null -ne $value -and [string]$value -ne '') { return [string]$value }\n }\n return $null\n}\n\nfunction Add-UnknownEventCounter {\n param(\n [string]$Path,\n [string]$SessionId,\n [string]$Reason\n )\n\n $counts = @{}\n if (Test-Path $Path) {\n foreach ($line in (Get-Content -Path $Path)) {\n if ([string]::IsNullOrWhiteSpace($line)) { continue }\n $parts = $line -split \"`t\"\n if ($parts.Count -lt 2) { continue }\n\n $sid = [string]$parts[0]\n $why = [string]$parts[1]\n $count = 1\n if ($parts.Count -ge 3) {\n $parsedCount = 0\n if ([int]::TryParse([string]$parts[2], [ref]$parsedCount)) {\n $count = [int][Math]::Max(0, $parsedCount)\n }\n }\n\n $key = \"$sid`t$why\"\n if (-not $counts.ContainsKey($key)) { $counts[$key] = 0 }\n $counts[$key] += $count\n }\n }\n\n $target = \"$SessionId`t$Reason\"\n if (-not $counts.ContainsKey($target)) { $counts[$target] = 0 }\n $counts[$target] += 1\n\n $rows = foreach ($entry in ($counts.GetEnumerator() | Sort-Object Name)) {\n \"{0}`t{1}\" -f $entry.Name, $entry.Value\n }\n Set-Content -Path $Path -Value $rows\n}\n\n$payload = $null\ntry { $payload = $inputText | ConvertFrom-Json -ErrorAction Stop } catch {}\n\n$sessionId = 'unknown-session'\n$actorName = 'unknown'\n$actorType = 'unknown'\n$toolName = 'unknown'\n$toolCallId = 'unknown'\n$delegatedAgentName = 'unknown'\n$modelUsed = 'unknown'\n\nif ($null -ne $payload) {\n $sessionIdCandidate = Get-FirstString -Object $payload -Paths @('sessionId', 'session_id', 'conversationId', 'conversation_id', 'chatId', 'chat_id')\n if ($sessionIdCandidate) { $sessionId = $sessionIdCandidate }\n\n $actorNameCandidate = Get-FirstString -Object $payload -Paths @('actorName', 'actor_name', 'subagentName', 'subagent_name', 'agentName', 'agent_name')\n if ($actorNameCandidate) { $actorName = $actorNameCandidate }\n\n $actorTypeCandidate = Get-FirstString -Object $payload -Paths @('actorType', 'actor_type')\n if ($actorTypeCandidate) { $actorType = $actorTypeCandidate }\n\n $toolNameCandidate = Get-FirstString -Object $payload -Paths @('toolName', 'tool_name', 'recipient_name', 'tool.name', 'toolCall.name', 'toolCall.toolName', 'tool_call.name', 'tool_call.tool_name')\n if ($toolNameCandidate) { $toolName = $toolNameCandidate }\n\n $toolCallIdCandidate = Get-FirstString -Object $payload -Paths @('toolCallId', 'tool_call_id', 'callId', 'call_id', 'toolCall.id', 'tool_call.id')\n if ($toolCallIdCandidate) { $toolCallId = $toolCallIdCandidate }\n\n $delegatedCandidate = Get-FirstString -Object $payload -Paths @('delegatedAgentName', 'delegated_agent_name', 'runSubagent.agentName', 'runSubagent.agent_name', 'arguments.agentName', 'arguments.agent_name', 'args.agentName', 'args.agent_name', 'tool.arguments.agentName', 'tool.arguments.agent_name', 'toolCall.arguments.agentName', 'toolCall.arguments.agent_name', 'tool_call.arguments.agentName', 'tool_call.arguments.agent_name')\n if ($delegatedCandidate) { $delegatedAgentName = $delegatedCandidate }\n\n $modelCandidate = Get-FirstString -Object $payload -Paths @('model', 'modelName', 'model_name', 'toolModel', 'tool_model', 'toolCall.model', 'tool_call.model', 'arguments.model', 'arguments.modelName', 'arguments.model_name', 'args.model', 'args.modelName', 'args.model_name', 'tool.arguments.model', 'tool.arguments.modelName', 'tool.arguments.model_name', 'toolCall.arguments.model', 'toolCall.arguments.modelName', 'toolCall.arguments.model_name', 'tool_call.arguments.model', 'tool_call.arguments.modelName', 'tool_call.arguments.model_name')\n if ($modelCandidate) { $modelUsed = $modelCandidate }\n}\n\nif ($actorType -eq 'unknown') {\n if ($actorName -match 'subagent') {\n $actorType = 'subagent'\n } elseif ($actorName -ne 'unknown') {\n $actorType = 'agent'\n }\n}\n\nif ($toolName -eq 'unknown') {\n $toolNameMatch = [regex]::Match($inputText, '[A-Za-z_][A-Za-z0-9_]*\\.[A-Za-z_][A-Za-z0-9_]*')\n if ($toolNameMatch.Success) { $toolName = $toolNameMatch.Value }\n}\n\nif ($toolName -eq 'runSubagent' -and $delegatedAgentName -eq 'unknown' -and $null -ne $payload) {\n $delegatedFallback = Get-FirstString -Object $payload -Paths @('agentName', 'agent_name')\n if ($delegatedFallback) { $delegatedAgentName = $delegatedFallback }\n}\n\nif ($actorName -eq 'unknown' -and $delegatedAgentName -eq 'unknown' -and $toolName -ne 'runSubagent') {\n Add-UnknownEventCounter -Path $unknownEventsPath -SessionId $sessionId -Reason 'postToolUse_unknown_context'\n exit 0\n}\n\n$eventName = if ($toolName -eq 'runSubagent') { 'delegationEnd' } else { 'postToolUse' }\n\n$sizeBytes = [System.Text.Encoding]::UTF8.GetByteCount($inputText)\n$hookExecutionMs = [int][Math]::Max(0, ((Get-Date) - $scriptStart).TotalMilliseconds)\n$record = @{\n timestamp = (Get-Date).ToUniversalTime().ToString('o')\n event = $eventName\n session_id = $sessionId\n actor_name = $actorName\n actor_type = $actorType\n tool_name = $toolName\n delegated_agent_name = $delegatedAgentName\n model_used = $modelUsed\n size_bytes = $sizeBytes\n estimated_tokens = [int][math]::Ceiling($sizeBytes / 4.0)\n hook_execution_ms = $hookExecutionMs\n} | ConvertTo-Json -Compress\nAdd-Content -Path (Join-Path $logDir 'hook-agent-call.log') -Value $record\n", + "cwd": ".", + "timeoutSec": 5 + } + ] + } +} diff --git a/.github/hooks/post-commit-security-scan.json b/.github/hooks/post-commit-security-scan.json index 2ad06787..b8ebb823 100644 --- a/.github/hooks/post-commit-security-scan.json +++ b/.github/hooks/post-commit-security-scan.json @@ -5,8 +5,8 @@ { "type": "command", "description": "Check staged diffs for secrets and log only security alerts.\n", - "bash": "input=\"$(cat)\"\nlog_name=\"${VSTACK_HOOK_LOG_NAME:-hook-security-alerts.log}\"\nlog_root=\"${VSTACK_HOOK_LOG_DIR:-.vstack/logs}\"\nlog_dir=\"$log_root/$(date -u +%Y%m%d)\"\nmkdir -p \"$log_dir\"\nlog_path=\"$log_dir/$log_name\"\nif ! printf '%s' \"$input\" | grep -Eiq 'git (commit|push|merge|rebase)'; then\n exit 0\nfi\nif git rev-parse --git-dir >/dev/null 2>&1 && \\\n git diff --cached 2>/dev/null | grep -Eiq 'AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9]{36,}|-----BEGIN (RSA|EC|OPENSSH) PRIVATE KEY-----'; then\n printf '%s\\n' 'possible-secrets-detected-in-staged-diff' >> \"$log_path\"\nfi\nif [ \"${VSTACK_HOOKS_MODE:-audit}\" = \"enforce\" ]; then\n if command -v gitleaks >/dev/null 2>&1; then\n gitleaks dir . --no-banner --redact >/dev/null 2>&1 || printf '%s\\n' 'gitleaks-reported-findings' >> \"$log_path\"\n else\n printf '%s\\n' 'gitleaks-not-installed' >> \"$log_path\"\n fi\nfi\n", - "powershell": "$inputText = [Console]::In.ReadToEnd()\n$logName = if ($env:VSTACK_HOOK_LOG_NAME) { $env:VSTACK_HOOK_LOG_NAME } else { 'hook-security-alerts.log' }\n$logRoot = if ($env:VSTACK_HOOK_LOG_DIR) { $env:VSTACK_HOOK_LOG_DIR } else { '.vstack/logs' }\n$logDir = Join-Path $logRoot (Get-Date).ToUniversalTime().ToString('yyyyMMdd')\nNew-Item -ItemType Directory -Force -Path $logDir | Out-Null\n$logPath = Join-Path $logDir $logName\nif ($inputText -notmatch 'git (commit|push|merge|rebase)') {\n exit 0\n}\nif ((git rev-parse --git-dir 2>$null) -and (git diff --cached | Select-String -Pattern 'AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9]{36,}|-----BEGIN (RSA|EC|OPENSSH) PRIVATE KEY-----' -Quiet)) {\n Add-Content -Path $logPath -Value 'possible-secrets-detected-in-staged-diff'\n}\n$mode = if ($env:VSTACK_HOOKS_MODE) { $env:VSTACK_HOOKS_MODE } else { 'audit' }\nif ($mode -eq 'enforce') {\n if (Get-Command gitleaks -ErrorAction SilentlyContinue) {\n gitleaks dir . --no-banner --redact *> $null\n if ($LASTEXITCODE -ne 0) {\n Add-Content -Path $logPath -Value 'gitleaks-reported-findings'\n }\n } else {\n Add-Content -Path $logPath -Value 'gitleaks-not-installed'\n }\n}\n", + "bash": "input=\"$(cat)\"\nappend_log() {\n log_name=\"${VSTACK_HOOK_LOG_NAME:-hook-security-alerts.log}\"\n log_root=\"${VSTACK_HOOK_LOG_DIR:-.vstack/logs}\"\n log_dir=\"$log_root/$(date -u +%Y%m%d)\"\n mkdir -p \"$log_dir\"\n printf '%s\\n' \"$1\" >> \"$log_dir/$log_name\"\n}\nif ! printf '%s' \"$input\" | grep -Eiq 'git (commit|push|merge|rebase)'; then\n exit 0\nfi\nif git rev-parse --git-dir >/dev/null 2>&1 && \\\n git diff --cached 2>/dev/null | grep -Eiq 'AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9]{36,}|-----BEGIN (RSA|EC|OPENSSH) PRIVATE KEY-----'; then\n append_log 'possible-secrets-detected-in-staged-diff'\nfi\nif [ \"${VSTACK_HOOKS_MODE:-audit}\" = \"enforce\" ]; then\n if command -v gitleaks >/dev/null 2>&1; then\n gitleaks dir . --no-banner --redact >/dev/null 2>&1 || append_log 'gitleaks-reported-findings'\n else\n append_log 'gitleaks-not-installed'\n fi\nfi\n", + "powershell": "$inputText = [Console]::In.ReadToEnd()\nfunction Write-HookLog {\n param([string]$Message)\n $logName = if ($env:VSTACK_HOOK_LOG_NAME) { $env:VSTACK_HOOK_LOG_NAME } else { 'hook-security-alerts.log' }\n $logRoot = if ($env:VSTACK_HOOK_LOG_DIR) { $env:VSTACK_HOOK_LOG_DIR } else { '.vstack/logs' }\n $logDir = Join-Path $logRoot (Get-Date).ToUniversalTime().ToString('yyyyMMdd')\n New-Item -ItemType Directory -Force -Path $logDir | Out-Null\n Add-Content -Path (Join-Path $logDir $logName) -Value $Message\n}\nif ($inputText -notmatch 'git (commit|push|merge|rebase)') {\n exit 0\n}\nif ((git rev-parse --git-dir 2>$null) -and (git diff --cached | Select-String -Pattern 'AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9]{36,}|-----BEGIN (RSA|EC|OPENSSH) PRIVATE KEY-----' -Quiet)) {\n Write-HookLog 'possible-secrets-detected-in-staged-diff'\n}\n$mode = if ($env:VSTACK_HOOKS_MODE) { $env:VSTACK_HOOKS_MODE } else { 'audit' }\nif ($mode -eq 'enforce') {\n if (Get-Command gitleaks -ErrorAction SilentlyContinue) {\n gitleaks dir . --no-banner --redact *> $null\n if ($LASTEXITCODE -ne 0) {\n Write-HookLog 'gitleaks-reported-findings'\n }\n } else {\n Write-HookLog 'gitleaks-not-installed'\n }\n}\n", "cwd": ".", "timeoutSec": 60 } diff --git a/.github/hooks/post-edit-format.json b/.github/hooks/post-edit-format.json index fd9c3945..01610f17 100644 --- a/.github/hooks/post-edit-format.json +++ b/.github/hooks/post-edit-format.json @@ -5,8 +5,8 @@ { "type": "command", "description": "Optionally run format in enforce mode; write alerts only on failures.\n", - "bash": "input=\"$(cat)\"\nlog_name=\"${VSTACK_HOOK_LOG_NAME:-hook-quality-alerts.log}\"\nlog_root=\"${VSTACK_HOOK_LOG_DIR:-.vstack/logs}\"\nlog_dir=\"$log_root/$(date -u +%Y%m%d)\"\nmkdir -p \"$log_dir\"\nlog_path=\"$log_dir/$log_name\"\nmode=\"${VSTACK_HOOKS_MODE:-audit}\"\nif [ \"$mode\" != \"enforce\" ]; then\n exit 0\nfi\nif printf '%s' \"$input\" | grep -Eq '\"toolName\"[[:space:]]*:[[:space:]]*\"(edit|create|write|replace)\"'; then\n if [ -f Makefile ] && command -v make >/dev/null 2>&1; then\n make format >/dev/null 2>&1 || printf '%s\\n' 'format-run-failed' >> \"$log_path\"\n else\n printf '%s\\n' 'format-skipped-missing-make-or-makefile' >> \"$log_path\"\n fi\nfi\n", - "powershell": "$inputText = [Console]::In.ReadToEnd()\n$logName = if ($env:VSTACK_HOOK_LOG_NAME) { $env:VSTACK_HOOK_LOG_NAME } else { 'hook-quality-alerts.log' }\n$logRoot = if ($env:VSTACK_HOOK_LOG_DIR) { $env:VSTACK_HOOK_LOG_DIR } else { '.vstack/logs' }\n$logDir = Join-Path $logRoot (Get-Date).ToUniversalTime().ToString('yyyyMMdd')\nNew-Item -ItemType Directory -Force -Path $logDir | Out-Null\n$logPath = Join-Path $logDir $logName\n$mode = if ($env:VSTACK_HOOKS_MODE) { $env:VSTACK_HOOKS_MODE } else { 'audit' }\nif ($mode -ne 'enforce') {\n exit 0\n}\ntry {\n $payload = $inputText | ConvertFrom-Json -ErrorAction Stop\n} catch {\n exit 0\n}\nif ($payload.toolName -match '^(edit|create|write|replace)$') {\n if ((Test-Path Makefile) -and (Get-Command make -ErrorAction SilentlyContinue)) {\n make format *> $null\n if ($LASTEXITCODE -ne 0) {\n Add-Content -Path $logPath -Value 'format-run-failed'\n }\n } else {\n Add-Content -Path $logPath -Value 'format-skipped-missing-make-or-makefile'\n }\n}\n", + "bash": "mode=\"${VSTACK_HOOKS_MODE:-audit}\"\nif [ \"$mode\" != \"enforce\" ]; then\n exit 0\nfi\ninput=\"$(cat)\"\nappend_log() {\n log_name=\"${VSTACK_HOOK_LOG_NAME:-hook-quality-alerts.log}\"\n log_root=\"${VSTACK_HOOK_LOG_DIR:-.vstack/logs}\"\n log_dir=\"$log_root/$(date -u +%Y%m%d)\"\n mkdir -p \"$log_dir\"\n printf '%s\\n' \"$1\" >> \"$log_dir/$log_name\"\n}\nif printf '%s' \"$input\" | grep -Eq '\"toolName\"[[:space:]]*:[[:space:]]*\"(edit|create|write|replace)\"'; then\n if [ -f Makefile ] && command -v make >/dev/null 2>&1; then\n make format >/dev/null 2>&1 || append_log 'format-run-failed'\n else\n append_log 'format-skipped-missing-make-or-makefile'\n fi\nfi\n", + "powershell": "$mode = if ($env:VSTACK_HOOKS_MODE) { $env:VSTACK_HOOKS_MODE } else { 'audit' }\nif ($mode -ne 'enforce') {\n exit 0\n}\n$inputText = [Console]::In.ReadToEnd()\nfunction Write-HookLog {\n param([string]$Message)\n $logName = if ($env:VSTACK_HOOK_LOG_NAME) { $env:VSTACK_HOOK_LOG_NAME } else { 'hook-quality-alerts.log' }\n $logRoot = if ($env:VSTACK_HOOK_LOG_DIR) { $env:VSTACK_HOOK_LOG_DIR } else { '.vstack/logs' }\n $logDir = Join-Path $logRoot (Get-Date).ToUniversalTime().ToString('yyyyMMdd')\n New-Item -ItemType Directory -Force -Path $logDir | Out-Null\n Add-Content -Path (Join-Path $logDir $logName) -Value $Message\n}\ntry {\n $payload = $inputText | ConvertFrom-Json -ErrorAction Stop\n} catch {\n exit 0\n}\nif ($payload.toolName -match '^(edit|create|write|replace)$') {\n if ((Test-Path Makefile) -and (Get-Command make -ErrorAction SilentlyContinue)) {\n make format *> $null\n if ($LASTEXITCODE -ne 0) {\n Write-HookLog 'format-run-failed'\n }\n } else {\n Write-HookLog 'format-skipped-missing-make-or-makefile'\n }\n}\n", "cwd": ".", "timeoutSec": 30 } diff --git a/.github/hooks/post-edit-markdown-quality.json b/.github/hooks/post-edit-markdown-quality.json index d20fba27..8043d265 100644 --- a/.github/hooks/post-edit-markdown-quality.json +++ b/.github/hooks/post-edit-markdown-quality.json @@ -5,8 +5,8 @@ { "type": "command", "description": "Run markdown formatting in enforce mode and log only failures/skips.\n", - "bash": "input=\"$(cat)\"\nlog_name=\"${VSTACK_HOOK_LOG_NAME:-hook-markdown-quality-alerts.log}\"\nlog_root=\"${VSTACK_HOOK_LOG_DIR:-.vstack/logs}\"\nlog_dir=\"$log_root/$(date -u +%Y%m%d)\"\nmkdir -p \"$log_dir\"\nlog_path=\"$log_dir/$log_name\"\nmode=\"${VSTACK_HOOKS_MODE:-audit}\"\nif [ \"$mode\" != \"enforce\" ]; then\n exit 0\nfi\nif ! printf '%s' \"$input\" | grep -Eiq '\\.md|docs/|\\.github/|\\.vstack/templates/|adr/|prompt\\.md|instructions\\.md|SKILL\\.md'; then\n exit 0\nfi\nif [ -f Makefile ] && command -v make >/dev/null 2>&1; then\n make markdown-format >/dev/null 2>&1 || \\\n make format >/dev/null 2>&1 || \\\n printf '%s\\n' 'markdown-format-run-failed' >> \"$log_path\"\nelse\n printf '%s\\n' 'markdown-format-skipped-missing-make-or-makefile' >> \"$log_path\"\nfi\n", - "powershell": "$inputText = [Console]::In.ReadToEnd()\n$logName = if ($env:VSTACK_HOOK_LOG_NAME) { $env:VSTACK_HOOK_LOG_NAME } else { 'hook-markdown-quality-alerts.log' }\n$logRoot = if ($env:VSTACK_HOOK_LOG_DIR) { $env:VSTACK_HOOK_LOG_DIR } else { '.vstack/logs' }\n$logDir = Join-Path $logRoot (Get-Date).ToUniversalTime().ToString('yyyyMMdd')\nNew-Item -ItemType Directory -Force -Path $logDir | Out-Null\n$logPath = Join-Path $logDir $logName\n$mode = if ($env:VSTACK_HOOKS_MODE) { $env:VSTACK_HOOKS_MODE } else { 'audit' }\nif ($mode -ne 'enforce') {\n exit 0\n}\nif ($inputText -notmatch '\\.md|docs/|\\.github/|\\.vstack/templates/|adr/|prompt\\.md|instructions\\.md|SKILL\\.md') {\n exit 0\n}\nif ((Test-Path Makefile) -and (Get-Command make -ErrorAction SilentlyContinue)) {\n make markdown-format *> $null\n if ($LASTEXITCODE -ne 0) {\n make format *> $null\n }\n if ($LASTEXITCODE -ne 0) {\n Add-Content -Path $logPath -Value 'markdown-format-run-failed'\n }\n} else {\n Add-Content -Path $logPath -Value 'markdown-format-skipped-missing-make-or-makefile'\n}\n", + "bash": "mode=\"${VSTACK_HOOKS_MODE:-audit}\"\nif [ \"$mode\" != \"enforce\" ]; then\n exit 0\nfi\ninput=\"$(cat)\"\nappend_log() {\n log_name=\"${VSTACK_HOOK_LOG_NAME:-hook-markdown-quality-alerts.log}\"\n log_root=\"${VSTACK_HOOK_LOG_DIR:-.vstack/logs}\"\n log_dir=\"$log_root/$(date -u +%Y%m%d)\"\n mkdir -p \"$log_dir\"\n printf '%s\\n' \"$1\" >> \"$log_dir/$log_name\"\n}\nif ! printf '%s' \"$input\" | grep -Eiq '\\.md|docs/|\\.github/|\\.vstack/templates/|adr/|prompt\\.md|instructions\\.md|SKILL\\.md'; then\n exit 0\nfi\nif [ -f Makefile ] && command -v make >/dev/null 2>&1; then\n make markdown-format >/dev/null 2>&1 || \\\n make format >/dev/null 2>&1 || \\\n append_log 'markdown-format-run-failed'\nelse\n append_log 'markdown-format-skipped-missing-make-or-makefile'\nfi\n", + "powershell": "$mode = if ($env:VSTACK_HOOKS_MODE) { $env:VSTACK_HOOKS_MODE } else { 'audit' }\nif ($mode -ne 'enforce') {\n exit 0\n}\n$inputText = [Console]::In.ReadToEnd()\nfunction Write-HookLog {\n param([string]$Message)\n $logName = if ($env:VSTACK_HOOK_LOG_NAME) { $env:VSTACK_HOOK_LOG_NAME } else { 'hook-markdown-quality-alerts.log' }\n $logRoot = if ($env:VSTACK_HOOK_LOG_DIR) { $env:VSTACK_HOOK_LOG_DIR } else { '.vstack/logs' }\n $logDir = Join-Path $logRoot (Get-Date).ToUniversalTime().ToString('yyyyMMdd')\n New-Item -ItemType Directory -Force -Path $logDir | Out-Null\n Add-Content -Path (Join-Path $logDir $logName) -Value $Message\n}\nif ($inputText -notmatch '\\.md|docs/|\\.github/|\\.vstack/templates/|adr/|prompt\\.md|instructions\\.md|SKILL\\.md') {\n exit 0\n}\nif ((Test-Path Makefile) -and (Get-Command make -ErrorAction SilentlyContinue)) {\n make markdown-format *> $null\n if ($LASTEXITCODE -ne 0) {\n make format *> $null\n }\n if ($LASTEXITCODE -ne 0) {\n Write-HookLog 'markdown-format-run-failed'\n }\n} else {\n Write-HookLog 'markdown-format-skipped-missing-make-or-makefile'\n}\n", "cwd": ".", "timeoutSec": 30 } diff --git a/.github/hooks/pre-tool-safety-gate.json b/.github/hooks/pre-tool-safety-gate.json index 3e910fa9..cd345bb7 100644 --- a/.github/hooks/pre-tool-safety-gate.json +++ b/.github/hooks/pre-tool-safety-gate.json @@ -5,8 +5,8 @@ { "type": "command", "description": "Check for destructive bash patterns and block in enforce mode.\nLogs only policy decisions and errors in this hook.\n", - "bash": "input=\"$(cat)\"\nlog_name=\"${VSTACK_HOOK_LOG_NAME:-hook-security-alerts.log}\"\nlog_root=\"${VSTACK_HOOK_LOG_DIR:-.vstack/logs}\"\nlog_dir=\"$log_root/$(date -u +%Y%m%d)\"\nmkdir -p \"$log_dir\"\nlog_path=\"$log_dir/$log_name\"\nmode=\"${VSTACK_HOOKS_MODE:-audit}\"\nif [ \"$mode\" != \"enforce\" ]; then\n exit 0\nfi\ntool_name=\"$(printf '%s' \"$input\" | sed -n 's/.*\"toolName\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\nif [ -z \"$tool_name\" ]; then\n printf '%s\\n' 'payload-parse-failed' >> \"$log_path\"\n echo '{\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"Blocked request because payload could not be validated in enforce mode\"}'\n exit 0\nfi\nif [ \"$tool_name\" = \"bash\" ] && \\\n printf '%s' \"$input\" | grep -Eiq 'rm -rf /|mkfs|dd if=|DROP TABLE|TRUNCATE TABLE|git reset --hard|git clean -fdx|shutdown -h|shutdown now|reboot|poweroff|halt'; then\n printf '%s\\n' 'destructive-command-blocked' >> \"$log_path\"\n echo '{\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"Blocked potentially destructive shell command by vstack pre-tool safety policy\"}'\nfi\n", - "powershell": "$inputText = [Console]::In.ReadToEnd()\n$logName = if ($env:VSTACK_HOOK_LOG_NAME) { $env:VSTACK_HOOK_LOG_NAME } else { 'hook-security-alerts.log' }\n$logRoot = if ($env:VSTACK_HOOK_LOG_DIR) { $env:VSTACK_HOOK_LOG_DIR } else { '.vstack/logs' }\n$logDir = Join-Path $logRoot (Get-Date).ToUniversalTime().ToString('yyyyMMdd')\nNew-Item -ItemType Directory -Force -Path $logDir | Out-Null\n$logPath = Join-Path $logDir $logName\n$mode = if ($env:VSTACK_HOOKS_MODE) { $env:VSTACK_HOOKS_MODE } else { 'audit' }\nif ($mode -ne 'enforce') {\n exit 0\n}\ntry {\n $payload = $inputText | ConvertFrom-Json -ErrorAction Stop\n} catch {\n Add-Content -Path $logPath -Value 'payload-parse-failed'\n @{ permissionDecision = 'deny'; permissionDecisionReason = 'Blocked request because payload could not be validated in enforce mode' } | ConvertTo-Json -Compress\n exit 0\n}\nif (-not $payload.toolName) {\n Add-Content -Path $logPath -Value 'payload-parse-failed'\n @{ permissionDecision = 'deny'; permissionDecisionReason = 'Blocked request because payload could not be validated in enforce mode' } | ConvertTo-Json -Compress\n exit 0\n}\nif ($payload.toolName -eq 'bash' -and $inputText -match 'rm -rf /|mkfs|dd if=|DROP TABLE|TRUNCATE TABLE|git reset --hard|git clean -fdx|shutdown -h|shutdown now|reboot|poweroff|halt') {\n Add-Content -Path $logPath -Value 'destructive-command-blocked'\n @{ permissionDecision = 'deny'; permissionDecisionReason = 'Blocked potentially destructive shell command by vstack pre-tool safety policy' } | ConvertTo-Json -Compress\n}\n", + "bash": "mode=\"${VSTACK_HOOKS_MODE:-audit}\"\nif [ \"$mode\" != \"enforce\" ]; then\n exit 0\nfi\ninput=\"$(cat)\"\nappend_log() {\n log_name=\"${VSTACK_HOOK_LOG_NAME:-hook-security-alerts.log}\"\n log_root=\"${VSTACK_HOOK_LOG_DIR:-.vstack/logs}\"\n log_dir=\"$log_root/$(date -u +%Y%m%d)\"\n mkdir -p \"$log_dir\"\n printf '%s\\n' \"$1\" >> \"$log_dir/$log_name\"\n}\ntool_name=\"$(printf '%s' \"$input\" | sed -n 's/.*\"toolName\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\nif [ -z \"$tool_name\" ]; then\n append_log 'payload-parse-failed'\n echo '{\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"Blocked request because payload could not be validated in enforce mode\"}'\n exit 0\nfi\nif [ \"$tool_name\" = \"bash\" ] && \\\n printf '%s' \"$input\" | grep -Eiq 'rm -rf /|mkfs|dd if=|DROP TABLE|TRUNCATE TABLE|git reset --hard|git clean -fdx|shutdown -h|shutdown now|reboot|poweroff|halt'; then\n append_log 'destructive-command-blocked'\n echo '{\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"Blocked potentially destructive shell command by vstack pre-tool safety policy\"}'\nfi\n", + "powershell": "$mode = if ($env:VSTACK_HOOKS_MODE) { $env:VSTACK_HOOKS_MODE } else { 'audit' }\nif ($mode -ne 'enforce') {\n exit 0\n}\n$inputText = [Console]::In.ReadToEnd()\nfunction Write-HookLog {\n param([string]$Message)\n $logName = if ($env:VSTACK_HOOK_LOG_NAME) { $env:VSTACK_HOOK_LOG_NAME } else { 'hook-security-alerts.log' }\n $logRoot = if ($env:VSTACK_HOOK_LOG_DIR) { $env:VSTACK_HOOK_LOG_DIR } else { '.vstack/logs' }\n $logDir = Join-Path $logRoot (Get-Date).ToUniversalTime().ToString('yyyyMMdd')\n New-Item -ItemType Directory -Force -Path $logDir | Out-Null\n Add-Content -Path (Join-Path $logDir $logName) -Value $Message\n}\ntry {\n $payload = $inputText | ConvertFrom-Json -ErrorAction Stop\n} catch {\n Write-HookLog 'payload-parse-failed'\n @{ permissionDecision = 'deny'; permissionDecisionReason = 'Blocked request because payload could not be validated in enforce mode' } | ConvertTo-Json -Compress\n exit 0\n}\nif (-not $payload.toolName) {\n Write-HookLog 'payload-parse-failed'\n @{ permissionDecision = 'deny'; permissionDecisionReason = 'Blocked request because payload could not be validated in enforce mode' } | ConvertTo-Json -Compress\n exit 0\n}\nif ($payload.toolName -eq 'bash' -and $inputText -match 'rm -rf /|mkfs|dd if=|DROP TABLE|TRUNCATE TABLE|git reset --hard|git clean -fdx|shutdown -h|shutdown now|reboot|poweroff|halt') {\n Write-HookLog 'destructive-command-blocked'\n @{ permissionDecision = 'deny'; permissionDecisionReason = 'Blocked potentially destructive shell command by vstack pre-tool safety policy' } | ConvertTo-Json -Compress\n}\n", "cwd": ".", "timeoutSec": 15 } diff --git a/.github/hooks/session-audit.json b/.github/hooks/session-audit.json index 65e60457..58803c4b 100644 --- a/.github/hooks/session-audit.json +++ b/.github/hooks/session-audit.json @@ -5,50 +5,50 @@ { "type": "command", "description": "Centrally log tool call requests with size and estimated tokens.\n", - "bash": "input=\"$(cat)\"\nlog_level=\"${VSTACK_HOOKS_LOG_LEVEL:-minimal}\"\nlog_root=\"${VSTACK_HOOK_LOG_DIR:-.vstack/logs}\"\nlog_dir=\"$log_root/$(date -u +%Y%m%d)\"\nmkdir -p \"$log_dir\"\nif [ \"$log_level\" = \"off\" ]; then\n exit 0\nfi\nif [ \"$log_level\" = \"verbose\" ]; then\n printf '%s\\n' \"$input\" >> \"$log_dir/hook-tool-use.log\"\n exit 0\nfi\nts=\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"\nbytes=\"$(printf '%s' \"$input\" | wc -c | tr -d ' ')\"\nest_tokens=\"$(( (bytes + 3) / 4 ))\"\ntool_name=\"$(printf '%s' \"$input\" | sed -n 's/.*\"toolName\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\nif [ -z \"$tool_name\" ]; then\n tool_name=\"unknown\"\nfi\nprintf '{\"timestamp\":\"%s\",\"event\":\"preToolUse\",\"tool_name\":\"%s\",\"size_bytes\":%s,\"estimated_tokens\":%s}\\n' \"$ts\" \"$tool_name\" \"$bytes\" \"$est_tokens\" >> \"$log_dir/hook-tool-use.log\"\n", - "powershell": "$inputText = [Console]::In.ReadToEnd()\n$logLevel = if ($env:VSTACK_HOOKS_LOG_LEVEL) { $env:VSTACK_HOOKS_LOG_LEVEL } else { 'minimal' }\n$logRoot = if ($env:VSTACK_HOOK_LOG_DIR) { $env:VSTACK_HOOK_LOG_DIR } else { '.vstack/logs' }\n$logDir = Join-Path $logRoot (Get-Date).ToUniversalTime().ToString('yyyyMMdd')\nNew-Item -ItemType Directory -Force -Path $logDir | Out-Null\nif ($logLevel -eq 'off') {\n exit 0\n}\nif ($logLevel -eq 'verbose') {\n Add-Content -Path (Join-Path $logDir 'hook-tool-use.log') -Value $inputText\n exit 0\n}\n$toolName = 'unknown'\ntry {\n $payload = $inputText | ConvertFrom-Json -ErrorAction Stop\n if ($payload.toolName) {\n $toolName = [string]$payload.toolName\n }\n} catch {\n}\n$record = @{\n timestamp = (Get-Date).ToUniversalTime().ToString('o')\n event = 'preToolUse'\n tool_name = $toolName\n size_bytes = $inputText.Length\n estimated_tokens = [int][math]::Ceiling($inputText.Length / 4.0)\n} | ConvertTo-Json -Compress\nAdd-Content -Path (Join-Path $logDir 'hook-tool-use.log') -Value $record\n", + "bash": "script_start_ms=\"$(date +%s%3N 2>/dev/null || true)\"\nif [ -z \"$script_start_ms\" ]; then\n script_start_ms=\"$(( $(date +%s) * 1000 ))\"\nfi\ninput=\"$(cat)\"\nlog_level=\"${VSTACK_HOOKS_LOG_LEVEL:-minimal}\"\nlog_root=\"${VSTACK_HOOK_LOG_DIR:-.vstack/logs}\"\nlog_dir=\"$log_root/$(date -u +%Y%m%d)\"\nmkdir -p \"$log_dir\"\nif [ \"$log_level\" = \"off\" ]; then\n exit 0\nfi\nif [ \"$log_level\" = \"verbose\" ]; then\n printf '%s\\n' \"$input\" >> \"$log_dir/hook-tool-use.log\"\n exit 0\nfi\nts=\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"\nbytes=\"$(printf '%s' \"$input\" | wc -c | tr -d ' ')\"\nest_tokens=\"$(( (bytes + 3) / 4 ))\"\nextract_json_string() {\n key=\"$1\"\n printf '%s' \"$input\" | sed -n \"s/.*\\\"${key}\\\"[[:space:]]*:[[:space:]]*\\\"\\\\([^\\\"]*\\\\)\\\".*/\\\\1/p\" | head -n1\n}\ntool_name=\"$(extract_json_string toolName)\"\nif [ -z \"$tool_name\" ]; then\n tool_name=\"$(extract_json_string tool_name)\"\nfi\nif [ -z \"$tool_name\" ]; then\n tool_name=\"$(extract_json_string recipient_name)\"\nfi\nif [ -z \"$tool_name\" ]; then\n tool_name=\"$(printf '%s' \"$input\" | sed -n 's/.*\"tool\"[[:space:]]*:[[:space:]]*{[^}]*\"name\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\nfi\nif [ -z \"$tool_name\" ]; then\n tool_name=\"$(printf '%s' \"$input\" | sed -n 's/.*\"toolCall\"[[:space:]]*:[[:space:]]*{[^}]*\"name\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\nfi\nif [ -z \"$tool_name\" ]; then\n tool_name=\"$(printf '%s' \"$input\" | sed -n 's/.*\"toolCall\"[[:space:]]*:[[:space:]]*{[^}]*\"toolName\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\nfi\nif [ -z \"$tool_name\" ]; then\n tool_name=\"$(printf '%s' \"$input\" | sed -n 's/.*\"tool_call\"[[:space:]]*:[[:space:]]*{[^}]*\"name\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\nfi\nif [ -z \"$tool_name\" ]; then\n tool_name=\"$(printf '%s' \"$input\" | sed -n 's/.*\"tool_call\"[[:space:]]*:[[:space:]]*{[^}]*\"tool_name\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\nfi\nif [ -z \"$tool_name\" ]; then\n tool_name=\"$(printf '%s' \"$input\" | grep -Eo '[A-Za-z_][A-Za-z0-9_]*\\.[A-Za-z_][A-Za-z0-9_]*' | head -n1)\"\nfi\nif [ -z \"$tool_name\" ]; then\n tool_name=\"unknown\"\nfi\ntool_call_id=\"$(extract_json_string toolCallId)\"\nif [ -z \"$tool_call_id\" ]; then\n tool_call_id=\"$(extract_json_string tool_call_id)\"\nfi\nif [ -z \"$tool_call_id\" ]; then\n tool_call_id=\"$(extract_json_string callId)\"\nfi\nif [ -z \"$tool_call_id\" ]; then\n tool_call_id=\"$(extract_json_string call_id)\"\nfi\nif [ -z \"$tool_call_id\" ]; then\n tool_call_id=\"$(printf '%s' \"$input\" | sed -n 's/.*\"toolCall\"[[:space:]]*:[[:space:]]*{[^}]*\"id\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\nfi\nif [ -z \"$tool_call_id\" ]; then\n tool_call_id=\"$(printf '%s' \"$input\" | sed -n 's/.*\"tool_call\"[[:space:]]*:[[:space:]]*{[^}]*\"id\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\nfi\nif [ -z \"$tool_call_id\" ]; then\n tool_call_id=\"unknown\"\nfi\nscript_end_ms=\"$(date +%s%3N 2>/dev/null || true)\"\nif [ -z \"$script_end_ms\" ]; then\n script_end_ms=\"$(( $(date +%s) * 1000 ))\"\nfi\nhook_execution_ms=\"$((script_end_ms - script_start_ms))\"\nprintf '{\"timestamp\":\"%s\",\"event\":\"preToolUse\",\"tool_name\":\"%s\",\"tool_call_id\":\"%s\",\"size_bytes\":%s,\"estimated_tokens\":%s,\"hook_execution_ms\":%s}\\n' \"$ts\" \"$tool_name\" \"$tool_call_id\" \"$bytes\" \"$est_tokens\" \"$hook_execution_ms\" >> \"$log_dir/hook-tool-use.log\"\n", + "powershell": "$inputText = [Console]::In.ReadToEnd()\n$logLevel = if ($env:VSTACK_HOOKS_LOG_LEVEL) { $env:VSTACK_HOOKS_LOG_LEVEL } else { 'minimal' }\n$logRoot = if ($env:VSTACK_HOOK_LOG_DIR) { $env:VSTACK_HOOK_LOG_DIR } else { '.vstack/logs' }\n$logDir = Join-Path $logRoot (Get-Date).ToUniversalTime().ToString('yyyyMMdd')\nNew-Item -ItemType Directory -Force -Path $logDir | Out-Null\nif ($logLevel -eq 'off') {\n exit 0\n}\nif ($logLevel -eq 'verbose') {\n Add-Content -Path (Join-Path $logDir 'hook-tool-use.log') -Value $inputText\n exit 0\n}\n$scriptStart = Get-Date\nfunction Get-PathValue {\n param([object]$Object, [string]$Path)\n $current = $Object\n foreach ($part in $Path.Split('.')) {\n if ($null -eq $current) {\n return $null\n }\n $prop = $current.PSObject.Properties[$part]\n if ($null -eq $prop) {\n return $null\n }\n $current = $prop.Value\n }\n return $current\n}\nfunction Get-FirstString {\n param([object]$Object, [string[]]$Paths)\n foreach ($path in $Paths) {\n $value = Get-PathValue -Object $Object -Path $path\n if ($null -ne $value -and [string]$value -ne '') {\n return [string]$value\n }\n }\n return $null\n}\n$toolName = 'unknown'\n$toolCallId = 'unknown'\ntry {\n $payload = $inputText | ConvertFrom-Json -ErrorAction Stop\n $toolNameCandidate = Get-FirstString -Object $payload -Paths @('toolName', 'tool_name', 'recipient_name', 'tool.name', 'toolCall.name', 'toolCall.toolName', 'tool_call.name', 'tool_call.tool_name')\n if ($toolNameCandidate) {\n $toolName = $toolNameCandidate\n }\n $toolCallIdCandidate = Get-FirstString -Object $payload -Paths @('toolCallId', 'tool_call_id', 'callId', 'call_id', 'toolCall.id', 'tool_call.id')\n if ($toolCallIdCandidate) {\n $toolCallId = $toolCallIdCandidate\n }\n} catch {\n}\nif ($toolName -eq 'unknown') {\n $toolNameMatch = [regex]::Match($inputText, '[A-Za-z_][A-Za-z0-9_]*\\.[A-Za-z_][A-Za-z0-9_]*')\n if ($toolNameMatch.Success) {\n $toolName = $toolNameMatch.Value\n }\n}\n$hookExecutionMs = [int][Math]::Max(0, ((Get-Date) - $scriptStart).TotalMilliseconds)\n$record = @{\n timestamp = (Get-Date).ToUniversalTime().ToString('o')\n event = 'preToolUse'\n tool_name = $toolName\n tool_call_id = $toolCallId\n size_bytes = $inputText.Length\n estimated_tokens = [int][math]::Ceiling($inputText.Length / 4.0)\n hook_execution_ms = $hookExecutionMs\n} | ConvertTo-Json -Compress\nAdd-Content -Path (Join-Path $logDir 'hook-tool-use.log') -Value $record\n", "cwd": ".", - "timeoutSec": 10 + "timeoutSec": 5 } ], "postToolUse": [ { "type": "command", "description": "Centrally log tool responses with size and estimated tokens.\n", - "bash": "input=\"$(cat)\"\nlog_level=\"${VSTACK_HOOKS_LOG_LEVEL:-minimal}\"\nlog_root=\"${VSTACK_HOOK_LOG_DIR:-.vstack/logs}\"\nlog_dir=\"$log_root/$(date -u +%Y%m%d)\"\nmkdir -p \"$log_dir\"\nif [ \"$log_level\" = \"off\" ]; then\n exit 0\nfi\nif [ \"$log_level\" = \"verbose\" ]; then\n printf '%s\\n' \"$input\" >> \"$log_dir/hook-tool-use.log\"\n exit 0\nfi\nts=\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"\nbytes=\"$(printf '%s' \"$input\" | wc -c | tr -d ' ')\"\nest_tokens=\"$(( (bytes + 3) / 4 ))\"\ntool_name=\"$(printf '%s' \"$input\" | sed -n 's/.*\"toolName\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\nif [ -z \"$tool_name\" ]; then\n tool_name=\"unknown\"\nfi\nprintf '{\"timestamp\":\"%s\",\"event\":\"postToolUse\",\"tool_name\":\"%s\",\"size_bytes\":%s,\"estimated_tokens\":%s}\\n' \"$ts\" \"$tool_name\" \"$bytes\" \"$est_tokens\" >> \"$log_dir/hook-tool-use.log\"\n", - "powershell": "$inputText = [Console]::In.ReadToEnd()\n$logLevel = if ($env:VSTACK_HOOKS_LOG_LEVEL) { $env:VSTACK_HOOKS_LOG_LEVEL } else { 'minimal' }\n$logRoot = if ($env:VSTACK_HOOK_LOG_DIR) { $env:VSTACK_HOOK_LOG_DIR } else { '.vstack/logs' }\n$logDir = Join-Path $logRoot (Get-Date).ToUniversalTime().ToString('yyyyMMdd')\nNew-Item -ItemType Directory -Force -Path $logDir | Out-Null\nif ($logLevel -eq 'off') {\n exit 0\n}\nif ($logLevel -eq 'verbose') {\n Add-Content -Path (Join-Path $logDir 'hook-tool-use.log') -Value $inputText\n exit 0\n}\n$toolName = 'unknown'\ntry {\n $payload = $inputText | ConvertFrom-Json -ErrorAction Stop\n if ($payload.toolName) {\n $toolName = [string]$payload.toolName\n }\n} catch {\n}\n$record = @{\n timestamp = (Get-Date).ToUniversalTime().ToString('o')\n event = 'postToolUse'\n tool_name = $toolName\n size_bytes = $inputText.Length\n estimated_tokens = [int][math]::Ceiling($inputText.Length / 4.0)\n} | ConvertTo-Json -Compress\nAdd-Content -Path (Join-Path $logDir 'hook-tool-use.log') -Value $record\n", + "bash": "script_start_ms=\"$(date +%s%3N 2>/dev/null || true)\"\nif [ -z \"$script_start_ms\" ]; then\n script_start_ms=\"$(( $(date +%s) * 1000 ))\"\nfi\ninput=\"$(cat)\"\nlog_level=\"${VSTACK_HOOKS_LOG_LEVEL:-minimal}\"\nlog_root=\"${VSTACK_HOOK_LOG_DIR:-.vstack/logs}\"\nlog_dir=\"$log_root/$(date -u +%Y%m%d)\"\nmkdir -p \"$log_dir\"\nif [ \"$log_level\" = \"off\" ]; then\n exit 0\nfi\nif [ \"$log_level\" = \"verbose\" ]; then\n printf '%s\\n' \"$input\" >> \"$log_dir/hook-tool-use.log\"\n exit 0\nfi\nts=\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"\nbytes=\"$(printf '%s' \"$input\" | wc -c | tr -d ' ')\"\nest_tokens=\"$(( (bytes + 3) / 4 ))\"\nextract_json_string() {\n key=\"$1\"\n printf '%s' \"$input\" | sed -n \"s/.*\\\"${key}\\\"[[:space:]]*:[[:space:]]*\\\"\\\\([^\\\"]*\\\\)\\\".*/\\\\1/p\" | head -n1\n}\ntool_name=\"$(extract_json_string toolName)\"\nif [ -z \"$tool_name\" ]; then\n tool_name=\"$(extract_json_string tool_name)\"\nfi\nif [ -z \"$tool_name\" ]; then\n tool_name=\"$(extract_json_string recipient_name)\"\nfi\nif [ -z \"$tool_name\" ]; then\n tool_name=\"$(printf '%s' \"$input\" | sed -n 's/.*\"tool\"[[:space:]]*:[[:space:]]*{[^}]*\"name\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\nfi\nif [ -z \"$tool_name\" ]; then\n tool_name=\"$(printf '%s' \"$input\" | sed -n 's/.*\"toolCall\"[[:space:]]*:[[:space:]]*{[^}]*\"name\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\nfi\nif [ -z \"$tool_name\" ]; then\n tool_name=\"$(printf '%s' \"$input\" | sed -n 's/.*\"toolCall\"[[:space:]]*:[[:space:]]*{[^}]*\"toolName\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\nfi\nif [ -z \"$tool_name\" ]; then\n tool_name=\"$(printf '%s' \"$input\" | sed -n 's/.*\"tool_call\"[[:space:]]*:[[:space:]]*{[^}]*\"name\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\nfi\nif [ -z \"$tool_name\" ]; then\n tool_name=\"$(printf '%s' \"$input\" | sed -n 's/.*\"tool_call\"[[:space:]]*:[[:space:]]*{[^}]*\"tool_name\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\nfi\nif [ -z \"$tool_name\" ]; then\n tool_name=\"$(printf '%s' \"$input\" | grep -Eo '[A-Za-z_][A-Za-z0-9_]*\\.[A-Za-z_][A-Za-z0-9_]*' | head -n1)\"\nfi\nif [ -z \"$tool_name\" ]; then\n tool_name=\"unknown\"\nfi\ntool_call_id=\"$(extract_json_string toolCallId)\"\nif [ -z \"$tool_call_id\" ]; then\n tool_call_id=\"$(extract_json_string tool_call_id)\"\nfi\nif [ -z \"$tool_call_id\" ]; then\n tool_call_id=\"$(extract_json_string callId)\"\nfi\nif [ -z \"$tool_call_id\" ]; then\n tool_call_id=\"$(extract_json_string call_id)\"\nfi\nif [ -z \"$tool_call_id\" ]; then\n tool_call_id=\"$(printf '%s' \"$input\" | sed -n 's/.*\"toolCall\"[[:space:]]*:[[:space:]]*{[^}]*\"id\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\nfi\nif [ -z \"$tool_call_id\" ]; then\n tool_call_id=\"$(printf '%s' \"$input\" | sed -n 's/.*\"tool_call\"[[:space:]]*:[[:space:]]*{[^}]*\"id\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\nfi\nif [ -z \"$tool_call_id\" ]; then\n tool_call_id=\"unknown\"\nfi\nscript_end_ms=\"$(date +%s%3N 2>/dev/null || true)\"\nif [ -z \"$script_end_ms\" ]; then\n script_end_ms=\"$(( $(date +%s) * 1000 ))\"\nfi\nhook_execution_ms=\"$((script_end_ms - script_start_ms))\"\nprintf '{\"timestamp\":\"%s\",\"event\":\"postToolUse\",\"tool_name\":\"%s\",\"tool_call_id\":\"%s\",\"size_bytes\":%s,\"estimated_tokens\":%s,\"hook_execution_ms\":%s}\\n' \"$ts\" \"$tool_name\" \"$tool_call_id\" \"$bytes\" \"$est_tokens\" \"$hook_execution_ms\" >> \"$log_dir/hook-tool-use.log\"\n", + "powershell": "$inputText = [Console]::In.ReadToEnd()\n$logLevel = if ($env:VSTACK_HOOKS_LOG_LEVEL) { $env:VSTACK_HOOKS_LOG_LEVEL } else { 'minimal' }\n$logRoot = if ($env:VSTACK_HOOK_LOG_DIR) { $env:VSTACK_HOOK_LOG_DIR } else { '.vstack/logs' }\n$logDir = Join-Path $logRoot (Get-Date).ToUniversalTime().ToString('yyyyMMdd')\nNew-Item -ItemType Directory -Force -Path $logDir | Out-Null\nif ($logLevel -eq 'off') {\n exit 0\n}\nif ($logLevel -eq 'verbose') {\n Add-Content -Path (Join-Path $logDir 'hook-tool-use.log') -Value $inputText\n exit 0\n}\n$scriptStart = Get-Date\nfunction Get-PathValue {\n param([object]$Object, [string]$Path)\n $current = $Object\n foreach ($part in $Path.Split('.')) {\n if ($null -eq $current) {\n return $null\n }\n $prop = $current.PSObject.Properties[$part]\n if ($null -eq $prop) {\n return $null\n }\n $current = $prop.Value\n }\n return $current\n}\nfunction Get-FirstString {\n param([object]$Object, [string[]]$Paths)\n foreach ($path in $Paths) {\n $value = Get-PathValue -Object $Object -Path $path\n if ($null -ne $value -and [string]$value -ne '') {\n return [string]$value\n }\n }\n return $null\n}\n$toolName = 'unknown'\n$toolCallId = 'unknown'\ntry {\n $payload = $inputText | ConvertFrom-Json -ErrorAction Stop\n $toolNameCandidate = Get-FirstString -Object $payload -Paths @('toolName', 'tool_name', 'recipient_name', 'tool.name', 'toolCall.name', 'toolCall.toolName', 'tool_call.name', 'tool_call.tool_name')\n if ($toolNameCandidate) {\n $toolName = $toolNameCandidate\n }\n $toolCallIdCandidate = Get-FirstString -Object $payload -Paths @('toolCallId', 'tool_call_id', 'callId', 'call_id', 'toolCall.id', 'tool_call.id')\n if ($toolCallIdCandidate) {\n $toolCallId = $toolCallIdCandidate\n }\n} catch {\n}\nif ($toolName -eq 'unknown') {\n $toolNameMatch = [regex]::Match($inputText, '[A-Za-z_][A-Za-z0-9_]*\\.[A-Za-z_][A-Za-z0-9_]*')\n if ($toolNameMatch.Success) {\n $toolName = $toolNameMatch.Value\n }\n}\n$hookExecutionMs = [int][Math]::Max(0, ((Get-Date) - $scriptStart).TotalMilliseconds)\n$record = @{\n timestamp = (Get-Date).ToUniversalTime().ToString('o')\n event = 'postToolUse'\n tool_name = $toolName\n tool_call_id = $toolCallId\n size_bytes = $inputText.Length\n estimated_tokens = [int][math]::Ceiling($inputText.Length / 4.0)\n hook_execution_ms = $hookExecutionMs\n} | ConvertTo-Json -Compress\nAdd-Content -Path (Join-Path $logDir 'hook-tool-use.log') -Value $record\n", "cwd": ".", - "timeoutSec": 10 + "timeoutSec": 5 } ], "userPromptSubmitted": [ { "type": "command", "description": "Centrally log prompt events with configurable verbosity.\n", - "bash": "input=\"$(cat)\"\nlog_level=\"${VSTACK_HOOKS_LOG_LEVEL:-minimal}\"\nlog_root=\"${VSTACK_HOOK_LOG_DIR:-.vstack/logs}\"\nlog_dir=\"$log_root/$(date -u +%Y%m%d)\"\nmkdir -p \"$log_dir\"\nif [ \"$log_level\" = \"off\" ]; then\n exit 0\nfi\nif [ \"$log_level\" = \"verbose\" ]; then\n printf '%s\\n' \"$input\" >> \"$log_dir/hook-user-prompt.log\"\n exit 0\nfi\nts=\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"\nbytes=\"$(printf '%s' \"$input\" | wc -c | tr -d ' ')\"\nest_tokens=\"$(( (bytes + 3) / 4 ))\"\nprintf '{\"timestamp\":\"%s\",\"event\":\"userPromptSubmitted\",\"size_bytes\":%s,\"estimated_tokens\":%s}\\n' \"$ts\" \"$bytes\" \"$est_tokens\" >> \"$log_dir/hook-user-prompt.log\"\n", - "powershell": "$inputText = [Console]::In.ReadToEnd()\n$logLevel = if ($env:VSTACK_HOOKS_LOG_LEVEL) { $env:VSTACK_HOOKS_LOG_LEVEL } else { 'minimal' }\n$logRoot = if ($env:VSTACK_HOOK_LOG_DIR) { $env:VSTACK_HOOK_LOG_DIR } else { '.vstack/logs' }\n$logDir = Join-Path $logRoot (Get-Date).ToUniversalTime().ToString('yyyyMMdd')\nNew-Item -ItemType Directory -Force -Path $logDir | Out-Null\nif ($logLevel -eq 'off') {\n exit 0\n}\nif ($logLevel -eq 'verbose') {\n Add-Content -Path (Join-Path $logDir 'hook-user-prompt.log') -Value $inputText\n exit 0\n}\n$record = @{ timestamp = (Get-Date).ToUniversalTime().ToString('o'); event = 'userPromptSubmitted'; size_bytes = $inputText.Length; estimated_tokens = [int][math]::Ceiling($inputText.Length / 4.0) } | ConvertTo-Json -Compress\nAdd-Content -Path (Join-Path $logDir 'hook-user-prompt.log') -Value $record\n", + "bash": "script_start_ms=\"$(date +%s%3N 2>/dev/null || true)\"\nif [ -z \"$script_start_ms\" ]; then\n script_start_ms=\"$(( $(date +%s) * 1000 ))\"\nfi\ninput=\"$(cat)\"\nlog_level=\"${VSTACK_HOOKS_LOG_LEVEL:-minimal}\"\nlog_root=\"${VSTACK_HOOK_LOG_DIR:-.vstack/logs}\"\nlog_dir=\"$log_root/$(date -u +%Y%m%d)\"\nmkdir -p \"$log_dir\"\nif [ \"$log_level\" = \"off\" ]; then\n exit 0\nfi\nif [ \"$log_level\" = \"verbose\" ]; then\n printf '%s\\n' \"$input\" >> \"$log_dir/hook-user-prompt.log\"\n exit 0\nfi\nts=\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"\nbytes=\"$(printf '%s' \"$input\" | wc -c | tr -d ' ')\"\nest_tokens=\"$(( (bytes + 3) / 4 ))\"\nextract_json_string() {\n key=\"$1\"\n printf '%s' \"$input\" | sed -n \"s/.*\\\"${key}\\\"[[:space:]]*:[[:space:]]*\\\"\\\\([^\\\"]*\\\\)\\\".*/\\\\1/p\" | head -n1\n}\nprompt_text=\"$(extract_json_string prompt)\"\nif [ -z \"$prompt_text\" ]; then\n prompt_text=\"$(extract_json_string userPrompt)\"\nfi\nif [ -z \"$prompt_text\" ]; then\n prompt_text=\"$(extract_json_string user_prompt)\"\nfi\nif [ -z \"$prompt_text\" ]; then\n prompt_text=\"$(extract_json_string text)\"\nfi\nif [ -z \"$prompt_text\" ]; then\n prompt_text=\"$(printf '%s' \"$input\" | sed -n 's/.*\"prompt\"[[:space:]]*:[[:space:]]*{[^}]*\"text\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\"\nfi\nslash_command=\"none\"\nif [ -n \"$prompt_text\" ]; then\n slash_command_candidate=\"$(printf '%s' \"$prompt_text\" | sed -n 's#^/\\([^[:space:]]*\\).*$#/\\1#p' | head -n1)\"\n if [ -n \"$slash_command_candidate\" ]; then\n slash_command=\"$slash_command_candidate\"\n fi\nfi\nscript_end_ms=\"$(date +%s%3N 2>/dev/null || true)\"\nif [ -z \"$script_end_ms\" ]; then\n script_end_ms=\"$(( $(date +%s) * 1000 ))\"\nfi\nhook_execution_ms=\"$((script_end_ms - script_start_ms))\"\nprintf '{\"timestamp\":\"%s\",\"event\":\"userPromptSubmitted\",\"slash_command\":\"%s\",\"size_bytes\":%s,\"estimated_tokens\":%s,\"hook_execution_ms\":%s}\\n' \"$ts\" \"$slash_command\" \"$bytes\" \"$est_tokens\" \"$hook_execution_ms\" >> \"$log_dir/hook-user-prompt.log\"\n", + "powershell": "$inputText = [Console]::In.ReadToEnd()\n$logLevel = if ($env:VSTACK_HOOKS_LOG_LEVEL) { $env:VSTACK_HOOKS_LOG_LEVEL } else { 'minimal' }\n$logRoot = if ($env:VSTACK_HOOK_LOG_DIR) { $env:VSTACK_HOOK_LOG_DIR } else { '.vstack/logs' }\n$logDir = Join-Path $logRoot (Get-Date).ToUniversalTime().ToString('yyyyMMdd')\nNew-Item -ItemType Directory -Force -Path $logDir | Out-Null\nif ($logLevel -eq 'off') {\n exit 0\n}\nif ($logLevel -eq 'verbose') {\n Add-Content -Path (Join-Path $logDir 'hook-user-prompt.log') -Value $inputText\n exit 0\n}\n$scriptStart = Get-Date\nfunction Get-PathValue {\n param([object]$Object, [string]$Path)\n $current = $Object\n foreach ($part in $Path.Split('.')) {\n if ($null -eq $current) {\n return $null\n }\n $prop = $current.PSObject.Properties[$part]\n if ($null -eq $prop) {\n return $null\n }\n $current = $prop.Value\n }\n return $current\n}\nfunction Get-FirstString {\n param([object]$Object, [string[]]$Paths)\n foreach ($path in $Paths) {\n $value = Get-PathValue -Object $Object -Path $path\n if ($null -ne $value -and [string]$value -ne '') {\n return [string]$value\n }\n }\n return $null\n}\n$slashCommand = 'none'\ntry {\n $payload = $inputText | ConvertFrom-Json -ErrorAction Stop\n $promptText = Get-FirstString -Object $payload -Paths @('prompt', 'userPrompt', 'user_prompt', 'text', 'message.text', 'prompt.text')\n if ($promptText -and $promptText -match '^/([^\\s]+)') {\n $slashCommand = \"/$($Matches[1])\"\n }\n} catch {\n}\n$hookExecutionMs = [int][Math]::Max(0, ((Get-Date) - $scriptStart).TotalMilliseconds)\n$record = @{ timestamp = (Get-Date).ToUniversalTime().ToString('o'); event = 'userPromptSubmitted'; slash_command = $slashCommand; size_bytes = $inputText.Length; estimated_tokens = [int][math]::Ceiling($inputText.Length / 4.0); hook_execution_ms = $hookExecutionMs } | ConvertTo-Json -Compress\nAdd-Content -Path (Join-Path $logDir 'hook-user-prompt.log') -Value $record\n", "cwd": ".", - "timeoutSec": 10 + "timeoutSec": 5 } ], "sessionStart": [ { "type": "command", "description": "Centrally log session start with configurable verbosity.\n", - "bash": "input=\"$(cat)\"\nlog_level=\"${VSTACK_HOOKS_LOG_LEVEL:-minimal}\"\nlog_root=\"${VSTACK_HOOK_LOG_DIR:-.vstack/logs}\"\nlog_dir=\"$log_root/$(date -u +%Y%m%d)\"\nmkdir -p \"$log_dir\"\nif [ \"$log_level\" = \"off\" ]; then\n exit 0\nfi\nif [ \"$log_level\" = \"verbose\" ]; then\n printf '%s\\n' \"$input\" >> \"$log_dir/hook-session-start.log\"\n exit 0\nfi\nts=\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"\nbytes=\"$(printf '%s' \"$input\" | wc -c | tr -d ' ')\"\nest_tokens=\"$(( (bytes + 3) / 4 ))\"\nprintf '{\"timestamp\":\"%s\",\"event\":\"sessionStart\",\"size_bytes\":%s,\"estimated_tokens\":%s}\\n' \"$ts\" \"$bytes\" \"$est_tokens\" >> \"$log_dir/hook-session-start.log\"\n", - "powershell": "$inputText = [Console]::In.ReadToEnd()\n$logLevel = if ($env:VSTACK_HOOKS_LOG_LEVEL) { $env:VSTACK_HOOKS_LOG_LEVEL } else { 'minimal' }\n$logRoot = if ($env:VSTACK_HOOK_LOG_DIR) { $env:VSTACK_HOOK_LOG_DIR } else { '.vstack/logs' }\n$logDir = Join-Path $logRoot (Get-Date).ToUniversalTime().ToString('yyyyMMdd')\nNew-Item -ItemType Directory -Force -Path $logDir | Out-Null\nif ($logLevel -eq 'off') {\n exit 0\n}\nif ($logLevel -eq 'verbose') {\n Add-Content -Path (Join-Path $logDir 'hook-session-start.log') -Value $inputText\n exit 0\n}\n$record = @{ timestamp = (Get-Date).ToUniversalTime().ToString('o'); event = 'sessionStart'; size_bytes = $inputText.Length; estimated_tokens = [int][math]::Ceiling($inputText.Length / 4.0) } | ConvertTo-Json -Compress\nAdd-Content -Path (Join-Path $logDir 'hook-session-start.log') -Value $record\n", + "bash": "script_start_ms=\"$(date +%s%3N 2>/dev/null || true)\"\nif [ -z \"$script_start_ms\" ]; then\n script_start_ms=\"$(( $(date +%s) * 1000 ))\"\nfi\ninput=\"$(cat)\"\nlog_level=\"${VSTACK_HOOKS_LOG_LEVEL:-minimal}\"\nlog_root=\"${VSTACK_HOOK_LOG_DIR:-.vstack/logs}\"\nlog_dir=\"$log_root/$(date -u +%Y%m%d)\"\nmkdir -p \"$log_dir\"\nif [ \"$log_level\" = \"off\" ]; then\n exit 0\nfi\nif [ \"$log_level\" = \"verbose\" ]; then\n printf '%s\\n' \"$input\" >> \"$log_dir/hook-session-start.log\"\n exit 0\nfi\nts=\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"\nbytes=\"$(printf '%s' \"$input\" | wc -c | tr -d ' ')\"\nest_tokens=\"$(( (bytes + 3) / 4 ))\"\nscript_end_ms=\"$(date +%s%3N 2>/dev/null || true)\"\nif [ -z \"$script_end_ms\" ]; then\n script_end_ms=\"$(( $(date +%s) * 1000 ))\"\nfi\nhook_execution_ms=\"$((script_end_ms - script_start_ms))\"\nprintf '{\"timestamp\":\"%s\",\"event\":\"sessionStart\",\"size_bytes\":%s,\"estimated_tokens\":%s,\"hook_execution_ms\":%s}\\n' \"$ts\" \"$bytes\" \"$est_tokens\" \"$hook_execution_ms\" >> \"$log_dir/hook-session-start.log\"\n", + "powershell": "$inputText = [Console]::In.ReadToEnd()\n$logLevel = if ($env:VSTACK_HOOKS_LOG_LEVEL) { $env:VSTACK_HOOKS_LOG_LEVEL } else { 'minimal' }\n$logRoot = if ($env:VSTACK_HOOK_LOG_DIR) { $env:VSTACK_HOOK_LOG_DIR } else { '.vstack/logs' }\n$logDir = Join-Path $logRoot (Get-Date).ToUniversalTime().ToString('yyyyMMdd')\nNew-Item -ItemType Directory -Force -Path $logDir | Out-Null\nif ($logLevel -eq 'off') {\n exit 0\n}\nif ($logLevel -eq 'verbose') {\n Add-Content -Path (Join-Path $logDir 'hook-session-start.log') -Value $inputText\n exit 0\n}\n$scriptStart = Get-Date\n$hookExecutionMs = [int][Math]::Max(0, ((Get-Date) - $scriptStart).TotalMilliseconds)\n$record = @{ timestamp = (Get-Date).ToUniversalTime().ToString('o'); event = 'sessionStart'; size_bytes = $inputText.Length; estimated_tokens = [int][math]::Ceiling($inputText.Length / 4.0); hook_execution_ms = $hookExecutionMs } | ConvertTo-Json -Compress\nAdd-Content -Path (Join-Path $logDir 'hook-session-start.log') -Value $record\n", "cwd": ".", - "timeoutSec": 10 + "timeoutSec": 5 } ], "sessionEnd": [ { "type": "command", "description": "Centrally log session end for correlation with start.\n", - "bash": "input=\"$(cat)\"\nlog_level=\"${VSTACK_HOOKS_LOG_LEVEL:-minimal}\"\nlog_root=\"${VSTACK_HOOK_LOG_DIR:-.vstack/logs}\"\nlog_dir=\"$log_root/$(date -u +%Y%m%d)\"\nmkdir -p \"$log_dir\"\nif [ \"$log_level\" = \"off\" ]; then\n exit 0\nfi\nif [ \"$log_level\" = \"verbose\" ]; then\n printf '%s\\n' \"$input\" >> \"$log_dir/hook-session-end.log\"\n exit 0\nfi\nts=\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"\nbytes=\"$(printf '%s' \"$input\" | wc -c | tr -d ' ')\"\nest_tokens=\"$(( (bytes + 3) / 4 ))\"\nprintf '{\"timestamp\":\"%s\",\"event\":\"sessionEnd\",\"size_bytes\":%s,\"estimated_tokens\":%s}\\n' \"$ts\" \"$bytes\" \"$est_tokens\" >> \"$log_dir/hook-session-end.log\"\n", - "powershell": "$inputText = [Console]::In.ReadToEnd()\n$logLevel = if ($env:VSTACK_HOOKS_LOG_LEVEL) { $env:VSTACK_HOOKS_LOG_LEVEL } else { 'minimal' }\n$logRoot = if ($env:VSTACK_HOOK_LOG_DIR) { $env:VSTACK_HOOK_LOG_DIR } else { '.vstack/logs' }\n$logDir = Join-Path $logRoot (Get-Date).ToUniversalTime().ToString('yyyyMMdd')\nNew-Item -ItemType Directory -Force -Path $logDir | Out-Null\nif ($logLevel -eq 'off') {\n exit 0\n}\nif ($logLevel -eq 'verbose') {\n Add-Content -Path (Join-Path $logDir 'hook-session-end.log') -Value $inputText\n exit 0\n}\n$record = @{ timestamp = (Get-Date).ToUniversalTime().ToString('o'); event = 'sessionEnd'; size_bytes = $inputText.Length; estimated_tokens = [int][math]::Ceiling($inputText.Length / 4.0) } | ConvertTo-Json -Compress\nAdd-Content -Path (Join-Path $logDir 'hook-session-end.log') -Value $record\n", + "bash": "script_start_ms=\"$(date +%s%3N 2>/dev/null || true)\"\nif [ -z \"$script_start_ms\" ]; then\n script_start_ms=\"$(( $(date +%s) * 1000 ))\"\nfi\ninput=\"$(cat)\"\nlog_level=\"${VSTACK_HOOKS_LOG_LEVEL:-minimal}\"\nlog_root=\"${VSTACK_HOOK_LOG_DIR:-.vstack/logs}\"\nlog_dir=\"$log_root/$(date -u +%Y%m%d)\"\nmkdir -p \"$log_dir\"\nif [ \"$log_level\" = \"off\" ]; then\n exit 0\nfi\nif [ \"$log_level\" = \"verbose\" ]; then\n printf '%s\\n' \"$input\" >> \"$log_dir/hook-session-end.log\"\n exit 0\nfi\nts=\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"\nbytes=\"$(printf '%s' \"$input\" | wc -c | tr -d ' ')\"\nest_tokens=\"$(( (bytes + 3) / 4 ))\"\nscript_end_ms=\"$(date +%s%3N 2>/dev/null || true)\"\nif [ -z \"$script_end_ms\" ]; then\n script_end_ms=\"$(( $(date +%s) * 1000 ))\"\nfi\nhook_execution_ms=\"$((script_end_ms - script_start_ms))\"\nprintf '{\"timestamp\":\"%s\",\"event\":\"sessionEnd\",\"size_bytes\":%s,\"estimated_tokens\":%s,\"hook_execution_ms\":%s}\\n' \"$ts\" \"$bytes\" \"$est_tokens\" \"$hook_execution_ms\" >> \"$log_dir/hook-session-end.log\"\n", + "powershell": "$inputText = [Console]::In.ReadToEnd()\n$logLevel = if ($env:VSTACK_HOOKS_LOG_LEVEL) { $env:VSTACK_HOOKS_LOG_LEVEL } else { 'minimal' }\n$logRoot = if ($env:VSTACK_HOOK_LOG_DIR) { $env:VSTACK_HOOK_LOG_DIR } else { '.vstack/logs' }\n$logDir = Join-Path $logRoot (Get-Date).ToUniversalTime().ToString('yyyyMMdd')\nNew-Item -ItemType Directory -Force -Path $logDir | Out-Null\nif ($logLevel -eq 'off') {\n exit 0\n}\nif ($logLevel -eq 'verbose') {\n Add-Content -Path (Join-Path $logDir 'hook-session-end.log') -Value $inputText\n exit 0\n}\n$scriptStart = Get-Date\n$hookExecutionMs = [int][Math]::Max(0, ((Get-Date) - $scriptStart).TotalMilliseconds)\n$record = @{ timestamp = (Get-Date).ToUniversalTime().ToString('o'); event = 'sessionEnd'; size_bytes = $inputText.Length; estimated_tokens = [int][math]::Ceiling($inputText.Length / 4.0); hook_execution_ms = $hookExecutionMs } | ConvertTo-Json -Compress\nAdd-Content -Path (Join-Path $logDir 'hook-session-end.log') -Value $record\n", "cwd": ".", - "timeoutSec": 10 + "timeoutSec": 5 } ] } diff --git a/.github/instructions/git.instructions.md b/.github/instructions/git.instructions.md index 814fed74..c0549cd8 100644 --- a/.github/instructions/git.instructions.md +++ b/.github/instructions/git.instructions.md @@ -41,4 +41,4 @@ Use these Git and release hygiene conventions in this project. 1. Prefer local verification before pushing release-impacting changes. - + diff --git a/.github/instructions/helm.instructions.md b/.github/instructions/helm.instructions.md index b5ebd729..b2f596a4 100644 --- a/.github/instructions/helm.instructions.md +++ b/.github/instructions/helm.instructions.md @@ -45,4 +45,4 @@ Use these Helm conventions in this project. - [Helm chart best practices](https://helm.sh/docs/chart_best_practices/) - + diff --git a/.github/instructions/java.instructions.md b/.github/instructions/java.instructions.md index 831e4abb..4ecbb781 100644 --- a/.github/instructions/java.instructions.md +++ b/.github/instructions/java.instructions.md @@ -56,4 +56,4 @@ Use these Java conventions in this project. 1. Do not suppress static analysis warnings without a documented, task-specific reason. - + diff --git a/.github/instructions/k8s.instructions.md b/.github/instructions/k8s.instructions.md index 217bc0f4..8317d189 100644 --- a/.github/instructions/k8s.instructions.md +++ b/.github/instructions/k8s.instructions.md @@ -51,4 +51,4 @@ Use these Kubernetes conventions in this project. - [Kubernetes API reference](https://kubernetes.io/docs/reference/kubernetes-api/) - + diff --git a/.github/instructions/markdown.instructions.md b/.github/instructions/markdown.instructions.md index 58785b84..09674dbc 100644 --- a/.github/instructions/markdown.instructions.md +++ b/.github/instructions/markdown.instructions.md @@ -51,4 +51,4 @@ Use these Markdown conventions in this project. 1. Keep examples accurate and runnable — a broken example is worse than no example. - + diff --git a/.github/instructions/python.instructions.md b/.github/instructions/python.instructions.md index c2defbcc..273e75bf 100644 --- a/.github/instructions/python.instructions.md +++ b/.github/instructions/python.instructions.md @@ -42,4 +42,4 @@ Use these Python conventions in this project. 1. Do not silence lint/type errors unless there is a documented, task-specific reason. - + diff --git a/.github/instructions/rancher.instructions.md b/.github/instructions/rancher.instructions.md index aa6420b1..74645675 100644 --- a/.github/instructions/rancher.instructions.md +++ b/.github/instructions/rancher.instructions.md @@ -44,4 +44,4 @@ Use these Rancher conventions in this project. - [Fleet docs](https://fleet.rancher.io/) - + diff --git a/.github/instructions/security.instructions.md b/.github/instructions/security.instructions.md index 75e4eec3..0bf57f53 100644 --- a/.github/instructions/security.instructions.md +++ b/.github/instructions/security.instructions.md @@ -42,4 +42,4 @@ Apply these security policies in this project. 1. Isolate privileged logic; keep it minimal, auditable, and separate from business logic. - + diff --git a/.github/instructions/terraform.instructions.md b/.github/instructions/terraform.instructions.md index 4a2aac4e..c355a8ae 100644 --- a/.github/instructions/terraform.instructions.md +++ b/.github/instructions/terraform.instructions.md @@ -60,4 +60,4 @@ Use these Terraform conventions in this project. - [tfsec](https://aquasecurity.github.io/tfsec/) · [checkov](https://www.checkov.io/) - + diff --git a/.github/instructions/terragrunt.instructions.md b/.github/instructions/terragrunt.instructions.md index 4f048b50..302f3e72 100644 --- a/.github/instructions/terragrunt.instructions.md +++ b/.github/instructions/terragrunt.instructions.md @@ -57,4 +57,4 @@ Use these Terragrunt conventions in this project. - [Terragrunt CLI reference](https://terragrunt.gruntwork.io/docs/reference/cli-options/) - + diff --git a/.github/instructions/testing.instructions.md b/.github/instructions/testing.instructions.md index fee33639..0507e1a7 100644 --- a/.github/instructions/testing.instructions.md +++ b/.github/instructions/testing.instructions.md @@ -43,4 +43,4 @@ Use these testing conventions in this project. 1. Treat flaky tests as bugs; do not merge code with known test reliability issues. - + diff --git a/.github/instructions/typescript.instructions.md b/.github/instructions/typescript.instructions.md index dfca9b4b..0a96373d 100644 --- a/.github/instructions/typescript.instructions.md +++ b/.github/instructions/typescript.instructions.md @@ -49,4 +49,4 @@ Use these TypeScript conventions in this project. 1. Do not suppress lint or type errors with inline disable comments unless there is a documented, task-specific reason. - + diff --git a/.github/prompts/api-design-review.prompt.md b/.github/prompts/api-design-review.prompt.md index ae5bb9df..8fd41dc8 100644 --- a/.github/prompts/api-design-review.prompt.md +++ b/.github/prompts/api-design-review.prompt.md @@ -55,4 +55,4 @@ List fields or objects that are missing required constraints, descriptions, or e - top priority fix in one sentence - + diff --git a/.github/prompts/architecture-risk.prompt.md b/.github/prompts/architecture-risk.prompt.md index 9f1c9156..e11c75c4 100644 --- a/.github/prompts/architecture-risk.prompt.md +++ b/.github/prompts/architecture-risk.prompt.md @@ -52,4 +52,4 @@ List security-specific risks not covered above: auth boundaries, sensitive data - one-sentence rationale - + diff --git a/.github/prompts/artifact-integrity.prompt.md b/.github/prompts/artifact-integrity.prompt.md new file mode 100644 index 00000000..9a60bd4b --- /dev/null +++ b/.github/prompts/artifact-integrity.prompt.md @@ -0,0 +1,59 @@ +--- +description: 'Check source templates against generated artifacts and identify drift or missing regeneration.' +name: artifact-integrity +argument-hint: '[artifact type, path, or full repo]' +agent: tester +model: GPT-5.3-Codex (copilot) +tools: + - read + - search +--- +Check source templates against generated artifacts and identify drift or missing regeneration. + +Compare `src/vstack/_templates/` sources against their installed counterparts in `.github/` +and the manifest at `.vstack/vstack.json`. Surface any mismatch, stale output, or gap. + +Output exactly in this format: + +## Drift Findings + +List every source-to-artifact mismatch detected. + +For each item: + +- source template: path under `src/vstack/_templates/` +- generated artifact: expected path under `.github/` +- drift type: MISSING | STALE | CHECKSUM-MISMATCH | UNTRACKED +- detail: one sentence describing the discrepancy + +## Regeneration Actions + +List the exact commands needed to bring generated artifacts back into sync. + +For each action: + +- command: the shell command to run (e.g. `python3 -m vstack install`) +- scope: which artifact types or names this command covers +- priority: CRITICAL | HIGH | LOW + +## Risk If Unfixed + +Describe the production risk if drift is left unresolved. + +For each drift item from above: + +- artifact affected +- risk: what could fail or mislead if the stale artifact ships + +## Verification Steps + +Provide a checklist to confirm the repository is back in a clean state after regeneration. + +- [ ] `python3 -m vstack install` completes without errors +- [ ] `vstack validate` reports no unresolved template tokens +- [ ] `make test-local` passes with 100% coverage +- [ ] `make markdown-format` reports no changes +- [ ] All regenerated files match their source checksums in `.vstack/vstack.json` + + + diff --git a/.github/prompts/ci-triage.prompt.md b/.github/prompts/ci-triage.prompt.md new file mode 100644 index 00000000..d7fe20bf --- /dev/null +++ b/.github/prompts/ci-triage.prompt.md @@ -0,0 +1,46 @@ +--- +description: 'Triage CI failures into root-cause clusters and prioritize the fastest safe recovery path.' +name: ci-triage +argument-hint: '[workflow run, failing job, or log scope]' +agent: engineer +model: GPT-5.3-Codex (copilot) +tools: + - read + - search +--- +Triage CI failures into root-cause clusters and prioritize the fastest safe recovery path. + +Use failing job logs, workflow config, and changed files. + +Output exactly in this format: + +## Failure Clusters + +For each cluster: + +- jobs affected +- probable root cause +- confidence: HIGH | MEDIUM | LOW + +## Priority Fix Order + +Ordered by unblock value. + +- fix action +- expected unblocked jobs +- owner role + +## Risk Notes + +- risky quick fixes to avoid +- possible hidden regressions + +## Recovery Checklist + +- [ ] apply highest-priority fix +- [ ] rerun targeted jobs +- [ ] rerun full workflow +- [ ] confirm no new failures + + + diff --git a/.github/prompts/code-review.prompt.md b/.github/prompts/code-review.prompt.md index 524bc914..ed73e5aa 100644 --- a/.github/prompts/code-review.prompt.md +++ b/.github/prompts/code-review.prompt.md @@ -51,4 +51,4 @@ End with: - Biggest remaining risk: one sentence - + diff --git a/.github/prompts/dependency-audit.prompt.md b/.github/prompts/dependency-audit.prompt.md index 0bcca348..301f7c89 100644 --- a/.github/prompts/dependency-audit.prompt.md +++ b/.github/prompts/dependency-audit.prompt.md @@ -60,4 +60,4 @@ List packages with unusual provenance concerns: abandoned maintainers, single-ma Ordered list of actions by priority (critical first). - + diff --git a/.github/prompts/incident-timeline.prompt.md b/.github/prompts/incident-timeline.prompt.md index 53f4ab5a..50de85d1 100644 --- a/.github/prompts/incident-timeline.prompt.md +++ b/.github/prompts/incident-timeline.prompt.md @@ -57,4 +57,4 @@ For each action: List the minimum controls needed to reduce repeat probability. - + diff --git a/.github/prompts/migration-plan.prompt.md b/.github/prompts/migration-plan.prompt.md new file mode 100644 index 00000000..4f379cbb --- /dev/null +++ b/.github/prompts/migration-plan.prompt.md @@ -0,0 +1,46 @@ +--- +description: 'Produce a safe migration plan with sequencing, fallback paths, and verification checkpoints.' +name: migration-plan +argument-hint: '[migration scope or affected components]' +agent: engineer +model: GPT-5.3-Codex (copilot) +tools: + - read + - search +--- +Produce a safe migration plan with sequencing, fallback paths, and verification checkpoints. + +Use source changes, schema/contracts, and deployment constraints. + +Output exactly in this format: + +## Migration Overview + +- scope +- dependencies +- compatibility strategy + +## Execution Plan + +For each phase: + +- phase +- changes applied +- validation checkpoint + +## Rollback Plan + +For each phase: + +- rollback trigger +- rollback steps +- data integrity check + +## Post-Migration Validation + +- required tests +- smoke checks +- success criteria + + + diff --git a/.github/prompts/migration-safety.prompt.md b/.github/prompts/migration-safety.prompt.md index 522e0462..fb22c4bf 100644 --- a/.github/prompts/migration-safety.prompt.md +++ b/.github/prompts/migration-safety.prompt.md @@ -52,4 +52,4 @@ List missing migration tests (forward, backward, data invariants, load-sensitive - biggest remaining risk in one sentence - + diff --git a/.github/prompts/ops-readiness.prompt.md b/.github/prompts/ops-readiness.prompt.md new file mode 100644 index 00000000..82c67f61 --- /dev/null +++ b/.github/prompts/ops-readiness.prompt.md @@ -0,0 +1,45 @@ +--- +description: 'Assess operational readiness across observability, runbooks, failure handling, and supportability.' +name: ops-readiness +argument-hint: '[service, component, or release scope]' +agent: tester +model: GPT-5.3-Codex (copilot) +tools: + - read + - search +--- +Assess operational readiness across observability, runbooks, failure handling, and supportability. + +Use evidence from code, tests, docs, and runbooks. + +Output exactly in this format: + +## Ops Readiness Scorecard + +- observability: READY | PARTIAL | MISSING +- alerting: READY | PARTIAL | MISSING +- runbooks: READY | PARTIAL | MISSING +- failure handling: READY | PARTIAL | MISSING + +## Gaps + +For each gap: + +- area +- finding +- risk if unresolved +- owner role + +## Must-Fix Before Release + +- blocker +- required evidence + +## Follow-up Actions + +- action +- owner role +- target milestone + + + diff --git a/.github/prompts/release-check.prompt.md b/.github/prompts/release-check.prompt.md new file mode 100644 index 00000000..ec135f70 --- /dev/null +++ b/.github/prompts/release-check.prompt.md @@ -0,0 +1,49 @@ +--- +description: 'Evaluate release gate readiness using required reports, artifacts, and sign-off evidence.' +name: release-check +argument-hint: '[release scope or target branch]' +agent: release +model: GPT-5.3-Codex (copilot) +tools: + - read + - search +--- +Evaluate release gate readiness using required reports, artifacts, and sign-off evidence. + +Use repository evidence only. + +Output exactly in this format: + +## Release Gate Status + +- overall status: READY | NOT-READY +- scope reviewed +- evidence sources checked + +## Missing or Stale Evidence + +For each item: + +- artifact +- issue: MISSING | STALE | INCOMPLETE +- impact +- owner role + +## Sign-off Gaps + +For each required role: + +- role +- verdict: OK | NOK | NOT-RECORDED +- blocking reason + +## Release Actions + +Ordered actions to reach READY. + +- action +- owner role +- verification step + + + diff --git a/.github/prompts/release-readiness.prompt.md b/.github/prompts/release-readiness.prompt.md deleted file mode 100644 index 4f3be2b1..00000000 --- a/.github/prompts/release-readiness.prompt.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -description: 'Evaluate release readiness from reports, risks, and unresolved blockers.' -name: release-readiness -argument-hint: '[scope, release date, or branch]' -agent: release -model: GPT-5.3-Codex (copilot) -tools: - - read - - search ---- -Assess whether this change set is ready to release. - -Review product, architecture, design, test, security, and performance evidence. -Prefer evidence-based findings tied to concrete artifacts. - -Output exactly in this format: - -## Release Gate Verdict - -- Verdict: READY | READY-WITH-CONDITIONS | NOT-READY -- Confidence: high | medium | low -- Scope assessed: one sentence - -## Blocking Issues - -List only release-blocking items. - -For each item: - -- artifact or file reference -- why this blocks release in one sentence -- concrete unblock action -- owner role (product | architect | designer | engineer | tester | release) - -## Conditions Before Release - -List non-blocking but mandatory follow-ups to ship safely. - -## Evidence Reviewed - -List the exact artifacts checked (reports, docs, CI evidence, manifests). -For each expected artifact that is missing, flag it explicitly as: MISSING — [artifact name]. - -## Recommended Next Action - -One clear next step for the team. - - - diff --git a/.github/prompts/repo-assessment.prompt.md b/.github/prompts/repo-assessment.prompt.md new file mode 100644 index 00000000..d3b3403f --- /dev/null +++ b/.github/prompts/repo-assessment.prompt.md @@ -0,0 +1,59 @@ +--- +description: 'Assess a repository for production-readiness gaps and prioritized improvements.' +name: repo-assessment +argument-hint: '[repository scope or component path]' +agent: engineer +model: GPT-5.3-Codex (copilot) +tools: + - read + - search +--- +Assess this repository for production-readiness gaps and prioritized improvement opportunities. + +Focus on evidence found in source files, tests, CI configuration, docs, and manifests. +Prefer concrete findings over speculative risks. + +Output exactly in this format: + +## Critical Risks + +List only issues that block safe production operation right now. + +For each item: + +- location: file or component reference +- risk: one sentence describing the production impact +- remediation: concrete action with owner role + +## High-ROI Improvements + +List high-value improvements that reduce operational risk or developer friction. +Rank by impact relative to effort. + +For each item: + +- area: the domain (security | reliability | observability | dx | performance | maintainability) +- finding: what is missing or suboptimal +- suggested action: one clear improvement step + +## Testing and Verification Gaps + +List behavioral paths that are untested or where test coverage provides false confidence. + +For each item: + +- uncovered behavior: what scenario is missing +- risk if untested: what could go wrong in production +- suggested test type: unit | integration | contract | e2e + +## Suggested Next Sprint Backlog + +Provide a short, actionable list of tasks suitable for the next sprint. +Order from highest to lowest priority. + +- task title +- owner role (product | architect | designer | engineer | tester | release) +- one-line rationale + + + diff --git a/.github/prompts/template-impact.prompt.md b/.github/prompts/template-impact.prompt.md new file mode 100644 index 00000000..087c5228 --- /dev/null +++ b/.github/prompts/template-impact.prompt.md @@ -0,0 +1,46 @@ +--- +description: 'Assess impact of a template change on generated artifacts, tests, and release risk.' +name: template-impact +argument-hint: '[template path or change scope]' +agent: engineer +model: GPT-5.3-Codex (copilot) +tools: + - read + - search +--- +Assess impact of a template change on generated artifacts, tests, and release risk. + +Use source-template and generated-artifact evidence. + +Output exactly in this format: + +## Change Surface + +- template scope +- affected artifact types +- likely generated paths + +## Impact Findings + +For each finding: + +- impacted path +- impact type: BEHAVIOR | DOCS | TESTS | TOOLING +- risk +- required validation + +## Regression Risk + +List highest-risk regressions first. + +- scenario +- likelihood: HIGH | MEDIUM | LOW +- mitigation + +## Verification Plan + +- commands to run +- expected pass criteria + + + diff --git a/.github/prompts/test-gaps.prompt.md b/.github/prompts/test-gaps.prompt.md new file mode 100644 index 00000000..60c600db --- /dev/null +++ b/.github/prompts/test-gaps.prompt.md @@ -0,0 +1,43 @@ +--- +description: 'Identify missing behavioral coverage and prioritize test additions by production risk.' +name: test-gaps +argument-hint: '[component, feature, or repository scope]' +agent: tester +model: GPT-5.3-Codex (copilot) +tools: + - read + - search +--- +Identify missing behavioral coverage and prioritize test additions by production risk. + +Focus on observable behavior and realistic failure paths. + +Output exactly in this format: + +## Coverage Gaps + +For each gap: + +- behavior +- current coverage weakness +- production risk +- suggested test type: unit | integration | contract | e2e + +## False Confidence Risks + +List tests that pass but may not protect real behavior. + +- location +- why confidence is false +- correction + +## Priority Test Backlog + +Ordered from highest to lowest risk reduction. + +- test title +- owner role +- acceptance check + + + diff --git a/.github/prompts/upgrade-plan.prompt.md b/.github/prompts/upgrade-plan.prompt.md new file mode 100644 index 00000000..9865d4ab --- /dev/null +++ b/.github/prompts/upgrade-plan.prompt.md @@ -0,0 +1,46 @@ +--- +description: 'Build a safe upgrade plan with sequencing, compatibility checks, and rollback points.' +name: upgrade-plan +argument-hint: '[target version, component, or full repo]' +agent: tester +model: GPT-5.3-Codex (copilot) +tools: + - read + - search +--- +Build a safe upgrade plan with sequencing, compatibility checks, and rollback points. + +Use repository docs, manifests, and test constraints. + +Output exactly in this format: + +## Upgrade Scope + +- target +- baseline version +- compatibility boundaries + +## Step Plan + +Ordered upgrade steps. + +- step +- dependency/precondition +- success check + +## Risk and Rollback + +For each major step: + +- risk +- trigger to rollback +- rollback action + +## Verification Matrix + +- check +- command +- pass criteria + + + diff --git a/.github/prompts/workflow-check.prompt.md b/.github/prompts/workflow-check.prompt.md new file mode 100644 index 00000000..2b24e1a5 --- /dev/null +++ b/.github/prompts/workflow-check.prompt.md @@ -0,0 +1,49 @@ +--- +description: 'Review workflow stage flow, gate usage, and handoff integrity across role artifacts.' +name: workflow-check +argument-hint: '[workflow scope or stage list]' +agent: planner +model: GPT-5.3-Codex (copilot) +tools: + - read + - search +--- +Review workflow stage flow, gate usage, and handoff integrity across role artifacts. + +Focus on evidence in workflow config, role templates, and docs. + +Output exactly in this format: + +## Flow Summary + +- workflow scope +- current stage order +- gate model used + +## Gate and Handoff Findings + +For each finding: + +- location: file path and section +- issue: one sentence +- impact: what can fail in execution +- fix: concrete correction + +## Blocking Risks + +List only issues that block reliable workflow execution. + +- blocker +- owner role +- required action + +## Recommended Next Steps + +Provide an ordered short list of actions. + +- action +- owner role +- expected result + + + diff --git a/.github/skills/adr/SKILL.md b/.github/skills/adr/SKILL.md index 6cca82c4..09c69c32 100644 --- a/.github/skills/adr/SKILL.md +++ b/.github/skills/adr/SKILL.md @@ -180,4 +180,4 @@ is a kebab-case title. After writing, state the file path and summary so the architect or product role can review. - + diff --git a/.github/skills/analyse/SKILL.md b/.github/skills/analyse/SKILL.md index 1d287fa8..be1d5f81 100644 --- a/.github/skills/analyse/SKILL.md +++ b/.github/skills/analyse/SKILL.md @@ -214,4 +214,4 @@ State conclusions with confidence level: ``` - + diff --git a/.github/skills/architecture/SKILL.md b/.github/skills/architecture/SKILL.md index fdd0a1b8..f96e38ea 100644 --- a/.github/skills/architecture/SKILL.md +++ b/.github/skills/architecture/SKILL.md @@ -278,4 +278,4 @@ For each significant structural decision made during this review (technology cho - Update `docs/architecture/overview.md` to reflect the final decisions. - + diff --git a/.github/skills/aws-cli/SKILL.md b/.github/skills/aws-cli/SKILL.md index 4e183038..4b6ac35f 100644 --- a/.github/skills/aws-cli/SKILL.md +++ b/.github/skills/aws-cli/SKILL.md @@ -374,4 +374,4 @@ aws ce get-cost-and-usage \ - [AWS CLI named profiles](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html) - + diff --git a/.github/skills/cicd/SKILL.md b/.github/skills/cicd/SKILL.md index a30bb66f..9490d4ec 100644 --- a/.github/skills/cicd/SKILL.md +++ b/.github/skills/cicd/SKILL.md @@ -221,4 +221,4 @@ Configure these in GitHub → Settings → Branches. - [GitHub-hosted runners](https://docs.github.com/en/actions/using-github-hosted-runners/using-github-hosted-runners/about-github-hosted-runners) - + diff --git a/.github/skills/cloudformation/SKILL.md b/.github/skills/cloudformation/SKILL.md index ba455d79..0b0830cb 100644 --- a/.github/skills/cloudformation/SKILL.md +++ b/.github/skills/cloudformation/SKILL.md @@ -344,4 +344,4 @@ AppSecurityGroup: - [AWS SAM documentation](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/) - + diff --git a/.github/skills/code-review/SKILL.md b/.github/skills/code-review/SKILL.md index 13c71e6f..ec347847 100644 --- a/.github/skills/code-review/SKILL.md +++ b/.github/skills/code-review/SKILL.md @@ -218,4 +218,4 @@ Confidence: [HIGH/MEDIUM/LOW — explain if not HIGH] ``` - + diff --git a/.github/skills/codeql/SKILL.md b/.github/skills/codeql/SKILL.md index 62ea71b5..471bef03 100644 --- a/.github/skills/codeql/SKILL.md +++ b/.github/skills/codeql/SKILL.md @@ -251,4 +251,4 @@ GITHUB_TOKEN= codeql github upload-results \ - [Supported languages and frameworks](https://docs.github.com/en/code-security/code-scanning/introduction-to-code-scanning/codeql-code-scanning-for-compiled-languages) - + diff --git a/.github/skills/concise/SKILL.md b/.github/skills/concise/SKILL.md index 5dcf750e..a3539465 100644 --- a/.github/skills/concise/SKILL.md +++ b/.github/skills/concise/SKILL.md @@ -147,4 +147,4 @@ Current mode unchanged: - [ ] User confirmation/status returned in deterministic format - + diff --git a/.github/skills/consult/SKILL.md b/.github/skills/consult/SKILL.md index 64648ce0..b0b20874 100644 --- a/.github/skills/consult/SKILL.md +++ b/.github/skills/consult/SKILL.md @@ -218,4 +218,4 @@ reason: [one sentence] ``` - + diff --git a/.github/skills/container/SKILL.md b/.github/skills/container/SKILL.md index 756a3886..139773a5 100644 --- a/.github/skills/container/SKILL.md +++ b/.github/skills/container/SKILL.md @@ -153,4 +153,4 @@ For production-like local testing, write a separate `docker-compose.prod.yml` wi - [Docker official images](https://hub.docker.com/search?image_filter=official) - + diff --git a/.github/skills/conventional-commit/SKILL.md b/.github/skills/conventional-commit/SKILL.md index dc3cd65f..d2fe0338 100644 --- a/.github/skills/conventional-commit/SKILL.md +++ b/.github/skills/conventional-commit/SKILL.md @@ -155,4 +155,4 @@ Remaining changes: If commit is blocked, report exact reason and proposed fix. - + diff --git a/.github/skills/copilot-ops/SKILL.md b/.github/skills/copilot-ops/SKILL.md new file mode 100644 index 00000000..43a155ed --- /dev/null +++ b/.github/skills/copilot-ops/SKILL.md @@ -0,0 +1,90 @@ +--- +name: copilot-ops +description: 'Operate and govern GitHub Copilot settings with an audit-first workflow. Covers policy checks, configuration drift, usage visibility, and safe change rollout.' +license: 'MIT' +compatibility: 'Requires admin-level GitHub permissions and gh CLI authentication for governance operations.' +metadata: + owner: vstack + maturity: candidate +allowed-tools: 'execute read search' +argument-hint: '[org/repo scope or ops objective]' +user-invocable: true +disable-model-invocation: false +--- +## Skill Context + +This skill is part of **vstack** — a VS Code-native AI engineering workflow system. + +### AskUserQuestion Format + +When you need clarification, use this exact format — never invent or guess: + +> **Question:** [The specific question] +> **Options:** A) … | B) … | C) … +> **Default if no response:** [What you'll do] + +Never ask more than one question at a time without waiting for the answer. + +### Diagram Convention + +When producing hand-authored Markdown outputs, prefer Mermaid for flow, +interaction, lifecycle, state, topology, dependency, and decision diagrams when +the format is supported and improves clarity. Use ASCII as a fallback when +Mermaid is unsupported or would be less readable. Keep ASCII/text trees for +directory structures and other scan-friendly hierarchies. + +# copilot-ops - Copilot Governance Operations + +Run Copilot governance changes safely with evidence, rollback intent, and verification. + +## When to use + +- Audit Copilot governance settings before release or compliance review +- Apply policy updates for repository or organization scope +- Investigate configuration drift between expected and actual Copilot controls + +## Procedure + +1. Capture current scope (repo/org/enterprise) and required permissions. +1. Pull current Copilot-relevant settings and record baseline evidence. +1. Compare baseline with expected policy and identify drift. +1. Propose minimal changes with explicit risk notes. +1. Apply approved changes using audited commands/workflows. +1. Re-read settings and confirm effective state. +1. Log follow-up checks and ownership. + +## Output format + +Provide this structure: + +### Baseline + +- scope reviewed +- settings checked +- evidence source + +### Drift Findings + +- setting +- expected value +- current value +- risk + +### Change Plan + +- proposed change +- approval needed +- rollback note + +### Verification + +- post-change check +- result +- residual risk + +## Escalation + +Escalate when permissions are insufficient, settings conflict across scopes, or policy intent is ambiguous. + + + diff --git a/.github/skills/debug/SKILL.md b/.github/skills/debug/SKILL.md index c59aac07..41345d65 100644 --- a/.github/skills/debug/SKILL.md +++ b/.github/skills/debug/SKILL.md @@ -258,4 +258,4 @@ Prevention: [any follow-up items] ``` - + diff --git a/.github/skills/dependabot/SKILL.md b/.github/skills/dependabot/SKILL.md index 442de499..ac188073 100644 --- a/.github/skills/dependabot/SKILL.md +++ b/.github/skills/dependabot/SKILL.md @@ -320,4 +320,4 @@ updates: - [Dependabot security updates](https://docs.github.com/en/code-security/dependabot/dependabot-security-updates/about-dependabot-security-updates) - + diff --git a/.github/skills/dependency/SKILL.md b/.github/skills/dependency/SKILL.md index 77b33f6c..b8b579a1 100644 --- a/.github/skills/dependency/SKILL.md +++ b/.github/skills/dependency/SKILL.md @@ -318,4 +318,4 @@ Action items (priority order): - [PyPI / npm / crates.io / Maven Central](https://pypi.org) (replace with the relevant registry) - + diff --git a/.github/skills/design/SKILL.md b/.github/skills/design/SKILL.md index 1a56bcf6..7e68dc72 100644 --- a/.github/skills/design/SKILL.md +++ b/.github/skills/design/SKILL.md @@ -243,4 +243,4 @@ Output a complete design document to `docs/design/overview.md` or `openapi.yaml` ``` - + diff --git a/.github/skills/docs/SKILL.md b/.github/skills/docs/SKILL.md index 14940fc3..3a5d7e9f 100644 --- a/.github/skills/docs/SKILL.md +++ b/.github/skills/docs/SKILL.md @@ -151,4 +151,4 @@ Skipped (n/a): ``` - + diff --git a/.github/skills/explore/SKILL.md b/.github/skills/explore/SKILL.md index 980543d1..de25faf0 100644 --- a/.github/skills/explore/SKILL.md +++ b/.github/skills/explore/SKILL.md @@ -224,4 +224,4 @@ Stack: [language, framework, runtime versions] ``` - + diff --git a/.github/skills/gdpr/SKILL.md b/.github/skills/gdpr/SKILL.md index a81650e6..a7230232 100644 --- a/.github/skills/gdpr/SKILL.md +++ b/.github/skills/gdpr/SKILL.md @@ -246,4 +246,4 @@ Use `@example.com` for all test email addresses. - [EDPB guidelines](https://www.edpb.europa.eu/our-work-tools/general-guidance/guidelines-recommendations-best-practices_en) - + diff --git a/.github/skills/gh-issues/SKILL.md b/.github/skills/gh-issues/SKILL.md index c6257a47..32cda576 100644 --- a/.github/skills/gh-issues/SKILL.md +++ b/.github/skills/gh-issues/SKILL.md @@ -230,4 +230,4 @@ https://github.com///issues/ - [GitHub Issues documentation](https://docs.github.com/en/issues) - + diff --git a/.github/skills/gh-release/SKILL.md b/.github/skills/gh-release/SKILL.md index 43f58835..25cbac72 100644 --- a/.github/skills/gh-release/SKILL.md +++ b/.github/skills/gh-release/SKILL.md @@ -214,4 +214,4 @@ If blocked, report exact blocker and required user action. - [GitHub Releases documentation](https://docs.github.com/en/repositories/releasing-projects-on-github/about-releases) - + diff --git a/.github/skills/guardrails/SKILL.md b/.github/skills/guardrails/SKILL.md index 57b6e706..bdcd4785 100644 --- a/.github/skills/guardrails/SKILL.md +++ b/.github/skills/guardrails/SKILL.md @@ -72,4 +72,4 @@ Activate careful mode for this session. Two behaviors are now enabled. Explicitly ask to "disable guardrails". - + diff --git a/.github/skills/helm/SKILL.md b/.github/skills/helm/SKILL.md index cfd00779..45a10083 100644 --- a/.github/skills/helm/SKILL.md +++ b/.github/skills/helm/SKILL.md @@ -140,4 +140,4 @@ Practices: - [Chart best practices](https://helm.sh/docs/chart_best_practices/) - + diff --git a/.github/skills/incident/SKILL.md b/.github/skills/incident/SKILL.md index bdea20fc..723822fb 100644 --- a/.github/skills/incident/SKILL.md +++ b/.github/skills/incident/SKILL.md @@ -251,4 +251,4 @@ Next: invoke @#rca and @#postmortem to produce written artifacts. ``` - + diff --git a/.github/skills/inspect/SKILL.md b/.github/skills/inspect/SKILL.md index c01344a9..f6a99571 100644 --- a/.github/skills/inspect/SKILL.md +++ b/.github/skills/inspect/SKILL.md @@ -155,4 +155,4 @@ Confirm for changed paths: ``` - + diff --git a/.github/skills/k8s/SKILL.md b/.github/skills/k8s/SKILL.md index fc8357bc..8b69ea44 100644 --- a/.github/skills/k8s/SKILL.md +++ b/.github/skills/k8s/SKILL.md @@ -144,4 +144,4 @@ Common failure classes: - [Kubernetes API reference](https://kubernetes.io/docs/reference/kubernetes-api/) - + diff --git a/.github/skills/migrate/SKILL.md b/.github/skills/migrate/SKILL.md index 707dd7f4..8d66f152 100644 --- a/.github/skills/migrate/SKILL.md +++ b/.github/skills/migrate/SKILL.md @@ -320,4 +320,4 @@ Pre-deploy checklist: ``` - + diff --git a/.github/skills/onboard/SKILL.md b/.github/skills/onboard/SKILL.md index 22088b3d..201114d4 100644 --- a/.github/skills/onboard/SKILL.md +++ b/.github/skills/onboard/SKILL.md @@ -302,4 +302,4 @@ Gaps remaining (if any): ``` - + diff --git a/.github/skills/openapi/SKILL.md b/.github/skills/openapi/SKILL.md index ad0f5af6..4143a37d 100644 --- a/.github/skills/openapi/SKILL.md +++ b/.github/skills/openapi/SKILL.md @@ -403,4 +403,4 @@ Summary: [N critical, N warnings, N info] - [Redocly CLI (linting)](https://redocly.com/docs/cli/) - + diff --git a/.github/skills/performance/SKILL.md b/.github/skills/performance/SKILL.md index 65aad9a2..ea3b6a69 100644 --- a/.github/skills/performance/SKILL.md +++ b/.github/skills/performance/SKILL.md @@ -242,4 +242,4 @@ For each bottleneck identified: ``` - + diff --git a/.github/skills/postmortem/SKILL.md b/.github/skills/postmortem/SKILL.md index 196ff0ae..9f7d813b 100644 --- a/.github/skills/postmortem/SKILL.md +++ b/.github/skills/postmortem/SKILL.md @@ -184,4 +184,4 @@ Status: Draft — ready for team review ``` - + diff --git a/.github/skills/pr/SKILL.md b/.github/skills/pr/SKILL.md index 1baf28eb..248c156f 100644 --- a/.github/skills/pr/SKILL.md +++ b/.github/skills/pr/SKILL.md @@ -151,4 +151,4 @@ Next steps depend on the repository CI/CD configuration: ``` - + diff --git a/.github/skills/rancher/SKILL.md b/.github/skills/rancher/SKILL.md index e91629ef..9bc615c9 100644 --- a/.github/skills/rancher/SKILL.md +++ b/.github/skills/rancher/SKILL.md @@ -113,4 +113,4 @@ Checks: - [Fleet documentation](https://fleet.rancher.io/) - + diff --git a/.github/skills/rca/SKILL.md b/.github/skills/rca/SKILL.md index 11d3bef7..112588d6 100644 --- a/.github/skills/rca/SKILL.md +++ b/.github/skills/rca/SKILL.md @@ -207,4 +207,4 @@ Status: Draft — ready for review ``` - + diff --git a/.github/skills/refactor/SKILL.md b/.github/skills/refactor/SKILL.md index 20c6563a..095a91ef 100644 --- a/.github/skills/refactor/SKILL.md +++ b/.github/skills/refactor/SKILL.md @@ -372,4 +372,4 @@ Behavior changed: No ``` - + diff --git a/.github/skills/release-notes/SKILL.md b/.github/skills/release-notes/SKILL.md index 0d4a9f1b..683a1ef8 100644 --- a/.github/skills/release-notes/SKILL.md +++ b/.github/skills/release-notes/SKILL.md @@ -147,4 +147,4 @@ Prepend a new entry at the top of `CHANGELOG.md`: Keep existing entries intact. - + diff --git a/.github/skills/requirements/SKILL.md b/.github/skills/requirements/SKILL.md index e2b48f67..4ab1418c 100644 --- a/.github/skills/requirements/SKILL.md +++ b/.github/skills/requirements/SKILL.md @@ -199,4 +199,4 @@ Write all findings to `docs/product/requirements.md`: After writing, summarize what was decided so the architect role can start. - + diff --git a/.github/skills/secret-scan/SKILL.md b/.github/skills/secret-scan/SKILL.md index ccf9870d..4c5c71ff 100644 --- a/.github/skills/secret-scan/SKILL.md +++ b/.github/skills/secret-scan/SKILL.md @@ -240,4 +240,4 @@ credential formats. - [Supported secret patterns](https://docs.github.com/en/code-security/secret-scanning/introduction/supported-secret-scanning-patterns) - + diff --git a/.github/skills/security/SKILL.md b/.github/skills/security/SKILL.md index 139e3039..5f060ed3 100644 --- a/.github/skills/security/SKILL.md +++ b/.github/skills/security/SKILL.md @@ -295,4 +295,4 @@ Scope: [full/diff/dependency/config] - [STRIDE threat modeling (Microsoft)](https://learn.microsoft.com/en-us/azure/security/develop/threat-modeling-tool-threats) - + diff --git a/.github/skills/space-setup/SKILL.md b/.github/skills/space-setup/SKILL.md new file mode 100644 index 00000000..2a6886e1 --- /dev/null +++ b/.github/skills/space-setup/SKILL.md @@ -0,0 +1,90 @@ +--- +name: space-setup +description: 'Set up and maintain a GitHub Copilot Space for a repository. Covers scope selection, source curation, refresh cadence, and context quality checks.' +license: 'MIT' +compatibility: 'Requires GitHub Copilot Spaces access in the target organization/repository.' +metadata: + owner: vstack + maturity: candidate +allowed-tools: 'execute read search' +argument-hint: '[repo scope, docs set, or space objective]' +user-invocable: true +disable-model-invocation: false +--- +## Skill Context + +This skill is part of **vstack** — a VS Code-native AI engineering workflow system. + +### AskUserQuestion Format + +When you need clarification, use this exact format — never invent or guess: + +> **Question:** [The specific question] +> **Options:** A) … | B) … | C) … +> **Default if no response:** [What you'll do] + +Never ask more than one question at a time without waiting for the answer. + +### Diagram Convention + +When producing hand-authored Markdown outputs, prefer Mermaid for flow, +interaction, lifecycle, state, topology, dependency, and decision diagrams when +the format is supported and improves clarity. Use ASCII as a fallback when +Mermaid is unsupported or would be less readable. Keep ASCII/text trees for +directory structures and other scan-friendly hierarchies. + +# space-setup - Copilot Space Setup and Maintenance + +Set up a project Space that keeps Copilot context focused, current, and easy to audit. + +## When to use + +- New repository onboarding to Copilot Spaces +- Space quality cleanup after major docs or architecture updates +- Regular context refresh before a release cycle + +## Procedure + +1. Define Space objective and audience. +1. Select core sources: requirements, architecture, design, README, and key ADRs. +1. Exclude noisy/generated paths and duplicate docs. +1. Create or update the Space using GitHub UI (or approved API workflow). +1. Validate discoverability: each key topic maps to at least one source document. +1. Record refresh cadence and owner. +1. Re-check after `vstack install` or release docs updates. + +## Output format + +Provide this structure: + +### Space Scope + +- objective +- audience +- included sources +- excluded sources + +### Setup Actions + +- action taken +- rationale +- owner + +### Quality Findings + +- missing context +- stale context +- duplicate/noisy context + +### Maintenance Plan + +- refresh trigger +- cadence +- owner + +## Escalation + +Escalate when required docs are missing, stale, or inconsistent across product/architecture/design baselines. + + + diff --git a/.github/skills/terraform/SKILL.md b/.github/skills/terraform/SKILL.md index 87cd0440..528328b9 100644 --- a/.github/skills/terraform/SKILL.md +++ b/.github/skills/terraform/SKILL.md @@ -335,4 +335,4 @@ Run `terraform plan` after every state operation to verify the outcome. - [tfsec rules](https://aquasecurity.github.io/tfsec/latest/checks/aws/) · [checkov checks](https://www.checkov.io/5.Policy%20Index/terraform.html) - + diff --git a/.github/skills/terragrunt/SKILL.md b/.github/skills/terragrunt/SKILL.md index 30adf6a8..44f7ca02 100644 --- a/.github/skills/terragrunt/SKILL.md +++ b/.github/skills/terragrunt/SKILL.md @@ -307,4 +307,4 @@ Use `--terragrunt-non-interactive` in CI to prevent hanging on prompts. - [Gruntwork module registry](https://www.gruntwork.io/) - + diff --git a/.github/skills/threat-model/SKILL.md b/.github/skills/threat-model/SKILL.md index 7cf3a8b3..20afb766 100644 --- a/.github/skills/threat-model/SKILL.md +++ b/.github/skills/threat-model/SKILL.md @@ -245,4 +245,4 @@ For each high-priority threat include: - Final report is written to `docs/architecture/threat-model.md`. - + diff --git a/.github/skills/verify/SKILL.md b/.github/skills/verify/SKILL.md index 8cc68a33..2681dadf 100644 --- a/.github/skills/verify/SKILL.md +++ b/.github/skills/verify/SKILL.md @@ -266,4 +266,4 @@ scope: [path/component/full] ``` - + diff --git a/.github/skills/vision/SKILL.md b/.github/skills/vision/SKILL.md index dd60c8be..ae6b54f5 100644 --- a/.github/skills/vision/SKILL.md +++ b/.github/skills/vision/SKILL.md @@ -206,4 +206,4 @@ For each finding: explain the tradeoff, give an opinionated recommendation, ask Present as: "Overall assessment: [READY/NEEDS REVISION/SCOPE CHANGE] because [1-2 sentence reason]." - + diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 88daab17..cc94cf05 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -57,6 +57,31 @@ jobs: - name: Test run: make test-local + fixture-tests-fast: + # Fast deterministic feedback for fixture drift and defect harness checks. + name: Fixture Tests (fast) + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + + - name: Install Poetry + run: pipx install "poetry==${POETRY_VERSION}" + + - name: Setup Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 + with: + python-version: ${{ env.PYTHON_VERSION }} + cache: poetry + cache-dependency-path: poetry.lock + + - name: Install dependencies + run: poetry install --no-interaction --no-ansi + + - name: Run fixture-only golden and defect tests + run: make test-fixtures + vstack-verify: # Ensures generated artifacts can be installed and validated in an isolated target. name: Artifact Install Verify @@ -91,3 +116,18 @@ jobs: - name: Verify source and installed output run: poetry run vstack verify --target "${TARGET_DIR}" + + - name: Drift guard for generated artifacts + run: | + poetry run vstack install + # Ignore drift caused only by VSTACK-META vstack_version differences + # between local tagged checkouts and CI shallow clones. + if ! git --no-pager diff --quiet -I '"vstack_version":' -- .github; then + echo "Generated artifact drift detected under .github/." + echo "Run 'python3 -m vstack install' locally and commit the resulting changes." + echo "Changed files:" + git --no-pager diff --name-only -I '"vstack_version":' -- .github + echo "Diff:" + git --no-pager diff -I '"vstack_version":' -- .github || true + exit 1 + fi diff --git a/.gitignore b/.gitignore index f0608bd0..5230db83 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ env/ # Testing .pytest_cache/ +.vstack/tmp/ .hypothesis/ .nox/ .coverage diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 055bf115..57eacc7a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,7 +29,7 @@ repos: additional_dependencies: - mdformat-gfm files: \.(md)$ - exclude: ^\.github/ + exclude: ^(\.github/|tests/_fixtures/golden/) - repo: https://github.com/DavidAnson/markdownlint-cli2 rev: v0.22.1 diff --git a/.vstack/vstack.json b/.vstack/vstack.json index b578d167..1b9834f4 100644 --- a/.vstack/vstack.json +++ b/.vstack/vstack.json @@ -1,316 +1,330 @@ { "manifest_version": 2, "hash_algorithm": "sha256", - "vstack_version": "3.2.0", - "installed_at": "2026-05-12T21:30:20.387171+00:00", + "vstack_version": "3.3.0", + "installed_at": "2026-05-14T15:15:33.016787+00:00", "artifacts": { "skills": [ { "name": "adr", "file": "skills/adr/SKILL.md", "version": "20260421003", - "checksum": "3680cda5cb1cb4eb857e62ff04ca21dfc4f4054e4bfdf53708523e81264b1f7f", + "checksum": "660cee62bfe5bdbc51c93e1e6d65c04ef26b0fc9c18b26dc8a9cfc6aa199994f", "checksum_algorithm": "sha256" }, { "name": "analyse", "file": "skills/analyse/SKILL.md", "version": "20260421004", - "checksum": "0f2081b3758d57a299cadf8d27b5174d649aeb0e047dd083136dfb52e862b2a2", + "checksum": "7fc5684df41ba2cfd0508617362bb9027050f0d0101fe444472ceeb1ee07dd1f", "checksum_algorithm": "sha256" }, { "name": "architecture", "file": "skills/architecture/SKILL.md", "version": "20260421005", - "checksum": "eedad88826672a810c981210ab9bb6c2d2f319574512c571bdb07e5c630729c7", + "checksum": "839a87531273f69845d2113954965cc9aa9e5fe045a42594b0fab4d5cd0e7338", "checksum_algorithm": "sha256" }, { "name": "aws-cli", "file": "skills/aws-cli/SKILL.md", "version": "20260502033", - "checksum": "177cb3605d6477391ba141ff6aa1eea38531c73334e60948dc00fc2a04785310", + "checksum": "6f3da80eb03666101bedddaabe3774d997f1fc31d4786dfd9b0ac8b2c4064a6c", "checksum_algorithm": "sha256" }, { "name": "cicd", "file": "skills/cicd/SKILL.md", "version": "20260421006", - "checksum": "14e41222a89bb42ec06057be54d5faec679b1e692b45d63770deb4fece28f206", + "checksum": "bdb38ffafcff74f7d8b9efb65d4cd58f7c2c9a860566fdd3e4b33146b3ac8d7c", "checksum_algorithm": "sha256" }, { "name": "cloudformation", "file": "skills/cloudformation/SKILL.md", "version": "20260502032", - "checksum": "f232b22b00073b7a944534d0d6ce768f77e69a62043917c39ff231199fd9d703", + "checksum": "f8359146f2627e4722d89eab80c635c42612c4577a761702f7b18ca26db6b791", "checksum_algorithm": "sha256" }, { "name": "code-review", "file": "skills/code-review/SKILL.md", "version": "20260421007", - "checksum": "ab478e07d91f770b97308f19c6ce25529adafaca76838b1b1863b7bac7a53680", + "checksum": "abf20f1908d61edd0e2d7aabfc1e0d35267cafa8ba275e351f533164dd16245e", "checksum_algorithm": "sha256" }, { "name": "codeql", "file": "skills/codeql/SKILL.md", "version": "20260502026", - "checksum": "139876ae155333377891df2cb33254b1993208220c827382ae97f3eade479320", + "checksum": "0639f1f709c1e59695dd10fca77eff0d95330d9d87022c8cfc69bd536779d95a", "checksum_algorithm": "sha256" }, { "name": "concise", "file": "skills/concise/SKILL.md", "version": "20260421008", - "checksum": "8d7916afe8f95090c53b9d80e9c2b75ed33b12a033b5d29435ab40d622df4488", + "checksum": "7e92fd137f5e53a61c8cd0be7900e7213f8c2664eece7964d747e2894b17c412", "checksum_algorithm": "sha256" }, { "name": "consult", "file": "skills/consult/SKILL.md", "version": "20260421009", - "checksum": "3d45d5b65cb69dd7d92ae0867f36cdb8a94ff066dc2a9255a642591f055293e5", + "checksum": "2531a34f02e26444a8b3949665a2efaaa783ff54a7dcf5359ea289c093aea15a", "checksum_algorithm": "sha256" }, { "name": "container", "file": "skills/container/SKILL.md", "version": "20260421010", - "checksum": "62184fc600a92a01154a6981b9bf381ce728104df040c38a536e0d26d99f30da", + "checksum": "d9850dbc2cc08d449cb8e3d209bf43c161c635edb94853b5791df96cdc4f51d8", "checksum_algorithm": "sha256" }, { "name": "conventional-commit", "file": "skills/conventional-commit/SKILL.md", "version": "20260502024", - "checksum": "876c9b5b0a681a59371ab65e472bd83769cd9238b7ae114bfa45739390b6b613", + "checksum": "31b240e3754ec56617fe1e9a63caa02b79219e70a924cd35cb14787d7e0be547", + "checksum_algorithm": "sha256" + }, + { + "name": "copilot-ops", + "file": "skills/copilot-ops/SKILL.md", + "version": "20260513012", + "checksum": "b5727ee50c1b43b3a3cc03bf885e0ff8d04103792edb6d416615c0e387e6af4b", "checksum_algorithm": "sha256" }, { "name": "debug", "file": "skills/debug/SKILL.md", "version": "20260421011", - "checksum": "6143f82cc71307033de52ec46b212596cc941e113c9dede2e7a83667b3ce4141", + "checksum": "acad2aa9f450cd3acf50e463ae4638f2fa0d0f826dcf89a3fd250759454d1259", "checksum_algorithm": "sha256" }, { "name": "dependabot", "file": "skills/dependabot/SKILL.md", "version": "20260502027", - "checksum": "91481833dd17ef38cbfc86774d5dd166451ce2ac8761f54227c14f0cd1c4d94a", + "checksum": "e8bec8a5d869e36db5e903f772ec3ec7cf4c53425f52ff344d6a3e50cf87cd74", "checksum_algorithm": "sha256" }, { "name": "dependency", "file": "skills/dependency/SKILL.md", "version": "20260421012", - "checksum": "770b6e736b7a905fd5e520a871c21587c3fa7efba9b844fc6fb736a14fefdfe2", + "checksum": "63bb8ea35a4cd91a8f3ed3cf4a63538f319d0998b55fc192a65698c2882924dc", "checksum_algorithm": "sha256" }, { "name": "design", "file": "skills/design/SKILL.md", "version": "20260421013", - "checksum": "004f9da848d3a8dd57511e694b5f74de005fcce95d38bb6c3bbb756012fa56b0", + "checksum": "38f5b8e4ae06b1c7ac5d34f3b20a20674889d7e8f111efd8cc7cbfc93883805c", "checksum_algorithm": "sha256" }, { "name": "docs", "file": "skills/docs/SKILL.md", "version": "20260421014", - "checksum": "b84c7d06cae49c8e39e7e4db515224cedc9320058c9705dff4d31908c7f2fcb8", + "checksum": "5b47bd9609692a0460cfbdb6c3a9a2d8df47bf33346248feccbf4932b342f193", "checksum_algorithm": "sha256" }, { "name": "explore", "file": "skills/explore/SKILL.md", "version": "20260421015", - "checksum": "a9042886c58d3b0e17f78b9bfd1cc89116de89bf6ee2c011f041af69193640e2", + "checksum": "ed54640365f70c28170381b4e38858e3b1661fa36b4813a445d7903f5f6c571d", "checksum_algorithm": "sha256" }, { "name": "gdpr", "file": "skills/gdpr/SKILL.md", "version": "20260502029", - "checksum": "4652e7ed56b66f57ba0c0229db7925d814a7a422bb7a582b376d57fff23895b4", + "checksum": "fc447778cf54e5cc2e9db9fa0afd148befd219bf75eca1b5cb52f5fab6a6b326", "checksum_algorithm": "sha256" }, { "name": "gh-issues", "file": "skills/gh-issues/SKILL.md", "version": "20260502025", - "checksum": "5efb56c6fb5dab6465d1dacdbb3d2101a954bac2493cd21a5b88ea64a82332d0", + "checksum": "c23fe777594698b8966cee3f441eb9b83be03fe44113ed1fe8cf7f7a39437f21", "checksum_algorithm": "sha256" }, { "name": "gh-release", "file": "skills/gh-release/SKILL.md", "version": "20260502023", - "checksum": "7f07970df0dc6f3f09d1322c29e53e2f4e30db2abdfacc04552a1bf3b5bdfe8d", + "checksum": "104748dbc32a0d71dc10b823aaf29cf40e5f043ebaf48e8d0ec05770c405a72f", "checksum_algorithm": "sha256" }, { "name": "guardrails", "file": "skills/guardrails/SKILL.md", "version": "20260421016", - "checksum": "ad26f458c7155ab4e540f4aa4bc49915c152adde51e762de9b87fad65d0da33b", + "checksum": "a68bd4ace0d0b8336259c38f7a9040a07f9d5c128e8341ed11de009e272772b7", "checksum_algorithm": "sha256" }, { "name": "helm", "file": "skills/helm/SKILL.md", "version": "20260502037", - "checksum": "d24368f0780d8e87e6183a47adab7ffab6b662b23582e4154f1e21703bb6841e", + "checksum": "f381e65e7f3d2f6d48e0f8bc4b60a43976007dd483df35fc6f47c0a4cc36f574", "checksum_algorithm": "sha256" }, { "name": "incident", "file": "skills/incident/SKILL.md", "version": "20260503002", - "checksum": "22848f73937ca593d26297d6d7d6be5cba89a25e05d4c845d79e1eb504086d21", + "checksum": "3f2f2d25d854f875fecacf0c37f011a371d9562c74fcdec33a2f6e2205280b86", "checksum_algorithm": "sha256" }, { "name": "inspect", "file": "skills/inspect/SKILL.md", "version": "20260421018", - "checksum": "ab252c148126029890475f7d7a6b39dea885590096a0fc973a240e273694d02b", + "checksum": "326118838d0765a986303f156be13a1ed911d93bb8b98031213ad60df072341c", "checksum_algorithm": "sha256" }, { "name": "k8s", "file": "skills/k8s/SKILL.md", "version": "20260502036", - "checksum": "4054d20ebe77f5e0e8ad269f3270f799caa4a92fc918ebb01c954ed816a73d47", + "checksum": "24c8b619676fa5dbdd9d37d303d0daa1749f9634b25d9a50f40145af92f58e1f", "checksum_algorithm": "sha256" }, { "name": "migrate", "file": "skills/migrate/SKILL.md", "version": "20260421019", - "checksum": "c2966f6b626ab20aa3919a22c2aee5a677afbd0e0b9b35278367b49f1369026e", + "checksum": "490cf5b4bd2c24c02f3582474342770de3185e127a1b5cabb1d11ac2e1368471", "checksum_algorithm": "sha256" }, { "name": "onboard", "file": "skills/onboard/SKILL.md", "version": "20260421020", - "checksum": "e58531d74a54654dd1098cce9f9bad4220c3918b9330cfe8c903d026625c40bf", + "checksum": "821ef6e1d5b966bc766e93de2d0fac5b07beff6b613df96edaddaa837cfcf2fa", "checksum_algorithm": "sha256" }, { "name": "openapi", "file": "skills/openapi/SKILL.md", "version": "20260421021", - "checksum": "00dd6560b086bfa8506adad89b9fade5c3774b4bb4fbcb3082c2c74db820dc98", + "checksum": "335ec305224de282ffebdac4ba7de85c39642ac24e5d5094ea75f3f65ec52458", "checksum_algorithm": "sha256" }, { "name": "performance", "file": "skills/performance/SKILL.md", "version": "20260421022", - "checksum": "f83f4da23e96849a9407020a00ab10b159724c3c38a5a503ed1702211b2f19d0", + "checksum": "b5dfd2444ed586d9b28072fed3a9c68083967138292d278947dd3de1bbdacbac", "checksum_algorithm": "sha256" }, { "name": "postmortem", "file": "skills/postmortem/SKILL.md", "version": "20260503001", - "checksum": "3cf943671659e19ac726987e49883cc1a934713b5c17454c87aaa23c755d4194", + "checksum": "47db46e73a7d89e5f88ecf590935443ec72ce3a2c8e617c745c64f976f4b3de8", "checksum_algorithm": "sha256" }, { "name": "pr", "file": "skills/pr/SKILL.md", "version": "20260502013", - "checksum": "13cd06ee9621eec04e14cc7ec6e42e92c4bc309f9a7ede271e42c4d1f1379733", + "checksum": "ddb2623a4a2f1b78928adc2c0b91d73bcde7e75f66971ab96e4e7a147029ce4b", "checksum_algorithm": "sha256" }, { "name": "rancher", "file": "skills/rancher/SKILL.md", "version": "20260502038", - "checksum": "9d1ede54f900248f2d7f29433960c929cc0e194f40fb463e8abdab4aa4612b1c", + "checksum": "78c74f5f34fbf8645cf1c8f12fd2e5dd4ecc4c7ca6538a175c5b374c8f283685", "checksum_algorithm": "sha256" }, { "name": "rca", "file": "skills/rca/SKILL.md", "version": "20260503001", - "checksum": "4a24b71f315a39721c5e8d9355bd6397db2cb38b4b1804b37c4f948d9fe5655b", + "checksum": "1b0e0b50fd1f0ffae4dc0f1efb2e7b11902e0cc82abc0db26eb4af3c33046e39", "checksum_algorithm": "sha256" }, { "name": "refactor", "file": "skills/refactor/SKILL.md", "version": "20260421023", - "checksum": "0185a7be9ad06bab25fa5a249b490d1a4a8144c93fc6ae09bd596259c81f657f", + "checksum": "49e8ca19efb904cc8a76b6dc0d6ae0e8d98c09ec955aa7801329fd0c31953906", "checksum_algorithm": "sha256" }, { "name": "release-notes", "file": "skills/release-notes/SKILL.md", "version": "20260502014", - "checksum": "071535ca88414f7f295d11d3d156bea31327aaa97499dcad74d411a2059a20d6", + "checksum": "c577aab7c8fbfb7c36f200df915f38740bd80801b4ffa99f295ce45e46630dfe", "checksum_algorithm": "sha256" }, { "name": "requirements", "file": "skills/requirements/SKILL.md", "version": "20260421024", - "checksum": "d0ca7be9b2d14ef25b18b4e6363f8168abc646671e765686b544c870ad7fac68", + "checksum": "3abbb55f2f40116f49a2afdd44998569798f4a31a7d2f7a17321e919577862ff", "checksum_algorithm": "sha256" }, { "name": "secret-scan", "file": "skills/secret-scan/SKILL.md", "version": "20260502028", - "checksum": "78776b0284d4135598107f7e3c0514abb5574a0e8a2925d637cfab1105a8e04e", + "checksum": "22e8c718ec7816a8ff5d7d23ab4a9b162078cca013a1a7dcb08ed70d44986a84", "checksum_algorithm": "sha256" }, { "name": "security", "file": "skills/security/SKILL.md", "version": "20260421025", - "checksum": "f053730a9dae9024cd2562a576a58691c452845bc9fa7aa5f09491ceda463ff8", + "checksum": "6d9932e00f748c279fa3d5797c10ab72f464417689239e3ab754d19635db56f8", + "checksum_algorithm": "sha256" + }, + { + "name": "space-setup", + "file": "skills/space-setup/SKILL.md", + "version": "20260513011", + "checksum": "b51fa34b06f0e525a676d7710afc3141651c264a5066b202d27024223a2f9116", "checksum_algorithm": "sha256" }, { "name": "terraform", "file": "skills/terraform/SKILL.md", "version": "20260502030", - "checksum": "2951cc833ce8d6a3bb0cbc06d1428d12671a0a9144fb2ad35af9d848ebb3947a", + "checksum": "9c444d7820abcab11b5a9f1852d48ee0a8849b6ae84fc3b55fefcd0be01afb45", "checksum_algorithm": "sha256" }, { "name": "terragrunt", "file": "skills/terragrunt/SKILL.md", "version": "20260502031", - "checksum": "acdd65b959bc69554f84f4174c061ed687c74001583a6820359313b554bff5cf", + "checksum": "c554b16835ecdb6a25f255d7259cfd8743b4b62229d2046b32f086be3c62f137", "checksum_algorithm": "sha256" }, { "name": "threat-model", "file": "skills/threat-model/SKILL.md", "version": "20260502021", - "checksum": "2896a157595ca01ee63fdff007b675229851c5b6d10ee7f6232d6e9c4fdbd2c3", + "checksum": "0f38e80568f3bdf79ad494c5a1a1df42e3f9a6266067e59912f7c3f94cfa6d3b", "checksum_algorithm": "sha256" }, { "name": "verify", "file": "skills/verify/SKILL.md", "version": "20260421026", - "checksum": "351d314924337a41d80e16fdc5f6958be070873f756d9c9d2ab845aabc0aa14f", + "checksum": "022a46d89e7f0649ebe48b83c8e45024d6b8dc5e98690b0e7f537c7bcd83b454", "checksum_algorithm": "sha256" }, { "name": "vision", "file": "skills/vision/SKILL.md", "version": "20260421027", - "checksum": "a3aaeb6e8b900e5fe62f118fdbd94a68aee894c82872e0e855788efb05f5e6bf", + "checksum": "0eba784c38eaa7923e3e74973c26aba96f50de1540a42e2e9f448ab3b7be26ac", "checksum_algorithm": "sha256" } ], @@ -318,94 +332,50 @@ { "name": "architect", "file": "agents/architect.agent.md", - "version": "20260503022", - "checksum": "50bd0a014734ce1b687897918e193587274902d6a3b3c721f71428b881df5568", + "version": "20260514001", + "checksum": "f5825dcefab201323fd02cc24c4116ea3ff4e7c296def9fc145d26d1dbfa772c", "checksum_algorithm": "sha256" }, { "name": "designer", "file": "agents/designer.agent.md", - "version": "20260503024", - "checksum": "820a907a8aa2f28b0a6fc82c4fcf32b8208b8365d4c05884682de9ebf5c240c6", + "version": "20260514001", + "checksum": "b7287f9e6f4b5b827cecf1046f3c5a9040e5c39670ac92e33160846536bf3f9a", "checksum_algorithm": "sha256" }, { "name": "engineer", "file": "agents/engineer.agent.md", - "version": "20260503024", - "checksum": "580c52364faf44be4ff03a537adb8d67ba3b0c4a75858e10bc9978f006c5466f", + "version": "20260514001", + "checksum": "61a849498e17ba97e7745e473e88ed87ce9ba8092fa52f8a1b4e83e3c52fe3c2", "checksum_algorithm": "sha256" }, { "name": "planner", "file": "agents/planner.agent.md", - "version": "20260510001", - "checksum": "5ac8d17ab4d45ed1fdac092c0ddc268e2a9e6018fe7da7c9395bbf39a271c056", + "version": "20260514001", + "checksum": "9e781e704a26ed3452a39d806f2707e2504573b19836470c1d990aa228dbf8c4", "checksum_algorithm": "sha256" }, { "name": "product", "file": "agents/product.agent.md", - "version": "20260503021", - "checksum": "f2b6daf2408b2849d25d1da272228f91e5594068f0644b34847a9426ada554f8", + "version": "20260514001", + "checksum": "dca487d907f07564402e5ee438c53f798333f036a1cffce64dc4d3e606374347", "checksum_algorithm": "sha256" }, { "name": "release", "file": "agents/release.agent.md", - "version": "20260503020", - "checksum": "91c5532268cdb68c5e09567a2a2a9a2c996f89fcd0ba00685000a04c49db40ce", + "version": "20260514001", + "checksum": "7facd81e618c2dde4269d34d974330b080d9a72b9ecdcb4d9b5212fbebfefba1", "checksum_algorithm": "sha256" }, { "name": "tester", "file": "agents/tester.agent.md", - "version": "20260503026", - "checksum": "ae2bad718957221b1193575b456b23234928a787eb5a31daf77cb33fb630edf2", - "checksum_algorithm": "sha256" - } - ], - "hooks": [ - { - "name": "log-retention-cleanup", - "file": "hooks/log-retention-cleanup.json", - "version": "3.1.1.post2.dev0+4d3419b", - "checksum": "76f2ecb32c1ed259ee155a2e3f3332a06dffdb3d25514b62b3e81e9c2a1b9cad", - "checksum_algorithm": "sha256" - }, - { - "name": "post-commit-security-scan", - "file": "hooks/post-commit-security-scan.json", - "version": "3.1.1.post2.dev0+4d3419b", - "checksum": "4f83d6421d5b46b8f8ac180d241433f9f52177b00911668ab7275f0b5f22cb94", - "checksum_algorithm": "sha256" - }, - { - "name": "post-edit-format", - "file": "hooks/post-edit-format.json", - "version": "3.1.1.post2.dev0+4d3419b", - "checksum": "65adaf057b03d92c4b80a52e66c281e4af814392ff7a1bf1ce3ad2d446e9da81", - "checksum_algorithm": "sha256" - }, - { - "name": "post-edit-markdown-quality", - "file": "hooks/post-edit-markdown-quality.json", - "version": "3.1.1.post2.dev0+4d3419b", - "checksum": "fab9f3326adfdcab5e98ab32a9d351108f422f027b6bd310c3d7eef4b084362b", - "checksum_algorithm": "sha256" - }, - { - "name": "pre-tool-safety-gate", - "file": "hooks/pre-tool-safety-gate.json", - "version": "3.1.1.post2.dev0+4d3419b", - "checksum": "1ebc5b9044c7e4553f3eed9df344092f8961725424ed45d5411c298c952fd321", - "checksum_algorithm": "sha256" - }, - { - "name": "session-audit", - "file": "hooks/session-audit.json", - "version": "3.1.1.post2.dev0+4d3419b", - "checksum": "4f529b63b6dc323f3aceb7d8a8515e95c61273356c650057c9d2d60ec17dbcec", + "version": "20260514001", + "checksum": "94afee30b6e36d163db40fe60df17887a88e925114ead51788ba699b13f894f6", "checksum_algorithm": "sha256" } ], @@ -414,84 +384,84 @@ "name": "git", "file": "instructions/git.instructions.md", "version": "20260421001", - "checksum": "2dc5aec79789109f7f0e188aaae1f769aea32c1cb968d8d961b3d8a116bf0dc6", + "checksum": "50292cc92514aabafc923c2d5602dc3798b2cc53c64d3a1f3670eeb3e653774a", "checksum_algorithm": "sha256" }, { "name": "helm", "file": "instructions/helm.instructions.md", "version": "20260502040", - "checksum": "21d70e900c1bece55d4eb03330715fc422c3a53bfcf18a92bacbeebbc6ef1acd", + "checksum": "5a790c2db1233c812f9f0489695007ed5279613679351b6d366733fb455673d1", "checksum_algorithm": "sha256" }, { "name": "java", "file": "instructions/java.instructions.md", "version": "20260502001", - "checksum": "5dded5e2cdaa9f49c64dc6c73a738ed2ba7ab1e4abdb766efbac6041a122f7d2", + "checksum": "62128ca849d5801303f08aa510fb5dffc828280dfa62afacfbdacd4cb1a79c95", "checksum_algorithm": "sha256" }, { "name": "k8s", "file": "instructions/k8s.instructions.md", "version": "20260502039", - "checksum": "c88684119cb50cff4d7e4212dce99eacd6999ca0f274cf8b4465b871c03cefed", + "checksum": "0cf9d21e7b5d8f1926fac733fdbfd57962e68fa261fabf069284094b36f1bca4", "checksum_algorithm": "sha256" }, { "name": "markdown", "file": "instructions/markdown.instructions.md", "version": "20260502002", - "checksum": "aa7705c69a42101ccb17f2250454d2d81a9302539d5b35a32dc45e449df42337", + "checksum": "29549348a1decc726120556759f4143969b76f09beae45e282f8f1de10525b5f", "checksum_algorithm": "sha256" }, { "name": "python", "file": "instructions/python.instructions.md", "version": "20260421002", - "checksum": "0b0294a49923cfccdadc3cc19ec8e4942569f45a5a70f26c0560cfdf7ef42698", + "checksum": "2048623b7be0ac88a7fd23506393e622623cd3adc08d3381b78f8b56246278f6", "checksum_algorithm": "sha256" }, { "name": "rancher", "file": "instructions/rancher.instructions.md", "version": "20260502041", - "checksum": "5515a9ddc993b31bb036da3c1c6d425db2000cd6b92c43ab6691cc91d314a64f", + "checksum": "ab02a63cb6608cbe401059abc7ddc53edf7babc81473beb6b79047fb630f827a", "checksum_algorithm": "sha256" }, { "name": "security", "file": "instructions/security.instructions.md", "version": "20260502003", - "checksum": "da1e5363a4ef104bdac48bd117b3f391e1039bbaf711c2225b9d5628cd7c2c71", + "checksum": "26e6a131d5fa4c5d18aa6387298bb6cac0671952d7f755f3d54c20543f522164", "checksum_algorithm": "sha256" }, { "name": "terraform", "file": "instructions/terraform.instructions.md", "version": "20260502034", - "checksum": "2a89d46487d316870c5ffb08a381e714c07d04651140ac84261b587ae14a036f", + "checksum": "1f289acb0fb7f5e40d5fa12b9089e61d66acb16b3102e80eb5a869b6ed8b1be0", "checksum_algorithm": "sha256" }, { "name": "terragrunt", "file": "instructions/terragrunt.instructions.md", "version": "20260502035", - "checksum": "4c778e2e6d003cd5b9e13023fcc19df7a251668415dbc2076990225da4a02660", + "checksum": "29d1fe7d0eb9acad2ccdbb08a64f6a27638302a95644b4889e1dcf27abfcb6ca", "checksum_algorithm": "sha256" }, { "name": "testing", "file": "instructions/testing.instructions.md", "version": "20260502004", - "checksum": "8a25627464ab7f5086b3600050d4977d39a4aecd4247c6e4e48ea1dd0f069441", + "checksum": "d9938a75a75a82b306fe70795493afdef6342636821a2fc3a5e583ecab119466", "checksum_algorithm": "sha256" }, { "name": "typescript", "file": "instructions/typescript.instructions.md", "version": "20260502005", - "checksum": "96b749496ac328ccb66786aef21ac0b585a46ddb80196aa2d9575e328a811c97", + "checksum": "941a2aa7fd520db9d7b0b30af5dde179135b12c3a9cba059de498a4874708264", "checksum_algorithm": "sha256" } ], @@ -500,49 +470,163 @@ "name": "api-design-review", "file": "prompts/api-design-review.prompt.md", "version": "20260502006", - "checksum": "f616ae468527ce32afe1be951fb186ee95ae52450a7ab4fcca556c4e1f467eed", + "checksum": "6144af4f462af890edf3a4e342feb05792fdf2ab678ff2187048e0f17a288a22", "checksum_algorithm": "sha256" }, { "name": "architecture-risk", "file": "prompts/architecture-risk.prompt.md", "version": "20260502007", - "checksum": "6e47a741bcf48a7f4c645b827dbe8ec3356c4d78ecafc284b9f1c51a832fdea1", + "checksum": "895bb3e17af65b472dfb010dfa956954dce26b5213c8fee60b846fa1581b1d36", + "checksum_algorithm": "sha256" + }, + { + "name": "artifact-integrity", + "file": "prompts/artifact-integrity.prompt.md", + "version": "20260513002", + "checksum": "a51ff1fb889996bf1c0256d304b647f9670413cea4506de951f722deaf65fbf1", + "checksum_algorithm": "sha256" + }, + { + "name": "ci-triage", + "file": "prompts/ci-triage.prompt.md", + "version": "20260513006", + "checksum": "7f0d5e006fac21b32dfebf4c00b5dfb918205a86816feaa6466375d4cc939ed4", "checksum_algorithm": "sha256" }, { "name": "code-review", "file": "prompts/code-review.prompt.md", "version": "20260502008", - "checksum": "b3a537fb098588a2920929954820e09a0830104e89131eda479fe1c82c49d824", + "checksum": "ad3d379f2305e693717f0a24decb6549fd82e30c021754d78a52b32e0f5e0b9e", "checksum_algorithm": "sha256" }, { "name": "dependency-audit", "file": "prompts/dependency-audit.prompt.md", "version": "20260502009", - "checksum": "86f5c563746148be8a2c0be4d008af3dac22c6450c8b05954c3ef198d1e943a9", + "checksum": "4d861e7b4c1fd55af72a0a260d9ce81487d7ab53bfa6bdb041de0c196e4e09a9", "checksum_algorithm": "sha256" }, { "name": "incident-timeline", "file": "prompts/incident-timeline.prompt.md", "version": "20260502010", - "checksum": "2bb3473cd31cb38022307a0f3c44d48b381f06549cf6b6b40ff78b907e347dee", + "checksum": "641bb8380390974ce7ce6753e30b748e6bd224175a6451b6a1077e4ab50891e0", + "checksum_algorithm": "sha256" + }, + { + "name": "migration-plan", + "file": "prompts/migration-plan.prompt.md", + "version": "20260513010", + "checksum": "27e71a0c33c1067e58fa6d0fd1dd73711cdb3b24ced0cca8fee2f9c93a056219", "checksum_algorithm": "sha256" }, { "name": "migration-safety", "file": "prompts/migration-safety.prompt.md", "version": "20260502011", - "checksum": "46544d76acbcca10e82591c97589a816b479367bbad8ec89e8b7a52c34ff292c", + "checksum": "7dce672410844c8f84f29f2b50b1e046aaaa9d2cf5512bfb6b34140ea3c54619", + "checksum_algorithm": "sha256" + }, + { + "name": "ops-readiness", + "file": "prompts/ops-readiness.prompt.md", + "version": "20260513009", + "checksum": "f900b61282f2fee3c55f2dbb09f9fd3b30c73cc61b4433152bf144625e48cb19", "checksum_algorithm": "sha256" }, { - "name": "release-readiness", - "file": "prompts/release-readiness.prompt.md", - "version": "20260502012", - "checksum": "f036668d9b73f709cb069f39f27b99c9a332aa6fed3e7ebf9731d56e92afbb14", + "name": "release-check", + "file": "prompts/release-check.prompt.md", + "version": "20260513004", + "checksum": "a86cfabdd3a578bb1a862d9d3f58da7b98b37ce17f99f43767192eb2c43a5dd5", + "checksum_algorithm": "sha256" + }, + { + "name": "repo-assessment", + "file": "prompts/repo-assessment.prompt.md", + "version": "20260513001", + "checksum": "3f6c684e633576ff96dae2dfc2f482f80494b590645cd313a207c59cbc54bc11", + "checksum_algorithm": "sha256" + }, + { + "name": "template-impact", + "file": "prompts/template-impact.prompt.md", + "version": "20260513005", + "checksum": "ce5afc1b4b9664530a897f5f432148c93cda6ef4ffceaa750df70012c1f5cded", + "checksum_algorithm": "sha256" + }, + { + "name": "test-gaps", + "file": "prompts/test-gaps.prompt.md", + "version": "20260513007", + "checksum": "a3e7cfcbe5c0725ddaaa9739d1b06554bd2bbdd2350ed42df893b17f4e0b6a36", + "checksum_algorithm": "sha256" + }, + { + "name": "upgrade-plan", + "file": "prompts/upgrade-plan.prompt.md", + "version": "20260513008", + "checksum": "0fdebb072412f09f090f57c921ead6324e531948f2eca686b146445afd25867b", + "checksum_algorithm": "sha256" + }, + { + "name": "workflow-check", + "file": "prompts/workflow-check.prompt.md", + "version": "20260513003", + "checksum": "e4c5ad55fa1f494c45b24e5cea49e798a3d57055992755f4c03187a177a65f15", + "checksum_algorithm": "sha256" + } + ], + "hooks": [ + { + "name": "agent-call-audit", + "file": "hooks/agent-call-audit.json", + "version": "3.3.0", + "checksum": "71552673112f56307087074e30563c726331fddb0f6b21256ac3ac083e6485ae", + "checksum_algorithm": "sha256" + }, + { + "name": "log-retention-cleanup", + "file": "hooks/log-retention-cleanup.json", + "version": "3.3.0", + "checksum": "76f2ecb32c1ed259ee155a2e3f3332a06dffdb3d25514b62b3e81e9c2a1b9cad", + "checksum_algorithm": "sha256" + }, + { + "name": "post-commit-security-scan", + "file": "hooks/post-commit-security-scan.json", + "version": "3.3.0", + "checksum": "56d0238d2a3ae556c62c1aa75fbd75449f1037fae70e85fdc4c88d87d262d183", + "checksum_algorithm": "sha256" + }, + { + "name": "post-edit-format", + "file": "hooks/post-edit-format.json", + "version": "3.3.0", + "checksum": "26fc1244e2e5d549c3327e1993ccad078c6f6522239979f0e6b010f1edf9b111", + "checksum_algorithm": "sha256" + }, + { + "name": "post-edit-markdown-quality", + "file": "hooks/post-edit-markdown-quality.json", + "version": "3.3.0", + "checksum": "ee0458ddcfca3d38714cf50c9b41741ef89ac3f99b3e4736ad449e5a4c07d972", + "checksum_algorithm": "sha256" + }, + { + "name": "pre-tool-safety-gate", + "file": "hooks/pre-tool-safety-gate.json", + "version": "3.3.0", + "checksum": "d6de87491ae43cfa1a99b7fe8293f4f18e0568125f9fcd37a2c6a58fba6c84b7", + "checksum_algorithm": "sha256" + }, + { + "name": "session-audit", + "file": "hooks/session-audit.json", + "version": "3.3.0", + "checksum": "df02c7bb679f6c2512171c51f266c2d9fed26908fe249a9f2912c44fbd7e8f44", "checksum_algorithm": "sha256" } ] diff --git a/CHANGELOG.md b/CHANGELOG.md index 683af4e7..01cf214d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,37 +4,33 @@ ## [3.3.0](https://github.com/eschaar/vstack/compare/3.2.0...3.3.0) (2026-05-12) - ### Features -* **agents:** add parallel delegation policy to all role prompts ([8aaf8c7](https://github.com/eschaar/vstack/commit/8aaf8c7a586aca572f914d26209c4d2bf6321366)) -* **config:** add depends_on to default workflow stages ([e5b7cf1](https://github.com/eschaar/vstack/commit/e5b7cf1c8d0c2301b3aafdbcea2260d942a590ae)) -* **planner:** add depends_on DAG execution model ([bedae77](https://github.com/eschaar/vstack/commit/bedae7752e8b4220dbbaf9b79ddf4c1661c9fcab)) -* **workflow:** add DAG dependency support ([ccfcb35](https://github.com/eschaar/vstack/commit/ccfcb354e66253aaa9455618d20b4c232851fe51)) -* **workflow:** validate stage graph and detect cycles ([165dab4](https://github.com/eschaar/vstack/commit/165dab4f2eeb8595b79eadc41cc1f443a2f929ff)) - +- **agents:** add parallel delegation policy to all role prompts ([8aaf8c7](https://github.com/eschaar/vstack/commit/8aaf8c7a586aca572f914d26209c4d2bf6321366)) +- **config:** add depends_on to default workflow stages ([e5b7cf1](https://github.com/eschaar/vstack/commit/e5b7cf1c8d0c2301b3aafdbcea2260d942a590ae)) +- **planner:** add depends_on DAG execution model ([bedae77](https://github.com/eschaar/vstack/commit/bedae7752e8b4220dbbaf9b79ddf4c1661c9fcab)) +- **workflow:** add DAG dependency support ([ccfcb35](https://github.com/eschaar/vstack/commit/ccfcb354e66253aaa9455618d20b4c232851fe51)) +- **workflow:** validate stage graph and detect cycles ([165dab4](https://github.com/eschaar/vstack/commit/165dab4f2eeb8595b79eadc41cc1f443a2f929ff)) ### Fixes -* **cli:** reject malformed workflow depends_on ([3b23981](https://github.com/eschaar/vstack/commit/3b239812c3074669c2a3d641e70952d35a470aea)) -* **cli:** tighten workflow dependency validation ([9aec187](https://github.com/eschaar/vstack/commit/9aec187af621a55e50c4cbb62c56b2d16f97536f)) -* **version:** add nearest-tag fallback for dev checkouts ([5971ad0](https://github.com/eschaar/vstack/commit/5971ad0600f26326d235745766582a49c3c81d9e)) - +- **cli:** reject malformed workflow depends_on ([3b23981](https://github.com/eschaar/vstack/commit/3b239812c3074669c2a3d641e70952d35a470aea)) +- **cli:** tighten workflow dependency validation ([9aec187](https://github.com/eschaar/vstack/commit/9aec187af621a55e50c4cbb62c56b2d16f97536f)) +- **version:** add nearest-tag fallback for dev checkouts ([5971ad0](https://github.com/eschaar/vstack/commit/5971ad0600f26326d235745766582a49c3c81d9e)) ### Documentation -* add depends_on DAG section and planner entry-point guidance ([25c37d8](https://github.com/eschaar/vstack/commit/25c37d890aa68cdc5518768a0c07af33e4f81b9d)) -* **maintenance:** normalize markdown list formatting ([1d0b5e3](https://github.com/eschaar/vstack/commit/1d0b5e3f55c0400518080fe79f85d6e9c7225dbd)) -* **product:** align roadmap and workflow docs ([d4346cd](https://github.com/eschaar/vstack/commit/d4346cd2cc84629dcd30442897f778ef43f85cc8)) -* **readme:** add agentic runbook and parallel troubleshooting ([d51a1d3](https://github.com/eschaar/vstack/commit/d51a1d32ac8aeb9c430db85716980a17e35facaf)) -* **roadmap:** defer multi-IDE support behind VS Code stability ([1fd26a3](https://github.com/eschaar/vstack/commit/1fd26a304365cc38394e9bf1520c4c69ac6b1c8b)) -* **roadmap:** reorder roadmap chronologically by release tag ([1236d38](https://github.com/eschaar/vstack/commit/1236d385d4b83bfbbe542b9e37c1d44331b55fbd)) -* **workflow:** document DAG and multi-agentic model ([4c3e947](https://github.com/eschaar/vstack/commit/4c3e947280f57432ba248fce84c8e0e595d7f244)) - +- add depends_on DAG section and planner entry-point guidance ([25c37d8](https://github.com/eschaar/vstack/commit/25c37d890aa68cdc5518768a0c07af33e4f81b9d)) +- **maintenance:** normalize markdown list formatting ([1d0b5e3](https://github.com/eschaar/vstack/commit/1d0b5e3f55c0400518080fe79f85d6e9c7225dbd)) +- **product:** align roadmap and workflow docs ([d4346cd](https://github.com/eschaar/vstack/commit/d4346cd2cc84629dcd30442897f778ef43f85cc8)) +- **readme:** add agentic runbook and parallel troubleshooting ([d51a1d3](https://github.com/eschaar/vstack/commit/d51a1d32ac8aeb9c430db85716980a17e35facaf)) +- **roadmap:** defer multi-IDE support behind VS Code stability ([1fd26a3](https://github.com/eschaar/vstack/commit/1fd26a304365cc38394e9bf1520c4c69ac6b1c8b)) +- **roadmap:** reorder roadmap chronologically by release tag ([1236d38](https://github.com/eschaar/vstack/commit/1236d385d4b83bfbbe542b9e37c1d44331b55fbd)) +- **workflow:** document DAG and multi-agentic model ([4c3e947](https://github.com/eschaar/vstack/commit/4c3e947280f57432ba248fce84c8e0e595d7f244)) ### Maintenance -* **artifacts:** refresh generated metadata for vstack 3.2.0 ([d728977](https://github.com/eschaar/vstack/commit/d7289771d8753be4c9c673e04aa8a94bd4f441cf)) +- **artifacts:** refresh generated metadata for vstack 3.2.0 ([d728977](https://github.com/eschaar/vstack/commit/d7289771d8753be4c9c673e04aa8a94bd4f441cf)) ## [3.2.0](https://github.com/eschaar/vstack/compare/3.1.1...3.2.0) (2026-05-12) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5ee85a77..38f59e9c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -46,11 +46,36 @@ make check | `make typecheck` | Type-check with mypy | | `make format` | Auto-format Python and Markdown | +### Layered verify profiles + +- Use `make verify-fast` during active development for deterministic checks: `format-check`, `lint`, `typecheck`, and `test-local`. +- Use `make verify-full` before opening or updating a PR to run the full local gate: `check` plus `vstack-validate`. +- CI `Verify` also includes a generated artifact drift guard. It runs `vstack install` and fails if `.github/` generated outputs are out of sync with committed files. + +### Change-to-check matrix + +Use this table to find the minimum set of checks to run after each type of change. +Run `make check` before opening a PR when your change touches multiple areas. + +| Change type | Required checks | +| ----------------------------------------------------------------------- | ---------------------------------------------------------------------- | +| `src/vstack/_templates/agents/`, `skills/`, `instructions/`, `prompts/` | `python3 -m vstack install`, `make test-local`, `make markdown-format` | +| `src/vstack/_templates/hooks/` | `python3 -m vstack install`, `make test-local` | +| `docs/**/*.md` | `make markdown-format` | +| `src/**/*.py` or `tests/**/*.py` | `make test-local` (run `make check` before merge) | +| CLI behavior, manifest logic, or workflow contract | `make check` | +| `pyproject.toml`, `Makefile`, or CI workflow files | `make check` | + ### Coverage requirement Test coverage is enforced at **100%** (`--cov-fail-under=100`). Every behavioral change must be accompanied by tests that keep all checks green. +### Pytest temp files + +- Pytest temporary files are standardized under `.pytest-tmp` (configured via `--basetemp` in `pyproject.toml`). +- Do not rely on the legacy `.vstack/tmp/test` location in scripts or local tooling. + ## Commit Message Guidance This repository uses a Conventional Commits baseline for release automation. diff --git a/Makefile b/Makefile index 4fd50a2e..052d8cd9 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ DEV_PACKAGES := pip setuptools wheel pytest pytest-cov ruff mypy pre-commit tox .PHONY: help help-all preflight bootstrap venv install build clean clean-deep format format-check lint lint-fix \ deps pre-commit-install setup all markdown-format markdown-format-check markdown-lint \ - typecheck test test-local check ci tox tox-all vstack-validate vstack-install vstack-install-global vstack-verify + typecheck test test-local test-fixtures check verify-fast verify-full ci tox tox-all vstack-validate vstack-install vstack-install-global vstack-verify preflight: @command -v $(POETRY) >/dev/null 2>&1 || { echo "Poetry not found. Install from https://python-poetry.org/docs/"; exit 1; } @@ -32,7 +32,10 @@ help: @echo " make lint - lint Python" @echo " make test - run tests on Python 3.11-3.14 via tox" @echo " make test-local - run pytest on current interpreter only" + @echo " make test-fixtures - run fixture-only generator tests without coverage" @echo " make check - full local quality gate" + @echo " make verify-fast - fast deterministic checks: format-check + lint + typecheck + test-local" + @echo " make verify-full - full verification profile: check + vstack-validate" @echo " make ci - check + vstack template validation" @echo " make tox - run pytest across Python 3.11-3.14 (if interpreters are available)" @echo " make clean - remove build/cache artifacts" @@ -61,7 +64,10 @@ help-all: @echo " make typecheck - run mypy on src/ and tests/" @echo " make test - run pytest matrix via tox (py311, py312, py313, py314)" @echo " make test-local - run pytest on current interpreter only" + @echo " make test-fixtures - run fixture-only generator tests without coverage" @echo " make check - run format-check + markdown checks + lint + typecheck + test" + @echo " make verify-fast - run format-check + lint + typecheck + test-local" + @echo " make verify-full - run check + vstack-validate" @echo " make ci - run check + vstack-validate" @echo " make tox - run tox pytest matrix (py311, py312, py313, py314)" @echo " make tox-all - run all tox envs (tests + lint + type)" @@ -125,11 +131,18 @@ typecheck: test-local: $(VENV_PYTHON) -m pytest -q +test-fixtures: + $(VENV_PYTHON) -m pytest -q tests/vstack/artifacts/test_generator.py -k "golden_fixture or defect_fixture" --no-cov + test: $(TOX_PYTHON) -m tox -e py311,py312,py313,py314 check: format-check markdown-lint lint typecheck test +verify-fast: format-check lint typecheck test-local + +verify-full: check vstack-validate + ci: check vstack-validate tox: diff --git a/README-pypi.md b/README-pypi.md index 4623256c..ccdfc0b2 100644 --- a/README-pypi.md +++ b/README-pypi.md @@ -16,324 +16,57 @@ vstack installs structured agents, skills, instructions, and prompts into `.gith It provides six delivery roles for end-to-end software work: `product`, `architect`, `designer`, `engineer`, `tester`, and `release`, coordinated by `planner`. -## Best for - -- Backend and API teams using GitHub Copilot Agent Mode in VS Code -- Repositories that want consistent planning, implementation, verification, and release flow -- Teams that want reusable AI workflows instead of one-off prompt crafting - -## What you get - -- Fixed role model: six delivery roles plus a planner coordinator -- Template-driven install model from `src/vstack/_templates/` -- Backend-first verification, security, and release discipline -- One runtime dependency: PyYAML - -## Building blocks - -| Artifact type | Purpose | Typical invocation | -| ------------- | ---------------------------------------------------------- | ---------------------- | -| Agents | Main operating interface for role-based work | `@product`, `@tester` | -| Skills | Reusable task procedures | `/verify`, `/security` | -| Instructions | Baseline policy and repository guardrails | auto-loaded by context | -| Prompts | Reusable prompt artifacts where direct prompting is useful | explicit prompt use | - -## Prompt catalog - -Prompts are `.prompt.md` files installed to `.github/prompts/`. Invoke them via the -VS Code command palette (`Chat: Run Prompt File`) or the Copilot Chat attach button. - -| Prompt | Purpose | -| ------------------- | --------------------------------------------------------- | -| `api-design-review` | Review an API design or OpenAPI spec for correctness | -| `architecture-risk` | Identify architectural risks and mitigation priorities | -| `code-review` | Review a change for bugs, regressions, and missing tests | -| `dependency-audit` | Audit dependencies for vulnerabilities and licence risks | -| `incident-timeline` | Build an evidence-based incident timeline and post-mortem | -| `migration-safety` | Review DB migration safety, rollback, and zero-downtime | -| `release-readiness` | Evaluate release readiness from reports and open blockers | - -## Quickstart — fresh install - -Install with `pipx`, then install vstack artifacts into your repository: +## Quickstart ```bash -# Install the CLI once, globally pipx install vstack - -# Move to your repository root and run install — no --target needed cd /path/to/your/project -vstack install # seeds .vstack/config.yaml and generates .github/ in the current directory -vstack validate # confirm no errors -``` - -When you omit `--target`, vstack uses the current working directory. The equivalent -explicit form is `vstack install --target /path/to/your/project`. - -Run a first task in Copilot Agent Mode: - -```text -@tester /verify Check this repository and summarize findings +vstack install +vstack validate ``` -Expected result: - -- `vstack validate` reports no unresolved template tokens -- Agent command returns a concrete verification summary for your repository +Then open Copilot Chat, switch to Agent mode, and select the `planner` agent. ## Quick upgrade -### Patch or minor version (e.g. v3.1 → v3.2, same major) - -Docs paths never change within a major version. Only `.github/` artifacts are updated. +Patch or minor upgrade: ```bash pipx upgrade vstack - cd /path/to/your/project -vstack init # idempotent — safe to run in CI +vstack init ``` -### Major version (e.g. v2 → v3) - -Docs paths may change on a major version bump. Run `vstack migrate` before `vstack init`. +Major upgrade: ```bash pipx upgrade vstack - cd /path/to/your/project -vstack migrate # moves docs files to their new paths (auto-detects installed version) -vstack init # regenerates .github/ artifacts - -# Only if you see "Legacy manifest schema detected" in the output above: -vstack manifest upgrade -vstack init -``` - -Preview the docs moves without touching any files: - -```bash -vstack migrate --dry-run -``` - -For upgrades spanning multiple major versions (e.g. v1 → v3), `vstack migrate` chains -all intermediate steps automatically. Use `--from` and `--to` to specify the range -explicitly if auto-detection from the manifest fails: - -```bash -vstack migrate --from 1 --to 3 +vstack migrate vstack init ``` -### Force reinstall (overwrite local edits) - -```bash -vstack install --force # overwrite all managed artifacts -vstack install --force-name agent/engineer # overwrite one specific artifact -``` - -## Why this helps - -- Consistent role boundaries for planning, implementation, validation, and release -- Reusable skills and instructions instead of ad hoc prompts -- Better release hygiene with documented workflows and CI alignment - -## Core commands +If you see a legacy manifest schema warning: ```bash -vstack --version -vstack validate - -# Run from your repository root (--target defaults to the current directory) -vstack install -vstack init -vstack migrate -vstack manifest verify -vstack manifest status vstack manifest upgrade - -# Or specify a path explicitly -vstack install --target /path/to/your/project -``` - -## Common usage patterns - -Repository-scoped install (recommended for teams): - -```bash -# Move to your repository root and install there -cd /path/to/your/project -vstack install - -# Or specify a path explicitly from any directory -vstack install --target /path/to/your/project -``` - -Profile-wide install (optional defaults for all projects): - -```bash -vstack install --global -``` - -`vstack install` is the first-run command: it seeds `.vstack/config.yaml` in your project (never overwrites), then generates `.github/` artifacts from templates. `vstack init` re-runs generation idempotently — safe to use in CI after upgrading vstack. - -By default, `vstack install` preserves existing unmanaged files and local edits to tracked files by comparing the current file contents with the SHA-256 checksum recorded in `.vstack/vstack.json`. Use `--adopt-name ` to start tracking one existing unmanaged file without overwriting it. `vstack uninstall` also preserves locally modified tracked files unless you explicitly pass `--force` or `--force-name `. Use `vstack manifest status --target ...` (or `vstack status --target ...`) to see what still matches the manifest. If a legacy manifest schema is detected, run `vstack manifest upgrade --target ...` first. - -To skip artifact types or individual artifacts you do not need, edit `.vstack/config.yaml`: - -```yaml -exclude: - skills: - - terraform - - helm - instructions: all # skip the entire type -``` - -If you already have agents, skills, or other files in `.github/`, run a dry-run first to see what would be preserved before committing: - -```bash -# Run from your repository root -vstack install --dry-run -``` - -The summary lists preserved files as `type/name` selectors (e.g. `agent/engineer`). Resolve each conflict with `--force-name type/name` to overwrite, `--adopt-name type/name` to take ownership without overwriting, or `--force` to overwrite everything. - -## Reading `.vstack/config.yaml` - -- Lines starting with `#` are comments, explanation, or example configuration and are not active. -- Only uncommented YAML keys are active configuration. -- To enable an example block, remove `#` from that block and keep valid YAML indentation. -- After any config change, run `vstack init` to apply it to generated `.github/` artifacts. - -## Workflow modes - -vstack supports three workflow modes via `.vstack/config.yaml`: - -```yaml -workflow: - mode: agentic # default -``` - -After changing `workflow.mode`, regenerate artifacts: - -```bash vstack init ``` -| Mode | Behavior | Planner file | Worker handoff buttons | -| --------- | ------------------------------------------------------------ | ------------- | ---------------------- | -| `agentic` | Planner orchestrates stage progression using subagents | generated | omitted | -| `manual` | User progresses stage-by-stage manually | not generated | shown | -| `hybrid` | Both planner orchestration and manual handoffs are available | generated | shown | - -Execution semantics: - -- `workflow.stages` order is the canonical progression order. -- `agentic` is stage-sequential by default: planner advances one stage at a time in configured order. -- Set `depends_on` to unlock a DAG topology and let planner run independent branches in parallel. - -**Parallel stages with `depends_on`:** By default each stage implicitly depends on the previous one. Add `depends_on` to declare explicit predecessors. The canonical vstack DAG (seeded automatically by `vstack install`): - -```yaml -workflow: - mode: agentic - version: 1 - stages: - - role: product - gate: required - hitl: always - - role: architect - gate: required - hitl: always - depends_on: [product] - - role: designer - gate: optional - hitl: on-change - depends_on: [product] # runs in parallel with architect - - role: engineer - gate: required - hitl: always - depends_on: [architect, designer] # waits for both - - role: tester - gate: required - hitl: always - depends_on: [engineer] - - role: release - gate: required - hitl: always - depends_on: [tester] -``` - -`depends_on: []` marks a root stage. Absent `depends_on` falls back to sequential. Circular dependencies are caught at install/init time. - -Handoff target semantics: - -- `handoffs.prompt` is the transition prompt text. -- If `handoffs.agent` is omitted, the target defaults to the next role in `workflow.stages`. -- You can set `handoffs.agent` explicitly to override that default target in `manual`/`hybrid`. -- In `agentic`, worker handoff buttons are hidden; planner controls progression. - -Mode quickstart in Copilot Agent Mode: - -In `agentic` mode, `@planner` is the primary entry point. Start every session with `@planner` and let it drive all stage transitions automatically. - -| Mode | Start here | First prompt example | -| --------- | ------------------------ | ------------------------------------------------------- | -| `agentic` | `@planner` | `@planner Run the workflow for this repository change.` | -| `manual` | `@product` | `@product Define requirements for this change.` | -| `hybrid` | `@planner` or `@product` | `@planner Run the workflow for this repository change.` | +## Try it now -What planner does: reads `workflow.stages` and `depends_on`, invokes each role agent as a subagent at the right time, runs independent branches in parallel when `depends_on` permits, pauses at each gate for human approval, and reports a structured stage outcome after each step. Valid role names: `product`, `architect`, `designer`, `engineer`, `tester`, `release`. - -Agentic runbook (copy/paste): +In Copilot Chat after selecting `planner`: ```text -@planner Run the workflow for this repository change. -@planner Show current stage status, ready stages, blocked stages, and next action. -@planner Continue with all ready stages in parallel where workflow.depends_on allows it. -@planner Pause at required HITL gates and ask for approval before advancing. -@planner Finalize with a release-readiness summary and list changed artifacts. +Run the workflow for this repository change. ``` -Troubleshooting: why planner is not running stages in parallel - -- Check `workflow.mode` in `.vstack/config.yaml`: parallel orchestration requires `agentic` (or planner-led `hybrid`). -- Validate stage dependencies: run `vstack validate` to catch invalid roles, self-dependencies, and cycles. -- Inspect `depends_on` shape: a stage runs only when all listed predecessors are `ready` or `skipped`. -- Check implicit sequential fallback: if `depends_on` is omitted, the stage depends on the previous stage. -- Check optional stage behavior: `gate: optional` can be skipped when unaffected, reducing apparent parallel fan-out. -- Check blockers in stage report: any `blocked` predecessor prevents dependent stages from becoming ready. -- Check mixed execution path: in `hybrid`, mixing manual handoffs and planner in one session can mask parallel readiness. -- Confirm you started with planner: in `agentic`, begin with `@planner`, not a worker role agent. - -Usage guidance: - -- Use `agentic` when you want one deterministic orchestration path. -- Use `manual` when your team prefers explicit user-controlled stage transitions. -- Use `hybrid` only when your team intentionally wants both options. - -Hybrid operating rule: - -- Choose one path per session (planner-led or manual handoffs) and stay on it. -- Mixing both paths in one session increases the chance of duplicate stage transitions. - -Hybrid warning: - -- In `hybrid`, users can click handoff buttons while a planner-led flow is also available. -- This can cause unintended progression jumps or duplicated transitions if your process assumes one strict path. - -## Fast troubleshooting - -- Command not found after install: ensure your `pipx` binary path is in `PATH` -- Validation error: rerun `vstack install` from your repository root and then `vstack validate` -- Agent results look generic: explicitly invoke a role (for example `@tester`) before a skill - -## Full documentation - -For complete documentation (including architecture details, workflow diagrams, and contributor guides), use GitHub: +## Full docs on GitHub -- [GitHub repository](https://github.com/eschaar/vstack) -- [Full README](https://github.com/eschaar/vstack/blob/main/README.md) -- [Documentation](https://github.com/eschaar/vstack/tree/main/docs) -- [Contributing guide](https://github.com/eschaar/vstack/blob/main/CONTRIBUTING.md) -- [Security policy](https://github.com/eschaar/vstack/blob/main/SECURITY.md) +- User docs index: +- Install and upgrade: +- Troubleshooting: +- CLI commands reference: +- Configuration reference: +- Workflow modes explanation: +- Full repository docs: diff --git a/README.md b/README.md index 9ca283b4..797112d4 100644 --- a/README.md +++ b/README.md @@ -40,1125 +40,92 @@ ______________________________________________________________________ ______________________________________________________________________ -## 🧭 Quick navigation - -For new users: - -- Quickstart -- Quick check -- Using vstack in Copilot Agent Mode -- Try it now -- Troubleshooting - -For experienced users: - -- Role summary -- Example usage -- All vstack CLI commands -- Install and upgrade guide -- Workflow -- Development -- CI and Release Automation - -### ⚡ Quick paths - -#### Quickstart — fresh install (2 minutes) - -```bash -# 1. Install the CLI once, globally -pipx install vstack - -# 2. Move to your repository root — all commands default to the current directory -cd /path/to/your/project -vstack install # seeds .vstack/config.yaml and generates .github/ here - -# 3. Confirm everything is in order -vstack validate -``` - -When you omit `--target`, vstack uses the current working directory. -The explicit form `vstack install --target /path/to/your/project` is equivalent -and useful when running from a different directory. - -Then open Copilot Agent Mode and run: - -```text -@tester /verify Check this repository and summarize findings -``` - -#### Quick upgrade — patch or minor version (same major) - -Docs paths never change within a major version. Only `.github/` artifacts are updated. - -```bash -# 1. Upgrade the CLI -pipx upgrade vstack - -# 2. From your repository root — regenerate .github/ artifacts -cd /path/to/your/project -vstack init -``` - -#### Quick upgrade — major version (e.g. v2 → v3) - -Docs paths may change on a major version bump. Run `vstack migrate` before `vstack init`. - -```bash -# 1. Upgrade the CLI -pipx upgrade vstack - -# 2. From your repository root -cd /path/to/your/project - -# 3. Move any docs files that changed path (reads installed version from .vstack/vstack.json) -vstack migrate - -# 4. Regenerate .github/ artifacts -vstack init - -# 5. If the manifest schema is outdated (you will see an error message telling you to do this) -vstack manifest upgrade -``` - -Preview the docs moves without touching any files: - -```bash -vstack migrate --dry-run -``` - -```mermaid -flowchart LR - A[Install CLI] --> B[Install artifacts per repo] - B --> C[Validate setup] - C --> D[Run @tester /verify] - D --> E[Role-based flow] -``` - -______________________________________________________________________ - ## 🚀 Quickstart -> New here? Run `pipx install vstack`, move to your repository root, run `vstack install`, then try `@tester /verify` in Copilot Agent Mode. - -### ⚡ Install with pipx (recommended) - -`pipx` installs vstack in its own isolated environment so it never conflicts with -your project dependencies. The `vstack` command is then available globally across -all projects, regardless of which virtual environment is active. - ```bash +# 1. Install the CLI once, globally pipx install vstack -``` - -Afterwards, the `vstack` command is available everywhere: - -```bash -# Recommended: move to your repository root and install there -cd /path/to/your/project -vstack install # generates .github/ in the current directory - -# Or specify a path explicitly when running from a different directory -vstack install --target /path/to/your/project - -# Optional: install profile-wide defaults for all VS Code projects -vstack install --global -``` - -### 🐙 Alternative: install directly from GitHub - -To install the latest unreleased version directly from the repository without cloning: - -```bash -pipx install git+https://github.com/eschaar/vstack.git -``` - -Or a specific branch or tag: - -```bash -pipx install git+https://github.com/eschaar/vstack.git@main -pipx install git+https://github.com/eschaar/vstack.git@1.3.0 -``` - -### 🐙 Alternative: manual clone and install -```bash -git clone git@github.com:eschaar/vstack.git -cd vstack -poetry install -poetry run vstack install --target /path/to/your/project -``` - -### 🤝 Team setup (recommended for teams) - -Use repository-scoped installation so every contributor and CI run uses the same agent setup. - -1. Install artifacts into the repository. -1. Commit the generated `.github/` artifacts. -1. Require `commit.yml`, `check.yml`, `verify.yml`, and `security.yml` checks before merge. - -```bash +# 2. Move to your repository root cd /path/to/your/project vstack install -git add .github -git commit -m "chore: install vstack artifacts" -``` - -Expected outcome: - -- Teammates get the same agents and skills after `git pull`. -- CI validates the same repository-level setup. -## 🚦 Quick check: Is vstack working? - -After install, run: - -```bash -vstack --version +# 3. Confirm setup vstack validate ``` -If you see the version and no errors, your install is working. - -Expected output (example): - -```text -vstack X.Y.Z -Validation passed: no unresolved template tokens -``` - -### First use example - -Open Copilot Agent Mode and run: - -```text -@tester /verify Check this repository and summarize findings -``` - -You should receive a concise verification summary for your current workspace. - -### 💬 Using vstack in Copilot Agent Mode - -For new users (first 5 minutes): - -- Follow the 3-step flow below exactly once. -- Start with `@tester /verify` to confirm the setup works. - -For experienced users: - -- Use direct role invocation (`@product`, `@architect`, `@engineer`, etc.) for context-rich execution. -- Use direct skills (`/verify`, `/security`, `/code-review`) for focused, faster runs. - -1. **Open Copilot Chat** - - Use `Ctrl+Shift+I` (Windows/Linux) or `Cmd+Shift+I` (Mac), or click the Copilot icon in the sidebar. -1. **Switch to Agent Mode** - - In the Copilot Chat panel, change the mode selector from `Ask` to `Agent`. -1. **Invoke a role agent** - - Type e.g.: - ```text - @product Review my plan for a payments service - @architect Review the API contracts in src/api/ - @tester /security Audit the authentication module - ``` - - The `@role` prefix selects the corresponding agent. You can add a skill command (e.g. `/security`) after the agent for a focused procedure. - -**How it works:** - -- When you use `@role` (e.g. `@tester`), the agent loads all relevant skills and instructions for that role. Skills are discovered automatically and routed by the agent based on your request. -- If you use only a skill (e.g. `/verify`), a prompt, or an instruction (without an explicit agent), VS Code Copilot Agent Mode will use the default agent for the context (typically `@tester` for verification-related skills, or the most relevant role based on your workspace and prompt). This means you can use `/verify`, `/security`, or other skills directly, and they will work even without specifying an agent. -- Agents are not invoked automatically; you must use the `@role` prefix to select a specific agent and role context. Skills, prompts, and instructions are always auto-discovered and available in the background. -- For maximum control and clarity, always specify the agent (`@role`) when you want a particular role's framing, default behavior, or skill routing. - -______________________________________________________________________ - -#### 🧩 Visual: How agents, skills, instructions, and prompts interact - -```mermaid -flowchart TD - subgraph "VS Code Copilot Agent Mode" - A["User prompt"] - B["Agent (e.g. @tester)"] - C["Skills (e.g. /verify, /security)"] - D["Instructions"] - E["Prompts"] - end - A --> B - B --> C - B --> D - B --> E - C -.-> B - E -.-> B - D -.-> B -``` - -**Legend:** - -- **Agents** (`@role`): Main entrypoint, routes and coordinates work. -- **Skills** (`/skill`): Reusable procedures, invoked by agents or directly. -- **Instructions**: Baseline policies, always loaded by agents. -- **Prompts**: Reusable prompt artifacts, used as needed. - -______________________________________________________________________ - -## 🧪 Try it now - -Open Copilot Agent Mode and enter: - -```text -@tester /verify Check this repo -``` - -You should see a verification summary for your current project. - -______________________________________________________________________ - -## 🧑‍💻 Role summary - -| Role | Emoji | Invocation | Primary areas | Example invocation | -| --------- | ----- | ------------ | ------------------------------------------------------- | ------------------------------------- | -| Product | 🧑‍💼 | `@product` | Vision, requirements, onboarding, docs | `@product Review my plan` | -| Architect | 🏗️ | `@architect` | Architecture, ADRs | `@architect Review the API contracts` | -| Designer | 🎨 | `@designer` | Service design, OpenAPI, DX review | `@designer Review the OpenAPI spec` | -| Engineer | 🛠️ | `@engineer` | Implementation, debugging, refactoring, dependency work | `@engineer /code-review` | -| Tester | 🧪 | `@tester` | Verification, security, incident review, performance | `@tester /verify` | -| Release | 🚀 | `@release` | Release notes, PR creation, release gating | `@release Prepare release notes` | - -### Role-to-skill mapping - -| Role | Invocation | Primary skills | Default concise mode | -| --------- | ------------ | ------------------------------------------------------- | -------------------- | -| product | `@product` | vision, requirements, onboard, docs | compact | -| architect | `@architect` | architecture, adr | normal | -| designer | `@designer` | design, openapi, consult, docs | compact | -| engineer | `@engineer` | code-review, debug, refactor, migrate, dependency, docs | compact | -| tester | `@tester` | verify, inspect, security, incident, dependency, docs | ultra | -| release | `@release` | release-notes, pr, docs | compact | - -______________________________________________________________________ - -> ℹ️ **Tip:** Use the `@role` prefix for full context and best results. Skills like `/verify` also work directly, but explicit roles give you more control. - -______________________________________________________________________ - -> 💡 **Pro tip:** Try combining agents and skills for focused tasks, e.g. `@tester /security` or `@engineer /code-review`. - -______________________________________________________________________ - -## 📝 Example usage - -### Idea to release - -1. `@product` to lock requirements and success criteria. -1. `@architect` to define service boundaries and ADRs. -1. `@designer` to define APIs, schemas, and flows. -1. `@engineer` to implement. -1. `@tester` to verify behavior and risk. -1. `@release` to prepare release artifacts and PR flow. - -### Direct skill usage - -| Goal | Agent invocation | Optional direct skill | -| ------------------- | ---------------- | --------------------- | -| Requirements | `@product` | | -| Architecture review | `@architect` | | -| API design | `@designer` | | -| Code review | `@engineer` | `/code-review` | -| Verification | `@tester` | `/verify` | -| Security audit | `@tester` | `/security` | -| Performance check | `@tester` | `/performance` | - -### Subagent orchestration pattern - -```text -@product Deliver a requirements-to-release plan for a new payments service -``` - -Typical downstream path: `@product` -> `@architect` -> `@designer` -> `@engineer` -> `@tester` -> `@release`. - -______________________________________________________________________ - -## ❓ FAQ - -**Q: Why don't I see agents in Copilot?** -A: In a specific repository, run `vstack install --target /path/to/your/project` (or run `vstack install` from the repo root), then reload VS Code. Use `--global` only when you want profile-wide defaults. - -**Q: Which Python version do I need?** -A: Python 3.11–3.14 (see badges above). - -**Q: How do I reset the install?** -A: Move to your repository root and run `vstack uninstall`, then `vstack install`. You can also use `vstack uninstall --target /path/to/your/project` from any directory. Use `--global` only for profile-wide defaults. - -**Q: Where can I ask questions or give feedback?** -A: [Start a discussion or ask a question here.](https://github.com/eschaar/vstack/discussions) - -______________________________________________________________________ - -## 🧹 Uninstall / Reset - -To remove vstack artifacts from your project or profile, use the CLI: - -```bash -# Move to your repository root and uninstall -cd /path/to/your/project -vstack uninstall - -# Or specify a path explicitly from any directory -vstack uninstall --target /path/to/your/project - -# Uninstall vstack artifacts from your global VS Code profile -vstack uninstall --global -``` - -To remove vstack itself (the CLI): - -```bash -# If installed with pipx -pipx uninstall vstack - -# If installed with pip in an active environment -pip uninstall vstack - -# If installed from a local clone for development -rm -rf .venv -``` - -You can also manually remove any leftover `.github/agents`, `.github/skills`, etc. if needed. - -## ⚡ Essential CLI commands - -```bash -vstack --version # Show vstack version -vstack validate # Validate source templates only -vstack manifest status --target . # Check installed files against manifest checksums -vstack manifest verify --target . # Verify installed output against manifest ownership/checksums -vstack manifest upgrade --target . # Upgrade legacy vstack.json schema -vstack install --target . # Install vstack artifacts into current project -vstack install --global # Install vstack artifacts globally -vstack uninstall --target . # Uninstall vstack artifacts from current project -vstack uninstall --global # Uninstall vstack artifacts globally -``` - -______________________________________________________________________ - -## 📖 All vstack CLI commands - -| Command | Description | -| -------------------------------------- | ---------------------------------------------------------------------------------------- | -| `vstack --version` | Show vstack version | -| `vstack validate` | Validate source templates only | -| `vstack verify` | Verify source templates and installed output, including checksum drift | -| `vstack status --target DIR` | Report which installed artifacts still match `.vstack/vstack.json` | -| `vstack manifest status --target DIR` | Manifest-scoped status for installed output and ownership | -| `vstack manifest verify --target DIR` | Manifest-scoped verify for installed output only | -| `vstack manifest upgrade --target DIR` | Upgrade a legacy `.vstack/vstack.json` schema to current format | -| `vstack verify --target DIR` | Verify installed artifacts in DIR/.github | -| `vstack verify --global` | Verify artifacts in your VS Code global profile | -| `vstack install --target DIR` | First-run setup: seeds `.vstack/`, then generates `.github/` artifacts from templates | -| `vstack install` | Same as above, using the current directory as target | -| `vstack install --global` | Install vstack artifacts into your VS Code profile, preserving local edits unless forced | -| `vstack install --dry-run` | Preview install actions without writing files | -| `vstack init --target DIR` | Idempotent regeneration: reads `.vstack/config.yaml` and updates `.github/` artifacts | -| `vstack init` | Same as above, using the current directory as target | -| `vstack uninstall --target DIR` | Uninstall tracked artifacts that still match the manifest | -| `vstack uninstall --global` | Uninstall vstack artifacts from your VS Code profile | -| `vstack uninstall` | Uninstall from the current directory default target | -| `vstack migrate --target DIR` | Move docs files from old paths to new paths after a major vstack upgrade | -| `vstack migrate --from M --to N` | Migrate docs paths across major versions M through N (chains intermediate steps) | -| `vstack migrate --dry-run` | Preview docs path moves without touching any files | - -By default, `vstack install` is conservative: if a target file already exists but is not tracked by `vstack`, it is left in place. For tracked files, `--update` only rewrites artifacts whose on-disk content still matches the SHA-256 checksum of the last installed version recorded in `.vstack/vstack.json`. Use `--force` to overwrite everything, `--force-name ` to overwrite one specific managed artifact, or `--adopt-name ` to start tracking one existing unmanaged file without overwriting it. - -If you already have agents, skills, or other files in `.github/`, run a dry-run first to see what would be preserved before committing: - -```bash -# Preview what install would do — no files are written (run from your repository root) -vstack install --dry-run -``` - -The summary shows every preserved file as a `type/name` selector (e.g. `agent/engineer`, `skill/verify`). You can then resolve each conflict selectively: - -```bash -# Overwrite a specific preserved artifact -vstack install --force-name agent/engineer - -# Take ownership of an existing file without overwriting it -vstack install --adopt-name agent/engineer - -# Overwrite everything -vstack install --force -``` - -When multiple artifact types share the same name (e.g. an `agent` and a `skill` both named `engineer`), use the `type/name` form to target one precisely. - -`vstack uninstall` is conservative as well: it removes only tracked artifacts whose current checksum still matches the manifest. If a tracked file was edited locally, it is preserved unless you explicitly pass `--force` or `--force-name`. Use `vstack manifest status` (or `vstack status`) for a read-only overview of managed, modified, missing, and conflicting files. - -When a legacy manifest schema is detected, verification/status/install paths now fail fast with an upgrade hint. Run `vstack manifest upgrade --target ...` once, then retry your normal commands. - -For smaller terminals, `vstack manifest status` (and `vstack status`) defaults to a compact issues-focused text view with color markers. For tooling or exports, use `--format json` or `--format yaml`. Add `--verbose` to include managed entries, and `--no-color` when plain text is preferred. - -### install vs init - -`vstack install` and `vstack init` are complementary: - -| Command | When to use | What it does | -| ---------------- | ----------------------------------------------- | --------------------------------------------------------------------------------- | -| `vstack install` | Once per project (or when onboarding a machine) | Seeds `.vstack/config.yaml` if missing (never overwrites), then runs `init` | -| `vstack init` | On every vstack upgrade, or in CI | Idempotent regeneration — reads `.vstack/config.yaml` and applies it on every run | - -Both commands default to the current working directory when `--target` is omitted — run them from the repository root. Both accept the same flags (`--only`, `--force`, `--update`, `--dry-run`, etc.). - -### `.vstack/config.yaml` — project configuration - -When you run `vstack install`, a `.vstack/config.yaml` file is seeded in your project. This file is yours: vstack never overwrites it. Commit it to version control — it expresses stable project preferences that apply on every future `vstack init` run. - -How to read this file: - -- Lines starting with `#` are comments, explanation, or example configuration and are not active. -- Only uncommented YAML keys are active configuration. -- To enable an example block, remove `#` from that block and keep valid YAML indentation. -- After any config change, run `vstack init` to apply it to generated `.github/` artifacts. - -The two most useful settings: - -**Exclude specific artifacts** — skip artifact types or individual artifacts you do not need: - -```yaml -exclude: - skills: - - terraform - - helm - - k8s - instructions: all # skip the entire instructions type - prompts: all # skip the entire prompts type -``` - -Type-level entries (`all`) remove the type from generation entirely. Name-level entries skip individual artifacts within a type while keeping the rest. - -**Override the docs root path** — change where agent work-item paths point (default: `docs`): - -```yaml -items: - root: documentation # use a different path prefix in generated agent files -``` - -Legacy compatibility: `artifacts.root` is still supported as a fallback for older configs. - -Migration path: - -1. Keep existing `artifacts.root` configs unchanged — current versions continue to read them. -1. Move to `items.root` when convenient; `items.root` takes precedence when both are present. -1. For teams with mixed versions, either key is accepted until all projects standardize on `items.root`. - -**Select workflow mode** — control whether users progress manually, via planner orchestration, or both: - -```yaml -workflow: - mode: agentic # default -``` - -After changing `workflow.mode`, regenerate artifacts: - -```bash -vstack init -``` - -Supported values: - -| Mode | Behavior | Planner file | Worker handoff buttons | -| --------- | ---------------------------------------- | ------------- | ---------------------- | -| `agentic` | Planner orchestrates stages as subagents | generated | omitted | -| `manual` | User progresses stage-by-stage manually | not generated | shown | -| `hybrid` | Both patterns are available | generated | shown | - -Execution semantics: - -- `workflow.stages` order is the canonical progression order. -- `agentic` is stage-sequential by default: planner advances one stage at a time in configured order. -- Parallelization is still possible inside a stage (independent subtasks), but cross-stage progression remains ordered by default. -- Set `depends_on` to unlock a DAG topology and let planner run independent branches in parallel. - -**Parallel stages with `depends_on`:** - -By default, each stage implicitly depends on the one before it (fully sequential). Add `depends_on` to any stage to declare explicit predecessors and create a dependency graph. Planner evaluates which stages are ready and can run independent branches in parallel. - -The canonical vstack DAG — seeded automatically by `vstack install`: - -```yaml -workflow: - mode: agentic - version: 1 - stages: - - role: product - gate: required - hitl: always - - role: architect - gate: required - hitl: always - depends_on: [product] - - role: designer - gate: optional - hitl: on-change - depends_on: [product] # runs in parallel with architect - - role: engineer - gate: required - hitl: always - depends_on: [architect, designer] # waits for both - - role: tester - gate: required - hitl: always - depends_on: [engineer] - - role: release - gate: required - hitl: always - depends_on: [tester] -``` - -Rules: - -- `depends_on: []` — marks the stage as a root (no predecessors). -- `depends_on` absent — stage implicitly depends on the previous stage (sequential fallback). -- Planner reads `depends_on` at runtime; no code change needed. -- Circular dependencies are detected at install/init time. - -Handoff target semantics: - -- `handoffs.prompt` is the transition prompt text. -- If `handoffs.agent` is omitted, the target defaults to the next role in `workflow.stages`. -- You can set `handoffs.agent` explicitly to override that default target in `manual`/`hybrid`. -- In `agentic`, worker handoff buttons are hidden; planner controls progression. - -Mode quickstart in Copilot Agent Mode: - -> **In `agentic` mode, `@planner` is the primary entry point.** Start every session -> with `@planner` and let it drive all stage transitions automatically. - -| Mode | Start here | First prompt example | -| --------- | ------------------------ | ------------------------------------------------------- | -| `agentic` | `@planner` | `@planner Run the workflow for this repository change.` | -| `manual` | `@product` | `@product Define requirements for this change.` | -| `hybrid` | `@planner` or `@product` | `@planner Run the workflow for this repository change.` | - -What planner does: - -- Reads `workflow.stages` and `depends_on` from your project config. -- Invokes each role agent as a subagent at the right time. -- Runs independent branches in parallel when `depends_on` permits. -- Pauses at each gate for human-in-the-loop approval (when `hitl: always` or `hitl: on-change`). -- Skips `gate: optional` stages that are not affected by the current change. -- Reports a structured stage outcome after each step: status, changes made, blockers, next action. +When you omit `--target`, vstack uses the current working directory. -Which roles planner knows: -`product`, `architect`, `designer`, `engineer`, `tester`, `release`. -These are the only valid role names in `workflow.stages`. +Then open Copilot Chat and choose the `planner` agent in the agent picker. -Agentic runbook (copy/paste): +For a direct specialist check, choose the `tester` agent and ask: ```text -@planner Run the workflow for this repository change. -@planner Show current stage status, ready stages, blocked stages, and next action. -@planner Continue with all ready stages in parallel where workflow.depends_on allows it. -@planner Pause at required HITL gates and ask for approval before advancing. -@planner Finalize with a release-readiness summary and list changed artifacts. -``` - -Troubleshooting: why planner is not running stages in parallel - -- Check `workflow.mode` in `.vstack/config.yaml`: parallel orchestration requires `agentic` (or planner-led `hybrid`). -- Validate stage dependencies: run `vstack validate` to catch invalid roles, self-dependencies, and cycles. -- Inspect `depends_on` shape: a stage runs only when all listed predecessors are `ready` or `skipped`. -- Check implicit sequential fallback: if `depends_on` is omitted, the stage depends on the previous stage. -- Check optional stage behavior: `gate: optional` can be skipped when unaffected, reducing apparent parallel fan-out. -- Check blockers in stage report: any `blocked` predecessor prevents dependent stages from becoming ready. -- Check mixed execution path: in `hybrid`, mixing manual handoffs and planner in one session can mask parallel readiness. -- Confirm you started with planner: in `agentic`, begin with `@planner`, not a worker role agent. - -Hybrid operating rule: - -- Choose one path per session (planner-led or manual handoffs) and stay on it. -- Mixing both paths in one session increases the chance of duplicate stage transitions. - -Hybrid mode warning: - -- In `hybrid`, users can continue via planner orchestration and via handoff buttons. -- This dual-path UX can create unintended jumps or duplicate progression in teams that expect a single strict path. -- If you want one deterministic flow, prefer `agentic`. - -```mermaid -flowchart LR - A[workflow.mode] --> B{Selected mode} - B --> C[agentic] - B --> D[manual] - B --> E[hybrid] - C --> C1[Planner generated] - C --> C2[Worker handoffs hidden] - D --> D1[Planner omitted] - D --> D2[Worker handoffs visible] - E --> E1[Planner generated] - E --> E2[Worker handoffs visible] +Verify this repository and summarize findings. ``` -All fields are optional. An absent or commented-out block restores the default behaviour. - ______________________________________________________________________ -## ⬆️ Install and upgrade guide - -vstack manages two separate layers. Knowing which layer each command touches prevents mistakes: - -| Layer | What it contains | Updated by | -| --------------------- | -------------------------------------------------------------------------------- | -------------------------------- | -| `.github/` | Agent, skill, instruction, and prompt files that Copilot reads | `vstack install` / `vstack init` | -| `docs/` | Docs files that agents read and write (paths may change on a major version bump) | `vstack migrate` | -| `.vstack/vstack.json` | Manifest — tracks which `.github/` files are managed and stores their checksums | `vstack manifest upgrade` | +## ⬆️ Quick upgrade -### Scenario 1 — Fresh install (no previous vstack) - -```bash -# Install the CLI once, globally -pipx install vstack - -# Move to your repository root -cd /path/to/your/project -vstack install # seeds .vstack/config.yaml and generates .github/ in the current directory -vstack validate # confirm no errors -``` - -All commands default to the current working directory when `--target` is omitted. -Run them from the repository root. The explicit form `vstack install --target /path/to/your/project` -is equivalent and useful when running from a different directory. - -### Scenario 2 — Fresh install over an existing version (force) - -Replace all managed artifacts, even if you have made local edits: - -```bash -# Preview what would happen first -vstack install --dry-run - -# Overwrite everything -vstack install --force -``` - -Or target a single artifact without touching the rest: - -```bash -vstack install --force-name agent/engineer -vstack install --force-name skill/verify -``` - -To take ownership of an existing unmanaged file without overwriting it: - -```bash -vstack install --adopt-name agent/architect -``` - -### Scenario 3 — Patch or minor upgrade (e.g. v3.1 → v3.2, same major) - -Docs paths never change within a major version. Only `.github/` artifacts need updating. - -```bash -pipx upgrade vstack - -cd /path/to/your/project -vstack init # idempotent regeneration — safe to run in CI -``` - -`vstack init` is safe to re-run at any time. It reads `.vstack/config.yaml` and regenerates `.github/` artifacts without touching anything else. - -### Scenario 4 — Major upgrade, single step (e.g. v2 → v3) - -Docs paths may change on a major version bump. Run `vstack migrate` before `vstack init`. +### Patch or minor (same major) ```bash pipx upgrade vstack - cd /path/to/your/project - -# Preview what migrate would move (no files are touched) -vstack migrate --dry-run - -# Apply the docs path moves (auto-detects your installed version from .vstack/vstack.json) -vstack migrate - -# Regenerate .github/ artifacts -vstack init - -# Only needed if you see: "Legacy manifest schema detected" in the output above -vstack manifest upgrade vstack init ``` -### Scenario 5 — Major upgrade, multiple steps (e.g. v1 → v3) - -`vstack migrate` chains all intermediate steps automatically. You do not need to run it once per version. +### Major (for example `v2 -> v3`) ```bash pipx upgrade vstack - cd /path/to/your/project - -# Auto-detects v1 from manifest, chains v1→v2→v3 automatically vstack migrate - -# Or specify the range explicitly if auto-detection fails -vstack migrate --from 1 --to 3 - vstack init ``` -If no migration record exists for an intermediate step (for example v1 → v2), that step is silently skipped. - -### Scenario 6 — Manifest schema is outdated - -If a command fails with: - -```text -Legacy manifest schema detected in vstack.json. Run: vstack manifest upgrade -``` - -Run: +If you see a legacy manifest schema warning: ```bash vstack manifest upgrade vstack init ``` -### Common mistakes to avoid - -| Mistake | Symptom | Fix | -| ---------------------------------------------------------------- | --------------------------------------------------------- | --------------------------------------------------------------------- | -| Running `vstack init` before `vstack migrate` on a major upgrade | Agents reference docs paths that no longer exist | Run `vstack migrate` first, then `vstack init` | -| Running `vstack migrate` without a manifest | `ERROR: could not detect installed version from manifest` | Run `vstack migrate --from ` to specify explicitly | -| Local edits not overwritten | `vstack init` skips modified files silently | Use `vstack install --force-name type/name` to overwrite one artifact | -| Manifest schema error after upgrade | `vstack init` fails with a schema error | Run `vstack manifest upgrade` first | - -______________________________________________________________________ - -## 🤝 How to contribute - -Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines, code style, and how to get started. - -______________________________________________________________________ - -## 🛠️ Troubleshooting - -Quick index: -[Installation and environment](#installation-and-environment) · [Copilot Agent Mode](#copilot-agent-mode) · [CI parity and badges](#ci-parity-and-badges) · [VS Code search noise](#vs-code-search-noise) - -```mermaid -flowchart TD - A[Problem observed] --> B{Install or environment issue?} - B -->|Yes| C[Check pipx/poetry/python version] - B -->|No| D{Agents visible in Copilot?} - D -->|No| E[Run vstack install --target and reload VS Code] - D -->|Yes| F{CI mismatch or badge confusion?} - F -->|CI mismatch| G[Run make bootstrap then make check or make ci] - F -->|Badge no status| H[Verify PR-based workflow trigger] - F -->|Search noisy| I[Set search.exclude and files.watcherExclude] - C --> J[Resolved] - E --> J - G --> J - H --> J - I --> J -``` - -### Installation and environment - -- Issue: `pipx: command not found` - Action: Install pipx with `pip install --user pipx`. -- Issue: `poetry: command not found` - Action: Follow the Poetry install guide at [https://python-poetry.org/docs/#installation](https://python-poetry.org/docs/#installation). -- Issue: `Python version not supported` - Action: Use Python 3.11-3.14. -- Issue: `Permission denied` during install or uninstall - Action: Check directory permissions and rerun with appropriate privileges. -- Issue: `Could not detect VS Code user data directory` - Action: Run `vstack install --global` to install into the VS Code user profile, or run `vstack install --target /path/to/your/project` to install into a specific project instead. - -### Copilot Agent Mode - -- Issue: Agents do not appear in one repository - Action: Run `vstack install --target /path/to/your/project` (or run `vstack install` from that repository root), then reload VS Code. -- Issue: Agents appear in one repository but not another - Action: Install per repository with `vstack install --target ...` in each repo, or use `vstack install --global` for profile-wide defaults. -- Issue: Agents still do not appear - Action: Confirm templates exist under `src/vstack/_templates/agents/`, then run `Developer: Reload Window` in VS Code. -- Issue: Agent does not execute actions - Action: Make sure Copilot is in Agent Mode, not Ask or Edit mode. -- Issue: Files were preserved during install and vstack agents are not visible - Action: Run `vstack install --dry-run --target .` to see which files were preserved. Then use `--force-name type/name` to overwrite a specific file (e.g. `--force-name agent/engineer`), `--adopt-name type/name` to take ownership without overwriting, or `--force` to overwrite everything. - -### CI parity and badges - -- Issue: Checks pass in CI but fail locally - Action: Run `make bootstrap` once per clone, then run `make check`. -- Issue: Need to mirror the CI quality gate locally - Action: Run `make ci`. -- Issue: Verify or Security badge shows no status - Action: These workflows are PR-based, so main may not always show a latest status. - -### VS Code search noise - -- Issue: Search results are noisy - Action: Exclude `.venv`, `venv`, `env`, `node_modules`, `__pycache__`, `dist`, `build`, and `.git`. -- Issue: Search still feels slow or cluttered - Action: Configure both `search.exclude` and `files.watcherExclude` in VS Code settings. - -______________________________________________________________________ - -## 🔄 Workflow - -```mermaid -flowchart LR - A[Product intent] --> B["@product"] - B --> C["@architect"] - C --> D["@designer"] - D --> E["@engineer"] - E --> F["@tester"] - F --> G["@release"] - B -. focused procedure .-> H["requirements or vision"] - F -. focused procedure .-> I["verify, security, performance"] -``` - -The exact deliverable can be a microservice, API, package, library, app, or broader -system. The product vision defines scope; vstack defines how the work is carried. - ______________________________________________________________________ -## 🧱 Building Blocks - -| Artifact type | Purpose | Typical invocation | -| ------------- | ---------------------------------------------------------- | ---------------------- | -| Agents | Main operating interface for role-based work | `@product`, `@tester` | -| Skills | Reusable task procedures | `/verify`, `/security` | -| Instructions | Baseline policy and repository guardrails | auto-loaded by context | -| Prompts | Reusable prompt artifacts where direct prompting is useful | explicit prompt use | - -### Prompt catalog - -Prompts are `.prompt.md` files installed to `.github/prompts/`. Use them when you want a focused, -structured output for a specific task without a full agent session. - -**How to invoke:** - -- **Command palette:** `Chat: Run Prompt File` → select the prompt by name. -- **Copilot Chat attach button:** click the paperclip icon → select "Prompt Files". -- Some prompts accept an argument — pass it as the first message after selecting the prompt. - -| Prompt | Purpose | Agent | Argument | -| ------------------- | --------------------------------------------------------- | ------------ | ------------------------------------------------ | -| `api-design-review` | Review an API design or OpenAPI spec for correctness | `@designer` | OpenAPI spec file, design doc, or endpoint scope | -| `architecture-risk` | Identify architectural risks and mitigation priorities | `@architect` | design doc, ADR, or architecture scope | -| `code-review` | Review a change for bugs, regressions, and missing tests | `@engineer` | scope or files to review | -| `dependency-audit` | Audit dependencies for vulnerabilities and licence risks | `@tester` | dependency manifest, lockfile, or package scope | -| `incident-timeline` | Build an evidence-based incident timeline and post-mortem | `@tester` | logs, alerts, timeline notes, or incident ID | -| `migration-safety` | Review DB migration safety, rollback, and zero-downtime | `@engineer` | migration files, schema, or rollout plan | -| `release-readiness` | Evaluate release readiness from reports and open blockers | `@release` | scope, release date, or branch | - -Boundary rule: - -- Policies belong in instructions. -- Procedures belong in skills. - -See [docs/design/instructions.md](docs/design/instructions.md), -[docs/design/skills.md](docs/design/skills.md), and -[docs/architecture/adr/013-instructions-vs-skills-boundary.md](docs/architecture/adr/013-instructions-vs-skills-boundary.md). - -______________________________________________________________________ - -## 🧠 Model Guidance - -| Use case | Recommended model floor (or higher) | -| ------------------------ | ---------------------------------------------------- | -| `@product`, `@architect` | Claude Sonnet 4.6+, GPT-5.3-Codex+, Claude Opus 4.6+ | -| `@tester`, `@engineer` | Claude Sonnet 4.6+ or GPT-5.3-Codex+ | -| `@release` | Claude Sonnet 4.6+ | -| Complex debugging | GPT-5.3-Codex+ or Claude Opus 4.6+ | -| Quick tasks | Any model with tool and agent-mode support | - -Why these version floors: - -- Reliable tool use and structured instruction following in Agent Mode. -- Better multi-step planning and stronger handling of long procedural prompts. -- Better compatibility with subagent-style orchestration and role handoffs. -- More stable output quality for repository-scale reviews and verification loops. - -Practical cost guidance: - -- Use Claude Sonnet 4.6+ as the default for most runs (best speed/cost balance). -- Use GPT-5.3-Codex+ for deep code reasoning, debugging, and implementation-heavy tasks. -- Use Claude Opus 4.6+ selectively for high-ambiguity architecture tradeoffs where the extra cost is justified. - -______________________________________________________________________ - -## 💡 Practical Tips - -### Give the agent project context - -```text -/verify Please first read CONTRIBUTING.md for test commands -``` - -### Scope the agent's focus - -```text -/code-review Review changes in src/api/ only -/security Audit the authentication module in src/auth/ -``` - -### Control response verbosity - -Every role agent supports the `concise` skill: - -```text -/concise normal - full explanations -/concise compact - shorter prose, same technical accuracy -/concise ultra - maximum brevity -/concise status - show active mode, session override, and agent default -/concise on - alias for compact -/concise off - alias for normal -``` - -The mode is session-scoped. Security warnings and destructive action prompts always -use `normal` regardless of active mode. - -### Typical workflow for a new feature - -```text -1. /vision -2. /architecture -3. (implement) -4. /verify -5. /release -``` - -______________________________________________________________________ - -More info: [docs/product/roadmap.md](docs/product/roadmap.md), [docs/architecture/overview.md](docs/architecture/overview.md) - -______________________________________________________________________ - -## 🛠️ Development - -Requires **Poetry** and **Python 3.11-3.14**. - -```bash -git clone git@github.com:eschaar/vstack.git -cd vstack -poetry install -``` - -### Common commands - -```bash -make help -make bootstrap -make install -make check -make vstack-install -make ci -poetry run vstack validate -poetry run vstack install -poetry run vstack verify -make test-local -make test -make tox -make tox-all -``` - -### Multi-version local testing with pyenv - -```bash -pyenv install 3.11.14 -pyenv install 3.12.12 -pyenv install 3.13.12 -pyenv install 3.14.3 -pyenv local 3.14.3 3.13.12 3.12.12 3.11.14 -``` - -### Editing templates - -Source of truth is always under `src/vstack/_templates/`. Do not edit generated -files in `.github/`. - -```bash -vim src/vstack/_templates/skills/verify/template.md -vim src/vstack/_templates/agents/engineer/template.md -vim src/vstack/_templates/instructions/python/template.md -vim src/vstack/_templates/instructions/typescript/template.md -poetry run vstack validate -poetry run pytest -poetry run vstack install -``` +## 🧪 Try it now -______________________________________________________________________ +Open Copilot Chat, switch to Agent mode, and select the `planner` agent. -## 🗂️ Repository Structure +Prompt to run: ```text -vstack/ -├── src/vstack/ ← Python package and source of truth -│ ├── artifacts/ ← generic artifact generation and metadata -│ ├── frontmatter/ ← parser, serializer, schema -│ ├── agents/ ← agent configuration and wrappers -│ ├── skills/ ← skill configuration and wrappers -│ ├── instructions/ ← instruction configuration and wrappers -│ ├── prompts/ ← prompt configuration and wrappers -│ ├── cli/ ← install, verify, uninstall, parser -│ └── _templates/ ← hand-authored templates -├── docs/ -│ ├── architecture/ ← architecture docs and ADRs -│ ├── design/ ← design, workflow, skills, instructions -│ └── product/ ← vision, requirements, roadmap -├── tests/ ← unit and integration coverage -├── .github/ ← generated artifacts and repository automation -├── pyproject.toml ← packaging and tooling config -└── Makefile ← local development tasks +Run the workflow for this repository change. ``` ______________________________________________________________________ -## 🚦 CI and Release Automation - -| Workflow | Trigger | Purpose | -| --------------- | --------------------------------------------- | ------------------------------------------------------------------- | -| `commit.yml` | Push to non-main branches and PR to `main` | commit/branch policy and lint/typecheck gate | -| `check.yml` | Push to non-main branches and PR to `main` | single-version unit tests (py3.11) | -| `verify.yml` | Pull request to `main` | cross-version test matrix (py3.11–3.14) and artifact install/verify | -| `security.yml` | Pull request to `main` | dependency audit and secret scanning | -| `codeql.yml` | Push/pull request to `main` + weekly schedule | code scanning for GitHub Actions and Python | -| `automerge.yml` | Pull request target to `main` | safe Dependabot auto-merge policy | -| `release.yml` | Push to `main` | Release Please orchestration (release PR, changelog, tags) | -| `publish.yml` | GitHub release `published` | build from release tag and publish to PyPI | - -Commit policy specifics: - -- Type validation is configured via `cchk.toml` and enforced by `commit-check` in `commit.yml`. -- Commit subject length is limited to 100 characters. -- Branch names use the `type/description` convention. -- Allowed branch types are `feature`, `bugfix`, `hotfix`, `release`, `chore`, `feat`, `fix`, `docs`, `refactor`, `perf`, `test`, `ci`, `build`, `style`, `opt`, `patch`, and `dependabot`. - -Recommended branch protection for `main`: - -- Require PR before merge. -- Require status checks from `commit.yml`, `check.yml`, `verify.yml`, `security.yml`, and `codeql.yml`. -- Disallow force pushes and branch deletion. - -Full pipeline documentation: [docs/design/cicd.md](docs/design/cicd.md) - -______________________________________________________________________ - -## 📚 Further Reading +## 📚 User documentation -- [docs/architecture/overview.md](docs/architecture/overview.md) -- [docs/design/overview.md](docs/design/overview.md) -- [docs/design/cicd.md](docs/design/cicd.md) -- [docs/design/workflow.md](docs/design/workflow.md) -- [docs/design/skills.md](docs/design/skills.md) -- [docs/product/roadmap.md](docs/product/roadmap.md) -- [CONTRIBUTING.md](CONTRIBUTING.md) +- [Start here](docs/user/start-here.md) +- [User docs index](docs/user/README.md) +- [Install and upgrade guide](docs/user/how-to/install-and-upgrade.md) +- [Troubleshooting](docs/user/how-to/troubleshooting.md) +- [CLI commands reference](docs/user/reference/cli-commands.md) +- [Configuration reference](docs/user/reference/configuration.md) +- [Hooks reference](docs/user/reference/hooks.md) +- [Prompts overview](docs/user/reference/prompts-overview.md) +- [Skills overview](docs/user/reference/skills-overview.md) +- [Instructions overview](docs/user/reference/instructions-overview.md) +- [Workflow modes explanation](docs/user/explanation/workflow-modes.md) ______________________________________________________________________ -## 📄 License +## 🤝 Contributing and project docs -MIT. See [LICENSE](LICENSE). +- [Contributing](CONTRIBUTING.md) +- [Security policy](SECURITY.md) +- [Architecture docs](docs/architecture/overview.md) +- [Design docs](docs/design/overview.md) +- [Product docs](docs/product/vision.md) diff --git a/docs/architecture/adr/017-checksum-backfill-on-upgrade.md b/docs/architecture/adr/017-checksum-backfill-on-upgrade.md index f55e0c61..1c0cca90 100644 --- a/docs/architecture/adr/017-checksum-backfill-on-upgrade.md +++ b/docs/architecture/adr/017-checksum-backfill-on-upgrade.md @@ -187,74 +187,3 @@ entries. No direct impact. Checksum backfill is a manifest-maintenance operation. It would not change if a multi-role orchestration layer is introduced. - -______________________________________________________________________ - -## designer handoff - -The following sections of `docs/design/design.md` must be updated to reflect this -decision before implementation begins. - -### 1. Section 1.1 — artifact lifecycle states - -Add `managed-legacy` as an explicit named state in the state table: - -```text -managed-legacy — in manifest; checksum absent; file exists but drift cannot be determined -``` - -Extend the state machine diagram with backfill transitions: - -```text -managed-legacy --> managed : manifest upgrade --backfill (VSTACK-META footer present) -managed-legacy --> managed-legacy : manifest upgrade --backfill (no footer; entry unchanged) -managed-legacy --> clean : install --force or --force-name rewrites file + records checksum -``` - -### 2. Section 6 — `manifest upgrade` command contract - -Add `--backfill` to the command synopsis and flag table: - -```bash -vstack manifest upgrade [--target ] [--backfill] -``` - -| Flag | Description | -| ------------ | --------------------------------------------------------------------------------------------------------------- | -| `--backfill` | Compute and store SHA-256 checksums for tracked entries with no checksum, gated by `VSTACK-META` identity check | - -Extend the exit-code table: - -| Exit code | Meaning | -| --------- | -------------------------------------------------------------------------- | -| `0` | Manifest upgraded (and backfilled if `--backfill`; partial backfill is OK) | -| `1` | Manifest missing, unreadable, or parse error | - -Document that `--backfill` is a one-way operation: current on-disk state becomes the -canonical baseline for backfilled entries. Users should be informed which entries were -backfilled and which were skipped (missing footer or missing file). - -### 3. Section 2.1 — manifest domain interface - -Document the `with_backfilled_checksums` method on `Manifest`: - -```python -def with_backfilled_checksums( - self, - install_dir: Path, -) -> tuple["Manifest", list[str], list[str]]: - """Return an updated manifest with checksums backfilled where possible. - - For each entry with ``checksum=None``, reads the on-disk file at - ``install_dir / entry.file``. Computes SHA-256 and stores it only when - the file contains a ``VSTACK-META`` footer comment. - - Returns: - (updated_manifest, backfilled_names, skipped_names) - where ``skipped_names`` are entries whose files existed but lacked the footer. - """ -``` - -Document: `CommandService.manifest_upgrade` calls `upgraded()` first (schema -migration), then `with_backfilled_checksums(install_dir)` when `--backfill` is set, -and passes the result to `manifest_file.write()`. diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index 83c77c9f..aad56039 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -147,7 +147,8 @@ tools: - vscode - todo - agent -agents: ["*"] +agents: + - architect target: vscode user-invocable: true --- @@ -171,6 +172,8 @@ dispatch flow. | Per-command modules | `install`, `init`, `verify`, `status`, `uninstall`, `validate`, `manifest` — one class each | | `helpers.py` | Shared install/uninstall utilities (name normalization, manifest preservation) | +For the full component table and dispatch flow, see `docs/design/overview.md`. + ______________________________________________________________________ ## non-functional requirements @@ -233,9 +236,12 @@ flowchart TD GR --> PR[PR opened] ``` -See `docs/architecture/adr/023-workflow-contract.md`, `docs/architecture/adr/024-subagent-orchestration.md`, -`docs/architecture/adr/028-dag-dependency-semantics.md`, `docs/architecture/adr/029-multi-agentic-execution-model.md`, -`docs/architecture/adr/010-artifact-flow.md`, and `docs/design/workflow.md` for pipeline and gate detail. +See [docs/architecture/adr/023-workflow-contract.md](adr/023-workflow-contract.md), +[docs/architecture/adr/024-subagent-orchestration.md](adr/024-subagent-orchestration.md), +[docs/architecture/adr/028-dag-dependency-semantics.md](adr/028-dag-dependency-semantics.md), +[docs/architecture/adr/029-multi-agentic-execution-model.md](adr/029-multi-agentic-execution-model.md), +[docs/architecture/adr/010-artifact-flow.md](adr/010-artifact-flow.md), and +[docs/design/workflow.md](../design/workflow.md) for pipeline and gate detail. ______________________________________________________________________ @@ -250,7 +256,7 @@ See individual files for context, decision, alternatives, and rationale. | 002 | Artifact naming and compatibility policy | accepted | | | 003 | Backend-first verify | accepted | | | 004 | Direct execution and orchestrated pipeline | superseded | Superseded by ADR-024 | -| 005 | VS Code prompt format | accepted | | +| 005 | VS Code prompt format | superseded | Superseded by ADR-009 | | 006 | No runtime dependency on external binaries | accepted | | | 007 | Python runtime | accepted | | | 008 | Agents over prompts | accepted | | diff --git a/docs/design/agents.md b/docs/design/agents.md index 375f2dd7..578671f1 100644 --- a/docs/design/agents.md +++ b/docs/design/agents.md @@ -1,7 +1,7 @@ # vstack — agents > Maintained by: **designer** role\ -> Last updated: 2026-05-13\ +> Last updated: 2026-05-14\ > VS Code docs: [custom agents](https://code.visualstudio.com/docs/copilot/customization/custom-agents) · [agents overview](https://code.visualstudio.com/docs/copilot/agents/overview) ## what are agents? @@ -61,27 +61,27 @@ ______________________________________________________________________ ## config.yaml fields -`config.yaml` is plain YAML (no `---` markers). vstack reads it at generation time and emits only recognised schema fields to the `.agent.md` frontmatter. Unknown fields (`version`, `handoffs`, …) are silently dropped from output. +`config.yaml` is plain YAML (no `---` markers). vstack reads it at generation time and emits only recognised schema fields to the `.agent.md` frontmatter. Unknown fields (for example `version`) are silently dropped from output. Style rule: long `description` and `handoffs.prompt` values should use YAML block scalars (`>`). A test enforces this when inline text exceeds 100 characters. ### emitted to frontmatter -| Field | Type | Required | Notes | -| -------------------------- | -------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `name` | string | no | Overrides filename as picker label | -| `description` | string | no | Shown as placeholder text in chat input | -| `argument-hint` | string | no | Hint text shown after `@agent` in chat | -| `tools` | list | no | Tools available to this agent (see below) | -| `agents` | list | no | Subagents this agent may invoke; `["*"]` = all | -| `model` | string or list | no | Force one or more model IDs. In this repo, model values are pinned in templates and validated by tests; change only after verifying support in your VS Code/Copilot environment. | -| `user-invocable` | bool | no | `true` = show in agents dropdown (default) | -| `disable-model-invocation` | bool | no | `true` = prevent other agents from calling this one | -| `target` | string | no | `vscode` (default) or `github-copilot` | -| `handoffs` | object-list | no | Sequential workflow handoffs — see [handoffs](#handoffs) below | -| `mcp-servers` | raw YAML | no | MCP server config (`github-copilot` target only) | -| `hooks` | raw YAML | no | Chat hooks (Preview — requires `chat.useCustomAgentHooks` setting) | -| `metadata` | raw YAML | no | String key/value annotations (`github-copilot` target only) | +| Field | Type | Required | Notes | +| -------------------------- | ----------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `name` | string | no | Overrides filename as picker label | +| `description` | string | no | Shown as placeholder text in chat input | +| `argument-hint` | string | no | Hint text shown after `@agent` in chat | +| `tools` | list | no | Tools available to this agent (see below) | +| `agents` | list | no | Subagents this agent may invoke; wildcard delegation (`["*"]`) is rejected by source verification, so use an explicit allowlist | +| `model` | list | no | Ordered model fallbacks (for example `auto` first, then role-appropriate pinned IDs). This preserves role intent while staying resilient to policy or availability drift. | +| `user-invocable` | bool | no | `true` = show in agents dropdown (default) | +| `disable-model-invocation` | bool | no | `true` = prevent other agents from calling this one | +| `target` | string | no | `vscode` (default) or `github-copilot` | +| `handoffs` | object-list | no | Sequential workflow handoffs — see [handoffs](#handoffs) below | +| `mcp-servers` | raw YAML | no | MCP server config (`github-copilot` target only) | +| `hooks` | raw YAML | no | Chat hooks (Preview — requires `chat.useCustomAgentHooks` setting) | +| `metadata` | raw YAML | no | String key/value annotations (`github-copilot` target only) | ### vstack-internal only (not emitted) @@ -308,7 +308,7 @@ ______________________________________________________________________ 1. Create `src/vstack/_templates/agents//config.yaml` with at minimum `name` and `description`. 1. Create `src/vstack/_templates/agents//template.md` with the agent instructions. 1. Regenerate: `vstack install` -1. Verify: `vstack verify` or `python3 -m pytest test/ -q` +1. Verify: `vstack verify` or `python3 -m pytest tests/ -q` ______________________________________________________________________ @@ -327,7 +327,8 @@ tools: - vscode - todo - agent -agents: ["*"] +agents: + - architect items: dir: architecture input: diff --git a/docs/design/cicd.md b/docs/design/cicd.md index e2a035d2..5e774df8 100644 --- a/docs/design/cicd.md +++ b/docs/design/cicd.md @@ -1,7 +1,7 @@ # vstack CI/CD Pipeline > Maintained by: **designer** role\ -> Last updated: 2026-04-27 +> Last updated: 2026-05-14 ## Overview @@ -19,16 +19,16 @@ Their jobs are merge-blocking when configured as required status checks in the ` ## What Runs On Which Event -| Workflow | Trigger | Responsibility | -| --------------------------------- | ------------------------------------------------- | ---------------------------------------------------------------------------------- | -| `.github/workflows/commit.yml` | push to non-main branches, pull_request to `main` | Commit/branch policy and lint/typecheck | -| `.github/workflows/check.yml` | push to non-main branches, pull_request to `main` | Single-version unit tests (py3.11) | -| `.github/workflows/verify.yml` | pull_request to `main` | Cross-version test matrix (py3.11–3.14) and artifact install verify | -| `.github/workflows/security.yml` | pull_request to `main` | Dependency vulnerability scan and secret scan | -| `.github/workflows/codeql.yml` | push/pull_request to `main` + weekly schedule | Code scanning for GitHub Actions and Python | -| `.github/workflows/automerge.yml` | pull_request_target to `main` | Dependabot auto-approve/auto-merge policy gate | -| `.github/workflows/release.yml` | push to `main`, workflow_dispatch | Release Please orchestration: release PR lifecycle, changelog, tag, GitHub release | -| `.github/workflows/publish.yml` | release `published` | Build artifacts from release tag and publish to PyPI | +| Workflow | Trigger | Responsibility | +| --------------------------------- | ------------------------------------------------- | ----------------------------------------------------------------------------------- | +| `.github/workflows/commit.yml` | push to non-main branches, pull_request to `main` | Commit/branch policy and lint/typecheck | +| `.github/workflows/check.yml` | push to non-main branches, pull_request to `main` | Single-version unit tests (py3.11) | +| `.github/workflows/verify.yml` | pull_request to `main` | Cross-version test matrix (py3.11–3.14), fixture tests, and artifact install verify | +| `.github/workflows/security.yml` | pull_request to `main` | Dependency vulnerability scan and secret scan | +| `.github/workflows/codeql.yml` | push/pull_request to `main` + weekly schedule | Code scanning for GitHub Actions and Python | +| `.github/workflows/automerge.yml` | pull_request_target to `main` | Dependabot auto-approve/auto-merge policy gate | +| `.github/workflows/release.yml` | push to `main`, workflow_dispatch | Release Please orchestration: release PR lifecycle, changelog, tag, GitHub release | +| `.github/workflows/publish.yml` | release `published` | Build artifacts from release tag and publish to PyPI | ## Human Sequence (Primary) @@ -51,7 +51,7 @@ sequenceDiagram GH->>GH: [4a] commit.yml / Validate Commit Messages GH->>GH: [4b] commit.yml / Format Lint Typecheck GH->>GH: [4c] check.yml / Unit Tests (py3.11) - GH->>GH: [4d] verify.yml / Tests (py3.11) — Tests (py3.14) [matrix ×4] + GH->>GH: [4d] verify.yml / Tests (py3.11) — Tests (py3.14) [matrix ×4] + Fixture Tests (fast) GH->>GH: [4e] verify.yml / Artifact Install Verify GH->>GH: [4f] security.yml / Dependency and Secret Scan Dev->>GH: [5] Approve and merge PR @@ -59,7 +59,7 @@ sequenceDiagram GH->>GH: [6] Push to main triggers release.yml GH->>RP: [7] release.yml / Release Please (PR + Tag + Notes) RP-->>GH: [8] Create or update release PR branch + PR - GH->>GH: [9a] verify.yml / Tests (py3.11) — Tests (py3.14) [matrix ×4] + GH->>GH: [9a] verify.yml / Tests (py3.11) — Tests (py3.14) [matrix ×4] + Fixture Tests (fast) GH->>GH: [9b] verify.yml / Artifact Install Verify GH->>GH: [9c] security.yml / Dependency and Secret Scan @@ -82,7 +82,7 @@ sequenceDiagram 1. `[4a]` `commit.yml / Validate Commit Messages` — validates commit messages, branch policy, and reserved `docs(changelog)` scope. 1. `[4b]` `commit.yml / Format Lint Typecheck` — runs `make format-check`, `make lint`, `make typecheck` on py3.11. 1. `[4c]` `check.yml / Unit Tests` — runs `make test-local` on py3.11. -1. `[4d]` `verify.yml / Tests (py3.11)` through `Tests (py3.14)` — 4 parallel matrix jobs run `make test-local`. +1. `[4d]` `verify.yml / Tests (py3.11)` through `Tests (py3.14)` plus `Fixture Tests (fast)` — matrix jobs run `make test-local`; fixture job runs `make test-fixtures`. 1. `[4e]` `verify.yml / Artifact Install Verify` — installs vstack into a temp dir and runs `vstack verify`. 1. `[4f]` `security.yml / Dependency and Secret Scan` — pip-audit + trufflehog diff scan. 1. `[5]` Approve and merge PR. @@ -94,7 +94,7 @@ sequenceDiagram 1. `[11]` Push to `main` triggers `release.yml` again. 1. `[12]` `release.yml / Release Please (PR + Tag + Notes)` creates the tag and GitHub release. 1. `[13]` `release: published` triggers `publish.yml`. Job runs only if `prerelease == false`, tag matches `X.Y.Z`, and release actor is trusted. -1. `[14]` `publish.yml / Build and Publish to PyPI` — builds wheel + sdist, smoke tests, validates artifact version, publishes via OIDC. +1. `[14]` `publish.yml / Build and Publish to PyPI` — builds wheel + sdist, smoke tests, validates artifact version, publishes via OIDC with API-token fallback when configured. ## Dependabot Sequence @@ -135,7 +135,7 @@ sequenceDiagram GH->>GH: [11] Push to main triggers release.yml GH->>RP: [12] release.yml / Release Please (PR + Tag + Notes) RP-->>GH: [13] Create or update release PR branch + PR - GH->>GH: [14a] verify.yml / Tests (py3.11) — Tests (py3.14) [matrix ×4] + GH->>GH: [14a] verify.yml / Tests (py3.11) — Tests (py3.14) [matrix ×4] + Fixture Tests (fast) GH->>GH: [14b] verify.yml / Artifact Install Verify GH->>GH: [14c] security.yml / Dependency and Secret Scan @@ -158,7 +158,7 @@ sequenceDiagram 1. `[4a]` `commit.yml / Validate Commit Messages` — commit message policy on PR commits. 1. `[4b]` `commit.yml / Format Lint Typecheck`. 1. `[4c]` `check.yml / Unit Tests`. -1. `[4d]` `verify.yml / Tests (py3.11)` through `Tests (py3.14)` — 4 parallel matrix jobs. +1. `[4d]` `verify.yml / Tests (py3.11)` through `Tests (py3.14)` plus `Fixture Tests (fast)`. 1. `[4e]` `verify.yml / Artifact Install Verify`. 1. `[4f]` `security.yml / Dependency and Secret Scan`. 1. `[5]` `automerge.yml` job runs only when `github.actor == 'dependabot[bot]'`. @@ -170,7 +170,7 @@ sequenceDiagram 1. `[11]-[13]` Push to `main` triggers `release.yml / Release Please (PR + Tag + Notes)`, release PR created/updated. 1. `[14a]`–`[14c]` `verify.yml` and `security.yml` run on the release PR — GitHub App token triggers `pull_request` events normally. 1. `[15]-[17]` Release PR manually approved and merged, then tag and GitHub release created. -1. `[18]-[19]` `publish.yml / Build and Publish to PyPI` runs if `prerelease == false`, SemVer tag, and trusted actor. +1. `[18]-[19]` `publish.yml / Build and Publish to PyPI` runs if `prerelease == false`, SemVer tag, and trusted actor, with API-token fallback if trusted publishing fails. ## Failure and Retry Behavior @@ -193,6 +193,7 @@ Configure via **Settings → Rules → Rulesets** on GitHub. - `Commit / Format Lint Typecheck` - `Check / Unit Tests` - `Verify / Tests (py3.11)`, `Verify / Tests (py3.12)`, `Verify / Tests (py3.13)`, `Verify / Tests (py3.14)` +- `Verify / Fixture Tests (fast)` - `Verify / Artifact Install Verify` - `Security / Dependency and Secret Scan` - `CodeQL / Analyze (actions)`, `CodeQL / Analyze (python)` @@ -226,7 +227,7 @@ Configure via **Settings → Environments**. ### Release PR checks (`verify.yml` / `security.yml`) -Release-please uses a GitHub App token (`APP_ID` + `APP_PRIVATE_KEY` secrets in `release.yml`). +Release-please uses a GitHub App token (`APP_CLIENT_ID` + `APP_PRIVATE_KEY` secrets in `release.yml`). PRs created via a GitHub App token are treated by GitHub as external-actor events, so `pull_request` triggers fire normally. As a result, `verify.yml` and `security.yml` run on release PRs the same as on any other PR to `main`. @@ -235,13 +236,11 @@ Release PRs only modify `CHANGELOG.md` and version metadata (e.g. version in `py The test matrix and artifact verify will pass as normal; the security diff scan will cover only the changelog and version file changes. No special Ruleset bypass configuration is needed. -### `pypa/gh-action-pypi-publish@release/v1` +### `pypa/gh-action-pypi-publish` pinning policy -This action intentionally uses a rolling `release/v1` branch reference, as PyPA's own -documented recommendation. Security patches (OIDC, attestation fixes) are delivered via -this rolling branch without requiring a separately versioned release from PyPA. Dependabot -monitors the `github-actions` ecosystem and opens a PR when the branch advances to a newer -commit. No manual tracking is needed. +`publish.yml` pins `pypa/gh-action-pypi-publish` to an immutable commit SHA. +This keeps supply-chain behavior deterministic while still allowing controlled updates +through Dependabot PRs. ## Related Files diff --git a/docs/design/hook-yaml-schema.md b/docs/design/hook-yaml-schema.md deleted file mode 100644 index 89195f7a..00000000 --- a/docs/design/hook-yaml-schema.md +++ /dev/null @@ -1,265 +0,0 @@ -# Hook YAML Template Format - -> Specification for repository hook YAML source templates in vstack. - -## Overview - -Hook templates are authored in YAML for readability and multiline command support. -The YAML file (`hook.yaml`) contains both metadata and the GitHub Copilot hooks definition. - -At install time, vstack: - -1. Loads hook template YAML -1. Validates metadata and structure -1. Generates JSON output at `.github/hooks/.json` - -This decouples human-friendly source (YAML with long commands) from machine-readable output (JSON). - -______________________________________________________________________ - -## Schema - -```yaml -version: - # Artifact template version, incremented on semantic changes. - # Example: 20260510003 - -metadata: - name: - # Hook identifier, used in URLs, file names, and logging. - # Must be lowercase, alphanumeric + hyphen. - # Example: "session-audit" - - description: - # Human-readable explanation of hook behavior and intent. - # Supports multi-line folded text. - # Example: "Records session start/end events to JSONL audit logs." - - purpose: - # Functional classification. - # - audit: event recording without enforcement - # - security: threat detection or access control - # - quality: code style and compliance - - security_level: - # Risk classification. - # - low: read-only or non-destructive operations - # - high: may block, modify, or enforce policy - - mode_default: - # Default VSTACK_HOOKS_MODE when environment variable is unset. - # - audit: non-intrusive logging and detection - # - enforce: strict policy, optional tool execution - # Most hooks default to 'audit' for safety. - - execution_context: - # Expected environment where this hook runs. - # - copilot-hook-runtime: GitHub Copilot hook system (default) - # - ci: CI/CD pipeline environment - # - local: developer machine - # Hints for tool and permission assumptions. - - dependencies: - required: [, ...] - # Tools/commands that MUST exist for hook to function. - # Hook fails or degrades if missing. - # Example: ["git", "make"] - - optional: [, ...] - # Tools used conditionally, usually in 'enforce' mode. - # Hook logs when missing but continues normally. - # Example: ["gitleaks", "make"] - -# GitHub Copilot hooks envelope -hooks: - : - # Event name: one of: - # - sessionStart, sessionEnd - # - userPromptSubmitted - # - preToolUse, postToolUse - # - errorOccurred - - - type: command - # Currently, only type "command" is supported. - - description: - # Optional: brief explanation of this action's behavior. - # Supports multi-line text. - - bash: - # Bash script to execute. Supports multi-line. - # Input arrives via stdin (JSON payload from Copilot). - # Output to stdout for responses (JSON for permission decisions). - # Variables: $VSTACK_HOOKS_MODE (audit|enforce) - - powershell: - # PowerShell script to execute. Supports multi-line. - # Read stdin with: $inputText = [Console]::In.ReadToEnd() - # Use environment variable: $env:VSTACK_HOOKS_MODE - - cwd: - # Working directory. "." = workspace root. - - timeoutSec: - # Maximum execution time in seconds. Default 30. -``` - -______________________________________________________________________ - -## Example: Complete Hook Template - -This is a minimal example to show the schema shape. Production hooks in vstack also -use a dated log directory pattern and dedicated retention cleanup logic. - -```yaml -version: 20260510003 - -metadata: - name: session-audit - description: | - Records session start and end events to structured JSONL logs - in .vstack/logs/ for audit trail and compliance verification. - purpose: audit - security_level: low - mode_default: audit - execution_context: copilot-hook-runtime - dependencies: - required: [] - optional: [] - -hooks: - sessionStart: - - type: command - description: | - Log session start event with timestamp and context. - bash: | - mkdir -p .vstack/logs - input="$(cat)" - printf '%s\n' "$input" >> .vstack/logs/hook-session-start.jsonl - powershell: | - New-Item -ItemType Directory -Force -Path .vstack/logs | Out-Null - $inputText = [Console]::In.ReadToEnd() - Add-Content -Path .vstack/logs/hook-session-start.jsonl -Value $inputText - cwd: "." - timeoutSec: 10 - - sessionEnd: - - type: command - description: | - Log session end event for correlation with start. - bash: | - mkdir -p .vstack/logs - input="$(cat)" - printf '%s\n' "$input" >> .vstack/logs/hook-session-end.jsonl - powershell: | - New-Item -ItemType Directory -Force -Path .vstack/logs | Out-Null - $inputText = [Console]::In.ReadToEnd() - Add-Content -Path .vstack/logs/hook-session-end.jsonl -Value $inputText - cwd: "." - timeoutSec: 10 -``` - -______________________________________________________________________ - -## Authoring Guidance - -### Multi-line Scripts - -Use YAML literal folded scalars (`|-` or `|`) for scripts: - -```yaml -bash: | - mkdir -p .vstack/logs - input="$(cat)" - if [ -z "$input" ]; then - exit 1 - fi - printf '%s\n' "$input" >> .vstack/logs/audit.jsonl -``` - -### Bash Best Practices - -- Read stdin early: `input="$(cat)"` -- Check tool availability: `command -v git >/dev/null 2>&1` -- Use early exits: `[ "$VSTACK_HOOKS_MODE" != "enforce" ] && exit 0` -- Log errors to `.vstack/logs/hook-*.log`, not stderr - -### PowerShell Best Practices - -- Read stdin: `$inputText = [Console]::In.ReadToEnd()` -- Check commands: `Get-Command gitleaks -ErrorAction SilentlyContinue` -- Suppress output: `command *> $null` -- Use `$env:VSTACK_HOOKS_MODE` for mode detection - -### Environment Variables - -- `VSTACK_HOOKS_MODE` (string: `audit` | `enforce`) - - Default: `audit` if unset - - User can override in session -- `VSTACK_HOOK_LOG_DIR` (string path) - - Default: `.vstack/logs` - - Hooks write under dated directories: `/YYYYMMDD/` -- `VSTACK_HOOKS_LOG_RETENTION_DAYS` (positive integer) - - Default: `7` - - Used by `log-retention-cleanup` to prune older dated directories -- `VSTACK_HOOKS_CLEANUP_LOG` (`0` | `1`) - - Default: `0` - - When set to `1`, retention hook emits cleanup summary logs even when no deletions occur -- `.vstack/config.yaml` available for shared settings - -______________________________________________________________________ - -## Generation and Installation - -### Source Workflow - -1. User authors hook template: `src/vstack/_templates/hooks//hook.yaml` -1. HookGenerator loads and validates YAML -1. Generator constructs GitHub Copilot hooks JSON envelope -1. Output written to `.github/hooks/.json` - -### Local Project Workflow - -1. User copies/modifies hook template to `.vstack/templates/hooks//hook.yaml` -1. User runs `vstack install` -1. HookGenerator regenerates `.github/hooks/.json` from YAML -1. Manifest updated with checksum - -______________________________________________________________________ - -## vstack Integration - -### Seeding - -When `vstack init` or `vstack install` runs locally, hook templates are seeded to: - -``` -.vstack/templates/hooks//hook.yaml -``` - -Users can modify these templates directly; `vstack install` will regenerate `.github/hooks/` JSON from them. - -### Manifest Tracking - -Hooks are tracked in `.vstack/vstack.json`: - -```json -{ - "hooks": { - "session-audit": { - "source_path": "src/vstack/_templates/hooks/session-audit/hook.yaml", - "output_path": ".github/hooks/session-audit.json", - "checksum": "sha256:abc123..." - } - } -} -``` - -### Verification - -`vstack verify` checks: - -- Source YAML is valid and well-formed -- Metadata fields are correct -- Hook structure complies with envelope spec -- Generated JSON checksums match manifest diff --git a/docs/design/hooks.md b/docs/design/hooks.md index 0341d7e5..71e78a43 100644 --- a/docs/design/hooks.md +++ b/docs/design/hooks.md @@ -1,7 +1,7 @@ # vstack - hooks design > Maintained by: **designer** role\ -> Last updated: 2026-05-11 +> Last updated: 2026-05-14 ## overview @@ -62,25 +62,49 @@ Hook templates follow the GitHub Copilot hooks envelope: } ``` +### 1.3 version fields and ownership + +Hook artifacts intentionally carry two independent version concepts: + +| Field | Location | Example | Purpose | +| ------------------ | ----------------------------------------------------------------- | ------------- | ----------------------------------------------------------------------------------------------- | +| Payload `version` | Generated `.github/hooks/.json` | `1` | Copilot hook envelope/contract version. | +| Artifact `version` | Source `src/vstack/_templates/hooks//hook.yaml` (top-level) | `20260514002` | vstack template revision token used in manifest and footer metadata for ownership/drift checks. | + +Design implication: + +- Do not couple payload contract versioning to template revision versioning. +- A future Copilot contract bump (`version: 2`) should be handled in hook generator render logic. +- Routine template changes should continue to bump only the artifact revision token. + ______________________________________________________________________ ## 2. baseline hook set -vstack ships six default repository hooks: +vstack ships seven default repository hooks: -| Hook name | Primary events | Intent | -| ---------------------------- | -------------------------------------------------------------------------------- | ---------------------------------------------------------------- | -| `session-audit` | `userPromptSubmitted`, `sessionStart`, `sessionEnd`, `preToolUse`, `postToolUse` | Structured JSONL session, prompt, and tool telemetry audit trail | -| `log-retention-cleanup` | `sessionStart` | Prune dated log directories by retention policy | -| `pre-tool-safety-gate` | `preToolUse`, `errorOccurred` | Deny destructive shell patterns + error logging | -| `post-edit-format` | `postToolUse` | Edit-event logging + optional `make format` run | -| `post-edit-markdown-quality` | `postToolUse` | vstack markdown/work-item formatting for docs and templates | -| `post-commit-security-scan` | `postToolUse`, `sessionEnd` | Git-mutation checks + optional `gitleaks` run | +| Hook name | Primary events | Intent | +| ---------------------------- | -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | +| `agent-call-audit` | `sessionStart`, `sessionEnd`, `preToolUse`, `postToolUse` | Actor and delegation visibility with per-session unknown counters, `unknownSummary`, and delegation start/end events | +| `session-audit` | `userPromptSubmitted`, `sessionStart`, `sessionEnd`, `preToolUse`, `postToolUse` | Generic session, prompt, and tool telemetry without actor fields | +| `log-retention-cleanup` | `sessionStart` | Prune dated log directories by retention policy | +| `pre-tool-safety-gate` | `preToolUse`, `errorOccurred` | Deny destructive shell patterns + error logging | +| `post-edit-format` | `postToolUse` | Edit-event logging + optional `make format` run | +| `post-edit-markdown-quality` | `postToolUse` | vstack markdown/work-item formatting for docs and templates | +| `post-commit-security-scan` | `postToolUse`, `sessionEnd` | Git-mutation checks + optional `gitleaks` run | These defaults are safe in audit mode and can be upgraded to enforcement behavior. For vstack specifically, the markdown-quality hook gives the baseline hook set a direct payoff on ADRs, design docs, prompts, instructions, and other generated work items. +### 2.1 agent-call-audit unknown-events sidecar + +`agent-call-audit` keeps `.vstack/logs/YYYYMMDD/hook-agent-call-unknown-events.tsv` as a per-day sidecar. + +- Why it exists: minimal-mode logging suppresses repeated unknown rows, so the sidecar accumulates unknown reasons per session instead of writing noisy duplicates. +- Operational value: it exposes parser quality and unknown-rate trends, and powers reliable `unknownSummary` emission at `sessionEnd`. +- When it is not needed: verbose mode (raw payload logs), disabled/off hook logging, or sessions with no unknown actor/tool extraction outcomes. + ______________________________________________________________________ ## 3. CLI contracts @@ -95,17 +119,30 @@ ______________________________________________________________________ hooks: enabled: true mode: audit + log_level: minimal + log_retention_days: 7 + log_dir: .vstack/logs hooks: pre-tool-safety-gate: mode: enforce + log: + level: verbose + name: hook-security-alerts.log + retention_days: 14 post-edit-markdown-quality: enabled: false ``` - `hooks.enabled: false` disables the generated baseline hook family. - `hooks.mode` sets the generated default fallback for `VSTACK_HOOKS_MODE`. +- `hooks.log_level` sets the generated default fallback for `VSTACK_HOOKS_LOG_LEVEL` (`off`, `minimal`, `verbose`). +- `hooks.log_retention_days` sets the generated default fallback for `VSTACK_HOOKS_LOG_RETENTION_DAYS`. +- `hooks.log_dir` sets the generated default fallback for `VSTACK_HOOK_LOG_DIR`. - `hooks.hooks..enabled: false` disables one named built-in hook. - `hooks.hooks..mode` overrides the default mode for one named hook. +- `hooks.hooks..log.level` overrides `VSTACK_HOOKS_LOG_LEVEL` for one hook. +- `hooks.hooks..log.name` overrides `VSTACK_HOOK_LOG_NAME` for one hook. +- `hooks.hooks..log.retention_days` overrides retention for one hook. ### 3.2 command behavior @@ -153,6 +190,26 @@ Hook templates support two operational modes through `VSTACK_HOOKS_MODE`: This keeps first-run installs non-disruptive while allowing stricter policy in CI or hardened repositories. +### 6.4 audit-hook runtime budget + +`session-audit` and `agent-call-audit` are intentionally bounded to `timeoutSec: 5`. +They do not depend on external tools and only perform local payload parsing and append-only log writes. +The shorter timeout reduces tail latency and keeps hook execution atomic and fast. + +### 6.5 minimal log schema baseline + +The internal logging contract keeps minimal mode compact and deterministic across both audit hooks: + +| Hook | Event scope | Minimal schema baseline | +| -------------------------------------- | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | +| `session-audit` | any event | `timestamp`, `event`, `size_bytes`, `estimated_tokens`, `hook_execution_ms` | +| `session-audit` | `preToolUse`, `postToolUse` | base fields + `tool_name`, `tool_call_id` | +| `agent-call-audit` | any event | `timestamp`, `event`, `size_bytes`, `estimated_tokens`, `hook_execution_ms` | +| `agent-call-audit` | `preToolUse`, `postToolUse` | base fields + `session_id`, `actor_name`, `actor_type`, `tool_name`, `delegated_agent_name`, `model_used` (no `tool_call_id`) | +| `session-audit` and `agent-call-audit` | `verbose` mode | raw payload passthrough | + +`base fields` means `timestamp`, `event`, `size_bytes`, `estimated_tokens`, and `hook_execution_ms`. + ## 7. execution context and dependencies ### 6.1 where hooks execute diff --git a/docs/design/overview.md b/docs/design/overview.md index f76a14a4..08fd7a3c 100644 --- a/docs/design/overview.md +++ b/docs/design/overview.md @@ -1,7 +1,7 @@ # vstack — design > Maintained by: **designer** role\ -> Last updated: 2026-05-03 +> Last updated: 2026-05-14 ## overview @@ -58,7 +58,7 @@ stateDiagram-v2 ### 1.2 manifest JSON schema -`vstack.json` — written by `install`, read by all other commands. +`.vstack/vstack.json` — written by `install`, read by all other commands. ```json { @@ -110,7 +110,7 @@ Per-artifact entry (`ArtifactEntry`) field contracts: value differs from `CURRENT_MANIFEST_VERSION`, the operation fails with: ```text -ERROR: Legacy manifest schema detected in vstack.json. +ERROR: Legacy manifest schema detected in .vstack/vstack.json. Run: vstack manifest upgrade --target . ``` @@ -185,8 +185,8 @@ class ManifestFile: # Stores message in read_error for user-facing diagnostics. def write(self, manifest: Manifest) -> None: ... - # Atomic write: stages to .tmp, then os.replace → . - # Never leaves vstack.json in a partially-written state on POSIX. + # Atomic write: stages to .tmp, then os.replace -> . + # Never leaves .vstack/vstack.json in a partially-written state on POSIX. def exists(self) -> bool: ... ``` @@ -280,7 +280,19 @@ class CommandLineInterface: class CommandService: - def __init__(self, templates_root: Path) -> None: ... + def __init__( + self, + templates_root: Path, + *, + items_root: str = ARTIFACTS_DOCS_ROOT, + workflow_stages: list[dict[str, str]] | None = None, + workflow_mode: str = "agentic", + hook_default_mode: str = "audit", + hook_default_log_level: str = "minimal", + hook_log_retention_days: int = 7, + hook_log_dir: str = ".vstack/logs", + ... + ) -> None: ... generators: list[GenericArtifactGenerator] @@ -288,7 +300,7 @@ class CommandService: # Returns path relative to template root when possible. def manifest_for(self, install_dir: Path) -> ManifestFile: ... - # Returns ManifestFile handle for /vstack.json + # Returns ManifestFile handle for .vstack/vstack.json in project installs. def artifact_control_state( self, @@ -315,8 +327,8 @@ class BaseCommand(ABC): # CommandContext carries args, install_dir, and only. -def build_command_registry(service: CommandService) -> dict[str, BaseCommand]: ... -# Returns {"install": InstallCommand, "verify": VerifyCommand, ...} +COMMAND_CATALOG: dict[str, CommandConfig] +# Declares parser registration and command factories for install/verify/status/... ``` ______________________________________________________________________ @@ -333,18 +345,18 @@ guidance, field semantics, examples, and template structure, see the dedicated d ### 3.1 skill frontmatter (`config.yaml`) -| Field | Type | Required | Constraints | -| -------------------------- | ------ | -------- | --------------------------------------------------------------------------- | -| `name` | string | **yes** | Lowercase kebab-case; max 64 chars; must match directory name | -| `version` | string | **yes** | Semver; used for manifest tracking only — not emitted to generated SKILL.md | -| `description` | string | **yes** | Max 1024 chars; what the skill does and when to invoke it | -| `license` | string | no | SPDX identifier | -| `compatibility` | string | no | Free text compatibility note | -| `metadata.owner` | string | no | — | -| `metadata.maturity` | string | no | `"stable"` \| `"beta"` \| `"experimental"` | -| `argument-hint` | string | no | Shown after `/skill-name` in chat input | -| `user-invocable` | bool | no | Default `true`; `false` hides skill from slash-command menu | -| `disable-model-invocation` | bool | no | Default `false`; `true` prevents Copilot from auto-loading this skill | +| Field | Type | Required | Constraints | +| -------------------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------ | +| `name` | string | **yes** | Lowercase kebab-case; max 64 chars; must match directory name | +| `version` | string | **yes** | Template revision token (`YYYYMMDDNNN`); used for manifest tracking only — not emitted to generated SKILL.md | +| `description` | string | **yes** | Max 1024 chars; what the skill does and when to invoke it | +| `license` | string | no | SPDX identifier | +| `compatibility` | string | no | Free text compatibility note | +| `metadata.owner` | string | no | — | +| `metadata.maturity` | string | no | `"stable"` \| `"beta"` \| `"experimental"` | +| `argument-hint` | string | no | Shown after `/skill-name` in chat input | +| `user-invocable` | bool | no | Default `true`; `false` hides skill from slash-command menu | +| `disable-model-invocation` | bool | no | Default `false`; `true` prevents Copilot from auto-loading this skill | ### 3.2 agent frontmatter (`config.yaml`) @@ -353,7 +365,7 @@ guidance, field semantics, examples, and template structure, see the dedicated d | `name` | string | no | Overrides filename as picker label | | `description` | string | no | Placeholder text in chat input | | `argument-hint` | string | no | Hint text shown after `@agent` in chat | -| `tools` | list | no | `read`, `search`, `edit`, `web`, `vscode`, `todo`, `agent` | +| `tools` | list | no | `read`, `search`, `edit`, `execute`, `web`, `vscode`, `todo`, `agent` | | `agents` | list | no | Subagents this agent may invoke; `["*"]` = all | | `model` | string or list | no | Force a specific model or list of models; omit to allow user selection | | `user-invocable` | bool | no | Default `true` | @@ -386,7 +398,8 @@ ______________________________________________________________________ ## 4. placeholder resolver Placeholders use `{{TOKEN}}` syntax. Resolution is literal string substitution — no -logic, no loops. Unresolved tokens are treated as errors by `validate` and `install`. +logic, no loops. Unresolved tokens are errors in `validate` and in output verification +for types with `fail_on_unresolved=true`; install logs unresolved tokens as warnings. Partials live in `src/vstack/_templates/skills/_partials/*.md`. File stem is converted from lowercase-kebab to `UPPER_SNAKE` to form the token: @@ -531,7 +544,7 @@ Exit 0 = all artifacts clean; exit 1 = any artifact not clean. ### `install` -Write artifact files to the target directory and record checksums in `vstack.json`. +Write artifact files to the target directory and record checksums in `.vstack/vstack.json`. ```bash vstack install [--target ] [--global] [--only ...] @@ -555,7 +568,7 @@ would have happened. ### `uninstall` -Remove artifacts recorded in `vstack.json`. Modified files are preserved by default. +Remove artifacts recorded in `.vstack/vstack.json`. Modified files are preserved by default. ```bash vstack uninstall [--target ] [--global] [--only ...] @@ -574,7 +587,7 @@ Same contracts as top-level `status` and `verify`, scoped to manifest-tracked ar ### `manifest upgrade` -Migrate legacy `vstack.json` (version < 2) to `manifest_version: 2`. +Migrate legacy `.vstack/vstack.json` (version < 2) to `manifest_version: 2`. ```bash vstack manifest upgrade [--target ] [--backfill] @@ -607,14 +620,14 @@ ______________________________________________________________________ ### 7.1 exception table -| Condition | Runtime behavior | -| ----------------------------- | --------------------------------------------------------------------------- | -| Missing `vstack.json` | `ManifestFile.read()` returns `None`; `read_error` is `None` | -| Invalid `vstack.json` JSON | `ManifestFile.read()` returns `None`; `read_error` = invalid format message | -| Legacy schema on read | `ManifestFile.read()` returns `None`; `read_error` = upgrade guidance | -| Unknown artifact type lookup | `CommandService.gen_for()` returns `None` | -| File I/O during state checks | `artifact_control_state()` returns `("unknown", message)` | -| CLI scope validation failures | `ValueError` raised in scope resolver; handled in `src/vstack/main.py` | +| Condition | Runtime behavior | +| ---------------------------------- | --------------------------------------------------------------------------- | +| Missing `.vstack/vstack.json` | `ManifestFile.read()` returns `None`; `read_error` is `None` | +| Invalid `.vstack/vstack.json` JSON | `ManifestFile.read()` returns `None`; `read_error` = invalid format message | +| Legacy schema on read | `ManifestFile.read()` returns `None`; `read_error` = upgrade guidance | +| Unknown artifact type lookup | `CommandService.gen_for()` returns `None` | +| File I/O during state checks | `artifact_control_state()` returns `("unknown", message)` | +| CLI scope validation failures | `ValueError` raised in scope resolver; handled in `src/vstack/main.py` | Top-level error mapping for CLI process exit lives in `src/vstack/main.py`. @@ -652,7 +665,7 @@ flowchart TD A[sys.argv] --> B[CommandLineParser.build] B --> C[args = parser.parse_args] C --> D[CommandService created with templates_root] - D --> E[build_command_registry → name→BaseCommand map] + D --> E[COMMAND_CATALOG → name→BaseCommand map] C --> F[resolve install_dir and only scope] E --> G[command.run(*, context=CommandContext(args, install_dir, only))] F --> G @@ -667,7 +680,7 @@ ______________________________________________________________________ | Module | Class / function | Responsibility | | -------------- | -------------------------- | --------------------------------------------------------------------------------------- | | `interface.py` | `CommandLineInterface` | Facade: parser construction, service creation, target/scope resolution, dispatch | -| `registry.py` | `build_command_registry` | Maps command names to `BaseCommand` instances | +| `catalog.py` | `COMMAND_CATALOG` | Maps command names to parser metadata and `BaseCommand` factories | | `service.py` | `CommandService` | Shared coordinator: generators, path labelling, manifest access, artifact state | | `base.py` | `BaseCommand` | ABC: all handlers implement `run(*, context: CommandContext) → int` | | `install.py` | `InstallCommand` | Install flow: per-artifact write, checksum recording, dry-run, force/adopt/update modes | @@ -684,11 +697,11 @@ ______________________________________________________________________ ## 11. design principles -1. **stdlib only at runtime.** No runtime dependencies beyond Python ≥ 3.11 stdlib — `pyyaml`, `jinja2`, etc. are intentionally absent. +1. **Minimal runtime dependencies.** Runtime uses Python >= 3.11 stdlib plus one external dependency: `pyyaml>=6.0`. 1. **Templates are source of truth.** No generated files live in `src/vstack/_templates/`. 1. **Flat resolver.** No template inheritance, no conditionals — literal substitution only. 1. **Install-time output.** Generated artifacts belong under `.github/`; never committed to the vstack source repo. 1. **Idempotent.** Running the generator twice with the same `--target` produces identical output. 1. **Conservative by default.** Untracked and modified files are never overwritten without an explicit escape-hatch flag. -1. **Atomic manifest writes.** `vstack.json` is never left in a partially-written state (staged via `.tmp` + `os.replace`). +1. **Atomic manifest writes.** `.vstack/vstack.json` is never left in a partially-written state (staged via `.tmp` + `os.replace`). 1. **Explicit upgrade gate.** Legacy manifest schemas are rejected with an actionable hint; never silently migrated. diff --git a/docs/design/skills.md b/docs/design/skills.md index 298ac62d..d87ae504 100644 --- a/docs/design/skills.md +++ b/docs/design/skills.md @@ -1,7 +1,7 @@ # vstack — skills > Maintained by: **designer** role\ -> Last updated: 2026-05-02\ +> Last updated: 2026-05-14\ > VS Code docs: [agent skills](https://code.visualstudio.com/docs/copilot/customization/agent-skills) ## what are skills? @@ -75,9 +75,11 @@ ______________________________________________________________________ | `openapi` | Write and review OpenAPI 3.1 specifications. Resource naming, HTTP semantics, status codes, error conventions, pagination, security schemes. | designer, engineer | `openapi.yaml` | | `refactor` | Structured refactoring without behavior change. Identify smells, plan incremental steps, execute, verify correctness. | engineer | refactored code + green tests | | `onboard` | Generate a contributor onboarding guide. Prerequisites, setup, tests, env vars, architecture overview, good first issues. | product | `CONTRIBUTING.md` + README dev section | +| `space-setup` | Set up and maintain a GitHub Copilot Space for a repository. Scope curation, refresh cadence, and context quality checks. | product | Space setup and maintenance checklist | | `dependency` | Dependency health audit. Vulnerability scanning, outdated packages, licence compliance, transitive risk, pinning policy, supply chain hygiene. | engineer, tester | dependency audit report | | `incident` | Incident analysis and blameless post-mortem writing. Timeline reconstruction, 5-Whys root cause, contributing factors, action items. | tester, engineer | `docs/postmortems/YYYY-MM-DD-*.md` | | `gh-issues` | Create, update, and manage GitHub Issues via gh CLI. Covers bug/feature/task templates, labels, assignees, milestones, and sub-issues. | release, product | GitHub Issues | +| `copilot-ops` | Operate and govern GitHub Copilot settings. Policy checks, configuration drift, usage visibility, and safe change rollout. | release | Copilot governance operations report | | `codeql` | Set up CodeQL code scanning via GitHub Actions or CLI. Language matrix, query suites, monorepo config, alert triage. | tester | `.github/workflows/codeql.yml` | | `dependabot` | Create or optimize `.github/dependabot.yml`. Ecosystem detection, grouping strategies, monorepo patterns, schedule, and PR customization. | engineer, tester | `.github/dependabot.yml` | | `secret-scan` | Configure GitHub secret scanning and push protection. Path exclusions, custom patterns, alert triage, and credential remediation. | tester, engineer | `.github/secret_scanning.yml` | @@ -100,7 +102,7 @@ ______________________________________________________________________ | `src/vstack/_templates/skills//template.md` | Source of truth — skill instructions body | | `src/vstack/_templates/skills/_partials/*.md` | Shared partials injected via `{{TOKEN}}` | | `.github/skills//SKILL.md` | Generated output — what VS Code loads | -| `.github/vstack.json` | Generated install manifest and artifact index for all installed artifact types | +| `.vstack/vstack.json` | Generated install manifest and artifact index for all installed artifact types | **Never edit `.github/skills/` directly.** Regenerate after every change: @@ -176,7 +178,7 @@ Template revision format is `YYYYMMDDNNN` (for example `20260502001`): 1. `NNN` is a zero-padded sequence for multiple updates on the same day. 1. The value is treated as an opaque, monotonically increasing revision token (not semantic versioning). -`allowed-tools` is currently not emitted by vstack because support is inconsistent across target agents. +`allowed-tools` is supported by vstack and emitted as plain frontmatter text when provided in `config.yaml`. `name` must satisfy the Agent Skills naming rules enforced by vstack: lowercase kebab-case, no leading/trailing hyphen, max 64 characters. diff --git a/docs/design/workflow.md b/docs/design/workflow.md index 45996a50..0c3999a9 100644 --- a/docs/design/workflow.md +++ b/docs/design/workflow.md @@ -1,7 +1,7 @@ # vstack — workflow > Maintained by: **designer** role\ -> Last updated: 2026-05-12 +> Last updated: 2026-05-14 ## overview @@ -28,6 +28,12 @@ Default mode: - `agentic` +Copilot Agent Mode entry point: + +- In `agentic` mode, select the `planner` agent in the Copilot Chat agent picker as the primary entry point. +- Use the `tester` agent and other role agents when you want a direct specialist pass rather than planner-led orchestration. +- Copilot Chat uses the mode selector and agent picker to choose the active agent. If the UI suggests mentions or other completions, ignore them and select the agent from the picker. + Mode semantics: | Mode | Primary progression model | Planner generated | Worker handoff buttons | @@ -45,6 +51,23 @@ Execution semantics: - A stage becomes ready only when all dependencies are complete. - Multiple ready stages can be orchestrated in parallel by the planner in `agentic` mode. +Planner correlation semantics: + +- Planner creates one `PLANNER_RUN_ID` per orchestration run. +- Planner forwards the same `PLANNER_RUN_ID` to every delegated worker stage. +- Worker stage reports must echo this value in `planner_run_id`. + +Stage report schema (planner and workers): + +- `status`: `ready` or `blocked` +- `changes_made`: `yes` or `no` +- `updated_items`: list of paths (or `none`) +- `blockers`: list (or `none`) +- `next_handoff_summary`: one short paragraph +- `planner_run_id`: correlation id from planner (or `none` outside planner orchestration) +- `model_used`: model identifier used for the stage +- `subagents_invoked`: list of delegated subagents used by that stage (or `none`) + Dependency semantics (`depends_on`): - `depends_on` must reference existing stage role names. @@ -265,8 +288,8 @@ flowchart TD ### flow principles -1. **User-gated progression:** every stage output is reviewed by the user before the next stage starts. -1. **All roles in every pipeline:** every use case runs through all six roles. Roles that are not affected by a change assess impact and pass through explicitly rather than being skipped. +1. **User-gated progression is configurable:** gate behavior follows `workflow.stages[*].hitl` (`always`, `on-change`, `never`). +1. **Stage participation is configurable:** stage execution follows `workflow.stages[*].gate` (`required`, `optional`, `skip`). 1. **Happy-path handoffs only:** handoff buttons are limited to one forward action named `Go to next stage: `. 1. **No automatic backtracking:** non-happy paths (`NOK`, blockers, missing artifacts) do not use handoff buttons; the user decides the next action. 1. **Subagent delegation mid-role:** engineer may invoke architect or designer as subagents to clarify constraints or contracts during implementation without going back to a full gate cycle. @@ -302,7 +325,7 @@ ______________________________________________________________________ ## user gate moments -There are **6 explicit user gate moments** where the pipeline pauses for human input: +The default six-stage pipeline has **up to 6 user gate moments**. Effective gate count depends on per-stage `gate` and `hitl` settings: | Gate | When | Who signs off | | -------------------------------- | ------------------------------------- | ------------- | @@ -313,9 +336,8 @@ There are **6 explicit user gate moments** where the pipeline pauses for human i | **5. Verification approval** | After tester reports are ready | User | | **6. Final merge approval** | After release readiness is complete | User | -Gates prevent automated pipelines from deploying without human review. In the current model, the user implicitly gates by choosing which skill to invoke next. -In the orchestrated model, the orchestrator pauses and waits for explicit confirmation. +In the orchestrated model, the planner pauses according to `hitl` policy and waits for explicit confirmation when required. ### handoff button convention diff --git a/docs/product/roadmap.md b/docs/product/roadmap.md index 97427edc..b165e4be 100644 --- a/docs/product/roadmap.md +++ b/docs/product/roadmap.md @@ -1,43 +1,53 @@ # vstack — roadmap > Maintained by: **product** role\ -> Last updated: 2026-05-13 +> Last updated: 2026-05-14 ______________________________________________________________________ ## feature status table -| Feature | Version | Status | Notes | -| ---------------------------------------- | ------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | -| foundation | v1.0.0 | shipped | Core template-driven install model is in place | -| backend-first verification | v1.0.0 | shipped | Verify/inspect focus on contracts, observability, security | -| VS Code agent migration | v1.x | shipped | Native `.github/agents/*.agent.md` output format implemented | -| role model + doc restructure | v1.1.0 | shipped | 6-role model, agent templates, and docs baseline established | -| new skill scaffolding | v2.2.0 | shipped | 42-skill set with canonical naming | -| agent skill wiring | v2.2.0 | shipped | Role-to-skill mapping, handoffs, and concise modes wired into all agents | -| CLI modularisation | v2.0.0 | shipped | 12 focused CLI modules; BaseCommand + CommandContext contract | -| manifest package | v2.0.0 | shipped | Dedicated `manifest/` package; atomic writes (ADR-016) | -| mypy type checking | v2.0.0 | shipped | Full mypy coverage enforced in CI; 100% test coverage gate | -| manifest schema versioning | v2.0.0 | shipped | `manifest_version: 2`; upgrade path via `manifest upgrade` (ADR-014) | -| checksum backfill | v2.0.0 | shipped | `manifest upgrade --backfill` adds SHA-256 for VSTACK-META-tagged files (ADR-017) | -| conservative install | v2.0.0 | shipped | Untracked files never overwritten; checksum-gated update (ADR-015, superseded by ADR-020) | -| dry-run install | v2.1.0 | shipped | `vstack install --dry-run` previews actions; type/name selectors in summary | -| project-scope directory | v3.0.0 | shipped | `.vstack/` directory: `config.yaml`, manifest, delta templates (ADR-019) | -| install/init command semantics | v3.0.0 | shipped | `install` = first-run setup; `init` = idempotent CI regeneration (ADR-020, breaking change) | -| manifest relocation | v3.0.0 | shipped | `vstack.json` moves from `.github/` to `.vstack/`; migration via `manifest upgrade` (ADR-014) | -| selective install | v3.0.0 | shipped | Per-type and per-name exclusions via `exclude:` in `.vstack/config.yaml`; agents always installed (ADR-022) | -| workflow contract source-of-truth | v3.1.0 | shipped | `workflow:` block in `.vstack/config.yaml`; `gate`, `hitl`, `handoffs` schema; `vstack migrate` command (ADR-023, ADR-026) | -| agent hooks support | v3.2.0 | shipped | First-class `hook` artifact type: generate `.github/hooks/.json` from templates and track in manifest | -| optional orchestrated role pipeline | v3.2.0 | shipped | `planner` coordinator agent implemented with mode-aware generation; default mode is `agentic` (`manual` and `hybrid` also supported) | -| parallel workflow via DAG model | t.b.d. | candidate | `depends_on` DAG semantics implemented in code; awaiting a release tag before being promoted to shipped | -| new skills (next batch) | t.b.d. | candidate | `spaces`: set up Copilot Spaces; `copilot-admin`: manage Copilot settings via `gh api` | -| team customization layer | t.b.d. | candidate | Deferred major update after VS Code-first model proves itself; custompacks, overlay merge rules, and install profiles all add major maintenance surface | -| multi-IDE support (IntelliJ first) | t.b.d. | candidate | Deferred until vstack proves stable in VS Code; likely a major follow-up because it needs separate targets, schemas, and more maintenance | -| heavy agent runtime framework | — | not planned | Keeps runtime lightweight and transparent | -| cloud control plane dependency | — | not planned | Keeps operation local/offline-capable | -| VS Code extension packaging | — | not planned | Not required for current install model | -| browser automation as default dependency | — | not planned | Backend/microservice-first remains default | -| install target directory override | — | not planned | Won't implement unless a concrete tool incompatibility with `.github/` arises | +| Feature | Version | Status | Notes | +| --------------------------------------------- | ------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | +| foundation | v1.0.0 | shipped | Core template-driven install model is in place | +| backend-first verification | v1.0.0 | shipped | Verify/inspect focus on contracts, observability, security | +| VS Code agent migration | v1.x | shipped | Native `.github/agents/*.agent.md` output format implemented | +| role model + doc restructure | v1.1.0 | shipped | 6-role model, agent templates, and docs baseline established | +| docs information architecture (Diataxis) | t.b.d. | candidate | Adopt `docs/user/` with segmented tutorials/how-to/reference/explanation routes; scaffold first, migration later | +| new skill scaffolding | v2.2.0 | shipped | 42-skill set with canonical naming | +| agent skill wiring | v2.2.0 | shipped | Role-to-skill mapping, handoffs, and concise modes wired into all agents | +| CLI modularisation | v2.0.0 | shipped | 12 focused CLI modules; BaseCommand + CommandContext contract | +| manifest package | v2.0.0 | shipped | Dedicated `manifest/` package; atomic writes (ADR-016) | +| mypy type checking | v2.0.0 | shipped | Full mypy coverage enforced in CI; 100% test coverage gate | +| manifest schema versioning | v2.0.0 | shipped | `manifest_version: 2`; upgrade path via `manifest upgrade` (ADR-014) | +| checksum backfill | v2.0.0 | shipped | `manifest upgrade --backfill` adds SHA-256 for VSTACK-META-tagged files (ADR-017) | +| conservative install | v2.0.0 | shipped | Untracked files never overwritten; checksum-gated update (ADR-015, superseded by ADR-020) | +| dry-run install | v2.1.0 | shipped | `vstack install --dry-run` previews actions; type/name selectors in summary | +| project-scope directory | v3.0.0 | shipped | `.vstack/` directory: `config.yaml`, manifest, delta templates (ADR-019) | +| install/init command semantics | v3.0.0 | shipped | `install` = first-run setup; `init` = idempotent CI regeneration (ADR-020, breaking change) | +| manifest relocation | v3.0.0 | shipped | `vstack.json` moves from `.github/` to `.vstack/`; migration via `manifest upgrade` (ADR-014) | +| selective install | v3.0.0 | shipped | Per-type and per-name exclusions via `exclude:` in `.vstack/config.yaml`; agents always installed (ADR-022) | +| workflow contract source-of-truth | v3.1.0 | shipped | `workflow:` block in `.vstack/config.yaml`; `gate`, `hitl`, `handoffs` schema; `vstack migrate` command (ADR-023, ADR-026) | +| agent hooks support | v3.2.0 | shipped | First-class `hook` artifact type: generate `.github/hooks/.json` from templates and track in manifest | +| optional orchestrated role pipeline | v3.2.0 | shipped | `planner` coordinator agent implemented with mode-aware generation; default mode is `agentic` (`manual` and `hybrid` also supported) | +| parallel workflow via DAG model | t.b.d. | candidate | `depends_on` DAG semantics implemented in code; awaiting a release tag before being promoted to shipped | +| new skills (next batch) | t.b.d. | candidate | `space-setup`: set up Copilot Spaces; `copilot-ops`: operate Copilot governance settings with audit-first checks | +| team customization layer | t.b.d. | candidate | Deferred major update after VS Code-first model proves itself; custompacks, overlay merge rules, and install profiles all add major maintenance surface | +| multi-IDE support (IntelliJ first) | t.b.d. | candidate | Deferred until vstack proves stable in VS Code; likely a major follow-up because it needs separate targets, schemas, and more maintenance | +| plugin/bundle distribution model | t.b.d. | candidate | A self-contained, versioned bundle of vstack artifacts as a distribution model with no external registry or repository mapping | +| artifact integrity and verification hardening | t.b.d. | candidate | Layered checks, generated-artifact drift detection, and deterministic fixtures to reduce release risk. | +| hook telemetry parser hardening (P2) | t.b.d. | candidate | Optional deeper payload normalization for actor/tool/model extraction across broader event shapes without adding runtime dependencies. | +| planner analytics enrichment (P2) | t.b.d. | candidate | Optional aggregation/reporting on stage-report telemetry (`planner_run_id`, `model_used`, `subagents_invoked`) for post-run analysis. | +| golden-fixture coverage expansion | t.b.d. | candidate | Extend deterministic golden fixtures to cover additional high-impact templates per artifact type. | +| defect-fixture matrix expansion | t.b.d. | candidate | Expand defect fixtures across artifact types and failure classes with stable expected error assertions. | +| template-change-aware fixture CI gate | t.b.d. | candidate | Require fixture drift checks automatically in CI whenever template sources are modified. | +| fixture update policy in PR workflow | t.b.d. | candidate | Enforce contributor guidance that intended generated-output changes include fixture updates in the same pull request. | +| deeper targeted verify tier | t.b.d. | candidate | Define and run a deeper pre-merge/release verification tier covering integration, fixture, and contract checks. | +| heavy agent runtime framework | — | not planned | Keeps runtime lightweight and transparent | +| cloud control plane dependency | — | not planned | Keeps operation local/offline-capable | +| VS Code extension packaging | — | not planned | Not required for current install model | +| browser automation as default dependency | — | not planned | Backend/microservice-first remains default | +| install target directory override | — | not planned | Won't implement unless a concrete tool incompatibility with `.github/` arises | ______________________________________________________________________ @@ -74,6 +84,18 @@ ______________________________________________________________________ - `docs/architecture/adr/` structure established - Docs rewritten to match agent-output format; per-role concise modes wired into all agents +### docs information architecture (Diataxis) [candidate — t.b.d.] + +Adopt a Diataxis-aligned structure under `docs/user/` with explicit routing boundaries: + +- `docs/user/tutorials/` for learning-oriented walkthroughs +- `docs/user/how-to/` for goal-driven operational tasks +- `docs/user/reference/` for lookup-oriented contracts and factual interfaces +- `docs/user/explanation/` for rationale, concepts, and trade-offs + +Scope in this phase is intentionally limited to structure and navigation guidance. +Existing documentation now lives under `docs/user/`. + ### new skill scaffolding [shipped — v2.2.0] 42 skills across 6 roles. Representative additions: @@ -182,6 +204,7 @@ Implemented: Shipped default hook set: - `session-audit` +- `agent-call-audit` - `log-retention-cleanup` - `pre-tool-safety-gate` - `post-edit-format` @@ -200,7 +223,7 @@ Ref: [GitHub — Customize agent workflows with hooks](https://docs.github.com/e Two new skills planned for the next skill expansion: -#### `spaces` +#### `space-setup` Guides setup and maintenance of a Copilot Space for a project. @@ -211,9 +234,9 @@ Guides setup and maintenance of a Copilot Space for a project. Ref: [GitHub — Copilot Spaces](https://docs.github.com/en/copilot/how-tos/copilot-on-github/customize-copilot/copilot-spaces) -#### `copilot-admin` +#### `copilot-ops` -Operational skill for Copilot administrators. Covers all `gh api` operations across the Copilot admin surface: +Operational skill for Copilot governance and admin workflows. Covers audit-first operations across the Copilot admin surface: - **Content exclusion** — query and set excluded paths at repo/org/enterprise scope; path-pattern conventions and anti-patterns - **MCP governance** — list and manage allowed MCP servers; define allowlist/denylist policy defaults @@ -451,6 +474,113 @@ Why this is deferred: Current decision: keep this as a later, optional major update and revisit it only after vstack has established traction in the VS Code workflow first. +### plugin/bundle distribution model [candidate — t.b.d.] + +This is a possible VS Code-first distribution path, not a near-term commitment. +The goal is to evaluate whether vstack can be consumed as a Copilot agent plugin +while preserving the current template-driven model and low operational overhead. + +Scope to evaluate: + +- Plugin packaging and install flow for vstack-managed agents/skills +- Compatibility with existing `.vstack/` and `.github/` generation outputs +- Security and policy behavior in organizations with restricted Copilot settings +- Migration path that does not require shipping a dedicated VS Code extension + +Reference: + +- [VS Code Copilot agent plugins](https://code.visualstudio.com/docs/copilot/customization/agent-plugins) + +### portable bundle format [candidate — t.b.d.] + +A portable bundle packages a curated set of vstack artifacts — agents, skills, instructions, +prompts, and hooks — as a single versioned unit. The bundle can be distributed and installed +without coupling to any external registry or specific repository. + +Scope to evaluate: + +- A standard container format for an artifact set that the `install` pipeline can consume + alongside the built-in templates +- Install, status, and uninstall operations that treat the bundle as one addressable unit + tracked in `.vstack/vstack.json` +- Human-readable, offline-capable format compatible with the existing template-driven model +- Versioned manifest entry so bundle provenance and update eligibility are visible in + `vstack status` + +Out of scope for this candidate: + +- No external registry, hosting service, or package index +- No dependency on any external repository or version-control system +- No new runtime dependencies beyond the existing Python toolchain + +### artifact integrity and verification hardening [candidate — t.b.d.] + +**Objective:** establish deterministic quality guarantees for template-driven artifact generation, +lower the risk of regressions slipping through the verify loop, and give contributors clear +guidance on which checks apply after each class of change. + +**Phase A — implement now:** + +- Add a change-trigger validation matrix to `CONTRIBUTING.md` that maps change types to required + checks (e.g. template change → `python3 -m vstack install` + `make test-local`). +- Expand the prompt catalog with `repo-assessment` and `artifact-integrity` prompts so engineers + can run structured repository and drift assessments on demand. + +**Phase B — generated-artifact drift guard:** + +- Baseline shipped (2026-05-13): golden fixture drift tests added for + `instruction/security`, `agent/planner`, and `prompt/code-review` rendering in + `tests/vstack/artifacts/test_generator.py`. + +- Expansion shipped (2026-05-13): golden fixture coverage now includes additional + artifact types with deterministic fixtures for `skill/concise` and + `instruction/testing`. + +- Expansion shipped (2026-05-13): golden fixture coverage expanded again with + deterministic fixtures for `agent/product`, `prompt/api-design-review`, and + `skill/verify`, ensuring at least two representatives per major artifact type. + +- Baseline shipped (2026-05-13): defect-fixture harness now asserts predictable + verification failures for malformed template input (schema violation and unknown + placeholder registry checks). + +- Add golden-fixture tests that render each template to a known-good output and assert that the + result is byte-for-byte stable. + +- Add a defect-fixture harness: deliberately malformed templates that must produce specific, + predictable error messages rather than silent failures or partial output. + +- Integrate drift detection into CI: fail the check run when regenerated artifacts differ from + committed output (equivalent to `python3 -m vstack install && git diff --exit-code`). + +**Phase C — layered verify profile:** + +- Baseline shipped (2026-05-13): a fixture-only fast CI job now runs targeted + generator fixture tests (`golden_fixture` + `defect_fixture`) for quick drift + feedback on pull requests. + +- Expansion shipped (2026-05-13): local fast-path target `make test-fixtures` + now runs fixture-only tests (`golden_fixture` + `defect_fixture`) without + coverage gating for deterministic feedback. + +- Split the verify loop into a fast deterministic tier (lint, typecheck, unit tests) and a + deeper targeted tier (integration tests, golden fixtures, contract checks). + +- Allow contributors to run the fast tier on every save and the full profile before merge. + +- Surface the tier split in `CONTRIBUTING.md` and the CI workflow matrix. + +#### Next follow-up steps + +Tracked as dedicated candidate items in the feature status table: golden-fixture coverage expansion, defect-fixture matrix expansion, template-change-aware fixture CI gate, fixture update policy in PR workflow, and deeper targeted verify tier. + +**Expected outcomes:** + +- Contributors always know which checks to run after any class of change. +- Regressions in artifact generation are caught before they reach main. +- Structured prompt artifacts enable repeatable repo-health reviews and drift audits. +- The verify loop is both faster for day-to-day work and more thorough at the release gate. + ______________________________________________________________________ ### heavy agent runtime framework [not planned] diff --git a/docs/product/vision.md b/docs/product/vision.md index 348fbfcd..9f0abaea 100644 --- a/docs/product/vision.md +++ b/docs/product/vision.md @@ -1,7 +1,7 @@ # vstack — vision > Maintained by: **product** role\ -> Last updated: 2026-05-13 +> Last updated: 2026-05-14 ## what is vstack @@ -54,9 +54,10 @@ ______________________________________________________________________ `skills//template.md` and `agents//{config.yaml,template.md}`. Generated files are overwritten on each build. Edit templates, then regenerate. -1. **Minimal runtime dependencies.** The runtime core is Python 3 stdlib only - (no Bun/Node runtime required). Developer workflow may use Poetry and dev tooling - (ruff, mypy, pytest) for quality checks. +1. **Minimal runtime dependencies.** The runtime core uses Python 3 stdlib plus + one external runtime dependency: `pyyaml>=6.0` (no Bun/Node runtime dependency). + Developer workflow may use Poetry and dev tooling (ruff, mypy, pytest) for + quality checks. 1. **Role model.** Six fixed roles (product, architect, designer, engineer, tester, release) define who produces what. Each role owns a specific set of @@ -101,4 +102,4 @@ Scope is not fixed by vstack — it is set by the product vision for each projec - No browser binary required by default - No VS Code extension packaging - No cloud service dependencies -- No external runtime Python packages +- No Node/Bun runtime dependency diff --git a/docs/releases/2026-04-01.md b/docs/releases/2026-04-01.md new file mode 100644 index 00000000..49b0847f --- /dev/null +++ b/docs/releases/2026-04-01.md @@ -0,0 +1,46 @@ +# Release 2026-04-01 — v1.0.0 + +## Summary + +vstack 1.0.0 is the initial public baseline for the VS Code-native AI engineering workflow +system. It establishes the core Python package, generic artifact generation pipeline, +frontmatter system, six fixed-role agent templates, and a canonical skill set. + +## What's new + +- **Python package** under `src/vstack/` with `vstack` CLI and `python -m vstack` entry points. +- **Generic artifact generation system** — `GenericArtifactGenerator`, `ArtifactTypeConfig`, + and manifest tracking via `vstack.json`. +- **Frontmatter system** — parser, schema validation, and builder supporting `str`, `bool`, + `list`, `object-list`, and `raw` field types. +- **Six fixed-role agent templates** — product, architect, designer, engineer, tester, release + — generated from source templates at install time. +- **Canonical skill set** — generation pipeline based on per-skill `config.yaml` and + `template.md` source templates. +- **Repository docs set** — architecture, design, workflow, and skills documentation under + `docs/`; full ADR set under `docs/architecture/adr/`. +- **Test suite** — coverage across artifacts, frontmatter, CLI, agents, and skills modules. + +## Internal + +- Migrated to VS Code Agent artifacts and install-time generation model. +- Skill metadata model moved to per-skill `config.yaml`; `template.md` is body-only. +- Skills follow a documented minimum body contract. +- Skill frontmatter output aligned to supported fields (`version` tracked in source config; + `allowed-tools` not emitted due to inconsistent support). +- Placeholder governance moved to an explicit registry mapping. +- Legacy generator scripts and template/registry structure removed. +- Full suite passing at release cut. Coverage at 100%. + +## Artifacts reviewed + +| Artifact | Status | +| ------------------------------- | ------ | +| `docs/architecture/overview.md` | ✓ | +| `docs/design/overview.md` | ✓ | +| `docs/design/skills.md` | ✓ | + +## Sign-offs + +_Retrospective record — compiled from `CHANGELOG.md` and repository history. Formal +per-role sign-off was not recorded at release time._ diff --git a/docs/releases/2026-04-17.md b/docs/releases/2026-04-17.md new file mode 100644 index 00000000..6cebb2b8 --- /dev/null +++ b/docs/releases/2026-04-17.md @@ -0,0 +1,33 @@ +# Release 2026-04-17 — v1.0.1 and v1.0.2 + +## Summary + +Two rapid releases to stabilize tooling, CI reproducibility, and local developer workflow +after the v1.0.0 baseline. No functional changes to the vstack artifact system. + +## Changes + +### v1.0.1 — Tooling and release hygiene + +- Added `.python-version` for consistent pyenv interpreter resolution. +- Added tox-based multi-version test execution across Python 3.11, 3.12, 3.13, and 3.14. +- Expanded pre-commit hooks: `trailing-whitespace`, `end-of-file-fixer`, `check-toml`, + `check-yaml`, `ruff`, `ruff-format`. +- `pyproject.toml` migrated from `[tool.poetry]` to PEP 621 `[project]` form; version + is a build-time placeholder overwritten by `poetry-dynamic-versioning`. +- Python support metadata explicitly bounded to 3.11–3.14. +- QA workflow now runs the test matrix across all supported runtimes. +- `make test` exercises every supported Python version when interpreters are installed. +- CI: `pipx install poetry` now runs before `actions/setup-python` to allow cache lookup. +- CI: bumped `actions/checkout` v4→v5 and `actions/setup-python` v5→v6 to resolve + Node.js 20 deprecation warnings. + +### v1.0.2 — Release workflow fix + +- Fixed CI release builds by correcting Poetry setup order and opting in to Node 24 to + resolve runner deprecation warnings. + +## Sign-offs + +_Retrospective record — compiled from `CHANGELOG.md` and repository history. Formal +per-role sign-off was not recorded at release time._ diff --git a/docs/releases/2026-04-19.md b/docs/releases/2026-04-19.md new file mode 100644 index 00000000..ab24db05 --- /dev/null +++ b/docs/releases/2026-04-19.md @@ -0,0 +1,52 @@ +# Release 2026-04-19 — v1.0.3, v1.0.4, and v1.0.5 + +## Summary + +Three releases shipping community health infrastructure, six new skills, and CI workflow +permission hardening. The skill library grows from the canonical baseline to include +domain-specific skills for database migrations, API design, refactoring, onboarding, +dependency auditing, and incident analysis. + +## What's new + +### v1.0.3 — Community health + +- `CODEOWNERS` file. +- GitHub issue templates: `bug_report.yml`, `feature_request.yml`, `config.yml`. +- Pull request template (`.github/pull_request_template.md`). +- `CODE_OF_CONDUCT.md`, `CONTRIBUTING.md`, `SECURITY.md` community health files. +- Release workflow title format updated to `Release vX.Y.Z (YYYY-MM-DD)`. + +### v1.0.4 — Skill expansion + +Six new skills added: + +| Skill | Purpose | +| ------------ | --------------------------------------------------------------------------------------- | +| `migrate` | Database migration review: zero-downtime, expand/contract, rollback, index safety | +| `openapi` | OpenAPI 3.1 spec writing and review: resource naming, HTTP semantics, error conventions | +| `refactor` | Structured refactoring without behavior change: smell identification, incremental plan | +| `onboard` | Contributor onboarding guide generation → `CONTRIBUTING.md` | +| `dependency` | Full dependency health audit: vulnerabilities, outdated packages, licence compliance | +| `incident` | Incident analysis and blameless post-mortem writing → `docs/postmortems/` | + +- `engineer`, `designer`, `tester`, `product` agent templates updated with references + to all new skills. +- `docs/design/skills.md` and `README.md` role–skill table updated with all new skills. +- `README.md` project structure diagram updated to include `instructions/` and `prompts/` + template directories. +- `.github/copilot-instructions.md` updated with complete four-artifact-type system structure. + +### v1.0.5 — CI permission hardening + +- GitHub Actions workflows now declare explicit `permissions` to satisfy policy checks + and follow least-privilege defaults. +- `release.yml` defaults to read-only workflow permissions; `contents: write` scoped + to the `version-and-release` job only. +- `qa.yml`, `security.yml`, and `verify.yml` all declare explicit workflow-level permissions. +- `verify.yml` normalized to use `on:` (unquoted) for style consistency. + +## Sign-offs + +_Retrospective record — compiled from `CHANGELOG.md` and repository history. Formal +per-role sign-off was not recorded at release time._ diff --git a/docs/releases/2026-04-20.md b/docs/releases/2026-04-20.md new file mode 100644 index 00000000..b7056592 --- /dev/null +++ b/docs/releases/2026-04-20.md @@ -0,0 +1,48 @@ +# Release 2026-04-20 — v1.1.0 + +## Summary + +vstack 1.1.0 introduces the `concise` skill — a runtime response-style controller that +lets users toggle between three output density modes without regenerating agents. All six +role agents are aligned to a consistent structural template and wired with per-role +default concise modes. + +## What's new + +- **`concise` skill** with three density modes: + - `concise normal` — full explanation depth. + - `concise compact` — shorter prose, unchanged technical accuracy (default for most roles). + - `concise ultra` — maximum brevity; narrative filler removed, technical correctness preserved. + - `concise status` — reports active mode, session override, agent default, and auto-clarity + override state. + - Aliases: `concise on` → `compact`, `concise off` → `normal`. +- **Per-role concise defaults** wired into all six agent templates: + `product=compact`, `architect=normal`, `designer=compact`, `engineer=compact`, + `tester=ultra`, `release=compact`. +- **Auto-clarity override**: security warnings, destructive actions, and multi-step sequences + always force `normal` regardless of active mode. +- **Agent template structural alignment** — all six roles now follow a shared structure: + responsibilities and scope, principles, communication style, gate moments and handoffs, + how you work, deliverables and success criteria, failure and escalation rules, skills + you use. +- **Shared agent-skill boundary partial** wired across all agents. +- **Release flow clarified**: `release-notes` owns both `docs/releases/{date}.md` and + `CHANGELOG.md`; `pr` remains responsible for push/PR creation. +- **Performance and observability requirements scoped**: tester and release gating now + treat performance baseline and observability evidence as required-for-scope artifacts + rather than unconditional requirements. +- `EXPECTED_CANONICAL_NAMES` in `tests/conftest.py` now imports from `vstack.cli.constants` + instead of duplicating the list. +- `README.md` updated with `concise` commands, per-role defaults table, and verbosity tips. +- `docs/design/skills.md` updated with `concise` row. + +## Internal + +- Procedural detail moved out of agents into skills to keep agents outcome-focused and + reduce template size. +- All six role agent templates regenerated and installed artifacts refreshed. + +## Sign-offs + +_Retrospective record — compiled from `CHANGELOG.md` and repository history. Formal +per-role sign-off was not recorded at release time._ diff --git a/docs/releases/2026-04-21.md b/docs/releases/2026-04-21.md new file mode 100644 index 00000000..fa4a1bba --- /dev/null +++ b/docs/releases/2026-04-21.md @@ -0,0 +1,47 @@ +# Release 2026-04-21 — v1.2.0, v1.2.1, v1.2.2, v1.2.3, v1.2.4, and v1.2.5 + +## Summary + +v1.2.0 is the headline release for this day, introducing CLI artifact provenance +verification and a documentation system alignment pass toward Mermaid-first diagrams. +Five patch releases on the same day address README rendering, and a sequence of +GitHub Actions dependency bumps. + +## What's new + +### v1.2.0 — CLI provenance verification and docs alignment + +- **CLI artifact provenance verification** against the install manifest. +- `CommandLineParser` class extracted from the CLI parser flow; install and verify + control flow simplified. +- Frontmatter serialization refactored: instance-method serializer, naming cleanup, + reduced nested parse/validation flow. +- Hand-authored Markdown docs aligned toward Mermaid-first diagram usage; ASCII + retained as fallback where needed. +- Skill template guidance updated; installed skill artifacts regenerated to adopt + the Mermaid-first convention. +- `README.md` branding refreshed: centered logo and badges, light/dark logo switching. +- Commit and branch policy centralized in `cchk.toml`; local `pre-commit` hooks + wired for commit-message and pre-push branch validation. +- Hard CI scope allowlist enforcement removed; commit scopes are now guidance-level. +- Multiple documentation link paths under `docs/design/` fixed. +- `CONTRIBUTING.md` commit-policy wording updated to match the current enforcement model. +- Generated skill footer tests strengthened to assert the `AUTO-GENERATED` and + `VSTACK-META` footer structure at end-of-file. + +### v1.2.1 — README rendering fix + +- Fixed Mermaid flowchart syntax in `README.md` so GitHub renders the role-flow + diagram correctly. + +### v1.2.2–v1.2.5 — CI dependency maintenance + +- v1.2.2: bumped `trufflesecurity/trufflehog` from 3.88.2 to 3.94.3. +- v1.2.3: bumped `softprops/action-gh-release` from v2 to v3. +- v1.2.4: bumped `actions/upload-artifact` from v4 to v7. +- v1.2.5: bumped `actions/checkout` from v5 to v6. + +## Sign-offs + +_Retrospective record — compiled from `CHANGELOG.md` and repository history. Formal +per-role sign-off was not recorded at release time._ diff --git a/docs/releases/2026-04-22.md b/docs/releases/2026-04-22.md new file mode 100644 index 00000000..5e3058ff --- /dev/null +++ b/docs/releases/2026-04-22.md @@ -0,0 +1,62 @@ +# Release 2026-04-22 — v1.3.0, v1.3.1, v1.3.2, v1.3.3, v1.3.4, v1.3.5, and v1.3.6 + +## Summary + +v1.3.0 delivers the DX, onboarding, and PyPI publishing milestone: GitHub Discussion +templates for onboarding feedback, a restructured README, and first-time PyPI publish +via OIDC trusted publishing. Six subsequent patch releases on the same day harden the +release build pipeline — addressing dynamic versioning, tag pinning, artifact integrity, +and PyPI metadata consistency. + +## What's new + +### v1.3.0 — DX, onboarding, and PyPI publishing + +- GitHub Discussion templates: `onboarding-feedback`, `first-run-report`, + `model-cost-feedback`. +- `README.md` restructured for faster onboarding: clearer quick paths, role usage + guidance, troubleshooting navigation, explicit expected output examples, and a + troubleshooting decision flowchart. +- PyPI publish job added to release workflow using OIDC trusted publishing (no API + tokens required). +- Architect and product agent template model ordering updated; installed agent + artifacts regenerated. +- Fixed `test_install_and_verify_exits_zero` writing generated artifacts into the + repository root instead of an isolated `tmp_path`. + +### v1.3.1–v1.3.4 — Release build hardening + +- v1.3.1: Fixed release workflow trigger (switched from `pull_request: closed` to + `push: branches: [main]`) and `download-artifact` version mismatch. +- v1.3.2: Fixed release build determinism by checking out `refs/tags/` and + validating that HEAD is pinned to the expected release tag before build. +- v1.3.3: Fixed `0.0.0` artifact publishing by installing `poetry-dynamic-versioning` + as a Poetry plugin via `pipx inject` and pinning the Poetry CLI version across all + workflows. +- v1.3.4: Fixed CI release builds still producing `0.0.0` by calling + `poetry dynamic-versioning` explicitly before `poetry build`. Added `fetch-depth: 0` + for full history checkout. Switched `pyproject.toml` to PEP 621 dynamic versioning. + Added workflow `concurrency` controls, GitHub release creation after PyPI publish + succeeds, tag existence validation, automatic tag deletion on build failure, and a + post-build wheel smoke test. + +### v1.3.5 — PyPI metadata alignment + +- Corrected changelog version history from 1.3.0 onward to align with actual tags. +- Switched published long description source to `README-pypi.md` for PyPI-compatible + rendering. +- Added `keywords`, `classifiers`, and `project.urls` to `pyproject.toml`. +- Added `README-pypi.md` with PyPI-safe links, badges, and concise DX-first quickstart. +- Added explicit guidance to keep `README-pypi.md` in sync with `README.md`. + +### v1.3.6 — README and PyPI badge fixes + +- Fixed oversized logo rendering in `README-pypi.md` by constraining image width. +- Fixed duplicate title/branding in `README-pypi.md` by removing redundant heading. +- Fixed badge ordering and PyPI version badge formatting in both README files. +- Fixed verify/security workflow badges by removing `branch=main` filter. + +## Sign-offs + +_Retrospective record — compiled from `CHANGELOG.md` and repository history. Formal +per-role sign-off was not recorded at release time._ diff --git a/docs/releases/2026-04-26.md b/docs/releases/2026-04-26.md index aa2b4a6f..1886cc5a 100644 --- a/docs/releases/2026-04-26.md +++ b/docs/releases/2026-04-26.md @@ -45,14 +45,16 @@ for retroactively adding checksums to legacy manifest entries. ## Artifacts reviewed -| Artifact | Status | -| ------------------------------- | ----------------------------------------- | -| `docs/product/requirements.md` | ✓ | -| `docs/architecture/overview.md` | ✓ | -| `docs/design/design.md` | ✓ | -| `docs/test-report.md` | ✓ updated (342/342, 100%) | -| `docs/security-report.md` | ✓ PASS with notes (no blockers) | -| `docs/performance-baseline.md` | ✓ present (not in scope for this release) | +Paths in this table are normalized to the current repository layout. + +| Artifact | Status | +| -------------------------------------- | ----------------------------------------- | +| `docs/product/requirements.md` | ✓ | +| `docs/architecture/overview.md` | ✓ | +| `docs/design/overview.md` | ✓ | +| `docs/reports/test-report.md` | ✓ updated (342/342, 100%) | +| `docs/reports/security-report.md` | ✓ PASS with notes (no blockers) | +| `docs/reports/performance-baseline.md` | ✓ present (not in scope for this release) | ## Sign-offs diff --git a/docs/releases/2026-04-27.md b/docs/releases/2026-04-27.md new file mode 100644 index 00000000..1b203b5b --- /dev/null +++ b/docs/releases/2026-04-27.md @@ -0,0 +1,16 @@ +# Release 2026-04-27 — v2.0.1 and v2.0.2 + +## Summary + +Two automated dependency maintenance releases following the v2.0.0 breaking release. +No functional changes to the vstack artifact system or CLI. + +## Changes + +- **v2.0.1**: bumped `actions/download-artifact` from v7 to v8 (Dependabot). +- **v2.0.2**: bumped `trufflesecurity/trufflehog` from 3.88.2 to 3.94.3 (Dependabot). + +## Sign-offs + +_Retrospective record — compiled from `CHANGELOG.md` and repository history. Formal +per-role sign-off was not recorded at release time._ diff --git a/docs/releases/2026-04-28.md b/docs/releases/2026-04-28.md new file mode 100644 index 00000000..5267065a --- /dev/null +++ b/docs/releases/2026-04-28.md @@ -0,0 +1,29 @@ +# Release 2026-04-28 — v2.0.3 and v2.0.4 + +## Summary + +Two releases hardening the CI release pipeline after the v2.0.0 major release. +v2.0.3 restructures the release workflow around release-please and a GitHub App token. +v2.0.4 fixes a manifest-ahead-of-tag CI check and tunes Dependabot automerge policy. + +## Changes + +### v2.0.3 — CI release pipeline restructure + +- Hardened release workflows and normalized CHANGELOG format. +- Restructured release pipeline to use release-please and a GitHub App token for + automated release management. + +### v2.0.4 — Release manifest and Dependabot + +- Fixed a CI check that rejected the release manifest when it was ahead of the latest + tag. +- Restored release-please CHANGELOG style for 2.0.x entries. +- Bumped automerge action dependencies. +- Tuned Dependabot automerge policy and cadence. +- Switched to app client ID for release token generation. + +## Sign-offs + +_Retrospective record — compiled from `CHANGELOG.md` and repository history. Formal +per-role sign-off was not recorded at release time._ diff --git a/docs/releases/2026-05-02.md b/docs/releases/2026-05-02.md new file mode 100644 index 00000000..dfd0683a --- /dev/null +++ b/docs/releases/2026-05-02.md @@ -0,0 +1,91 @@ +# Release 2026-05-02 — v2.1.0 and v2.2.0 + +## Summary + +v2.1.0 adds a dry-run install mode for safe first-install previews. v2.2.0 is a +major skill and instruction library expansion — adding 15 new skills covering +infrastructure, security tooling, and developer workflow domains; 8 new instruction +sets for language and framework conventions; and 8 new prompt templates for common +engineering workflows. Together these releases significantly extend the catalog +available to all six roles. + +## What's new + +### v2.1.0 — Dry-run install + +- `vstack install --dry-run`: prints a type/name selector summary and conflict + resolution hints without writing any files. Safe for first-install previews. +- CI: switched to job-scoped PyPI fallback token checks. +- CI: added trusted publishing fallback and publish concurrency group. +- Dependency bumps: cachetools 7.1.0, tox 4.53.1. +- Documentation improvements: Development Commands and CLI Architecture sections + added to `copilot-instructions.md`; dry-run workflow and preserved-file conflict + resolution documented; ADR path and gate count corrections. + +### v2.2.0 — Skill, instruction, and prompt expansion + +**15 new skills:** + +| Skill | Domain | +| --------------------- | ------------------------------------------------------------------ | +| `gdpr` | GDPR-compliant engineering: privacy by design, retention, erasure | +| `terraform` | Terraform IaC: resource design, modules, state, security hardening | +| `terragrunt` | DRY multi-environment Terraform with Terragrunt | +| `cloudformation` | AWS CloudFormation template writing and review | +| `aws-cli` | AWS CLI command patterns for backend engineers | +| `k8s` | Kubernetes manifests, workloads, RBAC, diagnostics | +| `helm` | Helm chart writing, review, and release lifecycle | +| `rancher` | Rancher/Fleet workload governance and operations | +| `gh-issues` | GitHub Issues management via `gh` CLI | +| `codeql` | CodeQL code scanning setup and configuration | +| `dependabot` | Dependabot configuration and dependency update strategies | +| `secret-scan` | GitHub secret scanning and push protection | +| `threat-model` | Threat modeling: STRIDE, DREAD, PASTA | +| `gh-release` | GitHub Release creation and management via `gh` CLI | +| `conventional-commit` | Conventional Commit message preparation and validation | + +**8 new instruction sets:** + +- `markdown` — Markdown authoring conventions +- `security` — Security policy for all code and configuration +- `testing` — Test authoring conventions +- `terraform` — Terraform coding conventions +- `terragrunt` — Terragrunt coding conventions +- `typescript` — TypeScript/JavaScript coding conventions +- `java` — Java coding conventions +- `k8s` — Kubernetes manifest conventions + +**8 new prompt templates** including `api-design-review`, `dependency-audit`, +`architecture-risk`, `incident-timeline`, `migration-safety`, and `release-readiness`. + +**Additional improvements:** + +- `pr` and `release-notes` skills genericized — no longer reference hardcoded + project paths. ADR-018 added documenting the skill-agent boundary. +- All skill templates audited and hardened for token efficiency. +- Agent templates enforce canonical template structure. +- Template versions migrated to `YYYYMMDDNNN` format. +- `docs/design/skills.md` and `docs/design/instructions.md` updated with full catalogs. +- Official documentation references added to all tool-specific skills and instructions. +- Roadmap expanded with copilot customization strategy. + +## Internal + +- Test suite: 100% coverage maintained (test count between v2.0.0 baseline of 342 + and v3.0.0 baseline of 428; exact count not recorded for this release). +- ruff: clean. mypy: 0 errors. +- `applyTo` glob quoting fixed in generated frontmatter; `test`/`tests` commit types + added to allowed list. + +## Artifacts reviewed + +| Artifact | Status | +| ----------------------------- | ------ | +| `docs/design/skills.md` | ✓ | +| `docs/design/instructions.md` | ✓ | +| `docs/product/roadmap.md` | ✓ | + +## Sign-offs + +_Retrospective record — compiled from `CHANGELOG.md` and repository history. Formal +per-role sign-off was not recorded at release time._ diff --git a/docs/releases/2026-05-06.md b/docs/releases/2026-05-06.md index 2fc9f769..f45524a5 100644 --- a/docs/releases/2026-05-06.md +++ b/docs/releases/2026-05-06.md @@ -97,19 +97,19 @@ Existing projects must run `vstack manifest upgrade` after upgrading to migrate ## Artifacts reviewed -| Artifact | Status | -| -------------------------------------- | ---------------- | -| `docs/product/requirements.md` | ✓ | -| `docs/product/roadmap.md` | ✓ | -| `docs/product/vision.md` | ✓ | -| `docs/architecture/overview.md` | ✓ | -| `docs/architecture/adr/019–022` | ✓ | -| `docs/design/overview.md` | ✓ | -| `docs/design/agents.md` | ✓ | -| `docs/design/workflow.md` | ✓ | -| `docs/reports/test-report.md` | ✓ 428/428 | -| `docs/reports/security-report.md` | ✓ PASS/notes | -| `docs/reports/performance-baseline.md` | ✓ no regressions | +| Artifact | Status | +| -------------------------------------------------------------------- | ---------------- | +| `docs/product/requirements.md` | ✓ | +| `docs/product/roadmap.md` | ✓ | +| `docs/product/vision.md` | ✓ | +| `docs/architecture/overview.md` | ✓ | +| `docs/architecture/adr/019-*.md` to `docs/architecture/adr/022-*.md` | ✓ | +| `docs/design/overview.md` | ✓ | +| `docs/design/agents.md` | ✓ | +| `docs/design/workflow.md` | ✓ | +| `docs/reports/test-report.md` | ✓ 428/428 | +| `docs/reports/security-report.md` | ✓ PASS/notes | +| `docs/reports/performance-baseline.md` | ✓ no regressions | ## Sign-offs diff --git a/docs/releases/2026-05-09.md b/docs/releases/2026-05-09.md new file mode 100644 index 00000000..9861e668 --- /dev/null +++ b/docs/releases/2026-05-09.md @@ -0,0 +1,79 @@ +# Release 2026-05-09 — v3.1.0 + +## Summary + +vstack 3.1.0 is a substantial feature release. The hand-rolled YAML parser is replaced +with PyYAML as a runtime dependency. A workflow contract schema is introduced in +`.vstack/config.yaml`, enabling structured multi-stage pipeline configuration. All 44 +skill configs gain an `allowed-tools` field. Agent configs are restructured around a +`defaults:` block. The OOP architecture is cleaned up with instance methods and an +inline registry. Four new ADRs document these decisions. + +## What's new + +- **PyYAML runtime dependency** (`parser` module): replaces the hand-rolled YAML parser. + Adds support for nested block YAML in object-list items (ADR-025). +- **Workflow contract schema** in `.vstack/config.yaml` — projects can now declare a + multi-stage pipeline with named stages, roles, skill invocations, and inter-stage + handoffs. Structure is validated at install/init time (ADR-023). +- **`allowed-tools` field** added to `SKILL_SCHEMA` and all 44 skill `config.yaml` files, + enabling per-skill tool restriction declarations. +- **Agent `defaults:` block and `baseline_artifact` flag** — agent config restructured + to a `defaults:` block; a `baseline_artifact` flag marks which agent config fields + are generated as baseline artifacts (ADR-021 alignment). +- **Missing execute/web tools** restored to product, architect, designer, and release + agent configs. +- **`vstack init` command** delegates artifact generation from `vstack install`, + keeping first-run setup clearly separated from ongoing regeneration (ADR-020, shipped). +- **Subagent delegation in engineer role** — engineer can invoke `@architect` or + `@designer` as subagents to clarify constraints without triggering a full gate cycle. +- **Passthrough conditions broadened** in product, architect, and designer handoffs — + any change outside a role's domain can trigger a passthrough, not only incidents. + +## Fixes + +- `_resolve_handoffs` fallback restored in agents; `None` guard fixed in migrate skill. +- CI: added `FORCE_JAVASCRIPT_ACTIONS_TO_NODE24` to all workflows; fixed `security.yml` + env block. +- Frontmatter: restored `@staticmethod` on `FrontmatterParser`; fixed dict/list + serialization. +- Manifest: re-exported removed helpers as deprecated shims to preserve backward + compatibility. +- `_extract_defaults`: removed dead `str` fallback; synced docs with PyYAML dependency. + +## Refactoring + +- OOP cleanup across CLI — instance methods, inline registry, extracted utils. + +## Documentation + +- Install and upgrade guide added with quickstart and quick upgrade paths. +- ADR-023 (workflow contract), ADR-024 (subagent orchestration), ADR-025 (PyYAML + runtime dependency), and ADR-026 (docs artifact migration policy) added. +- ADR-023 schema aligned; ADR-026 marked shipped; migrations README updated. +- PyPI `README.md` and `README-pypi.md` corrected: PyYAML is a required runtime + dependency, not optional. +- Roadmap: workflow contract source-of-truth marked shipped; orchestrated pipeline + status corrected from in-progress to candidate. + +## Internal + +- Test suite: 100% coverage maintained across py311–py314. Exact count not recorded + for this release (between 428 at v3.0.0 and 571 at v3.2.0). +- ruff: clean. mypy strict: 0 errors. +- bandit: 1 LOW informational (B404 `subprocess` advisory — unchanged). +- pip-audit: 1 advisory (pip dev tooling only — unchanged). + +## Artifacts reviewed + +| Artifact | Status | +| -------------------------------------------------------------------- | ------ | +| `docs/architecture/overview.md` | ✓ | +| `docs/architecture/adr/023-*.md` to `docs/architecture/adr/026-*.md` | ✓ | +| `docs/design/workflow.md` | ✓ | +| `docs/product/roadmap.md` | ✓ | + +## Sign-offs + +_Retrospective record — compiled from `CHANGELOG.md` and repository history. Formal +per-role sign-off was not recorded at release time._ diff --git a/docs/releases/2026-05-10.md b/docs/releases/2026-05-10.md new file mode 100644 index 00000000..8cb8a0b3 --- /dev/null +++ b/docs/releases/2026-05-10.md @@ -0,0 +1,15 @@ +# Release 2026-05-10 — v3.1.1 + +## Summary + +Single patch release fixing a CI publish smoke test failure introduced in v3.1.0. +No functional changes to the vstack artifact system or CLI. + +## Fix + +- **CI**: install wheel dependencies in the publish smoke test step so `pip install --no-deps` resolves correctly without a dependency resolution failure. + +## Sign-offs + +_Retrospective record — compiled from `CHANGELOG.md` and repository history. Formal +per-role sign-off was not recorded at release time._ diff --git a/docs/releases/2026-05-12.md b/docs/releases/2026-05-12.md new file mode 100644 index 00000000..97e8dd5a --- /dev/null +++ b/docs/releases/2026-05-12.md @@ -0,0 +1,107 @@ +# Release 2026-05-12 — v3.2.0 and v3.3.0 + +## Summary + +Two feature releases shipping on the same day. v3.2.0 introduces hooks as a first-class +artifact type with a configurable baseline and per-hook log retention. v3.3.0 adds +DAG-based dependency support to the workflow contract, enabling stage ordering and +cycle detection in multi-stage pipelines. Both releases include documentation, ADR +records, and hardening fixes. + +## What's new + +### v3.2.0 — Hooks as a first-class artifact type + +- **Hooks artifact type** (`hook`) added to the CLI generator, manifest tracking, + and install pipeline (ADR-027). +- **Four built-in hooks** in the default baseline: + - `session-audit` — per-session audit trail. + - `pre-tool-safety-gate` — safety checks before destructive tool calls. + - `post-edit-format` — auto-format after edits. + - `post-commit-security-scan` — security scan after commits. +- **Configurable baseline** — hooks can be enabled, disabled, or extended per project + via `.vstack/config.yaml`. Hook source templates live in + `src/vstack/_templates/hooks//hook.yaml`. +- **Per-hook `retention_days` override** — each hook can declare its own retention + period; the global default applies when no override is set. +- **`.vstack/.gitignore`** updated to ignore hook log output (`logs/`). +- Hook generator log filename overrides sanitized to basename-only values. +- Runtime default injection no longer depends on hard-coded fallback literals. +- Dependency maintenance: bumped mypy and types-pyyaml; refreshed poetry lockfile; + refreshed dependency and action baselines. +- `docs/design/hooks.md` added; architecture overview and design overview aligned + to include hooks. + +### v3.3.0 — DAG dependency support in workflow contract + +- **`depends_on` field** for workflow stages: each stage can declare an ordered list + of prerequisite stage names (ADR-028). +- **Stage graph validation** and cycle detection — malformed or cyclic `depends_on` + values are rejected at parse time with an explicit error (ADR-029). +- **Parallel delegation policy** added to all six role agent templates — roles can + delegate independent workstreams to subagents with a defined merge point. +- **`depends_on` in default workflow stages** — the out-of-the-box `.vstack/config.yaml` + template now demonstrates stage ordering. +- **Planner `depends_on` execution model** — the planner agent applies the DAG to + determine stage scheduling and sequential/parallel execution paths. +- **Version fallback for dev checkouts** — nearest-tag fallback added for working tree + states where `git describe` resolves to a dev distance. +- Roadmap docs aligned chronologically by release tag; multi-IDE support deferred + behind VS Code stability milestone. + +## Fixes + +- **v3.2.0**: hooks retention decoupled from CI automation; hook generator hardened. +- **v3.3.0**: malformed `depends_on` (wrong type, empty list, unknown stage reference) + rejected at parse and validation time — no silent degradation. + +## Internal + +- v3.2.0: test suite at 571 tests, 100% coverage. ruff clean. mypy 0 errors. +- v3.3.0: test suite at 635 tests, 100% coverage across 2,806 measured statements. + ruff clean. mypy clean (51 files, 0 errors). Full py311–py314 matrix passing. +- Verify source coverage for agents extended to include `planner` in + `EXPECTED_INPUT_NAMES["agent"]`. +- SHA-pinned workflow actions across CI/security/release workflows. +- Dependabot GitHub Actions updates reduced from daily to weekly. + +## Artifacts reviewed + +| Artifact | Status | +| -------------------------------------- | --------------------------------- | +| `docs/architecture/overview.md` | ✓ hooks and DAG alignment | +| `docs/architecture/adr/027-*.md` | ✓ hooks artifact type | +| `docs/architecture/adr/028-*.md` | ✓ DAG dependency semantics | +| `docs/architecture/adr/029-*.md` | ✓ multi-agentic execution model | +| `docs/design/hooks.md` | ✓ new | +| `docs/design/workflow.md` | ✓ DAG and multi-agentic model | +| `docs/reports/test-report.md` | ✓ 635/635, 100% coverage | +| `docs/reports/security-report.md` | ✓ PASS — advisories resolved | +| `docs/reports/performance-baseline.md` | ✓ baseline refreshed (2026-05-14) | +| `docs/product/roadmap.md` | ✓ aligned | + +## Release readiness + +All pre-release verification points are now green and documented: + +- **Test gate**: 656 tests passing, 100% coverage (final run: 2026-05-14T14:21:41Z). +- **Security audit**: No known vulnerabilities; pip-audit clean. One low-severity + informational advisory (B404, non-blocking). +- **Performance baseline**: Freshly measured 2026-05-14 with timeit and subprocess timing. + All metrics within acceptable thresholds. + +See [docs/reports/test-report.md](docs/reports/test-report.md), +[docs/reports/security-report.md](docs/reports/security-report.md), and +[docs/reports/performance-baseline.md](docs/reports/performance-baseline.md) for full details. + +## Sign-offs + +| Role | Sign-off | +| --------- | ---------------------------------------------------------------------- | +| Tester | OK — 635/635 passed, 100% coverage; ruff clean; mypy clean (51 files) | +| Architect | OK — ADR-027/028/029 present and accepted; DAG validation design sound | +| Product | OK — hooks baseline and DAG workflow delivered; roadmap aligned | +| Release | OK — artifacts reviewed; CHANGELOG updated; sign-offs collected | + +_v3.2.0 sign-offs: retrospective record — compiled from `CHANGELOG.md` and repository +history. Formal per-role sign-off for v3.2.0 was not recorded separately at release time._ diff --git a/docs/reports/performance-baseline.md b/docs/reports/performance-baseline.md index 99470cb6..73149db0 100644 --- a/docs/reports/performance-baseline.md +++ b/docs/reports/performance-baseline.md @@ -1,55 +1,67 @@ # Performance Baseline -**Branch:** `feat/workflow_update`\ -**Date:** 2026-05-06\ -**Scope:** CLI hot-path operations — parser build, target resolution, registry build; `.vstack/` project-scope directory (ADR-019); `vstack install`/`vstack init` command semantics (ADR-020); manifest relocation (ADR-021); selective install with `exclude:` filter (ADR-022); `artifacts.root` config override; `.vstack/.gitignore` seeding; agent `artifacts:` section generation; ADR terminology update (Option A/B → direct execution/orchestrated pipeline); roadmap cleanup and gh-issues skill MCP-first guidance\ -**Method:** `timeit.repeat` micro-benchmarks (Python 3.13.12, Linux) +**Branch:** `chore/split-docs-and-hardening`\ +**Date:** 2026-05-14\ +**Scope:** CLI hot-path operations baseline for parser build, target resolution, registry build, and cold import.\ +**Method:** `timeit.repeat` micro-benchmarks (Python 3.13.12, Linux). ______________________________________________________________________ ## Verdict -> **No regressions detected.** All measured operations are sub-2ms. The CLI is a local dev tool with no throughput or latency SLAs — these baselines exist to catch accidental regressions from future changes. +> **Baseline refreshed.** New measurements were captured after test and security remediation on this branch. ______________________________________________________________________ ## Benchmark Results +Latest benchmark capture window (UTC): **2026-05-14T14:23:18+00:00 → 2026-05-14T14:23:38+00:00**. + +Command used: + +```bash +source .venv/bin/activate +python - <<'PY' +# timeit.repeat for parser build+parse, resolve_targets, build_command_registry, +# plus subprocess-based cold import timing (5 repeats) +PY +``` + All times in **milliseconds (ms)** per single call. -| Operation | Mean | Min | Max | Stdev | Iterations | -| ---------------------------------------------- | -------- | -------- | -------- | ----- | ---------- | -| `CommandLineParser().build()` + `parse_args()` | 1.852 ms | 1.783 ms | 2.018 ms | — | 5 × 1000 | -| `parser.resolve_targets(args)` | 0.009 ms | 0.008 ms | 0.012 ms | — | 5 × 1000 | -| `build_command_registry(svc)` | 0.002 ms | 0.002 ms | 0.002 ms | — | 5 × 1000 | -| Cold `import vstack` | 69.6 ms | 66.0 ms | 75.9 ms | — | 5 × 1 | +| Operation | Mean | Min | Max | Stdev | Iterations | +| ---------------------------------------------- | ---------- | ---------- | ---------- | ----- | ---------- | +| `CommandLineParser().build()` + `parse_args()` | 2.953 ms | 2.552 ms | 3.608 ms | 0.385 | 5 × 1000 | +| `parser.resolve_targets(args)` | 0.022 ms | 0.020 ms | 0.025 ms | 0.002 | 5 × 1000 | +| `build_command_registry(svc)` | 0.003 ms | 0.002 ms | 0.003 ms | 0.000 | 5 × 1000 | +| Cold `import vstack` | 142.279 ms | 134.769 ms | 155.209 ms | 8.994 | 5 × 1 | ______________________________________________________________________ ## Notes -### CLI parser build + parse (~1.85 ms) +### CLI parser build + parse (~2.95 ms) -`CommandLineParser().build()` constructs the full argparse tree — 6 top-level commands and all manifest subcommands — followed by `parse_args()`. The `CommandLineParser()` constructor itself is essentially a no-op (\<0.001 ms); the build step is what dominates. At ~1.85 ms end-to-end, this remains well within acceptable startup overhead for an interactive CLI. +`CommandLineParser().build()` constructs the full argparse tree and `parse_args(['validate'])` parses a minimal command path. At ~2.95 ms end-to-end, this remains under the configured regression threshold. -No regression from the backfill feature addition: the new `--backfill` flag on the `manifest upgrade` subcommand adds one `add_argument` call, which is negligible at this scale. +No threshold breach detected. -### Target resolution (\<0.01 ms) +### Target resolution (~0.02 ms) -`resolve_targets` is essentially free — filesystem path manipulation with a single `Path.exists()` check in the `--global` path. No blocking I/O in the default (CWD) path. +`resolve_targets` remains effectively free for CLI use. -### Registry build (\<0.01 ms) +### Registry build (~0.003 ms) -`build_command_registry` constructs the command map from the catalog. The catalog-driven approach (dict comprehension over ~8 entries) is negligible. +`build_command_registry` remains negligible. -### Cold import (~70 ms) +### Cold import (~142 ms) -The first `import vstack` in a fresh Python process takes ~70 ms on average (59–80 ms observed across 5 runs). This includes: +The first `import vstack` in a fresh Python process now measures ~142 ms on average. This includes: - `subprocess` call to `git tag --points-at HEAD` (in `constants.py`) for version detection -- Stdlib imports (`pathlib`, `argparse`, `importlib.metadata`, `re`, `subprocess`, `yaml`) +- stdlib and package import overhead -This is the dominant startup cost. The git subprocess call is the likely bottleneck. For a CLI tool this is acceptable — it runs once at startup. +This is still the dominant startup cost, but it remains below the 500 ms threshold. ______________________________________________________________________ @@ -71,10 +83,12 @@ ______________________________________________________________________ ## Test Suite Wall Time ``` -428 tests passed in 5.90s (pytest, with coverage) +Final verification run (UTC): 2026-05-14T14:21:41Z → 2026-05-14T14:22:04Z +command: pytest -q +result: 656 passed in 21.51s ``` -Acceptable. No slow test outliers observed. +This is now a green run and can be used as the current wall-time reference for this branch. ______________________________________________________________________ diff --git a/docs/reports/security-report.md b/docs/reports/security-report.md index c10be670..d4b13323 100644 --- a/docs/reports/security-report.md +++ b/docs/reports/security-report.md @@ -1,23 +1,23 @@ # Security Report -**Branch:** `feat/workflow_update`\ -**Date:** 2026-05-06\ -**Scope:** Full source tree — `.vstack/` project-scope directory (ADR-019); `vstack install`/`vstack init` command semantics (ADR-020); manifest relocation from `.github/` to `.vstack/` (ADR-021); selective install with `exclude:` filter (ADR-022); `artifacts.root` config override; `.vstack/.gitignore` seeding; agent `artifacts:` section generation; ADR terminology update (Option A/B → direct execution/orchestrated pipeline); roadmap cleanup and gh-issues skill MCP-first guidance. Static analysis (bandit) + dependency audit (pip-audit); security fixes for S-001 (assert guards) and S-002 (subprocess nosec)\ -**Method:** OWASP Top 10 + STRIDE (static analysis on a local CLI tool; no network surface, no auth surface, no DB) +**Branch:** `chore/split-docs-and-hardening`\ +**Date:** 2026-05-14\ +**Scope:** Current repository snapshot using static analysis and dependency audit.\ +**Method:** OWASP Top 10 + STRIDE framing for a local CLI tool (no network/auth/database surface), with local dependency remediation and re-audit. ______________________________________________________________________ ## Verdict -| Category | Findings | Blocking | -| ------------------------ | --------------------------- | ---------------------------------------- | -| Static analysis (bandit) | 1 LOW | No — informational; import advisory only | -| Dependency CVEs | 1 (pip, LOW) | No — dev/build tooling only | -| Secrets in source | None | — | -| Injection risk | None identified | — | -| Auth / access control | N/A (local CLI, no network) | — | +| Category | Findings | Blocking | +| ------------------------ | --------------------------------- | ---------------------------------------- | +| Static analysis (bandit) | 1 LOW | No — informational; import advisory only | +| Dependency CVEs | 0 known CVEs in current local env | No | +| Secrets in source | None | — | +| Injection risk | None identified | — | +| Auth / access control | N/A (local CLI, no network) | — | -> **Ship readiness: PASS with notes** — no blocking security findings. Advisory items documented below. +> **Ship readiness: PASS** — no blocking security findings and local tooling advisories were remediated. ______________________________________________________________________ @@ -25,18 +25,18 @@ ______________________________________________________________________ This is a **local CLI tool** — no web server, no user sessions, no database, no network endpoints. Most OWASP categories are not applicable. Relevant categories are assessed below. -| # | Category | Status | Notes | -| --- | ------------------------- | -------- | --------------------------------------------------------------------------- | -| A01 | Broken Access Control | N/A | Local filesystem operations only | -| A02 | Cryptographic Failures | PASS | No cryptographic operations in source | -| A03 | Injection | PASS | Subprocess uses fixed list args (no shell=True, no user-interpolated input) | -| A04 | Insecure Design | PASS | No privileged operations, no credential storage | -| A05 | Security Misconfiguration | PASS | No config files with secrets; no exposed ports | -| A06 | Vulnerable Components | ADVISORY | pip 26.0.1 has CVE-2026-3219 (dev tooling, not shipped) | -| A07 | Auth / Identity Failures | N/A | No authentication surface | -| A08 | Software/Data Integrity | PASS | Checksums used for artifact validation in install/uninstall | -| A09 | Logging Failures | PASS | No sensitive data logged | -| A10 | SSRF | N/A | No HTTP client usage | +| # | Category | Status | Notes | +| --- | ------------------------- | ------ | --------------------------------------------------------------------------- | +| A01 | Broken Access Control | N/A | Local filesystem operations only | +| A02 | Cryptographic Failures | PASS | No cryptographic operations in source | +| A03 | Injection | PASS | Subprocess uses fixed list args (no shell=True, no user-interpolated input) | +| A04 | Insecure Design | PASS | No privileged operations, no credential storage | +| A05 | Security Misconfiguration | PASS | No config files with secrets; no exposed ports | +| A06 | Vulnerable Components | PASS | Local environment advisories remediated and re-verified | +| A07 | Auth / Identity Failures | N/A | No authentication surface | +| A08 | Software/Data Integrity | PASS | Checksums used for artifact validation in install/uninstall | +| A09 | Logging Failures | PASS | No sensitive data logged | +| A10 | SSRF | N/A | No HTTP client usage | ______________________________________________________________________ @@ -44,59 +44,75 @@ ______________________________________________________________________ ``` bandit -r src/ +Run started: 2026-05-14 14:22:20+00:00 Issues: 1 HIGH=0 MED=0 LOW=1 [LOW] B404 vstack/constants.py:6 — Consider possible security implications associated with the subprocess module. ``` -### S-001 — `assert` used for runtime validation (RESOLVED) +### S-001 — B404 subprocess import (LOW, informational) -**File:** `src/vstack/cli/report.py:224, 241, 243` (fixed) +**File:** `src/vstack/constants.py:6` -**Resolution:** Replaced all three `assert isinstance(...)` guards with explicit `if not isinstance(...): raise TypeError(...)` guards. These now execute correctly in optimised builds (`python -O`) and are no longer flagged by bandit. +```python +import subprocess +``` -______________________________________________________________________ +Bandit flags any file that imports `subprocess`. This is a blanket informational note, not a finding tied to unsafe usage. The actual call site remains guarded (`# nosec B603 B607`) and uses a fixed arg list. -### S-002 — subprocess call flagged (RESOLVED) +**Severity:** LOW — informational; not actionable. -**File:** `src/vstack/constants.py:53` (fixed) +______________________________________________________________________ -**Resolution:** Added `# nosec B603 B607` comment at the `subprocess.check_output(...)` call to suppress the advisory and make the review decision explicit. The call uses a fixed argument list (`["git", "-C", str(repo_root), "tag", "--points-at", "HEAD"]`), no `shell=True`, and no user-controlled input. It remains safe. +## Dependency Audit (pip-audit) -The remaining bandit finding (B404 at line 6) is an **import-level advisory** with no associated code risk. It cannot be suppressed without disabling B404 globally. +Path note: -______________________________________________________________________ +- CI workflow uses `pip-audit --requirement <(poetry export --without-hashes -f requirements.txt)`. +- In this local environment, `poetry export` is unavailable (`The requested command export does not exist`). +- Closest repo-used equivalent for local verification: direct `pip-audit` against the active virtualenv. -### S-003 (residual) — B404 subprocess import (LOW, informational) +### Initial reproduction (before remediation) -**File:** `src/vstack/constants.py:6` +```bash +source .venv/bin/activate +pip-audit +``` -```python -import subprocess +```text +Found 4 known vulnerabilities in 2 packages +pip 26.0.1 CVE-2026-3219 +pip 26.0.1 CVE-2026-6357 fixed in 26.1 +urllib3 2.6.3 CVE-2026-44431 fixed in 2.7.0 +urllib3 2.6.3 CVE-2026-44432 fixed in 2.7.0 ``` -Bandit flags any file that imports `subprocess`. This is a blanket informational note, not a finding tied to unsafe usage. The actual call (line 53) is safe and suppressed via `# nosec B603 B607`. +### Remediation applied -**Severity:** LOW — informational; not actionable. +```bash +source .venv/bin/activate +python -m pip install --upgrade 'pip>=26.1' 'urllib3>=2.7.0' +``` -______________________________________________________________________ +Installed versions: -## Dependency Audit (pip-audit) +- `pip 26.1.1` +- `urllib3 2.7.0` -``` -pip-audit result: - Name Version ID Fix Versions - pip 26.0.1 CVE-2026-3219 (none listed) -``` +### Final verification -### CVE-2026-3219 — pip tar+ZIP dual-format handling +``` +security check window (UTC): 2026-05-14T14:22:18Z → 2026-05-14T14:22:20Z +command: pip-audit +result: No known vulnerabilities found -pip processes concatenated tar+ZIP archives as ZIP regardless of filename, potentially leading to incorrect file installation from ambiguous archives. +note: pip-audit reports one non-blocking skip item for local package `vstack (0.0.0)` because it is not published on PyPI. +``` -**Assessment:** This affects `pip` itself as a **build/dev tool**, not vstack's shipped package or any runtime dependency. vstack has no runtime dependencies beyond Python stdlib. Users installing vstack from PyPI are not exposed by this CVE in any production path. +### S-002 — dependency advisories in local tooling environment (RESOLVED) -**Recommendation:** Upgrade pip in the development virtualenv (`pip install --upgrade pip`) once a fixed version is released. Track the CVE for a fix version. +`pip-audit` initially reported advisories in local tooling packages (`pip`, `urllib3`). Those packages were upgraded in the active virtual environment and re-audited. -**Severity:** LOW — dev tooling only; not a shipping blocker. +**Final status:** resolved. ______________________________________________________________________ @@ -125,10 +141,9 @@ ______________________________________________________________________ ## Summary of Advisory Items -| ID | Severity | File | Action | -| ----- | -------- | --------------------------- | ------------------------------------------------------------------ | -| S-001 | LOW | `cli/report.py:224,241,243` | Replace `assert` with explicit `TypeError` guards | -| S-002 | LOW | `constants.py:53` | Add `# nosec B603 B607` to suppress false-positive bandit advisory | -| S-003 | LOW | `pip 26.0.1` | Upgrade pip in dev venv when fix is available | +| ID | Severity | Location / package | Action | +| ----- | -------- | -------------------------- | -------------------------------------------------------------- | +| S-001 | LOW | `src/vstack/constants.py` | Keep B404 import advisory documented; no unsafe subprocess use | +| S-002 | LOW | `pip`, `urllib3` (dev env) | **Resolved** by upgrading to `pip 26.1.1` and `urllib3 2.7.0` | -None of these items block release. +No remaining dependency CVEs were detected in the audited local environment. No security item blocks release. diff --git a/docs/reports/test-report.md b/docs/reports/test-report.md index 90411192..cf7f1cf3 100644 --- a/docs/reports/test-report.md +++ b/docs/reports/test-report.md @@ -1,8 +1,8 @@ # Test Report -**Branch:** `feature/workflow-dag-validation`\ -**Date:** 2026-05-13\ -**Scope:** Full repository — workflow DAG semantics (`depends_on` validation, sequential fallback compatibility, planner-ready stage scheduling); agent parallel delegation policy; planner template orchestration; roadmap/version alignment; product/docs consistency updates; runtime version fallback fix; historical validation context retained for continuity. +**Branch:** `chore/split-docs-and-hardening`\ +**Date:** 2026-05-14\ +**Scope:** Full repository verification snapshot after resolving open report points. ______________________________________________________________________ @@ -10,82 +10,76 @@ ______________________________________________________________________ | Dimension | Result | | ------------- | ------------------------------------------ | -| Functional | **PASS** — 635/635 tests green | +| Functional | **PASS** — 656 passed | | Lint / Style | **PASS** — ruff clean | -| Type checking | **PASS** — mypy clean (51 files, 0 errors) | -| Coverage | **PASS** — 100.00% (fail-under=100) | +| Type checking | **PASS** — mypy clean (119 source files) | +| Coverage | **PASS** — 100.00% | | Security | See `docs/reports/security-report.md` | | Performance | See `docs/reports/performance-baseline.md` | -> **Ship readiness: READY** — all verification gates currently pass. +> **Ship readiness: READY** — test gate is green. ______________________________________________________________________ -## Test Execution +## Reproduction of Previous Failures -``` -platform: linux, Python 3.13.12-final-0 -runner: pytest 9.0.3 + pytest-cov 7.1.0 -command: pytest -q -635 passed in 21.03s -``` +Initial reproduction command: -All tests pass. No flaky, skipped, or xfail tests observed. - -______________________________________________________________________ - -## Coverage Summary +```bash +source .venv/bin/activate +pytest -q +``` -Total: 100.00% — 0 missed statements across 2,806 measured +Reproduced result: -`fail-under=100` is configured in `pyproject.toml`. This gate is **passing**. +- 8 failed, 646 passed +- Failures were all golden-fixture drift checks in `tests/vstack/artifacts/test_generator.py` +- Coverage gate also failed at 99.93% because the failing run stopped before complete branch coverage -All modules are now at 100% statement coverage. +______________________________________________________________________ -### Resolution summary +## Fixes Applied -Blockage coverage findings were resolved by adding targeted unit tests for: +1. Updated golden fixtures to match current generated artifacts for: + - instructions: `security`, `testing` + - skills: `concise`, `verify` + - agents: `planner`, `product` + - prompts: `code-review`, `api-design-review` +1. Corrected agent golden tests to use `AgentGenerator` (agent templates now require agent-specific placeholder expansion). +1. Added targeted regression tests to close coverage gaps introduced during failure remediation: + - `tests/vstack/agents/test_generator.py` for non-list `agents` verification path + - `tests/vstack/frontmatter/test_serializer.py` for YAML-special list-item quoting -- per-module test files replacing the `test_commands.py` / `test_coverage_blockers.py` catch-all (test count: 288 → 428) -- manifest subcommand dispatch and missing-action path -- service wrappers and `manifest_upgrade` success/error branches -- parser config guard rails (`scope_help` / `only_help` validation) -- interface scope resolution edge cases -- report YAML/JSON/text rendering branches -- command `run()` wrappers and context forwarding -- manifest-store defensive parsing branches -- install/verify/uninstall error-path behavior -- `manifest upgrade --backfill` branches: missing file, unreadable file, no VSTACK-META footer, existing checksum, unknown algorithm fallback +## Final Verification Run -Recent additions also covered malformed `depends_on` inputs at parse and validation time so the workflow DAG no longer degrades silently when config types are wrong. +```bash +source .venv/bin/activate +pytest -q +``` -______________________________________________________________________ +Run window (UTC): -## Lint and Type Checking +- started: `2026-05-14T14:21:41Z` +- finished: `2026-05-14T14:22:04Z` -``` -ruff check src tests → All checks passed! -python -m mypy src tests → Success: no issues found in 51 source files -``` +Result: -No lint or type findings. +- 656 passed in 21.51s +- Coverage: 100.00% (fail-under=100 satisfied) ______________________________________________________________________ -## Syntax +## Lint and Type Checks ``` -python -m py_compile → All checked files compile cleanly +ruff check src tests -> All checks passed +python -m mypy src tests -> Success: no issues found in 119 source files ``` -______________________________________________________________________ - -## Blocking Issues - -No blocking test findings remain. +No lint or type issues were introduced by the test-fix changes. ______________________________________________________________________ ## Handoff -Continue monitoring this area by extending tests whenever new command branches or parser flags are introduced, to preserve the 100% coverage gate. +Open test-failure point is fully resolved. Use this report as the new green baseline for this branch. diff --git a/docs/user/README.md b/docs/user/README.md new file mode 100644 index 00000000..3825d76c --- /dev/null +++ b/docs/user/README.md @@ -0,0 +1,36 @@ +# User docs + +This subtree contains user-facing documentation. + +## Navigation + +- [Start Here](start-here.md): guided path selection for setup, upgrades, and operations. +- [Tutorials](tutorials/README.md): learning-oriented paths from zero. + - [First time using vstack](tutorials/first-time-using-vstack.md) + - [First planner run](tutorials/first-planner-run.md) + - [Upgrade with checks](tutorials/upgrade-with-checks.md) +- [How-to guides](how-to/README.md): task-focused procedures. + - [Fresh install](how-to/fresh-install.md) + - [Global install](how-to/global-install.md) + - [Uninstall](how-to/uninstall.md) + - [Upgrade](how-to/upgrade.md) + - [Reinitialize](how-to/reinitialize.md) + - [Partial upgrade](how-to/partial-upgrade.md) + - [Configure workflow modes](how-to/configure-workflow-modes.md) + - [Choose agent, skill, or prompt](how-to/choose-agent-skill-or-prompt.md) + - [How to install a skill](how-to/how-to-install-a-skill.md) + - [Install and upgrade](how-to/install-and-upgrade.md) + - [Troubleshooting](how-to/troubleshooting.md) +- [Reference](reference/README.md): factual and contract-style lookup. + - [CLI commands](reference/cli-commands.md) + - [Configuration](reference/configuration.md) + - [Hooks](reference/hooks.md) + - [Prompts overview](reference/prompts-overview.md) + - [Skills overview](reference/skills-overview.md) + - [Instructions overview](reference/instructions-overview.md) + - [Artifact checks](reference/artifact-checks.md) + - [Work items](reference/work-items.md) +- [Explanation](explanation/README.md): rationale and conceptual context. + - [Community patterns](explanation/community-patterns.md) + - [Workflow modes](explanation/workflow-modes.md) + - [What `.vstack/templates` is for](explanation/vstack-templates.md) diff --git a/docs/user/explanation/README.md b/docs/user/explanation/README.md new file mode 100644 index 00000000..5a94d63f --- /dev/null +++ b/docs/user/explanation/README.md @@ -0,0 +1,45 @@ +# Explanation + +Explanation documentation provides context, rationale, and trade-off analysis. + +Use this section to explain why vstack is designed the way it is, including: + +- architectural choices +- product and workflow principles +- boundaries and non-goals + +Explanation pages should help readers build mental models, not complete tasks. + +## What Belongs Here + +Use Explanation pages for user-level understanding: + +- mental models for core concepts +- why a concept exists and when to use it +- how concepts relate at a high level + +Keep focus on concept understanding over implementation details. + +## Out of Scope + +Do not put internal engineering details here, such as: + +- architecture decisions, component internals, or system decomposition +- implementation design, module structure, or generation mechanics +- product planning, roadmap details, or requirement tracking + +## Internal Docs Boundaries + +For internal details, use: + +- [Architecture docs](../../architecture/) +- [Design docs](../../design/) +- [Product docs](../../product/) + +## Related User Guides + +- [Community patterns](community-patterns.md) +- [Workflow modes](workflow-modes.md) +- [What `.vstack/templates` is for](vstack-templates.md) +- [Configure workflow modes](../how-to/configure-workflow-modes.md) +- [Configuration](../reference/configuration.md) diff --git a/docs/user/explanation/community-patterns.md b/docs/user/explanation/community-patterns.md new file mode 100644 index 00000000..5b9322f3 --- /dev/null +++ b/docs/user/explanation/community-patterns.md @@ -0,0 +1,21 @@ +# Community patterns + +Community patterns are shared, named ways to use vstack that teams have validated in real work. +In vstack terms, a pattern is a repeatable combination of workflow mode, role handoff behavior, selected skills, and optional hooks. + +## Why they matter + +Patterns reduce setup friction for teams with similar constraints. +They provide a practical starting baseline instead of requiring every team to compose a workflow from scratch. +They also make trade-offs visible by documenting what a pattern optimizes for. + +## Where to document them + +Document community patterns in user Explanation docs, starting with this page and linked concept pages under `docs/user/explanation/`. +Put task steps in How-to guides and keep command details in Reference pages. + +## What this is not + +- Not a new artifact type or install mechanism. +- Not a mandatory standard for all teams. +- Not a replacement for understanding core vstack concepts. diff --git a/docs/user/explanation/vstack-templates.md b/docs/user/explanation/vstack-templates.md new file mode 100644 index 00000000..80bfd94b --- /dev/null +++ b/docs/user/explanation/vstack-templates.md @@ -0,0 +1,33 @@ +# What `.vstack/templates` is for + +This page explains the role of `.vstack/templates/` in a repository installation. + +## What it is + +When you run `vstack install` in a repository, vstack can seed starter files under `.vstack/templates/`. + +These files are project-owned guidance stubs. They help teams document local role workflows and artifact patterns. + +## Ownership and update model + +- Existing files in `.vstack/templates/` are not overwritten by later `vstack init` runs. +- New files may be added by newer vstack versions. +- Files in this directory are outside manifest-managed `.github/` artifact checks. + +## What to do with these stubs + +- Keep useful stubs and adapt them to your team conventions. +- Commit your local edits if they are part of your repository workflow. +- Use them as project-local guidance, not as generated output. + +## What not to expect + +Editing `.vstack/templates/` does not regenerate `.github/` artifacts. + +Use `vstack init` to regenerate managed artifacts under `.github/` from installed vstack templates and your `.vstack/config.yaml` settings. + +## Related docs + +- [Fresh install](../how-to/fresh-install.md) +- [Install and upgrade](../how-to/install-and-upgrade.md) +- [Configuration](../reference/configuration.md) diff --git a/docs/user/explanation/workflow-modes.md b/docs/user/explanation/workflow-modes.md new file mode 100644 index 00000000..55604c92 --- /dev/null +++ b/docs/user/explanation/workflow-modes.md @@ -0,0 +1,105 @@ +# Workflow modes + +Workflow modes define how teams move through vstack stages. + +Use this page to choose the right mode for team maturity, review style, and release risk. + +## Why modes exist + +Different teams need different execution control: + +- some want strict orchestration, +- some want explicit manual checkpoints, +- some need both depending on change type. + +A single mode cannot optimize all of those goals. + +## Agentic mode + +In `agentic`, planner orchestrates stage progression using configured workflow rules. + +Choose `agentic` when you want: + +- consistent stage ordering, +- centralized orchestration, +- less manual transition overhead, +- easier repeatability for routine delivery flows. + +Trade-off: less interactive control per transition. + +## Manual mode + +In `manual`, users move stage-by-stage using explicit handoffs. + +Choose `manual` when you want: + +- explicit human control for each transition, +- tighter compliance or review sign-off points, +- training workflows where teams learn each stage boundary. + +Trade-off: slower throughput and more operator effort. + +## Hybrid mode + +In `hybrid`, both planner orchestration and manual handoffs are available. + +Choose `hybrid` when: + +- teams are transitioning from manual to planner-led execution, +- different work streams need different control styles, +- you want fallback manual control without disabling planner paths. + +Trade-off: mixed use in one session can cause duplicated transitions or confusion. + +## How to choose + +A practical default policy: + +1. Start with `agentic` for standard delivery work. +1. Use `manual` for high-governance or training contexts. +1. Use `hybrid` only when both patterns are intentionally required. + +In `hybrid`, pick one execution path per session and stay on it. + +## Planner vs. direct agent invocation + +Choosing a workflow mode is separate from deciding whether to use the planner at all. +The planner is built for coordinating a full multi-role delivery pipeline. +It adds overhead — multiple model calls, orchestration logic, gate pauses — that is only +worthwhile when multiple roles genuinely need to collaborate in sequence. + +**Use the planner when:** + +- Delivering a feature, fix, or change that spans multiple roles (product → architect → designer → engineer → tester → release). +- You want explicit gate approvals at each stage boundary. +- The work is large enough that cross-role coordination matters. + +**Use a specialist agent directly when:** + +- The task clearly belongs to one role: update architecture docs → `@architect`, write release notes → `@release`, fix a test → `@engineer`, review an API contract → `@designer`. +- You are doing focused day-to-day maintenance: refreshing a report, writing an ADR, bumping a dependency. +- You want the fastest path to a result. + +**Why the planner cannot automatically do this better:** +The planner works from a stage-based pipeline, not from artifact ownership. It cannot +infer that "update the architecture docs" should skip product, designer, engineer, tester, +and release and route only to architect. Per-task stage qualification does not scale — +there is no finite list of task types that maps cleanly onto pipeline subsets. + +The effective pattern is: you make the routing decision. The planner handles orchestration +when the full pipeline is genuinely needed. Direct invocation handles everything else. + +| Situation | Best approach | +| ------------------------------- | ---------------------------------------------------- | +| Full feature delivery | `@planner` — run the pipeline | +| Update a specific doc | `@architect`, `@designer`, `@release`, etc. directly | +| Fix a bug | `@engineer` directly | +| Run verification | `@tester` directly | +| Prepare a release | `@release` directly | +| Unsure which role owns the work | `@planner` — it will route to the right specialist | + +## Related docs + +- [Configure workflow modes](../how-to/configure-workflow-modes.md) +- [Configuration](../reference/configuration.md) +- [CLI commands](../reference/cli-commands.md) diff --git a/docs/user/how-to/README.md b/docs/user/how-to/README.md new file mode 100644 index 00000000..f56be71f --- /dev/null +++ b/docs/user/how-to/README.md @@ -0,0 +1,19 @@ +# How-to guides + +How-to guides are problem-oriented documents. + +Use this section when you already know what you want to achieve and need direct, task-level instructions. +Each guide should focus on one outcome and avoid conceptual digressions. + +- [How to install a skill](how-to-install-a-skill.md) +- [Choose agent, skill, or prompt](choose-agent-skill-or-prompt.md) +- [Install and upgrade](install-and-upgrade.md) +- [Global install](global-install.md) +- [Uninstall](uninstall.md) +- [Fresh install](fresh-install.md) +- [Upgrade](upgrade.md) +- [Reinitialize](reinitialize.md) +- [Partial upgrade](partial-upgrade.md) +- [Update managed artifacts](update-managed-artifacts.md) +- [Configure workflow modes](configure-workflow-modes.md) +- [Troubleshooting](troubleshooting.md) diff --git a/docs/user/how-to/choose-agent-skill-or-prompt.md b/docs/user/how-to/choose-agent-skill-or-prompt.md new file mode 100644 index 00000000..6f506d48 --- /dev/null +++ b/docs/user/how-to/choose-agent-skill-or-prompt.md @@ -0,0 +1,49 @@ +# Choose agent, skill, or prompt + +Use this guide to decide how to run work in Copilot Chat with vstack. + +## Quick decision rules + +Use an agent when: + +- you want role-based ownership and structured handoffs, +- you are running end-to-end work across multiple stages, +- you want behavior aligned with the project role model. + +Use a skill when: + +- you need one focused capability (for example `verify`, `debug`, or `openapi`), +- you already know the scope and want a direct playbook, +- you do not need full multi-role orchestration. + +Use a prompt when: + +- you need lightweight, one-off guidance, +- you want to adapt wording for a specific context quickly, +- you are not changing the role model or skill inventory. + +## Practical examples + +| Situation | Best fit | Why | +| -------------------------------------------------------------------------- | ----------------- | ------------------------------------------------------ | +| Plan and execute a feature across design, implementation, and verification | Agent (`planner`) | Planner coordinates role flow and handoffs. | +| Fix failing checks before merge | Skill (`verify`) | Verify provides targeted fix-loop behavior. | +| Investigate a production bug cause | Skill (`debug`) | Debug is optimized for root-cause-first investigation. | +| Ask for a one-off API naming suggestion | Prompt | Fast and local, no orchestration needed. | +| Generate release artifacts from completed work | Agent (`release`) | Release role owns release gating outputs. | + +## Escalation pattern + +Start small, then escalate only when needed: + +1. Start with a prompt for low-risk, narrow tasks. +1. Move to a skill for repeatable specialist work. +1. Move to an agent when ownership boundaries and workflow handoffs matter. + +## Related docs + +- [Start Here](../start-here.md) +- [First planner run](../tutorials/first-planner-run.md) +- [Skills overview](../reference/skills-overview.md) +- [Prompts overview](../reference/prompts-overview.md) +- [Work items](../reference/work-items.md) diff --git a/docs/user/how-to/configure-workflow-modes.md b/docs/user/how-to/configure-workflow-modes.md new file mode 100644 index 00000000..bbaf0774 --- /dev/null +++ b/docs/user/how-to/configure-workflow-modes.md @@ -0,0 +1,67 @@ +# Configure Workflow Modes + +Use this guide to set and use workflow modes in `.vstack/config.yaml`. + +## Supported Modes + +- `agentic`: planner orchestrates stage progression. +- `manual`: you move stages with explicit handoffs. +- `hybrid`: both planner orchestration and manual handoffs are available. + +For conceptual guidance, see [Workflow modes](../explanation/workflow-modes.md). + +## Set a Mode + +Edit `.vstack/config.yaml`: + +```yaml +workflow: + mode: agentic +``` + +Then apply changes: + +```bash +vstack init +``` + +## Verify Configuration + +```bash +vstack validate +vstack manifest status --target . +``` + +## How to Use Each Mode + +### Agentic + +- Start your workflow with planner-led orchestration. +- Let stage progression follow configured workflow rules. +- Use this as default for routine delivery. + +### Manual + +- Run each stage explicitly and review handoff results. +- Use this for high-governance changes or onboarding. +- Expect slower throughput but tighter control. + +### Hybrid + +- Choose one execution path per session. +- Do not mix planner and manual transitions in the same flow. +- Use this when teams are transitioning process style. + +## Team Policy Example + +A practical policy: + +1. Default to `agentic`. +1. Switch to `manual` for high-risk changes. +1. Use `hybrid` only for controlled transition periods. + +## Related Docs + +- [Workflow modes](../explanation/workflow-modes.md) +- [Configuration](../reference/configuration.md) +- [CLI commands](../reference/cli-commands.md) diff --git a/docs/user/how-to/fresh-install.md b/docs/user/how-to/fresh-install.md new file mode 100644 index 00000000..0f83bbfc --- /dev/null +++ b/docs/user/how-to/fresh-install.md @@ -0,0 +1,82 @@ +# Fresh Install + +Use this guide when installing vstack in a repository that does not have a previous vstack setup. + +## Before You Start + +- Install the CLI with `pipx` (recommended). +- Open your repository root in VS Code. +- Confirm the repository does not already contain `.vstack/vstack.json`. + +## Install vstack + +```bash +pipx install vstack +vstack --version +``` + +## Initialize the Repository + +Run install from the repository root: + +```bash +vstack install +``` + +What this does: + +- creates `.vstack/` when missing, +- seeds project config when needed, +- seeds starter files under `.vstack/templates/` when available, +- generates managed artifacts under `.github/`. + +About `.vstack/templates/`: + +- These files are project-owned stubs and are safe to customize. +- New files can be added by future versions, but existing files are not overwritten. +- They are not the source for `.github/` regeneration. + +## Validate the Result + +Run the standard checks: + +```bash +vstack validate +vstack manifest status --target . +vstack manifest verify --target . +``` + +Expected outcome: + +- `validate` passes template/config checks, +- `manifest status` reports managed artifact state, +- `manifest verify` confirms checksum ownership and output consistency. + +## Resolve Existing File Conflicts + +If your repository already has overlapping `.github/` files, preview first: + +```bash +vstack install --dry-run +``` + +Then choose one strategy: + +```bash +# Overwrite one managed artifact +vstack install --force-name agent/engineer + +# Adopt one existing artifact without overwrite +vstack install --adopt-name agent/engineer + +# Overwrite all managed artifacts +vstack install --force +``` + +## Related Docs + +- [Install and upgrade](install-and-upgrade.md) +- [CLI commands](../reference/cli-commands.md) +- [Configuration](../reference/configuration.md) +- [Artifact checks](../reference/artifact-checks.md) +- [What `.vstack/templates` is for](../explanation/vstack-templates.md) diff --git a/docs/user/how-to/global-install.md b/docs/user/how-to/global-install.md new file mode 100644 index 00000000..4afa1e9d --- /dev/null +++ b/docs/user/how-to/global-install.md @@ -0,0 +1,53 @@ +# Global install + +Use global install when you want vstack artifacts in your VS Code user profile instead of a single repository. + +## When to choose global install + +Use `--global` when you want: + +- one shared setup across multiple repositories, +- personal defaults in your VS Code user scope, +- no repository-local `.vstack/` project scaffold. + +Use repository install when you need project-specific configuration and committed artifacts. + +## Install globally + +```bash +vstack install --global +``` + +Install selected artifact families only: + +```bash +vstack install --global --only agent skill prompt instruction hook +``` + +## Update global artifacts + +After upgrading the CLI: + +```bash +pipx upgrade vstack +vstack init --global +``` + +## Verify global state + +```bash +vstack manifest status --global +vstack manifest verify --global +``` + +## Notes + +- Global install does not seed repository `.vstack/config.yaml`. +- Repository exclusions in `.vstack/config.yaml` do not apply to global scope. +- Use `--only` for selective global generation. + +## Related docs + +- [Install and upgrade](install-and-upgrade.md) +- [Uninstall](uninstall.md) +- [CLI commands](../reference/cli-commands.md) diff --git a/docs/user/how-to/how-to-install-a-skill.md b/docs/user/how-to/how-to-install-a-skill.md new file mode 100644 index 00000000..b6f2c644 --- /dev/null +++ b/docs/user/how-to/how-to-install-a-skill.md @@ -0,0 +1,60 @@ +# How to install a skill + +Use this guide to include skill artifacts in your repository and verify that the result is managed correctly. + +## Goal + +Ensure the skills you want are generated under `.github/skills/` and not excluded by project config. + +## 1. Confirm skill exclusions + +Open `.vstack/config.yaml` and check the `exclude.skills` block. + +- If `exclude.skills` contains a skill name, that skill is skipped. +- Remove the skill name (or remove the whole `exclude.skills` block) to include it. + +Example exclusion block: + +```yaml +exclude: + skills: + - terraform + - helm +``` + +## 2. Regenerate skill artifacts + +From repository root: + +```bash +vstack init --only skill +``` + +This regenerates only the `skill` artifact family and applies your current exclusions. + +If you are onboarding a repository that already has unmanaged skill files, adopt first when needed: + +```bash +vstack init --only skill --adopt-name skill/ +``` + +## 3. Verify results + +Check generated skill files: + +```bash +ls .github/skills +``` + +Then verify managed state: + +```bash +vstack manifest status --only skill +vstack manifest verify --only skill +``` + +## Related docs + +- [Skills overview](../reference/skills-overview.md) +- [Configuration (`exclude` settings)](../reference/configuration.md#exclude-configuration) +- [Reinitialize](reinitialize.md) diff --git a/docs/user/how-to/install-and-upgrade.md b/docs/user/how-to/install-and-upgrade.md new file mode 100644 index 00000000..f5c56383 --- /dev/null +++ b/docs/user/how-to/install-and-upgrade.md @@ -0,0 +1,133 @@ +# Install and upgrade + +Use this guide for operational install and upgrade scenarios. + +## Fresh install (recommended) + +Use `pipx` so the CLI stays isolated from project dependencies. + +```bash +pipx install vstack +cd /path/to/your/project +vstack install +vstack validate +``` + +Equivalent explicit target form: + +```bash +vstack install --target /path/to/your/project +``` + +For a dedicated step-by-step flow, see [Fresh install](fresh-install.md). + +After a fresh repository install, vstack may seed starter stubs in `.vstack/templates/`. +Treat them as project-owned guidance files. Keep and adapt them as needed, but use `vstack init` for `.github/` regeneration. + +## Adopt or overwrite existing files + +When `.github/` already contains files, run a dry run first: + +```bash +cd /path/to/your/project +vstack install --dry-run +``` + +Resolve preserved-file conflicts with one of these strategies: + +```bash +# Overwrite one managed artifact +vstack install --force-name agent/engineer + +# Take ownership without overwriting +vstack install --adopt-name agent/engineer + +# Overwrite all managed artifacts +vstack install --force +``` + +Use selectors as `type/name` (for example `agent/engineer`, `skill/verify`). + +## Patch or minor upgrade (same major) + +For upgrades like `v3.1 -> v3.2`, docs paths stay stable. + +```bash +pipx upgrade vstack +cd /path/to/your/project +vstack init +``` + +For a focused upgrade flow, see [Upgrade](upgrade.md). + +`vstack init` is idempotent and safe in CI. + +## Major upgrade + +For upgrades like `v2 -> v3`, run docs migration before regeneration. + +```bash +pipx upgrade vstack +cd /path/to/your/project +vstack migrate +vstack init +``` + +Preview migration without file changes: + +```bash +vstack migrate --dry-run +``` + +For multi-major jumps, you can force a range: + +```bash +vstack migrate --from 1 --to 3 +vstack init +``` + +For incremental regeneration by artifact family, see [Partial upgrade](partial-upgrade.md). + +## Manifest schema upgrade + +If commands fail with a legacy schema message, upgrade the manifest first. + +```bash +cd /path/to/your/project +vstack manifest upgrade +vstack init +``` + +Typical sequence after a major migration warning: + +1. Run `vstack manifest upgrade`. +1. Re-run `vstack init`. +1. Confirm with `vstack manifest status`. + +If you need a clean regeneration after migration or config edits, see [Reinitialize](reinitialize.md). + +## Common mistakes + +- Running from the wrong directory: + - Symptom: artifacts are generated in an unexpected location. + - Fix: run from repository root, or pass `--target` explicitly. +- Skipping `vstack init` after upgrade: + - Symptom: old generated artifacts remain. + - Fix: run `vstack init` after every CLI upgrade. +- Using `--force` too early: + - Symptom: local manual edits are overwritten. + - Fix: prefer `--dry-run`, then `--force-name` or `--adopt-name`. +- Ignoring legacy manifest warnings: + - Symptom: install/status/verify commands fail. + - Fix: run `vstack manifest upgrade` once, then retry. +- Trying to control installed output by editing package internals: + - Symptom: changes do not persist, or commands are run in the wrong repository. + - Fix: use repository controls instead: update exclusions in `.vstack/config.yaml` or adopt existing files with `--adopt-name`, then run `vstack init` and `vstack manifest verify`. + +## Related docs + +- [Fresh install](fresh-install.md) +- [Upgrade](upgrade.md) +- [Global install](global-install.md) +- [Uninstall](uninstall.md) +- [What `.vstack/templates` is for](../explanation/vstack-templates.md) diff --git a/docs/user/how-to/partial-upgrade.md b/docs/user/how-to/partial-upgrade.md new file mode 100644 index 00000000..b4e06c4b --- /dev/null +++ b/docs/user/how-to/partial-upgrade.md @@ -0,0 +1,55 @@ +# Partial Upgrade + +Use this guide when you want to upgrade only selected artifact families instead of regenerating everything. + +## When to Use Partial Upgrade + +Common scenarios: + +- you only changed one artifact family in templates, +- you want lower-risk incremental rollout, +- you want faster local verification during migration. + +## Upgrade Selected Artifact Types + +```bash +pipx upgrade vstack +vstack init --only agent +``` + +Multiple types: + +```bash +vstack init --only skill prompt instruction +``` + +Hooks only: + +```bash +vstack init --only hook +``` + +## Verify Selected Types + +```bash +vstack manifest status --target . --only agent +vstack manifest verify --target . --only agent +``` + +## Recommended Rollout Pattern + +1. Upgrade one type with `vstack init --only `. +1. Run `manifest status` and `manifest verify` for the same type. +1. Repeat for the next type. +1. Finish with a full verification pass. + +## Risks and Limits + +- Partial upgrade can leave other artifact types on older generated output. +- For broad CLI upgrades, a full `vstack init` is still the safest final step. + +## Related Docs + +- [Upgrade](upgrade.md) +- [Reinitialize](reinitialize.md) +- [CLI commands](../reference/cli-commands.md) diff --git a/docs/user/how-to/reinitialize.md b/docs/user/how-to/reinitialize.md new file mode 100644 index 00000000..929acd9d --- /dev/null +++ b/docs/user/how-to/reinitialize.md @@ -0,0 +1,50 @@ +# Reinitialize + +Use this guide when you need to re-apply vstack generation from current project configuration. + +Typical cases: + +- after editing `.vstack/config.yaml`, +- after upgrading vstack, +- after resolving conflicts in managed `.github/` artifacts. + +## Reinitialize the Project + +```bash +vstack init +``` + +With explicit target: + +```bash +vstack init --target /path/to/repo +``` + +## Reinitialize Specific Artifact Types + +Use `--only` when you want a narrower regeneration scope: + +```bash +vstack init --only agent +vstack init --only skill prompt +vstack init --only hook +``` + +## Verify the Reinitialized State + +```bash +vstack manifest status --target . +vstack manifest verify --target . +``` + +## Troubleshooting + +- If a file is locally modified and not updated as expected, inspect `manifest status` output. +- If you need explicit overwrite semantics, use install flags such as `--force-name` or `--force`. + +## Related Docs + +- [Upgrade](upgrade.md) +- [Partial upgrade](partial-upgrade.md) +- [Install and upgrade](install-and-upgrade.md) +- [Artifact checks](../reference/artifact-checks.md) diff --git a/docs/user/how-to/troubleshooting.md b/docs/user/how-to/troubleshooting.md new file mode 100644 index 00000000..72db3b14 --- /dev/null +++ b/docs/user/how-to/troubleshooting.md @@ -0,0 +1,103 @@ +# Troubleshooting + +Use this guide when install or day-to-day usage does not behave as expected. + +## Install and environment + +### `vstack` command not found + +Cause: `pipx` binary path is not on `PATH`. + +Checks: + +```bash +pipx --version +pipx ensurepath +``` + +Then restart your shell and verify: + +```bash +vstack --version +``` + +### Validation fails right after install + +Run from repository root: + +```bash +cd /path/to/your/project +vstack install +vstack validate +``` + +If manifest-related errors mention a legacy schema: + +```bash +vstack manifest upgrade +vstack init +``` + +### Install appears to skip files + +vstack defaults to conservative behavior and preserves unmanaged files. + +```bash +vstack install --dry-run +``` + +Then resolve preserved entries with `--adopt-name`, `--force-name`, or `--force`. + +## Copilot visibility and behavior + +### Agents are not visible in Copilot Chat + +1. Confirm artifacts exist in `.github/` for your repository. +1. Run VS Code command `Developer: Reload Window`. +1. Re-open Copilot Chat and pick an agent. + +### Results look generic + +Cause: no explicit role context. + +Fix: choose a role in the agent picker first (for example `planner` or `tester`), then run your prompt or skill. + +## CI parity and local parity + +### CI behavior differs from local generation + +Most common cause: local changes were not regenerated with current templates. + +Local parity checklist: + +1. Run `vstack init` in the repository root. +1. Run `vstack validate`. +1. Run `vstack manifest status --target .`. +1. Commit generated `.github/` changes when expected. + +For upgrades, ensure CI and local both use the same installed CLI major version. + +## Search noise in large repositories + +When searching generated artifacts or docs, results can be noisy. + +Practical approaches: + +- Narrow search scope to source directories first (for example `src/`, `tests/`). +- Exclude generated or dependency folders in search UI: + - `.github/` (when not reviewing generated output) + - `.venv/`, `node_modules/`, `dist/`, `build/` +- Use `vstack manifest status` to inspect managed artifact drift instead of raw text search. + +## Useful diagnostics commands + +```bash +vstack --version +vstack validate +vstack manifest status --target . +vstack manifest verify --target . +``` + +If the issue persists, open a discussion with command output and repository context: + +- diff --git a/docs/user/how-to/uninstall.md b/docs/user/how-to/uninstall.md new file mode 100644 index 00000000..25228093 --- /dev/null +++ b/docs/user/how-to/uninstall.md @@ -0,0 +1,75 @@ +# Uninstall + +Use this guide to remove vstack-managed artifacts safely. + +## Safety first + +`vstack uninstall` removes only manifest-tracked artifacts in the selected scope. + +By default, locally modified tracked files are preserved. + +## Uninstall in a repository + +From repository root: + +```bash +vstack uninstall +``` + +Or use an explicit path: + +```bash +vstack uninstall --target /path/to/repository +``` + +## Uninstall in global scope + +Remove profile-scoped artifacts: + +```bash +vstack uninstall --global +``` + +## Selective uninstall + +Remove one artifact family: + +```bash +vstack uninstall --only skill +``` + +Force-remove one modified artifact by name: + +```bash +vstack uninstall --force-name verify +``` + +## Force behavior + +Use force only after reviewing what is preserved by default. + +```bash +vstack uninstall --force +``` + +This can remove modified tracked files that would otherwise be preserved. + +## Verify result + +For repository scope: + +```bash +vstack manifest status +``` + +For global scope: + +```bash +vstack manifest status --global +``` + +## Related docs + +- [Global install](global-install.md) +- [Install and upgrade](install-and-upgrade.md) +- [CLI commands](../reference/cli-commands.md) diff --git a/docs/user/how-to/update-managed-artifacts.md b/docs/user/how-to/update-managed-artifacts.md new file mode 100644 index 00000000..96378a1f --- /dev/null +++ b/docs/user/how-to/update-managed-artifacts.md @@ -0,0 +1,85 @@ +# Update managed artifacts + +Use this guide when you want to refresh generated artifacts and control overwrite behavior. + +## Update all managed artifacts + +Run a full idempotent refresh from project config: + +```bash +vstack init +``` + +## Update one or more artifact types + +Use `--only` to refresh a subset: + +```bash +vstack init --only hook +vstack init --only hook agent +``` + +## Force overwrite managed artifacts + +Force overwrite for selected type scope: + +```bash +vstack init --only hook --force +``` + +Force overwrite for all managed artifacts: + +```bash +vstack install --force +``` + +Force overwrite one named artifact: + +```bash +vstack install --force-name hook/agent-call-audit +vstack install --force-name agent/engineer +``` + +Use selectors as `type/name`. + +## Concrete examples + +Refresh hooks and verify hook state: + +```bash +vstack init --only hook +vstack manifest status --target . --only hook +vstack manifest verify --target . --only hook +``` + +Refresh agents and verify agent state: + +```bash +vstack init --only agent +vstack manifest status --target . --only agent +vstack manifest verify --target . --only agent +``` + +## What happens after manual edits + +- A normal `vstack init` preserves locally modified managed artifacts. +- `vstack manifest status --target .` shows those files as modified. +- `vstack manifest verify --target .` reports checksum mismatch. +- The file is overwritten only when you use an explicit force command. + +## Safe recovery flow + +1. Run `vstack manifest status --target .` to list modified managed files. +1. Run `vstack manifest verify --target .` to confirm checksum mismatches. +1. Decide scope: + - one artifact: `vstack install --force-name /` + - one type: `vstack init --only --force` + - all managed artifacts: `vstack install --force` +1. Re-run `vstack manifest status --target .`. +1. Re-run `vstack manifest verify --target .`. + +## Related Docs + +- [Artifact checks](../reference/artifact-checks.md) +- [CLI commands](../reference/cli-commands.md) +- [Reinitialize](reinitialize.md) diff --git a/docs/user/how-to/upgrade.md b/docs/user/how-to/upgrade.md new file mode 100644 index 00000000..f4fc8949 --- /dev/null +++ b/docs/user/how-to/upgrade.md @@ -0,0 +1,67 @@ +# Upgrade + +Use this guide to upgrade vstack in an existing repository while keeping local project configuration. + +## 1. Upgrade the CLI + +```bash +pipx upgrade vstack +vstack --version +``` + +## 2. Re-generate Managed Artifacts + +```bash +vstack init +``` + +`vstack init` is idempotent. You can run it repeatedly after upgrades. + +## 3. Verify After Upgrade + +The commands in this guide assume you run them from the repository root, so `--target` is omitted. + +```bash +vstack validate +vstack manifest status +vstack manifest verify +``` + +## 4. Major Upgrade Path + +For major version jumps, migrate docs artifacts first: + +```bash +vstack migrate +vstack init +``` + +Preview migration without writing files: + +```bash +vstack migrate --dry-run +``` + +For multi-major jumps: + +```bash +vstack migrate --from 1 --to 3 +vstack init +``` + +## 5. Legacy Manifest Upgrade + +If you get a legacy manifest schema warning: + +```bash +vstack manifest upgrade +vstack init +vstack manifest status +``` + +## Related Docs + +- [Install and upgrade](install-and-upgrade.md) +- [Partial upgrade](partial-upgrade.md) +- [Reinitialize](reinitialize.md) +- [CLI commands](../reference/cli-commands.md) diff --git a/docs/user/reference/README.md b/docs/user/reference/README.md new file mode 100644 index 00000000..b3664f63 --- /dev/null +++ b/docs/user/reference/README.md @@ -0,0 +1,22 @@ +# Reference + +Reference documentation contains factual, lookup-oriented material. + +Use this section for stable interfaces and contracts, including: + +- CLI command behavior and flags +- configuration schema details +- artifact formats and field definitions + +Reference pages should be precise, complete, and free of tutorial flow. + +## Pages + +- [CLI commands](cli-commands.md) +- [Configuration](configuration.md) +- [Hooks](hooks.md) +- [Prompts overview](prompts-overview.md) +- [Skills overview](skills-overview.md) +- [Instructions overview](instructions-overview.md) +- [Artifact checks](artifact-checks.md) +- [Work items](work-items.md) diff --git a/docs/user/reference/artifact-checks.md b/docs/user/reference/artifact-checks.md new file mode 100644 index 00000000..7b44a6d1 --- /dev/null +++ b/docs/user/reference/artifact-checks.md @@ -0,0 +1,87 @@ +# Artifact Checks + +This page explains how vstack checks generated artifacts and how to read check results. + +## Why Checks Exist + +Artifact checks protect against drift between: + +- generated files in `.github/`, +- manifest ownership in `.vstack/vstack.json`, +- current template output. + +## Core Commands + +```bash +vstack validate +vstack manifest status --target . +vstack manifest verify --target . +``` + +What each command does: + +- `validate`: checks template/config validity before install output concerns. +- `manifest status`: reports managed, modified, missing, and related ownership states. +- `manifest verify`: verifies installed output and checksum alignment with manifest entries. + +## Checksum and Ownership Basics + +vstack records checksums for managed artifacts in `.vstack/vstack.json`. + +During verification, vstack compares current file content to recorded checksum: + +- match: file is managed and unchanged, +- mismatch: file is modified relative to manifest, +- missing file: manifest entry exists but output file does not. + +## Managed State and Update Behavior + +Unchanged managed files: + +- Full regeneration updates them during `vstack init`. +- Type-scoped regeneration updates them during `vstack init --only `. + +Modified managed files: + +- Normal `vstack init` preserves local manual edits. +- Normal `vstack init --only ` also preserves manual edits in scope. +- Overwrite requires explicit force (`vstack init --only --force`, `vstack install --force`, or `vstack install --force-name /`). + +Verification outcomes: + +- Unchanged managed files verify cleanly. +- Modified managed files appear as modified in status and fail checksum verification. +- After explicit force overwrite, files return to managed and unchanged state. + +## Typical Failure Cases + +- Manual edits to managed `.github/` artifacts. +- Running commands in the wrong target directory. +- Upgraded CLI without running `vstack init`. +- Legacy manifest schema not upgraded yet. + +## Recommended Recovery Flow + +1. Run `vstack manifest status --target .`. +1. Inspect modified or missing entries. +1. Re-run `vstack init` (or `vstack init --only `) for non-modified files. +1. For modified managed files, use explicit force overwrite: + - `vstack install --force-name /` + - `vstack init --only --force` + - `vstack install --force` +1. Re-run `vstack manifest verify --target .`. + +If schema upgrade is required: + +```bash +vstack manifest upgrade +vstack init +``` + +## Related Docs + +- [CLI commands](cli-commands.md) +- [Update managed artifacts](../how-to/update-managed-artifacts.md) +- [Install and upgrade](../how-to/install-and-upgrade.md) +- [Partial upgrade](../how-to/partial-upgrade.md) +- [Reinitialize](../how-to/reinitialize.md) diff --git a/docs/user/reference/cli-commands.md b/docs/user/reference/cli-commands.md new file mode 100644 index 00000000..59d673e8 --- /dev/null +++ b/docs/user/reference/cli-commands.md @@ -0,0 +1,58 @@ +# CLI commands + +This page is the command reference for `vstack`. + +## Essential commands + +Use these for daily operation: + +```bash +vstack --version +vstack validate +vstack install +vstack init +vstack migrate +vstack manifest status --target . +vstack manifest verify --target . +vstack manifest upgrade --target . +``` + +## Full command reference + +| Command | Description | +| ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | +| `vstack --version` | Show vstack version. | +| `vstack validate` | Validate source templates and configuration constraints. | +| `vstack verify` | Verify source templates and installed output, including checksum drift. | +| `vstack install` | First-run setup in the current directory target. Seeds `.vstack/config.yaml` if missing, then generates `.github/` artifacts. | +| `vstack install --target DIR` | Same as `install`, but explicit target path. | +| `vstack install --global` | Install artifacts into the VS Code user profile scope. | +| `vstack install --dry-run` | Preview install actions without writing files. | +| `vstack install --force` | Overwrite all managed artifacts in target scope. | +| `vstack install --force-name TYPE/NAME` | Overwrite one managed artifact selector. | +| `vstack install --adopt-name TYPE/NAME` | Track an existing unmanaged artifact without overwriting it. | +| `vstack init` | Idempotent regeneration in current directory based on `.vstack/config.yaml`. | +| `vstack init --target DIR` | Same as `init`, but explicit target path. | +| `vstack uninstall` | Remove tracked artifacts from current directory target when checksums still match manifest. | +| `vstack uninstall --target DIR` | Same as `uninstall`, but explicit target path. | +| `vstack uninstall --global` | Uninstall profile-scoped artifacts. | +| `vstack uninstall --force` | Remove tracked artifacts even when locally modified. | +| `vstack uninstall --force-name TYPE/NAME` | Force removal for one managed artifact selector. | +| `vstack status --target DIR` | Alias-style status report for installed artifact drift and ownership. | +| `vstack manifest status --target DIR` | Manifest-scoped status report for managed, modified, missing, and conflicting files. | +| `vstack manifest verify --target DIR` | Manifest-scoped verification for installed output and checksums. | +| `vstack manifest upgrade --target DIR` | Upgrade legacy `.vstack/vstack.json` schema to current format. | +| `vstack migrate --target DIR` | Move docs files to newer paths for major upgrades. | +| `vstack migrate --dry-run` | Preview migration actions only. | +| `vstack migrate --from M --to N` | Run a specific major-version migration range. | + +## Install vs init + +| Command | Primary use | Behavior | +| ---------------- | ------------------------------------------------------ | ---------------------------------------------------------------- | +| `vstack install` | First setup or onboarding a machine | Seeds `.vstack/config.yaml` if missing, then runs generation. | +| `vstack init` | Repeatable regeneration after upgrades or config edits | Re-applies generation idempotently from existing project config. | + +Both commands default to current working directory when `--target` is omitted. + +For manual-edit preservation and force-overwrite flows, see [Update managed artifacts](../how-to/update-managed-artifacts.md). diff --git a/docs/user/reference/configuration.md b/docs/user/reference/configuration.md new file mode 100644 index 00000000..0b4bcc9a --- /dev/null +++ b/docs/user/reference/configuration.md @@ -0,0 +1,142 @@ +# Configuration + +Project configuration is read from `.vstack/config.yaml`. + +## How to read `.vstack/config.yaml` + +- Lines starting with `#` are comments or examples and are not active. +- Only uncommented YAML keys are active configuration. +- Keep indentation valid when enabling example blocks. +- After changing configuration, run `vstack init` to apply it. + +## Exclude configuration + +Use `exclude` to skip artifact types or specific artifacts. + +```yaml +exclude: + skills: + - terraform + - helm + - k8s + instructions: all + prompts: all + hook: all +``` + +Rules: + +- `type: all` skips an entire artifact type. +- `type: [name, ...]` skips only listed artifacts. +- Use exclusions to reduce generated surface for team needs. + +## Workflow mode + +Set workflow execution style in config: + +```yaml +workflow: + mode: agentic +``` + +Supported values: + +| Mode | Meaning | +| --------- | ------------------------------------------------------------- | +| `agentic` | Planner orchestrates stage progression automatically. | +| `manual` | User advances stages manually with handoffs. | +| `hybrid` | Both planner orchestration and manual handoffs are available. | + +Run `vstack init` after changing mode. + +## Workflow version + +`workflow.version` is the schema version for the `workflow` block in `.vstack/config.yaml`. + +```yaml +workflow: + mode: agentic + version: 1 +``` + +Guidance: + +- Current supported value is `1`. +- `vstack install` seeds `version: 1` in new project config. +- Keep the existing value unless release notes explicitly require a change. +- Preserve this field when editing `workflow.stages` so tooling can validate semantics correctly. + +Task-focused setup steps: + +- [Configure workflow modes](../how-to/configure-workflow-modes.md) +- [Workflow modes](../explanation/workflow-modes.md) + +## Hooks configuration + +Use `hooks` to control repository hook generation and defaults. + +```yaml +hooks: + enabled: true + mode: audit + log_level: minimal + log_retention_days: 7 + log_dir: .vstack/logs + hooks: + post-edit-markdown-quality: + enabled: false + pre-tool-safety-gate: + mode: enforce +``` + +Common controls: + +- `hooks.enabled`: globally enable or disable baseline hooks. +- `hooks.mode`: set default hook mode (`audit` or `enforce`). +- `hooks.hooks..enabled`: disable one named hook. +- `hooks.hooks..mode`: override mode for one named hook. + +Run `vstack init --only hook` (or `vstack init`) after changing hooks config. + +## `depends_on` semantics + +`workflow.stages` order is canonical. Without `depends_on`, each stage implicitly depends on the previous stage (sequential flow). + +Add `depends_on` to create explicit graph dependencies and parallel-ready branches: + +```yaml +workflow: + mode: agentic + version: 1 + stages: + - role: product + gate: required + hitl: always + - role: architect + gate: required + hitl: always + depends_on: [product] + - role: designer + gate: optional + hitl: on-change + depends_on: [product] + - role: engineer + gate: required + hitl: always + depends_on: [architect, designer] +``` + +Semantics: + +- `depends_on: []` marks a root stage. +- Omitted `depends_on` falls back to sequential predecessor. +- A stage becomes ready only when all dependencies are ready or skipped. +- Cycles and invalid dependencies are rejected during validation/install. + +## Related docs + +- [Workflow modes](../explanation/workflow-modes.md) +- [Configure workflow modes](../how-to/configure-workflow-modes.md) +- [Hooks](./hooks.md) +- [Install and upgrade](../how-to/install-and-upgrade.md) +- [CLI commands](./cli-commands.md) diff --git a/docs/user/reference/hooks.md b/docs/user/reference/hooks.md new file mode 100644 index 00000000..010ffc9b --- /dev/null +++ b/docs/user/reference/hooks.md @@ -0,0 +1,159 @@ +# Hooks + +This page explains what repository hooks do in vstack and how to enable or disable them. + +## What Hooks Are + +vstack manages repository-level hook artifacts under `.github/hooks/*.json`. + +Hooks run on Copilot session and tool events. They support safety, audit, and quality checks. + +## Version Semantics + +Hooks use two different version concepts with different owners: + +| Field | Example | Owner | Meaning | +| --------------------------- | --------------------------------------------------------------- | ---------------------------- | -------------------------------------------------------------------------------------------------------------------- | +| Hook payload `version` | `1` in `.github/hooks/.json` | GitHub Copilot hook contract | Envelope/schema version for the JSON hook payload (`{"version": 1, "hooks": {...}}`). | +| Artifact template `version` | `20260514002` in `src/vstack/_templates/hooks//hook.yaml` | vstack template lifecycle | Revision token for the hook template artifact, tracked in `.vstack/vstack.json` and used for drift/ownership checks. | + +These fields are intentionally different and should not be forced to match. + +- If Copilot changes the hook envelope contract in the future, payload `version` may move from `1` to `2` or higher. +- Hook template revision tokens can still increment independently for normal template updates. + +Operational rule: treat payload `version` as contract compatibility, and artifact `version` as vstack-managed template revision. + +## Built-in Hooks + +vstack ships these baseline hooks: + +| Hook | Purpose | +| ---------------------------- | ------------------------------------------------------------------------ | +| `agent-call-audit` | Records actor and delegation visibility for agent and subagent flows. | +| `session-audit` | Records generic session, prompt, and tool telemetry without actor data. | +| `log-retention-cleanup` | Prunes old hook log directories based on retention policy. | +| `pre-tool-safety-gate` | Detects risky tool actions before execution and applies policy behavior. | +| `post-edit-format` | Runs or records formatting-related checks after edit operations. | +| `post-edit-markdown-quality` | Applies markdown quality checks for docs-oriented changes. | +| `post-commit-security-scan` | Runs post-commit safety/security checks when configured. | + +## Enable or Disable Hooks Globally + +In `.vstack/config.yaml`: + +```yaml +hooks: + enabled: true + mode: audit +``` + +- `enabled: false` disables generation of baseline hook artifacts. +- `mode` sets default behavior mode for generated hooks. + +## Enable or Disable Individual Hooks + +```yaml +hooks: + hooks: + post-edit-markdown-quality: + enabled: false + pre-tool-safety-gate: + mode: enforce +``` + +## Log Settings + +```yaml +hooks: + log_level: minimal + log_retention_days: 7 + log_dir: .vstack/logs + hooks: + session-audit: + log: + level: verbose + name: session-audit.log + retention_days: 14 +``` + +Runtime pruning is enforced for hook logs: + +- On each hook event path, vstack prunes old day directories under `log_dir`. +- Pruning only targets directories named `YYYYMMDD`. +- `agent-call-audit` applies the same retention to `hook-agent-call-unknown-events.tsv`, because it is stored in the per-day directory that is pruned. +- Retention defaults to `metadata.log.retention_days` (currently `7`) and can be overridden with `VSTACK_HOOK_RETENTION_DAYS`. +- Invalid override values (non-numeric or non-positive) safely fall back to `7`. + +## Audit Boundary Model + +- `session-audit` is for generic telemetry only. It tracks payload volume and event metadata for session and tool activity. +- `agent-call-audit` is for actor and delegation telemetry only. It tracks who acted and where control was delegated. + +## Session Audit Minimal Logging + +When `session-audit` runs in `minimal` mode, logs include compact visibility fields without storing full prompt payload text: + +- Every minimal record includes `timestamp`, `event`, `size_bytes`, `estimated_tokens`, and `hook_execution_ms`. +- `preToolUse` and `postToolUse` also include `tool_name` and `tool_call_id`. +- `userPromptSubmitted` also includes `slash_command` when a prompt starts with `/...`; otherwise it records `none`. + +Raw event payloads are written only in `verbose` mode. + +## Agent Call Audit Minimal Logging + +When `agent-call-audit` runs in `minimal` mode, it writes compact JSONL records to `.vstack/logs/YYYYMMDD/hook-agent-call.log`: + +- Every minimal record includes `timestamp`, `event`, `size_bytes`, `estimated_tokens`, and `hook_execution_ms`. +- `sessionStart` and `sessionEnd` include `session_id`, `actor_name`, `actor_type`, and `model_used`. +- `preToolUse` and `postToolUse` include `session_id`, `actor_name`, `actor_type`, `tool_name`, `delegated_agent_name`, and `model_used`. +- Delegation calls are explicit: + - `preToolUse` logs `delegationStart` when `tool_name` is `runSubagent`. + - `postToolUse` logs `delegationEnd` when `tool_name` is `runSubagent`. +- `delegated_agent_name` is best-effort and extracted when delegation payload fields are present, especially for `runSubagent` calls. +- Unknown suppression in minimal mode is counter-based: + - Suppressed unknown events persist in `.vstack/logs/YYYYMMDD/hook-agent-call-unknown-events.tsv` as `session_idreasoncount`. + - The hook aggregates counts per `(session_id, reason)` instead of appending duplicate rows. + - Legacy 2-column rows (`session_idreason`) are treated as `count=1` when summarizing and rewriting. + - `sessionEnd` emits an `unknownSummary` record with aggregated per-session counts before cleanup. + +### Why this file exists + +`hook-agent-call-unknown-events.tsv` is an internal counter file for minimal-mode unknown suppression. +It preserves per-session unknown reasons so `sessionEnd` can emit an accurate `unknownSummary`, and it helps operators track parser quality and unknown-rate drift over time. + +`actor_name`, `actor_type`, `tool_name`, `delegated_agent_name`, and `model_used` are best-effort and may remain `unknown` when payload fields are unavailable. + +## Minimal Log Schema + +Use this compact reference to see which fields each hook emits per mode and event. + +| Hook | Mode | Event | Emitted fields | +| ------------------ | --------- | --------------------- | --------------------------------------------------------------------------------------------------------- | +| `session-audit` | `minimal` | any event | `timestamp`, `event`, `size_bytes`, `estimated_tokens`, `hook_execution_ms` | +| `session-audit` | `minimal` | `preToolUse` | base fields + `tool_name`, `tool_call_id` | +| `session-audit` | `minimal` | `postToolUse` | base fields + `tool_name`, `tool_call_id` | +| `session-audit` | `minimal` | `userPromptSubmitted` | base fields + `slash_command` | +| `session-audit` | `verbose` | any event | raw payload passthrough | +| `agent-call-audit` | `minimal` | any event | `timestamp`, `event`, `size_bytes`, `estimated_tokens`, `hook_execution_ms` | +| `agent-call-audit` | `minimal` | `sessionStart` | base fields + `session_id`, `actor_name`, `actor_type`, `model_used` | +| `agent-call-audit` | `minimal` | `sessionEnd` | base fields + `session_id`, `actor_name`, `actor_type`, `model_used` | +| `agent-call-audit` | `minimal` | `preToolUse` | base fields + `session_id`, `actor_name`, `actor_type`, `tool_name`, `delegated_agent_name`, `model_used` | +| `agent-call-audit` | `minimal` | `postToolUse` | base fields + `session_id`, `actor_name`, `actor_type`, `tool_name`, `delegated_agent_name`, `model_used` | +| `agent-call-audit` | `verbose` | any event | raw payload passthrough | + +`base fields` in this table means `timestamp`, `event`, `size_bytes`, `estimated_tokens`, and `hook_execution_ms`. + +## Apply and Verify Hook Changes + +```bash +vstack init --only hook +vstack manifest status --target . --only hook +vstack manifest verify --target . --only hook +``` + +## Related Docs + +- [Configuration](configuration.md) +- [CLI commands](cli-commands.md) +- [Artifact checks](artifact-checks.md) diff --git a/docs/user/reference/instructions-overview.md b/docs/user/reference/instructions-overview.md new file mode 100644 index 00000000..f566a2c3 --- /dev/null +++ b/docs/user/reference/instructions-overview.md @@ -0,0 +1,38 @@ +# Instructions Overview + +This page lists instruction sets built into the installed vstack package and explains where they apply. + +## What Instructions Are + +Instructions define repository conventions that apply automatically by file pattern. + +In vstack, instructions are generated into `.github/instructions/*.instructions.md`. + +## Built-in Instructions + +| Instruction | What It Helps With | +| ------------ | --------------------------------------------------------------------------------- | +| `git` | Git and release hygiene conventions for branches, commits, and release workflows. | +| `helm` | Helm chart conventions for chart templates and values files. | +| `java` | Java coding conventions for source, tests, and build configuration. | +| `k8s` | Kubernetes manifest conventions for workloads and services. | +| `markdown` | Markdown authoring standards for docs, READMEs, and ADRs. | +| `python` | Python coding conventions for modules, tests, and CLI code. | +| `rancher` | Rancher and Fleet conventions for governance and cluster config. | +| `security` | Security policy for code, configuration, and infrastructure. | +| `terraform` | Terraform coding conventions for modules and root configs. | +| `terragrunt` | Terragrunt conventions for DRY multi-environment setups. | +| `testing` | Test authoring conventions for behavior-focused coverage. | +| `typescript` | TypeScript and JavaScript coding conventions. | + +## How Instructions Are Applied + +- Each instruction has an `applyTo` pattern. +- Matching files inherit the instruction constraints. +- Multiple matching instructions can apply together. + +## Related Docs + +- [Configuration](configuration.md) +- [Skills overview](skills-overview.md) +- [Prompts overview](prompts-overview.md) diff --git a/docs/user/reference/prompts-overview.md b/docs/user/reference/prompts-overview.md new file mode 100644 index 00000000..21c34b3f --- /dev/null +++ b/docs/user/reference/prompts-overview.md @@ -0,0 +1,36 @@ +# Prompts Overview + +This page lists prompt templates built into the installed vstack package and explains what they are for. + +## What Prompts Are + +Prompts are reusable review and analysis templates that guide a focused task. + +In vstack, prompts are generated into `.github/prompts/*.prompt.md`. + +## Built-in Prompts + +| Prompt | What It Helps With | +| -------------------- | --------------------------------------------------------------------------------------------------- | +| `api-design-review` | Review an API design or OpenAPI spec for correctness, completeness, and consistency. | +| `architecture-risk` | Identify architectural risks, tradeoffs, and mitigation priorities for a proposed design. | +| `artifact-integrity` | Check source templates against generated artifacts and identify drift or missing regeneration. | +| `ci-triage` | Triage CI failures into root-cause clusters and prioritize the fastest safe recovery path. | +| `code-review` | Review a change for bugs, regressions, and missing tests. | +| `dependency-audit` | Audit dependencies for vulnerabilities, outdated versions, licence risks, and supply chain hygiene. | +| `incident-timeline` | Build a structured, evidence-based incident timeline and action-oriented postmortem summary. | +| `migration-plan` | Produce a safe migration plan with sequencing, fallback paths, and verification checkpoints. | +| `migration-safety` | Review database migration safety, rollback strategy, and zero-downtime risk. | +| `ops-readiness` | Assess operational readiness across observability, runbooks, failure handling, and supportability. | +| `release-check` | Evaluate release gate readiness using required reports, artifacts, and sign-off evidence. | +| `repo-assessment` | Assess a repository for production-readiness gaps and prioritized improvements. | +| `template-impact` | Assess impact of a template change on generated artifacts, tests, and release risk. | +| `test-gaps` | Identify missing behavioral coverage and prioritize test additions by production risk. | +| `upgrade-plan` | Build a safe upgrade plan with sequencing, compatibility checks, and rollback points. | +| `workflow-check` | Review workflow stage flow, gate usage, and handoff integrity across role artifacts. | + +## Related Docs + +- [Skills overview](skills-overview.md) +- [Instructions overview](instructions-overview.md) +- [Work items](work-items.md) diff --git a/docs/user/reference/skills-overview.md b/docs/user/reference/skills-overview.md new file mode 100644 index 00000000..e75d1baa --- /dev/null +++ b/docs/user/reference/skills-overview.md @@ -0,0 +1,66 @@ +# Skills Overview + +This page lists skills built into the installed vstack package and explains what each skill is used for. + +## What Skills Are + +Skills provide domain-specific execution playbooks. Agents use them to perform focused technical work. + +In vstack, skills are generated into `.github/skills//SKILL.md`. + +## Built-in Skills + +| Skill | What It Helps With | +| --------------------- | ------------------------------------------------------------------------- | +| `adr` | Architecture Decision Record writing for significant technical decisions. | +| `analyse` | Cross-cutting impact, tradeoff, and feasibility analysis. | +| `architecture` | Engineering-lead architecture and execution-plan review. | +| `aws-cli` | AWS CLI operations across core AWS services. | +| `cicd` | GitHub Actions CI/CD workflow authoring and hardening. | +| `cloudformation` | CloudFormation template authoring and review. | +| `code-review` | Pre-merge review for bugs, regressions, and risk gaps. | +| `codeql` | CodeQL scanning setup and workflow configuration. | +| `concise` | Runtime response-density control (normal/compact/ultra). | +| `consult` | DX-focused review with prioritized usability improvements. | +| `container` | Dockerfile and container runtime setup and hardening. | +| `conventional-commit` | Conventional Commit message preparation and validation. | +| `copilot-ops` | Copilot policy/configuration operations and governance. | +| `debug` | Root-cause-first debugging using scientific investigation flow. | +| `dependabot` | Dependabot configuration strategy for update hygiene. | +| `dependency` | Dependency health audit, upgrade strategy, and supply-chain checks. | +| `design` | API and service interface design standards and contracts. | +| `docs` | Post-release documentation alignment with shipped behavior. | +| `explore` | Repository architecture discovery and onboarding analysis. | +| `gdpr` | GDPR-compliant engineering review for data handling and flows. | +| `gh-issues` | GitHub issue lifecycle management via `gh`. | +| `gh-release` | GitHub Release drafting and publication flow via `gh`. | +| `guardrails` | Session safety controls for destructive-command confirmation. | +| `helm` | Helm chart authoring, upgrade, and release troubleshooting. | +| `incident` | Incident coordination, sequencing, and action definition. | +| `inspect` | Read-only verification audit with severity-ranked findings. | +| `k8s` | Kubernetes manifest authoring and operational troubleshooting. | +| `migrate` | Safe database migration planning and review. | +| `onboard` | Contributor onboarding guide creation. | +| `openapi` | OpenAPI 3.1 specification writing and review. | +| `performance` | Benchmarking, profiling, and performance regression analysis. | +| `postmortem` | Blameless stakeholder-facing incident postmortem writing. | +| `pr` | Commit/push and pull-request creation workflow support. | +| `rancher` | Rancher and Fleet workload/governance operations. | +| `rca` | Technical root cause analysis for incidents and defects. | +| `refactor` | Behavior-preserving structural refactoring. | +| `release-notes` | Release note artifact and changelog preparation. | +| `requirements` | Structured requirements gathering and documentation. | +| `secret-scan` | Secret scanning and push-protection setup and triage. | +| `security` | OWASP/STRIDE security audit across code and config. | +| `space-setup` | Copilot Space setup and context quality management. | +| `terraform` | Terraform IaC authoring and review. | +| `terragrunt` | Terragrunt DRY multi-environment configuration design. | +| `threat-model` | Threat modeling with STRIDE-first prioritization. | +| `verify` | Verification fix-loop with targeted re-checking. | +| `vision` | Scope and strategy review from first principles. | + +## Related Docs + +- [Prompts overview](prompts-overview.md) +- [Instructions overview](instructions-overview.md) +- [Work items](work-items.md) diff --git a/docs/user/reference/work-items.md b/docs/user/reference/work-items.md new file mode 100644 index 00000000..64051da9 --- /dev/null +++ b/docs/user/reference/work-items.md @@ -0,0 +1,47 @@ +# Work Items + +This page explains what work-item documents vstack roles create and what each item is used for. + +## What Work Items Are + +Work items are role-owned documents (and related outputs) that move work through the delivery flow. + +They provide durable handoff context between stages. + +## Role Outputs at a Glance + +| Role | Primary Outputs | Purpose | +| ----------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | +| `product` | `docs/product/vision.md`, `docs/product/requirements.md`, `docs/product/roadmap.md` | Define product direction, scope, and outcomes. | +| `architect` | `docs/architecture/overview.md`, `docs/architecture/adr/*.md` | Define system blueprint and major decisions. | +| `designer` | `docs/design/overview.md`, optional additional design docs (for example `docs/design/ux.md`) | Define interfaces, contracts, and interaction-level specifications. | +| `engineer` | `src/**/*`, `tests/**/*`, optional issue RCA/postmortem docs | Implement approved behavior and tests. | +| `tester` | `docs/reports/**/*.md`, optional test updates in `tests/**/*` | Verify correctness, risk, and release readiness evidence. | +| `release` | `docs/releases/*.md` | Capture release notes and sign-off records. | + +## Planner Role + +`planner` orchestrates stage progression and reads workflow docs across the repository. It does not own a dedicated role-specific output artifact family. + +## Typical Flow + +1. Product defines what should be built. +1. Architect defines structure and constraints. +1. Designer defines implementation-facing design specs. +1. Engineer writes code and tests. +1. Tester validates behavior and risk. +1. Release assembles release evidence and notes. + +## Practical Notes + +- Work-item paths are project conventions and can evolve with approved workflow configuration. +- Role ownership matters: each role should write its own output artifacts. +- Use role outputs as handoff baseline instead of ad-hoc chat-only context. + +## Related Docs + +- [Workflow modes explanation](../explanation/workflow-modes.md) +- [Configure workflow modes](../how-to/configure-workflow-modes.md) +- [Configuration](configuration.md) +- [Prompts overview](prompts-overview.md) +- [Skills overview](skills-overview.md) diff --git a/docs/user/start-here.md b/docs/user/start-here.md new file mode 100644 index 00000000..233c5b88 --- /dev/null +++ b/docs/user/start-here.md @@ -0,0 +1,51 @@ +# Start Here + +Choose the path that matches your goal, then follow the checklist in order. + +## Quick commands + +```bash +# Check current manifest status +vstack manifest status --target . + +# Update hooks only +vstack init --only hook + +# Force one artifact (example: agent/engineer) +vstack install --force-name agent/engineer + +# Force update one type (hooks) +vstack init --only hook --force + +# Verify manifest health +vstack manifest verify --target . +``` + +## 1. New User Path + +1. Learn the end-to-end flow in [First time using vstack](tutorials/first-time-using-vstack.md). +1. Run your first orchestrated workflow in [First planner run](tutorials/first-planner-run.md). +1. Learn shared team baselines in [Community patterns](explanation/community-patterns.md). +1. Run your baseline setup with [Fresh install](how-to/fresh-install.md). +1. Learn daily commands in [CLI commands](reference/cli-commands.md). +1. Configure defaults in [Configuration](reference/configuration.md). +1. If anything fails, use [Troubleshooting](how-to/troubleshooting.md). + +## 2. Upgrade Path + +1. Start with [Upgrade with checks](tutorials/upgrade-with-checks.md) for a walkthrough. +1. Use the operational guide in [Upgrade](how-to/upgrade.md). +1. Use [Partial upgrade](how-to/partial-upgrade.md) when you only need selected updates. +1. Rebuild generated artifacts if needed with [Reinitialize](how-to/reinitialize.md). +1. Confirm output health with [Artifact checks](reference/artifact-checks.md). +1. Validate command behavior with [CLI commands](reference/cli-commands.md). + +## 3. Operations Path + +1. Set operating behavior in [Configure workflow modes](how-to/configure-workflow-modes.md). +1. Choose execution style with [Choose agent, skill, or prompt](how-to/choose-agent-skill-or-prompt.md). +1. Tune project settings in [Configuration](reference/configuration.md). +1. Manage automation points in [Hooks](reference/hooks.md). +1. Review role-owned outputs and handoff artifacts in [Work items](reference/work-items.md). +1. Use [Troubleshooting](how-to/troubleshooting.md) for incidents and recovery. +1. If you need profile-scoped setup, use [Global install](how-to/global-install.md) and [Uninstall](how-to/uninstall.md). diff --git a/docs/user/tutorials/README.md b/docs/user/tutorials/README.md new file mode 100644 index 00000000..042e5341 --- /dev/null +++ b/docs/user/tutorials/README.md @@ -0,0 +1,10 @@ +# Tutorials + +Tutorials are learning-oriented guides. + +Use this section for step-by-step paths that teach vstack from zero knowledge. +Each tutorial should have a clear start state, a sequence of actions, and a visible result. + +- [First time using vstack](first-time-using-vstack.md) +- [First planner run](first-planner-run.md) +- [Upgrade with checks](upgrade-with-checks.md) diff --git a/docs/user/tutorials/first-planner-run.md b/docs/user/tutorials/first-planner-run.md new file mode 100644 index 00000000..a9695a01 --- /dev/null +++ b/docs/user/tutorials/first-planner-run.md @@ -0,0 +1,56 @@ +# First planner run + +This tutorial walks through your first end-to-end run with the `planner` agent in GitHub Copilot Agent mode. + +## Goal + +By the end, you will run one planner prompt from a configured repository and understand what outputs to expect. + +## Prerequisites + +1. Install the vstack CLI with `pipx`: + +```bash +pipx install vstack +vstack --version +``` + +1. Open your repository root in VS Code. +1. Install project artifacts from repository root: + +```bash +vstack install +vstack validate +``` + +1. Confirm `.github/agents/planner.agent.md` exists. + +## Setup in VS Code + +1. Open Copilot Chat. +1. Switch to Agent mode. +1. In the agent picker, select `planner`. + +## Run your first prompt + +Use a small, concrete prompt so the first run is easy to inspect: + +```text +Run the workflow for this repository change. +``` + +## Expected output + +On a healthy setup, planner output usually includes: + +- a staged workflow response across role boundaries, +- explicit handoff points (for example from planning to implementation), +- references to generated role artifacts under `.github/`. + +If output does not look role-structured, verify that Agent mode is enabled and `planner` is selected. + +## Next steps + +1. Practice role targeting with [Choose agent, skill, or prompt](../how-to/choose-agent-skill-or-prompt.md). +1. Learn upgrade checks with [Upgrade with checks](upgrade-with-checks.md). +1. Keep command behavior handy with [CLI commands](../reference/cli-commands.md). diff --git a/docs/user/tutorials/first-time-using-vstack.md b/docs/user/tutorials/first-time-using-vstack.md new file mode 100644 index 00000000..03d41d08 --- /dev/null +++ b/docs/user/tutorials/first-time-using-vstack.md @@ -0,0 +1,57 @@ +# First time using vstack + +This tutorial introduces the minimal happy path for running vstack in a repository. + +## Goal + +By the end, you can install vstack artifacts and understand where generated files live. + +## Prerequisites + +1. `pipx` is installed. +1. You are in a repository where vstack should be installed. + +## Steps + +1. Install vstack: + +```bash +pipx install vstack +vstack --version +``` + +1. From repository root, install managed artifacts: + +```bash +vstack install +``` + +1. Validate the baseline setup: + +```bash +vstack validate +``` + +1. Review generated output under `.github/`. +1. Review project-scoped state under `.vstack/`. + +## What to do with `.vstack/templates` + +After `vstack install`, you may see starter templates under `.vstack/templates/`. + +- Treat these as project-owned stubs you can adapt for your team. +- Keep useful edits there for local project guidance. +- Do not expect edits in `.vstack/templates/` to regenerate `.github/` artifacts. + +Use `vstack init` to regenerate managed `.github/` artifacts from current vstack templates and project config. + +## Result + +You now have a baseline vstack installation and can continue with task-specific how-to guides. + +## Next steps + +- [First planner run](first-planner-run.md) +- [Fresh install](../how-to/fresh-install.md) +- [CLI commands](../reference/cli-commands.md) +- [What `.vstack/templates` is for](../explanation/vstack-templates.md) diff --git a/docs/user/tutorials/upgrade-with-checks.md b/docs/user/tutorials/upgrade-with-checks.md new file mode 100644 index 00000000..93e45883 --- /dev/null +++ b/docs/user/tutorials/upgrade-with-checks.md @@ -0,0 +1,100 @@ +# Upgrade with checks + +This tutorial shows a safe upgrade flow with explicit checkpoints for both same-major and major upgrades. + +## Goal + +By the end, you can upgrade vstack and confirm generated artifacts are healthy. + +## Prerequisites + +1. `vstack` is already installed in your repository. +1. You can run commands from repository root. + +All commands below assume you are in repository root, so `--target` is omitted. + +## Path A: patch or minor upgrade + +Use this path for upgrades like `v3.1 -> v3.2`. + +1. Upgrade the CLI: + +```bash +pipx upgrade vstack +vstack --version +``` + +1. Re-generate artifacts: + +```bash +vstack init +``` + +1. Verify results: + +```bash +vstack validate +vstack manifest status +vstack manifest verify +``` + +## Path B: major upgrade + +Use this path for upgrades like `v2 -> v3`. + +1. Upgrade the CLI: + +```bash +pipx upgrade vstack +vstack --version +``` + +1. Run migration, then regeneration: + +```bash +vstack migrate +vstack init +``` + +1. Verify results: + +```bash +vstack validate +vstack manifest status +vstack manifest verify +``` + +## Optional migration checks + +Preview migration before writing files: + +```bash +vstack migrate --dry-run +``` + +For multi-major jumps, run an explicit range: + +```bash +vstack migrate --from 1 --to 3 +vstack init +``` + +If you see a legacy manifest warning: + +```bash +vstack manifest upgrade +vstack init +vstack manifest status +``` + +## Expected checkpoints + +- `vstack validate` passes. +- `vstack manifest status` reports expected managed state. +- `vstack manifest verify` confirms checksum integrity. + +## Next steps + +1. Use [Upgrade](../how-to/upgrade.md) for a compact operational version. +1. Use [Reinitialize](../how-to/reinitialize.md) when you need targeted regeneration. +1. Use [Artifact checks](../reference/artifact-checks.md) for drift triage. diff --git a/pyproject.toml b/pyproject.toml index a3d72ffa..3b4e91ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -98,6 +98,7 @@ python_functions = ["test_*"] pythonpath = ["src"] addopts = [ "--import-mode=importlib", + "--basetemp=.pytest-tmp", "--cov=src/vstack", "--cov-report=term-missing", "--cov-fail-under=100", diff --git a/src/vstack/_templates/agents/_partials/stage-report-contract.md b/src/vstack/_templates/agents/_partials/stage-report-contract.md new file mode 100644 index 00000000..b9852be7 --- /dev/null +++ b/src/vstack/_templates/agents/_partials/stage-report-contract.md @@ -0,0 +1,10 @@ +Use this exact stage report schema at the end of your response: + +- `status`: `ready` or `blocked` +- `changes_made`: `yes` or `no` +- `updated_items`: list of paths (or `none`) +- `blockers`: list (or `none`) +- `next_handoff_summary`: one short paragraph +- `planner_run_id`: value received in `PLANNER_RUN_ID` (or `none` when not provided) +- `model_used`: model identifier used for this stage (or `unknown`) +- `subagents_invoked`: list of delegated subagents called during this stage (or `none`) diff --git a/src/vstack/_templates/agents/architect/config.yaml b/src/vstack/_templates/agents/architect/config.yaml index dd67a6bc..bc0bfa1a 100644 --- a/src/vstack/_templates/agents/architect/config.yaml +++ b/src/vstack/_templates/agents/architect/config.yaml @@ -1,5 +1,5 @@ name: architect -version: 20260503022 +version: 20260514001 description: > Senior software architect. Sets the system blueprint: service decomposition, technology direction, standards, NFRs, and organizational constraints. @@ -17,10 +17,17 @@ tools: - todo - agent model: + - auto - Claude Sonnet 4.6 (copilot) - GPT-5.3-Codex (copilot) - Claude Opus 4.7 (copilot) -agents: ["*"] +agents: + - product + - architect + - designer + - engineer + - tester + - release target: vscode defaults: handoffs: diff --git a/src/vstack/_templates/agents/architect/template.md b/src/vstack/_templates/agents/architect/template.md index b5262c75..2571bbc2 100644 --- a/src/vstack/_templates/agents/architect/template.md +++ b/src/vstack/_templates/agents/architect/template.md @@ -70,7 +70,9 @@ Handoffs you own: Planner-coordinated mode (`@planner` invokes this role as a subagent): - Execute architect-stage scope only; do not invoke downstream roles unless explicitly asked. -- End with a stage report containing: `status`, `changes_made`, `updated_items`, `blockers`, and `next_handoff_summary`. +- End with a structured stage report using this schema: + +{{STAGE_REPORT_CONTRACT}} ## assess current state diff --git a/src/vstack/_templates/agents/designer/config.yaml b/src/vstack/_templates/agents/designer/config.yaml index 91d00c7e..d08bfc7b 100644 --- a/src/vstack/_templates/agents/designer/config.yaml +++ b/src/vstack/_templates/agents/designer/config.yaml @@ -1,5 +1,5 @@ name: designer -version: 20260503024 +version: 20260514001 description: > Senior interaction designer. Translates architecture blueprint into developer-ready specifications: API contracts, event schemas, data flows, @@ -16,9 +16,16 @@ tools: - todo - agent model: + - auto - Claude Sonnet 4.6 (copilot) - GPT-5.3-Codex (copilot) -agents: ["*"] +agents: + - product + - architect + - designer + - engineer + - tester + - release target: vscode defaults: handoffs: diff --git a/src/vstack/_templates/agents/designer/template.md b/src/vstack/_templates/agents/designer/template.md index a00da9c6..169eedf0 100644 --- a/src/vstack/_templates/agents/designer/template.md +++ b/src/vstack/_templates/agents/designer/template.md @@ -85,7 +85,9 @@ Handoffs you own: Planner-coordinated mode (`@planner` invokes this role as a subagent): - Execute designer-stage scope only; do not invoke downstream roles unless explicitly asked. -- End with a stage report containing: `status`, `changes_made`, `updated_items`, `blockers`, and `next_handoff_summary`. +- End with a structured stage report using this schema: + +{{STAGE_REPORT_CONTRACT}} ## assess current state diff --git a/src/vstack/_templates/agents/engineer/config.yaml b/src/vstack/_templates/agents/engineer/config.yaml index 44ea8ff5..567c0e16 100644 --- a/src/vstack/_templates/agents/engineer/config.yaml +++ b/src/vstack/_templates/agents/engineer/config.yaml @@ -1,5 +1,5 @@ name: engineer -version: 20260503024 +version: 20260514001 description: > Senior software engineer. Implements features, bug fixes, and unit tests based on the approved design, architecture, and ADRs. Reviews code for @@ -16,9 +16,16 @@ tools: - todo - agent model: + - auto - GPT-5.3-Codex (copilot) - Claude Sonnet 4.6 (copilot) -agents: ["*"] +agents: + - product + - architect + - designer + - engineer + - tester + - release target: vscode defaults: handoffs: diff --git a/src/vstack/_templates/agents/engineer/template.md b/src/vstack/_templates/agents/engineer/template.md index 44090dc1..385c37bf 100644 --- a/src/vstack/_templates/agents/engineer/template.md +++ b/src/vstack/_templates/agents/engineer/template.md @@ -62,7 +62,9 @@ Handoffs you own: Planner-coordinated mode (`@planner` invokes this role as a subagent): - Execute engineer-stage scope only; do not invoke downstream roles unless explicitly asked. -- End with a stage report containing: `status`, `changes_made`, `updated_items`, `blockers`, and `next_handoff_summary`. +- End with a structured stage report using this schema: + +{{STAGE_REPORT_CONTRACT}} ## parallel delegation diff --git a/src/vstack/_templates/agents/planner/config.yaml b/src/vstack/_templates/agents/planner/config.yaml index f6758170..c17ebc9d 100644 --- a/src/vstack/_templates/agents/planner/config.yaml +++ b/src/vstack/_templates/agents/planner/config.yaml @@ -1,5 +1,5 @@ name: planner -version: 20260510001 +version: 20260514001 description: > vstack orchestration coordinator. Reads workflow stages from project config, invokes role subagents in sequence, applies gate and human-approval policy, @@ -11,6 +11,7 @@ tools: - todo - agent model: + - auto - GPT-5.3-Codex (copilot) - Claude Sonnet 4.6 (copilot) agents: diff --git a/src/vstack/_templates/agents/planner/template.md b/src/vstack/_templates/agents/planner/template.md index b228cb61..a60de942 100644 --- a/src/vstack/_templates/agents/planner/template.md +++ b/src/vstack/_templates/agents/planner/template.md @@ -2,13 +2,16 @@ ## identity and purpose -You are the **vstack orchestration planner**. You coordinate stage execution by -invoking role agents as subagents and enforcing explicit gate progression. +You are the **vstack orchestration planner**. Your role is to **plan and delegate — not to execute**. + +You coordinate stage execution by invoking the right role agent for each stage and enforcing +explicit gate progression. Every piece of substantive work belongs to a worker agent. The planner +never does that work itself — it assigns, tracks, and advances. ## responsibilities - Read the configured workflow stages and evaluate `depends_on` to determine execution order. -- Invoke the correct role agent for each stage when all its predecessors are complete. +- Invoke the designated worker agent for each ready stage and collect its stage report; never perform the stage work yourself. - Run independent branches in parallel when their `depends_on` sets do not overlap. - Apply gate and human-in-the-loop policy at each transition. - Keep a concise execution log: completed, skipped, blocked, and pending stages. @@ -23,18 +26,58 @@ invoking role agents as subagents and enforcing explicit gate progression. ## scope and boundaries -- Planner owns orchestration and progression logic. -- Worker role agents own domain decisions and artifact updates. -- Planner does not replace role-specific analysis, coding, testing, or release work. +- Planner owns **orchestration only**: dependency evaluation, agent invocation, gate enforcement, and execution tracking. +- Planner produces **no work product of its own**: no code, no architecture decisions, no API contracts, no test results, no release artifacts. All of that belongs to the worker agents. +- When a task or question surfaces, the default answer is: **which worker agent owns this?** Route it. Do not answer it yourself. +- Only coordination tasks with no worker-agent owner (dependency evaluation, gate checks, execution logging, status reporting) stay with the planner. ## limitations and do not do -- Do not perform role-specific work that belongs to worker agents. +The planner does not execute work. It delegates. + +Every work type has a designated worker agent. Route to the right one immediately: + +| Work type | Delegate to | +| --------------------------------------------------- | ------------ | +| Code implementation, review, debugging, refactoring | `@engineer` | +| Architecture decisions, ADRs, service decomposition | `@architect` | +| API contracts, schemas, service interaction flows | `@designer` | +| Requirements, user stories, product specifications | `@product` | +| Verification, security audits, performance analysis | `@tester` | +| Release notes, changelogs, PR preparation | `@release` | + +If you find yourself writing code, drafting an architecture decision, reviewing an API contract, or producing any other domain artifact — stop. That is a worker agent's job. Delegate it. + +Additional constraints: + - Do not auto-advance a blocked stage without explicit user approval. - Do not skip required stages without a clear policy reason. +## request classification — do this first, before starting the pipeline + +Before doing anything else, classify the incoming request into one of three types: + +| Type | Description | Action | +| ----------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ | +| **Full pipeline** | Delivering a feature, fix, or release that spans multiple roles (product → architect → … → release) | Start the stage pipeline | +| **Focused task** | A clearly scoped task owned by one role (e.g. "update the architecture docs", "write an ADR", "fix this bug", "run the tests") | Route directly to the single owning specialist — do not start the pipeline | +| **Query** | A question about the system, status, or plan | Answer from context, or route to the owning specialist if domain expertise is needed | + +**Focused task routing is the most common case for day-to-day work.** When a request maps cleanly to a single role's domain (see the routing table above), invoke only that specialist — not the full pipeline. The pipeline exists for coordinated multi-role delivery, not for every individual task. + +Signs a request is a focused task (not a pipeline run): + +- It names a specific artifact: "update the ADR", "fix the failing test", "write the release notes" +- It targets a single domain: architecture, design, verification, or release — not all of them +- It does not require cross-role handoffs to produce a meaningful result +- It is a maintenance task: documentation update, report refresh, dependency bump + +When in doubt, ask: "Does this need more than one role to complete?" If not, route directly. + ## working principles +- **Classify before orchestrating.** Determine whether the request is a full pipeline run or a focused task before starting any stage. Starting the pipeline for a focused task is overhead without benefit. +- **Delegate always.** The planner does not perform substantive work — it assigns it to the right worker agent and relays the outcome. This is not a fallback strategy; it is the primary operating mode. - Use the configured workflow contract as source of truth. - Evaluate `depends_on` before each stage: a stage is **ready** when all its listed predecessors have status `ready` or `skipped`. A stage without `depends_on` implicitly depends on the @@ -44,6 +87,20 @@ invoking role agents as subagents and enforcing explicit gate progression. - Prefer explicit user confirmation at gate boundaries. - Keep summaries short, factual, and stage-oriented. +## how to delegate + +For every ready stage or domain question: + +1. **Check for a specialist first.** Identify which worker agent owns this type of work (see specialist routing table above). +1. **Compose a focused context prompt:** include the stage goal, relevant predecessor outputs, and changed scope. +1. **Ensure planner correlation is set:** generate one `PLANNER_RUN_ID` at the start of the orchestration run and reuse it for every delegated stage. +1. **Invoke the worker agent:** `@ ` and include `PLANNER_RUN_ID=` in the delegated prompt. +1. **Wait** for the structured stage report or answer from the worker agent. +1. **Relay the output** to the user or the next stage; do not redo, second-guess, or supplement the agent's work. +1. **Evaluate gate and hitl policy** before advancing to the next stage. + +If a domain question surfaces mid-orchestration that no stage report has answered, route it to the relevant specialist instead of answering it yourself. + ## decision guidelines - If workflow config is missing or invalid, stop and report exactly what is wrong. @@ -80,13 +137,15 @@ Execution model: d. Evaluate gate and hitl policy. Pause for user approval where required before continuing. 1. Continue until the release stage completes or a blocker stops progression. +Planner run correlation: + +- At run start, create one stable `PLANNER_RUN_ID` (for example, UTC timestamp + short suffix). +- Pass the same `PLANNER_RUN_ID` to every delegated worker stage. +- Require each worker stage report to echo the same value in `planner_run_id`. + When invoking a worker stage, require this structured stage report at the end: -- `status`: `ready` or `blocked` -- `changes_made`: `yes` or `no` -- `updated_items`: list of paths -- `blockers`: list (or `none`) -- `next_handoff_summary`: one short paragraph +{{STAGE_REPORT_CONTRACT}} ## success criteria diff --git a/src/vstack/_templates/agents/product/config.yaml b/src/vstack/_templates/agents/product/config.yaml index 83cbdd77..3db6bd4e 100644 --- a/src/vstack/_templates/agents/product/config.yaml +++ b/src/vstack/_templates/agents/product/config.yaml @@ -1,5 +1,5 @@ name: product -version: 20260503021 +version: 20260514001 description: > Senior product manager. Defines vision, requirements, and roadmap for new products, new features, and major scope changes. Baseline-first on branch: @@ -16,10 +16,17 @@ tools: - todo - agent model: + - auto - Claude Sonnet 4.6 (copilot) - GPT-5.3-Codex (copilot) - Claude Opus 4.7 (copilot) -agents: ["*"] +agents: + - product + - architect + - designer + - engineer + - tester + - release target: vscode defaults: handoffs: diff --git a/src/vstack/_templates/agents/product/template.md b/src/vstack/_templates/agents/product/template.md index 1878ce59..3466d712 100644 --- a/src/vstack/_templates/agents/product/template.md +++ b/src/vstack/_templates/agents/product/template.md @@ -72,7 +72,9 @@ Handoffs you own: Planner-coordinated mode (`@planner` invokes this role as a subagent): - Execute product-stage scope only; do not invoke downstream roles unless explicitly asked. -- End with a stage report containing: `status`, `changes_made`, `updated_items`, `blockers`, and `next_handoff_summary`. +- End with a structured stage report using this schema: + +{{STAGE_REPORT_CONTRACT}} ## how you work @@ -124,4 +126,5 @@ that requires changes to upstream items, flag it and trigger a reverse handoff. - `@#analyse` — impact analysis, tradeoffs, feasibility - `@#adr` — architecture decision record writing (if significant decisions) - `@#onboard` — contributor onboarding guide generation +- `@#space-setup` — set up and maintain Copilot Spaces for project context curation - `@#gh-issues` — create and manage GitHub Issues for requirements, tasks, and user stories diff --git a/src/vstack/_templates/agents/release/config.yaml b/src/vstack/_templates/agents/release/config.yaml index 83232d90..f52aa3d0 100644 --- a/src/vstack/_templates/agents/release/config.yaml +++ b/src/vstack/_templates/agents/release/config.yaml @@ -1,5 +1,5 @@ name: release -version: 20260503020 +version: 20260514001 description: > Senior platform and release engineer. Acts as release gatekeeper: verifies baseline items are complete across all roles, collects explicit cross-role @@ -16,9 +16,16 @@ tools: - todo - agent model: + - auto - Claude Sonnet 4.6 (copilot) - GPT-5.3-Codex (copilot) -agents: ["*"] +agents: + - product + - architect + - designer + - engineer + - tester + - release target: vscode defaults: items: diff --git a/src/vstack/_templates/agents/release/template.md b/src/vstack/_templates/agents/release/template.md index b8574bba..d21c8158 100644 --- a/src/vstack/_templates/agents/release/template.md +++ b/src/vstack/_templates/agents/release/template.md @@ -68,7 +68,9 @@ and wait for explicit user routing decisions. Planner-coordinated mode (`@planner` invokes this role as a subagent): - Execute release-stage scope only. -- End with a stage report containing: `status`, `changes_made`, `updated_items`, `blockers`, and `next_handoff_summary`. +- End with a structured stage report using this schema: + +{{STAGE_REPORT_CONTRACT}} ## how you work @@ -123,3 +125,4 @@ that requires changes to upstream items, flag it and trigger a reverse handoff. - `@#explore` — codebase discovery and mapping - `@#code-review` — final review before PR is opened - `@#gh-issues` — create and manage GitHub Issues for tracking work and bug reports +- `@#copilot-ops` — operate Copilot governance settings with audit-first change control diff --git a/src/vstack/_templates/agents/tester/config.yaml b/src/vstack/_templates/agents/tester/config.yaml index ecc9e9ce..e5227503 100644 --- a/src/vstack/_templates/agents/tester/config.yaml +++ b/src/vstack/_templates/agents/tester/config.yaml @@ -1,5 +1,5 @@ name: tester -version: 20260503026 +version: 20260514001 description: > Senior QA, security, and reliability engineer. Runs functional, security, and performance tests. Produces verification reports based on the approved @@ -15,9 +15,16 @@ tools: - todo - agent model: + - auto - Claude Sonnet 4.6 (copilot) - GPT-5.3-Codex (copilot) -agents: ["*"] +agents: + - product + - architect + - designer + - engineer + - tester + - release target: vscode defaults: handoffs: diff --git a/src/vstack/_templates/agents/tester/template.md b/src/vstack/_templates/agents/tester/template.md index 7d480141..b04317d2 100644 --- a/src/vstack/_templates/agents/tester/template.md +++ b/src/vstack/_templates/agents/tester/template.md @@ -70,7 +70,9 @@ Handoffs you own: Planner-coordinated mode (`@planner` invokes this role as a subagent): - Execute tester-stage scope only; do not invoke downstream roles unless explicitly asked. -- End with a stage report containing: `status`, `changes_made`, `updated_items`, `blockers`, and `next_handoff_summary`. +- End with a structured stage report using this schema: + +{{STAGE_REPORT_CONTRACT}} ## assess current state diff --git a/src/vstack/_templates/hooks/agent-call-audit/hook.yaml b/src/vstack/_templates/hooks/agent-call-audit/hook.yaml new file mode 100644 index 00000000..7dcb71bc --- /dev/null +++ b/src/vstack/_templates/hooks/agent-call-audit/hook.yaml @@ -0,0 +1,1345 @@ +version: 20260514002 + +metadata: + name: agent-call-audit + description: | + Records best-effort agent and subagent call flow telemetry for + session boundaries and tool call events. + + Maintains an internal unknown-events TSV accumulator to emit per-session + `unknownSummary` records and assess telemetry/parser quality over time. + log: + retention_days: 7 + purpose: audit + security_level: low + mode_default: audit + execution_context: copilot-hook-runtime + dependencies: + required: [] + optional: [] + +hooks: + sessionStart: + - type: command + description: | + Log session start with best-effort actor identity and model metadata. + bash: | + script_start_ms="$(date +%s%3N 2>/dev/null || true)" + if [ -z "$script_start_ms" ]; then + script_start_ms="$(( $(date +%s) * 1000 ))" + fi + input="$(cat)" + log_level="${VSTACK_HOOKS_LOG_LEVEL:-minimal}" + log_root="${VSTACK_HOOK_LOG_DIR:-.vstack/logs}" + retention_days="${VSTACK_HOOK_RETENTION_DAYS:-7}" + if ! printf '%s' "$retention_days" | grep -Eq '^[1-9][0-9]*$'; then + retention_days=7 + fi + if [ -d "$log_root" ]; then + cutoff_day="$(date -u -d "$retention_days days ago" +%Y%m%d 2>/dev/null || true)" + if [ -n "$cutoff_day" ]; then + for day_dir in "$log_root"/*; do + [ -d "$day_dir" ] || continue + day_name="$(basename "$day_dir")" + case "$day_name" in + [0-9][0-9][0-9][0-9][0-1][0-9][0-3][0-9]) ;; + *) continue ;; + esac + if [ "$day_name" -lt "$cutoff_day" ]; then + rm -rf "$day_dir" || true + fi + done + fi + fi + log_dir="$log_root/$(date -u +%Y%m%d)" + mkdir -p "$log_dir" + if [ "$log_level" = "off" ]; then + exit 0 + fi + if [ "$log_level" = "verbose" ]; then + printf '%s\n' "$input" >> "$log_dir/hook-agent-call.log" + exit 0 + fi + + ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)" + unknown_events_file="$log_dir/hook-agent-call-unknown-events.tsv" + + extract_json_string() { + key="$1" + printf '%s' "$input" | sed -n "s/.*\"${key}\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p" | head -n1 + } + + increment_unknown_event() { + sid="$1" + reason="$2" + tmp_file="$unknown_events_file.tmp.$$" + if [ -f "$unknown_events_file" ]; then + awk -F '\t' -v sid="$sid" -v reason="$reason" 'BEGIN { OFS="\t" } + NF >= 2 { + key = $1 SUBSEP $2 + count = 1 + if (NF >= 3 && $3 ~ /^[0-9]+$/) { + count = $3 + 0 + } + counts[key] += count + } + END { + target = sid SUBSEP reason + counts[target] += 1 + for (k in counts) { + split(k, parts, SUBSEP) + print parts[1], parts[2], counts[k] + } + }' "$unknown_events_file" > "$tmp_file" && mv "$tmp_file" "$unknown_events_file" + else + printf '%s\t%s\t1\n' "$sid" "$reason" > "$unknown_events_file" + fi + } + + session_id="$(extract_json_string sessionId)" + [ -z "$session_id" ] && session_id="$(extract_json_string session_id)" + [ -z "$session_id" ] && session_id="$(extract_json_string conversationId)" + [ -z "$session_id" ] && session_id="$(extract_json_string conversation_id)" + [ -z "$session_id" ] && session_id="$(extract_json_string chatId)" + [ -z "$session_id" ] && session_id="$(extract_json_string chat_id)" + [ -z "$session_id" ] && session_id="unknown-session" + + actor_name="$(extract_json_string actorName)" + [ -z "$actor_name" ] && actor_name="$(extract_json_string actor_name)" + [ -z "$actor_name" ] && actor_name="$(extract_json_string subagentName)" + [ -z "$actor_name" ] && actor_name="$(extract_json_string subagent_name)" + [ -z "$actor_name" ] && actor_name="$(extract_json_string agentName)" + [ -z "$actor_name" ] && actor_name="$(extract_json_string agent_name)" + [ -z "$actor_name" ] && actor_name="unknown" + + actor_type="$(extract_json_string actorType)" + [ -z "$actor_type" ] && actor_type="$(extract_json_string actor_type)" + if [ -z "$actor_type" ]; then + if printf '%s' "$actor_name" | grep -qi 'subagent'; then + actor_type="subagent" + elif [ "$actor_name" != "unknown" ]; then + actor_type="agent" + else + actor_type="unknown" + fi + fi + + model_used="$(extract_json_string model)" + [ -z "$model_used" ] && model_used="$(extract_json_string modelName)" + [ -z "$model_used" ] && model_used="$(extract_json_string model_name)" + [ -z "$model_used" ] && model_used="$(extract_json_string toolModel)" + [ -z "$model_used" ] && model_used="$(extract_json_string tool_model)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"toolCall"[[:space:]]*:[[:space:]]*{[^}]*"model"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"arguments"[[:space:]]*:[[:space:]]*{[^}]*"model"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"arguments"[[:space:]]*:[[:space:]]*{[^}]*"modelName"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"arguments"[[:space:]]*:[[:space:]]*{[^}]*"model_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"args"[[:space:]]*:[[:space:]]*{[^}]*"model"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"args"[[:space:]]*:[[:space:]]*{[^}]*"modelName"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"args"[[:space:]]*:[[:space:]]*{[^}]*"model_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"tool"[[:space:]]*:[[:space:]]*{[^}]*"arguments"[[:space:]]*:[[:space:]]*{[^}]*"model"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"tool"[[:space:]]*:[[:space:]]*{[^}]*"arguments"[[:space:]]*:[[:space:]]*{[^}]*"modelName"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"tool"[[:space:]]*:[[:space:]]*{[^}]*"arguments"[[:space:]]*:[[:space:]]*{[^}]*"model_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"toolCall"[[:space:]]*:[[:space:]]*{[^}]*"arguments"[[:space:]]*:[[:space:]]*{[^}]*"model"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"toolCall"[[:space:]]*:[[:space:]]*{[^}]*"arguments"[[:space:]]*:[[:space:]]*{[^}]*"modelName"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"toolCall"[[:space:]]*:[[:space:]]*{[^}]*"arguments"[[:space:]]*:[[:space:]]*{[^}]*"model_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"tool_call"[[:space:]]*:[[:space:]]*{[^}]*"arguments"[[:space:]]*:[[:space:]]*{[^}]*"model"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"tool_call"[[:space:]]*:[[:space:]]*{[^}]*"arguments"[[:space:]]*:[[:space:]]*{[^}]*"modelName"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"tool_call"[[:space:]]*:[[:space:]]*{[^}]*"arguments"[[:space:]]*:[[:space:]]*{[^}]*"model_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="unknown" + + if [ "$actor_name" = "unknown" ]; then + increment_unknown_event "$session_id" "sessionStart_unknown_actor" + exit 0 + fi + + bytes="$(printf '%s' "$input" | wc -c | tr -d ' ')" + est_tokens="$(( (bytes + 3) / 4 ))" + script_end_ms="$(date +%s%3N 2>/dev/null || true)" + if [ -z "$script_end_ms" ]; then + script_end_ms="$(( $(date +%s) * 1000 ))" + fi + hook_execution_ms="$((script_end_ms - script_start_ms))" + + printf '{"timestamp":"%s","event":"sessionStart","session_id":"%s","actor_name":"%s","actor_type":"%s","model_used":"%s","size_bytes":%s,"estimated_tokens":%s,"hook_execution_ms":%s}\n' \ + "$ts" "$session_id" "$actor_name" "$actor_type" "$model_used" "$bytes" "$est_tokens" "$hook_execution_ms" >> "$log_dir/hook-agent-call.log" + powershell: | + $inputText = [Console]::In.ReadToEnd() + $logLevel = if ($env:VSTACK_HOOKS_LOG_LEVEL) { $env:VSTACK_HOOKS_LOG_LEVEL } else { 'minimal' } + $logRoot = if ($env:VSTACK_HOOK_LOG_DIR) { $env:VSTACK_HOOK_LOG_DIR } else { '.vstack/logs' } + $retentionDaysRaw = if ($env:VSTACK_HOOK_RETENTION_DAYS) { $env:VSTACK_HOOK_RETENTION_DAYS } else { '7' } + $retentionDays = 7 + if (-not ([int]::TryParse([string]$retentionDaysRaw, [ref]$retentionDays) -and $retentionDays -gt 0)) { + $retentionDays = 7 + } + try { + if (Test-Path -LiteralPath $logRoot) { + $cutoffDay = (Get-Date).ToUniversalTime().AddDays(-$retentionDays).ToString('yyyyMMdd') + Get-ChildItem -LiteralPath $logRoot -Directory -ErrorAction Stop | + Where-Object { $_.Name -match '^\d{8}$' -and $_.Name -lt $cutoffDay } | + ForEach-Object { Remove-Item -LiteralPath $_.FullName -Recurse -Force -ErrorAction SilentlyContinue } + } + } catch {} + $logDir = Join-Path $logRoot (Get-Date).ToUniversalTime().ToString('yyyyMMdd') + New-Item -ItemType Directory -Force -Path $logDir | Out-Null + if ($logLevel -eq 'off') { exit 0 } + if ($logLevel -eq 'verbose') { + Add-Content -Path (Join-Path $logDir 'hook-agent-call.log') -Value $inputText + exit 0 + } + + $scriptStart = Get-Date + $unknownEventsPath = Join-Path $logDir 'hook-agent-call-unknown-events.tsv' + + function Get-PathValue { + param([object]$Object, [string]$Path) + $current = $Object + foreach ($part in $Path.Split('.')) { + if ($null -eq $current) { return $null } + $prop = $current.PSObject.Properties[$part] + if ($null -eq $prop) { return $null } + $current = $prop.Value + } + return $current + } + + function Get-FirstString { + param([object]$Object, [string[]]$Paths) + foreach ($path in $Paths) { + $value = Get-PathValue -Object $Object -Path $path + if ($null -ne $value -and [string]$value -ne '') { return [string]$value } + } + return $null + } + + function Add-UnknownEventCounter { + param( + [string]$Path, + [string]$SessionId, + [string]$Reason + ) + + $counts = @{} + if (Test-Path $Path) { + foreach ($line in (Get-Content -Path $Path)) { + if ([string]::IsNullOrWhiteSpace($line)) { continue } + $parts = $line -split "`t" + if ($parts.Count -lt 2) { continue } + + $sid = [string]$parts[0] + $why = [string]$parts[1] + $count = 1 + if ($parts.Count -ge 3) { + $parsedCount = 0 + if ([int]::TryParse([string]$parts[2], [ref]$parsedCount)) { + $count = [int][Math]::Max(0, $parsedCount) + } + } + + $key = "$sid`t$why" + if (-not $counts.ContainsKey($key)) { $counts[$key] = 0 } + $counts[$key] += $count + } + } + + $target = "$SessionId`t$Reason" + if (-not $counts.ContainsKey($target)) { $counts[$target] = 0 } + $counts[$target] += 1 + + $rows = foreach ($entry in ($counts.GetEnumerator() | Sort-Object Name)) { + "{0}`t{1}" -f $entry.Name, $entry.Value + } + Set-Content -Path $Path -Value $rows + } + + $payload = $null + try { $payload = $inputText | ConvertFrom-Json -ErrorAction Stop } catch {} + + $sessionId = 'unknown-session' + $actorName = 'unknown' + $actorType = 'unknown' + $modelUsed = 'unknown' + + if ($null -ne $payload) { + $sessionIdCandidate = Get-FirstString -Object $payload -Paths @('sessionId', 'session_id', 'conversationId', 'conversation_id', 'chatId', 'chat_id') + if ($sessionIdCandidate) { $sessionId = $sessionIdCandidate } + + $actorNameCandidate = Get-FirstString -Object $payload -Paths @('actorName', 'actor_name', 'subagentName', 'subagent_name', 'agentName', 'agent_name') + if ($actorNameCandidate) { $actorName = $actorNameCandidate } + + $actorTypeCandidate = Get-FirstString -Object $payload -Paths @('actorType', 'actor_type') + if ($actorTypeCandidate) { $actorType = $actorTypeCandidate } + + $modelCandidate = Get-FirstString -Object $payload -Paths @('model', 'modelName', 'model_name', 'toolModel', 'tool_model', 'toolCall.model', 'tool_call.model', 'arguments.model', 'arguments.modelName', 'arguments.model_name', 'args.model', 'args.modelName', 'args.model_name', 'tool.arguments.model', 'tool.arguments.modelName', 'tool.arguments.model_name', 'toolCall.arguments.model', 'toolCall.arguments.modelName', 'toolCall.arguments.model_name', 'tool_call.arguments.model', 'tool_call.arguments.modelName', 'tool_call.arguments.model_name') + if ($modelCandidate) { $modelUsed = $modelCandidate } + } + + if ($actorType -eq 'unknown') { + if ($actorName -match 'subagent') { + $actorType = 'subagent' + } elseif ($actorName -ne 'unknown') { + $actorType = 'agent' + } + } + + if ($actorName -eq 'unknown') { + Add-UnknownEventCounter -Path $unknownEventsPath -SessionId $sessionId -Reason 'sessionStart_unknown_actor' + exit 0 + } + + $sizeBytes = [System.Text.Encoding]::UTF8.GetByteCount($inputText) + $hookExecutionMs = [int][Math]::Max(0, ((Get-Date) - $scriptStart).TotalMilliseconds) + $record = @{ + timestamp = (Get-Date).ToUniversalTime().ToString('o') + event = 'sessionStart' + session_id = $sessionId + actor_name = $actorName + actor_type = $actorType + model_used = $modelUsed + size_bytes = $sizeBytes + estimated_tokens = [int][math]::Ceiling($sizeBytes / 4.0) + hook_execution_ms = $hookExecutionMs + } | ConvertTo-Json -Compress + Add-Content -Path (Join-Path $logDir 'hook-agent-call.log') -Value $record + cwd: "." + timeoutSec: 5 + + sessionEnd: + - type: command + description: | + Log session end with best-effort actor identity and unknown-summary counters. + bash: | + script_start_ms="$(date +%s%3N 2>/dev/null || true)" + if [ -z "$script_start_ms" ]; then + script_start_ms="$(( $(date +%s) * 1000 ))" + fi + input="$(cat)" + log_level="${VSTACK_HOOKS_LOG_LEVEL:-minimal}" + log_root="${VSTACK_HOOK_LOG_DIR:-.vstack/logs}" + retention_days="${VSTACK_HOOK_RETENTION_DAYS:-7}" + if ! printf '%s' "$retention_days" | grep -Eq '^[1-9][0-9]*$'; then + retention_days=7 + fi + if [ -d "$log_root" ]; then + cutoff_day="$(date -u -d "$retention_days days ago" +%Y%m%d 2>/dev/null || true)" + if [ -n "$cutoff_day" ]; then + for day_dir in "$log_root"/*; do + [ -d "$day_dir" ] || continue + day_name="$(basename "$day_dir")" + case "$day_name" in + [0-9][0-9][0-9][0-9][0-1][0-9][0-3][0-9]) ;; + *) continue ;; + esac + if [ "$day_name" -lt "$cutoff_day" ]; then + rm -rf "$day_dir" || true + fi + done + fi + fi + log_dir="$log_root/$(date -u +%Y%m%d)" + mkdir -p "$log_dir" + if [ "$log_level" = "off" ]; then + exit 0 + fi + if [ "$log_level" = "verbose" ]; then + printf '%s\n' "$input" >> "$log_dir/hook-agent-call.log" + exit 0 + fi + + ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)" + unknown_events_file="$log_dir/hook-agent-call-unknown-events.tsv" + + extract_json_string() { + key="$1" + printf '%s' "$input" | sed -n "s/.*\"${key}\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p" | head -n1 + } + + increment_unknown_event() { + sid="$1" + reason="$2" + tmp_file="$unknown_events_file.tmp.$$" + if [ -f "$unknown_events_file" ]; then + awk -F '\t' -v sid="$sid" -v reason="$reason" 'BEGIN { OFS="\t" } + NF >= 2 { + key = $1 SUBSEP $2 + count = 1 + if (NF >= 3 && $3 ~ /^[0-9]+$/) { + count = $3 + 0 + } + counts[key] += count + } + END { + target = sid SUBSEP reason + counts[target] += 1 + for (k in counts) { + split(k, parts, SUBSEP) + print parts[1], parts[2], counts[k] + } + }' "$unknown_events_file" > "$tmp_file" && mv "$tmp_file" "$unknown_events_file" + else + printf '%s\t%s\t1\n' "$sid" "$reason" > "$unknown_events_file" + fi + } + + session_id="$(extract_json_string sessionId)" + [ -z "$session_id" ] && session_id="$(extract_json_string session_id)" + [ -z "$session_id" ] && session_id="$(extract_json_string conversationId)" + [ -z "$session_id" ] && session_id="$(extract_json_string conversation_id)" + [ -z "$session_id" ] && session_id="$(extract_json_string chatId)" + [ -z "$session_id" ] && session_id="$(extract_json_string chat_id)" + [ -z "$session_id" ] && session_id="unknown-session" + + actor_name="$(extract_json_string actorName)" + [ -z "$actor_name" ] && actor_name="$(extract_json_string actor_name)" + [ -z "$actor_name" ] && actor_name="$(extract_json_string subagentName)" + [ -z "$actor_name" ] && actor_name="$(extract_json_string subagent_name)" + [ -z "$actor_name" ] && actor_name="$(extract_json_string agentName)" + [ -z "$actor_name" ] && actor_name="$(extract_json_string agent_name)" + [ -z "$actor_name" ] && actor_name="unknown" + + actor_type="$(extract_json_string actorType)" + [ -z "$actor_type" ] && actor_type="$(extract_json_string actor_type)" + if [ -z "$actor_type" ]; then + if printf '%s' "$actor_name" | grep -qi 'subagent'; then + actor_type="subagent" + elif [ "$actor_name" != "unknown" ]; then + actor_type="agent" + else + actor_type="unknown" + fi + fi + + model_used="$(extract_json_string model)" + [ -z "$model_used" ] && model_used="$(extract_json_string modelName)" + [ -z "$model_used" ] && model_used="$(extract_json_string model_name)" + [ -z "$model_used" ] && model_used="$(extract_json_string toolModel)" + [ -z "$model_used" ] && model_used="$(extract_json_string tool_model)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"toolCall"[[:space:]]*:[[:space:]]*{[^}]*"model"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"arguments"[[:space:]]*:[[:space:]]*{[^}]*"model"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"arguments"[[:space:]]*:[[:space:]]*{[^}]*"modelName"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"arguments"[[:space:]]*:[[:space:]]*{[^}]*"model_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"args"[[:space:]]*:[[:space:]]*{[^}]*"model"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"args"[[:space:]]*:[[:space:]]*{[^}]*"modelName"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"args"[[:space:]]*:[[:space:]]*{[^}]*"model_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"tool"[[:space:]]*:[[:space:]]*{[^}]*"arguments"[[:space:]]*:[[:space:]]*{[^}]*"model"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"tool"[[:space:]]*:[[:space:]]*{[^}]*"arguments"[[:space:]]*:[[:space:]]*{[^}]*"modelName"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"tool"[[:space:]]*:[[:space:]]*{[^}]*"arguments"[[:space:]]*:[[:space:]]*{[^}]*"model_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"toolCall"[[:space:]]*:[[:space:]]*{[^}]*"arguments"[[:space:]]*:[[:space:]]*{[^}]*"model"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"toolCall"[[:space:]]*:[[:space:]]*{[^}]*"arguments"[[:space:]]*:[[:space:]]*{[^}]*"modelName"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"toolCall"[[:space:]]*:[[:space:]]*{[^}]*"arguments"[[:space:]]*:[[:space:]]*{[^}]*"model_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"tool_call"[[:space:]]*:[[:space:]]*{[^}]*"arguments"[[:space:]]*:[[:space:]]*{[^}]*"model"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"tool_call"[[:space:]]*:[[:space:]]*{[^}]*"arguments"[[:space:]]*:[[:space:]]*{[^}]*"modelName"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"tool_call"[[:space:]]*:[[:space:]]*{[^}]*"arguments"[[:space:]]*:[[:space:]]*{[^}]*"model_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="unknown" + + if [ "$actor_name" = "unknown" ]; then + increment_unknown_event "$session_id" "sessionEnd_unknown_actor" + else + bytes="$(printf '%s' "$input" | wc -c | tr -d ' ')" + est_tokens="$(( (bytes + 3) / 4 ))" + script_end_ms="$(date +%s%3N 2>/dev/null || true)" + if [ -z "$script_end_ms" ]; then + script_end_ms="$(( $(date +%s) * 1000 ))" + fi + hook_execution_ms="$((script_end_ms - script_start_ms))" + + printf '{"timestamp":"%s","event":"sessionEnd","session_id":"%s","actor_name":"%s","actor_type":"%s","model_used":"%s","size_bytes":%s,"estimated_tokens":%s,"hook_execution_ms":%s}\n' \ + "$ts" "$session_id" "$actor_name" "$actor_type" "$model_used" "$bytes" "$est_tokens" "$hook_execution_ms" >> "$log_dir/hook-agent-call.log" + fi + + if [ -f "$unknown_events_file" ]; then + unknown_total="$(awk -F '\t' -v sid="$session_id" '$1==sid {count=1; if (NF>=3 && $3 ~ /^[0-9]+$/) count=$3+0; total+=count} END {print total+0}' "$unknown_events_file")" + if [ "$unknown_total" -gt 0 ]; then + unknown_breakdown="$(awk -F '\t' -v sid="$session_id" '$1==sid {count=1; if (NF>=3 && $3 ~ /^[0-9]+$/) count=$3+0; counts[$2]+=count} END {for (k in counts) {if (out != "") out=out ";"; out=out k "=" counts[k]} print out}' "$unknown_events_file")" + printf '{"timestamp":"%s","event":"unknownSummary","session_id":"%s","unknown_total":%s,"unknown_breakdown":"%s","model_used":"%s"}\n' \ + "$ts" "$session_id" "$unknown_total" "$unknown_breakdown" "$model_used" >> "$log_dir/hook-agent-call.log" + fi + tmp_file="$unknown_events_file.tmp.$$" + awk -F '\t' -v sid="$session_id" 'BEGIN { OFS="\t" } $1!=sid && NF>=2 {count=1; if (NF>=3 && $3 ~ /^[0-9]+$/) count=$3+0; print $1, $2, count}' "$unknown_events_file" > "$tmp_file" && mv "$tmp_file" "$unknown_events_file" + fi + powershell: | + $inputText = [Console]::In.ReadToEnd() + $logLevel = if ($env:VSTACK_HOOKS_LOG_LEVEL) { $env:VSTACK_HOOKS_LOG_LEVEL } else { 'minimal' } + $logRoot = if ($env:VSTACK_HOOK_LOG_DIR) { $env:VSTACK_HOOK_LOG_DIR } else { '.vstack/logs' } + $retentionDaysRaw = if ($env:VSTACK_HOOK_RETENTION_DAYS) { $env:VSTACK_HOOK_RETENTION_DAYS } else { '7' } + $retentionDays = 7 + if (-not ([int]::TryParse([string]$retentionDaysRaw, [ref]$retentionDays) -and $retentionDays -gt 0)) { + $retentionDays = 7 + } + try { + if (Test-Path -LiteralPath $logRoot) { + $cutoffDay = (Get-Date).ToUniversalTime().AddDays(-$retentionDays).ToString('yyyyMMdd') + Get-ChildItem -LiteralPath $logRoot -Directory -ErrorAction Stop | + Where-Object { $_.Name -match '^\d{8}$' -and $_.Name -lt $cutoffDay } | + ForEach-Object { Remove-Item -LiteralPath $_.FullName -Recurse -Force -ErrorAction SilentlyContinue } + } + } catch {} + $logDir = Join-Path $logRoot (Get-Date).ToUniversalTime().ToString('yyyyMMdd') + New-Item -ItemType Directory -Force -Path $logDir | Out-Null + if ($logLevel -eq 'off') { exit 0 } + if ($logLevel -eq 'verbose') { + Add-Content -Path (Join-Path $logDir 'hook-agent-call.log') -Value $inputText + exit 0 + } + + $scriptStart = Get-Date + $unknownEventsPath = Join-Path $logDir 'hook-agent-call-unknown-events.tsv' + + function Get-PathValue { + param([object]$Object, [string]$Path) + $current = $Object + foreach ($part in $Path.Split('.')) { + if ($null -eq $current) { return $null } + $prop = $current.PSObject.Properties[$part] + if ($null -eq $prop) { return $null } + $current = $prop.Value + } + return $current + } + + function Get-FirstString { + param([object]$Object, [string[]]$Paths) + foreach ($path in $Paths) { + $value = Get-PathValue -Object $Object -Path $path + if ($null -ne $value -and [string]$value -ne '') { return [string]$value } + } + return $null + } + + function Add-UnknownEventCounter { + param( + [string]$Path, + [string]$SessionId, + [string]$Reason + ) + + $counts = @{} + if (Test-Path $Path) { + foreach ($line in (Get-Content -Path $Path)) { + if ([string]::IsNullOrWhiteSpace($line)) { continue } + $parts = $line -split "`t" + if ($parts.Count -lt 2) { continue } + + $sid = [string]$parts[0] + $why = [string]$parts[1] + $count = 1 + if ($parts.Count -ge 3) { + $parsedCount = 0 + if ([int]::TryParse([string]$parts[2], [ref]$parsedCount)) { + $count = [int][Math]::Max(0, $parsedCount) + } + } + + $key = "$sid`t$why" + if (-not $counts.ContainsKey($key)) { $counts[$key] = 0 } + $counts[$key] += $count + } + } + + $target = "$SessionId`t$Reason" + if (-not $counts.ContainsKey($target)) { $counts[$target] = 0 } + $counts[$target] += 1 + + $rows = foreach ($entry in ($counts.GetEnumerator() | Sort-Object Name)) { + "{0}`t{1}" -f $entry.Name, $entry.Value + } + Set-Content -Path $Path -Value $rows + } + + $payload = $null + try { $payload = $inputText | ConvertFrom-Json -ErrorAction Stop } catch {} + + $sessionId = 'unknown-session' + $actorName = 'unknown' + $actorType = 'unknown' + $modelUsed = 'unknown' + + if ($null -ne $payload) { + $sessionIdCandidate = Get-FirstString -Object $payload -Paths @('sessionId', 'session_id', 'conversationId', 'conversation_id', 'chatId', 'chat_id') + if ($sessionIdCandidate) { $sessionId = $sessionIdCandidate } + + $actorNameCandidate = Get-FirstString -Object $payload -Paths @('actorName', 'actor_name', 'subagentName', 'subagent_name', 'agentName', 'agent_name') + if ($actorNameCandidate) { $actorName = $actorNameCandidate } + + $actorTypeCandidate = Get-FirstString -Object $payload -Paths @('actorType', 'actor_type') + if ($actorTypeCandidate) { $actorType = $actorTypeCandidate } + + $modelCandidate = Get-FirstString -Object $payload -Paths @('model', 'modelName', 'model_name', 'toolModel', 'tool_model', 'toolCall.model', 'tool_call.model', 'arguments.model', 'arguments.modelName', 'arguments.model_name', 'args.model', 'args.modelName', 'args.model_name', 'tool.arguments.model', 'tool.arguments.modelName', 'tool.arguments.model_name', 'toolCall.arguments.model', 'toolCall.arguments.modelName', 'toolCall.arguments.model_name', 'tool_call.arguments.model', 'tool_call.arguments.modelName', 'tool_call.arguments.model_name') + if ($modelCandidate) { $modelUsed = $modelCandidate } + } + + if ($actorType -eq 'unknown') { + if ($actorName -match 'subagent') { + $actorType = 'subagent' + } elseif ($actorName -ne 'unknown') { + $actorType = 'agent' + } + } + + if ($actorName -eq 'unknown') { + Add-UnknownEventCounter -Path $unknownEventsPath -SessionId $sessionId -Reason 'sessionEnd_unknown_actor' + } else { + $sizeBytes = [System.Text.Encoding]::UTF8.GetByteCount($inputText) + $hookExecutionMs = [int][Math]::Max(0, ((Get-Date) - $scriptStart).TotalMilliseconds) + $record = @{ + timestamp = (Get-Date).ToUniversalTime().ToString('o') + event = 'sessionEnd' + session_id = $sessionId + actor_name = $actorName + actor_type = $actorType + model_used = $modelUsed + size_bytes = $sizeBytes + estimated_tokens = [int][math]::Ceiling($sizeBytes / 4.0) + hook_execution_ms = $hookExecutionMs + } | ConvertTo-Json -Compress + Add-Content -Path (Join-Path $logDir 'hook-agent-call.log') -Value $record + } + + if (Test-Path $unknownEventsPath) { + $matchingCounts = @{} + $remainingCounts = @{} + foreach ($line in (Get-Content -Path $unknownEventsPath)) { + if ([string]::IsNullOrWhiteSpace($line)) { continue } + $parts = $line -split "`t" + if ($parts.Count -lt 2) { continue } + + $lineSessionId = [string]$parts[0] + $reason = [string]$parts[1] + $count = 1 + if ($parts.Count -ge 3) { + $parsedCount = 0 + if ([int]::TryParse([string]$parts[2], [ref]$parsedCount)) { + $count = [int][Math]::Max(0, $parsedCount) + } + } + + if ($lineSessionId -eq $sessionId) { + if (-not $matchingCounts.ContainsKey($reason)) { $matchingCounts[$reason] = 0 } + $matchingCounts[$reason] += $count + } else { + $remainingKey = "$lineSessionId`t$reason" + if (-not $remainingCounts.ContainsKey($remainingKey)) { $remainingCounts[$remainingKey] = 0 } + $remainingCounts[$remainingKey] += $count + } + } + + if ($matchingCounts.Count -gt 0) { + $unknownTotal = 0 + $summaryPairs = @() + foreach ($entry in ($matchingCounts.GetEnumerator() | Sort-Object Name)) { + $unknownTotal += [int]$entry.Value + $summaryPairs += ("{0}={1}" -f $entry.Name, $entry.Value) + } + $summary = $summaryPairs -join ';' + $summaryRecord = @{ + timestamp = (Get-Date).ToUniversalTime().ToString('o') + event = 'unknownSummary' + session_id = $sessionId + unknown_total = $unknownTotal + unknown_breakdown = $summary + model_used = $modelUsed + } | ConvertTo-Json -Compress + Add-Content -Path (Join-Path $logDir 'hook-agent-call.log') -Value $summaryRecord + } + + $remaining = @() + foreach ($entry in ($remainingCounts.GetEnumerator() | Sort-Object Name)) { + $remaining += ("{0}`t{1}" -f $entry.Name, $entry.Value) + } + Set-Content -Path $unknownEventsPath -Value $remaining + } + cwd: "." + timeoutSec: 5 + + preToolUse: + - type: command + description: | + Log tool invocation requests with best-effort actor, delegation, and model metadata. + bash: | + script_start_ms="$(date +%s%3N 2>/dev/null || true)" + if [ -z "$script_start_ms" ]; then + script_start_ms="$(( $(date +%s) * 1000 ))" + fi + input="$(cat)" + log_level="${VSTACK_HOOKS_LOG_LEVEL:-minimal}" + log_root="${VSTACK_HOOK_LOG_DIR:-.vstack/logs}" + retention_days="${VSTACK_HOOK_RETENTION_DAYS:-7}" + if ! printf '%s' "$retention_days" | grep -Eq '^[1-9][0-9]*$'; then + retention_days=7 + fi + if [ -d "$log_root" ]; then + cutoff_day="$(date -u -d "$retention_days days ago" +%Y%m%d 2>/dev/null || true)" + if [ -n "$cutoff_day" ]; then + for day_dir in "$log_root"/*; do + [ -d "$day_dir" ] || continue + day_name="$(basename "$day_dir")" + case "$day_name" in + [0-9][0-9][0-9][0-9][0-1][0-9][0-3][0-9]) ;; + *) continue ;; + esac + if [ "$day_name" -lt "$cutoff_day" ]; then + rm -rf "$day_dir" || true + fi + done + fi + fi + log_dir="$log_root/$(date -u +%Y%m%d)" + mkdir -p "$log_dir" + if [ "$log_level" = "off" ]; then + exit 0 + fi + if [ "$log_level" = "verbose" ]; then + printf '%s\n' "$input" >> "$log_dir/hook-agent-call.log" + exit 0 + fi + + ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)" + unknown_events_file="$log_dir/hook-agent-call-unknown-events.tsv" + + extract_json_string() { + key="$1" + printf '%s' "$input" | sed -n "s/.*\"${key}\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p" | head -n1 + } + + increment_unknown_event() { + sid="$1" + reason="$2" + tmp_file="$unknown_events_file.tmp.$$" + if [ -f "$unknown_events_file" ]; then + awk -F '\t' -v sid="$sid" -v reason="$reason" 'BEGIN { OFS="\t" } + NF >= 2 { + key = $1 SUBSEP $2 + count = 1 + if (NF >= 3 && $3 ~ /^[0-9]+$/) { + count = $3 + 0 + } + counts[key] += count + } + END { + target = sid SUBSEP reason + counts[target] += 1 + for (k in counts) { + split(k, parts, SUBSEP) + print parts[1], parts[2], counts[k] + } + }' "$unknown_events_file" > "$tmp_file" && mv "$tmp_file" "$unknown_events_file" + else + printf '%s\t%s\t1\n' "$sid" "$reason" > "$unknown_events_file" + fi + } + + session_id="$(extract_json_string sessionId)" + [ -z "$session_id" ] && session_id="$(extract_json_string session_id)" + [ -z "$session_id" ] && session_id="$(extract_json_string conversationId)" + [ -z "$session_id" ] && session_id="$(extract_json_string conversation_id)" + [ -z "$session_id" ] && session_id="$(extract_json_string chatId)" + [ -z "$session_id" ] && session_id="$(extract_json_string chat_id)" + [ -z "$session_id" ] && session_id="unknown-session" + + actor_name="$(extract_json_string actorName)" + [ -z "$actor_name" ] && actor_name="$(extract_json_string actor_name)" + [ -z "$actor_name" ] && actor_name="$(extract_json_string subagentName)" + [ -z "$actor_name" ] && actor_name="$(extract_json_string subagent_name)" + [ -z "$actor_name" ] && actor_name="$(extract_json_string agentName)" + [ -z "$actor_name" ] && actor_name="$(extract_json_string agent_name)" + [ -z "$actor_name" ] && actor_name="unknown" + + actor_type="$(extract_json_string actorType)" + [ -z "$actor_type" ] && actor_type="$(extract_json_string actor_type)" + if [ -z "$actor_type" ]; then + if printf '%s' "$actor_name" | grep -qi 'subagent'; then + actor_type="subagent" + elif [ "$actor_name" != "unknown" ]; then + actor_type="agent" + else + actor_type="unknown" + fi + fi + + tool_name="$(extract_json_string toolName)" + [ -z "$tool_name" ] && tool_name="$(extract_json_string tool_name)" + [ -z "$tool_name" ] && tool_name="$(extract_json_string recipient_name)" + [ -z "$tool_name" ] && tool_name="$(printf '%s' "$input" | sed -n 's/.*"tool"[[:space:]]*:[[:space:]]*{[^}]*"name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$tool_name" ] && tool_name="$(printf '%s' "$input" | sed -n 's/.*"toolCall"[[:space:]]*:[[:space:]]*{[^}]*"name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$tool_name" ] && tool_name="$(printf '%s' "$input" | sed -n 's/.*"toolCall"[[:space:]]*:[[:space:]]*{[^}]*"toolName"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$tool_name" ] && tool_name="$(printf '%s' "$input" | sed -n 's/.*"tool_call"[[:space:]]*:[[:space:]]*{[^}]*"name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$tool_name" ] && tool_name="$(printf '%s' "$input" | sed -n 's/.*"tool_call"[[:space:]]*:[[:space:]]*{[^}]*"tool_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$tool_name" ] && tool_name="$(printf '%s' "$input" | grep -Eo '[A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*' | head -n1)" + [ -z "$tool_name" ] && tool_name="unknown" + + delegated_agent_name="unknown" + if [ "$tool_name" = "runSubagent" ]; then + delegated_agent_name="$(extract_json_string delegatedAgentName)" + [ -z "$delegated_agent_name" ] && delegated_agent_name="$(extract_json_string delegated_agent_name)" + [ -z "$delegated_agent_name" ] && delegated_agent_name="$(extract_json_string agentName)" + [ -z "$delegated_agent_name" ] && delegated_agent_name="$(extract_json_string agent_name)" + [ -z "$delegated_agent_name" ] && delegated_agent_name="$(printf '%s' "$input" | sed -n 's/.*"runSubagent"[[:space:]]*:[[:space:]]*{[^}]*"agentName"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$delegated_agent_name" ] && delegated_agent_name="$(printf '%s' "$input" | sed -n 's/.*"runSubagent"[[:space:]]*:[[:space:]]*{[^}]*"agent_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$delegated_agent_name" ] && delegated_agent_name="$(printf '%s' "$input" | sed -n 's/.*"arguments"[[:space:]]*:[[:space:]]*{[^}]*"agentName"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$delegated_agent_name" ] && delegated_agent_name="$(printf '%s' "$input" | sed -n 's/.*"arguments"[[:space:]]*:[[:space:]]*{[^}]*"agent_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$delegated_agent_name" ] && delegated_agent_name="unknown" + fi + + tool_call_id="$(extract_json_string toolCallId)" + [ -z "$tool_call_id" ] && tool_call_id="$(extract_json_string tool_call_id)" + [ -z "$tool_call_id" ] && tool_call_id="$(extract_json_string callId)" + [ -z "$tool_call_id" ] && tool_call_id="$(extract_json_string call_id)" + [ -z "$tool_call_id" ] && tool_call_id="$(printf '%s' "$input" | sed -n 's/.*"toolCall"[[:space:]]*:[[:space:]]*{[^}]*"id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$tool_call_id" ] && tool_call_id="$(printf '%s' "$input" | sed -n 's/.*"tool_call"[[:space:]]*:[[:space:]]*{[^}]*"id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$tool_call_id" ] && tool_call_id="unknown" + + model_used="$(extract_json_string model)" + [ -z "$model_used" ] && model_used="$(extract_json_string modelName)" + [ -z "$model_used" ] && model_used="$(extract_json_string model_name)" + [ -z "$model_used" ] && model_used="$(extract_json_string toolModel)" + [ -z "$model_used" ] && model_used="$(extract_json_string tool_model)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"toolCall"[[:space:]]*:[[:space:]]*{[^}]*"model"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"arguments"[[:space:]]*:[[:space:]]*{[^}]*"model"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"arguments"[[:space:]]*:[[:space:]]*{[^}]*"modelName"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"arguments"[[:space:]]*:[[:space:]]*{[^}]*"model_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"args"[[:space:]]*:[[:space:]]*{[^}]*"model"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"args"[[:space:]]*:[[:space:]]*{[^}]*"modelName"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"args"[[:space:]]*:[[:space:]]*{[^}]*"model_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"tool"[[:space:]]*:[[:space:]]*{[^}]*"arguments"[[:space:]]*:[[:space:]]*{[^}]*"model"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"tool"[[:space:]]*:[[:space:]]*{[^}]*"arguments"[[:space:]]*:[[:space:]]*{[^}]*"modelName"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"tool"[[:space:]]*:[[:space:]]*{[^}]*"arguments"[[:space:]]*:[[:space:]]*{[^}]*"model_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"toolCall"[[:space:]]*:[[:space:]]*{[^}]*"arguments"[[:space:]]*:[[:space:]]*{[^}]*"model"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"toolCall"[[:space:]]*:[[:space:]]*{[^}]*"arguments"[[:space:]]*:[[:space:]]*{[^}]*"modelName"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"toolCall"[[:space:]]*:[[:space:]]*{[^}]*"arguments"[[:space:]]*:[[:space:]]*{[^}]*"model_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"tool_call"[[:space:]]*:[[:space:]]*{[^}]*"arguments"[[:space:]]*:[[:space:]]*{[^}]*"model"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"tool_call"[[:space:]]*:[[:space:]]*{[^}]*"arguments"[[:space:]]*:[[:space:]]*{[^}]*"modelName"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"tool_call"[[:space:]]*:[[:space:]]*{[^}]*"arguments"[[:space:]]*:[[:space:]]*{[^}]*"model_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="unknown" + + if [ "$actor_name" = "unknown" ] && [ "$delegated_agent_name" = "unknown" ] && [ "$tool_name" != "runSubagent" ]; then + increment_unknown_event "$session_id" "preToolUse_unknown_context" + exit 0 + fi + + event_name="preToolUse" + if [ "$tool_name" = "runSubagent" ]; then + event_name="delegationStart" + fi + + bytes="$(printf '%s' "$input" | wc -c | tr -d ' ')" + est_tokens="$(( (bytes + 3) / 4 ))" + script_end_ms="$(date +%s%3N 2>/dev/null || true)" + if [ -z "$script_end_ms" ]; then + script_end_ms="$(( $(date +%s) * 1000 ))" + fi + hook_execution_ms="$((script_end_ms - script_start_ms))" + + printf '{"timestamp":"%s","event":"%s","session_id":"%s","actor_name":"%s","actor_type":"%s","tool_name":"%s","delegated_agent_name":"%s","model_used":"%s","size_bytes":%s,"estimated_tokens":%s,"hook_execution_ms":%s}\n' \ + "$ts" "$event_name" "$session_id" "$actor_name" "$actor_type" "$tool_name" "$delegated_agent_name" "$model_used" "$bytes" "$est_tokens" "$hook_execution_ms" >> "$log_dir/hook-agent-call.log" + powershell: | + $inputText = [Console]::In.ReadToEnd() + $logLevel = if ($env:VSTACK_HOOKS_LOG_LEVEL) { $env:VSTACK_HOOKS_LOG_LEVEL } else { 'minimal' } + $logRoot = if ($env:VSTACK_HOOK_LOG_DIR) { $env:VSTACK_HOOK_LOG_DIR } else { '.vstack/logs' } + $retentionDaysRaw = if ($env:VSTACK_HOOK_RETENTION_DAYS) { $env:VSTACK_HOOK_RETENTION_DAYS } else { '7' } + $retentionDays = 7 + if (-not ([int]::TryParse([string]$retentionDaysRaw, [ref]$retentionDays) -and $retentionDays -gt 0)) { + $retentionDays = 7 + } + try { + if (Test-Path -LiteralPath $logRoot) { + $cutoffDay = (Get-Date).ToUniversalTime().AddDays(-$retentionDays).ToString('yyyyMMdd') + Get-ChildItem -LiteralPath $logRoot -Directory -ErrorAction Stop | + Where-Object { $_.Name -match '^\d{8}$' -and $_.Name -lt $cutoffDay } | + ForEach-Object { Remove-Item -LiteralPath $_.FullName -Recurse -Force -ErrorAction SilentlyContinue } + } + } catch {} + $logDir = Join-Path $logRoot (Get-Date).ToUniversalTime().ToString('yyyyMMdd') + New-Item -ItemType Directory -Force -Path $logDir | Out-Null + if ($logLevel -eq 'off') { exit 0 } + if ($logLevel -eq 'verbose') { + Add-Content -Path (Join-Path $logDir 'hook-agent-call.log') -Value $inputText + exit 0 + } + + $scriptStart = Get-Date + $unknownEventsPath = Join-Path $logDir 'hook-agent-call-unknown-events.tsv' + + function Get-PathValue { + param([object]$Object, [string]$Path) + $current = $Object + foreach ($part in $Path.Split('.')) { + if ($null -eq $current) { return $null } + $prop = $current.PSObject.Properties[$part] + if ($null -eq $prop) { return $null } + $current = $prop.Value + } + return $current + } + + function Get-FirstString { + param([object]$Object, [string[]]$Paths) + foreach ($path in $Paths) { + $value = Get-PathValue -Object $Object -Path $path + if ($null -ne $value -and [string]$value -ne '') { return [string]$value } + } + return $null + } + + function Add-UnknownEventCounter { + param( + [string]$Path, + [string]$SessionId, + [string]$Reason + ) + + $counts = @{} + if (Test-Path $Path) { + foreach ($line in (Get-Content -Path $Path)) { + if ([string]::IsNullOrWhiteSpace($line)) { continue } + $parts = $line -split "`t" + if ($parts.Count -lt 2) { continue } + + $sid = [string]$parts[0] + $why = [string]$parts[1] + $count = 1 + if ($parts.Count -ge 3) { + $parsedCount = 0 + if ([int]::TryParse([string]$parts[2], [ref]$parsedCount)) { + $count = [int][Math]::Max(0, $parsedCount) + } + } + + $key = "$sid`t$why" + if (-not $counts.ContainsKey($key)) { $counts[$key] = 0 } + $counts[$key] += $count + } + } + + $target = "$SessionId`t$Reason" + if (-not $counts.ContainsKey($target)) { $counts[$target] = 0 } + $counts[$target] += 1 + + $rows = foreach ($entry in ($counts.GetEnumerator() | Sort-Object Name)) { + "{0}`t{1}" -f $entry.Name, $entry.Value + } + Set-Content -Path $Path -Value $rows + } + + $payload = $null + try { $payload = $inputText | ConvertFrom-Json -ErrorAction Stop } catch {} + + $sessionId = 'unknown-session' + $actorName = 'unknown' + $actorType = 'unknown' + $toolName = 'unknown' + $toolCallId = 'unknown' + $delegatedAgentName = 'unknown' + $modelUsed = 'unknown' + + if ($null -ne $payload) { + $sessionIdCandidate = Get-FirstString -Object $payload -Paths @('sessionId', 'session_id', 'conversationId', 'conversation_id', 'chatId', 'chat_id') + if ($sessionIdCandidate) { $sessionId = $sessionIdCandidate } + + $actorNameCandidate = Get-FirstString -Object $payload -Paths @('actorName', 'actor_name', 'subagentName', 'subagent_name', 'agentName', 'agent_name') + if ($actorNameCandidate) { $actorName = $actorNameCandidate } + + $actorTypeCandidate = Get-FirstString -Object $payload -Paths @('actorType', 'actor_type') + if ($actorTypeCandidate) { $actorType = $actorTypeCandidate } + + $toolNameCandidate = Get-FirstString -Object $payload -Paths @('toolName', 'tool_name', 'recipient_name', 'tool.name', 'toolCall.name', 'toolCall.toolName', 'tool_call.name', 'tool_call.tool_name') + if ($toolNameCandidate) { $toolName = $toolNameCandidate } + + $toolCallIdCandidate = Get-FirstString -Object $payload -Paths @('toolCallId', 'tool_call_id', 'callId', 'call_id', 'toolCall.id', 'tool_call.id') + if ($toolCallIdCandidate) { $toolCallId = $toolCallIdCandidate } + + $delegatedCandidate = Get-FirstString -Object $payload -Paths @('delegatedAgentName', 'delegated_agent_name', 'runSubagent.agentName', 'runSubagent.agent_name', 'arguments.agentName', 'arguments.agent_name', 'args.agentName', 'args.agent_name', 'tool.arguments.agentName', 'tool.arguments.agent_name', 'toolCall.arguments.agentName', 'toolCall.arguments.agent_name', 'tool_call.arguments.agentName', 'tool_call.arguments.agent_name') + if ($delegatedCandidate) { $delegatedAgentName = $delegatedCandidate } + + $modelCandidate = Get-FirstString -Object $payload -Paths @('model', 'modelName', 'model_name', 'toolModel', 'tool_model', 'toolCall.model', 'tool_call.model', 'arguments.model', 'arguments.modelName', 'arguments.model_name', 'args.model', 'args.modelName', 'args.model_name', 'tool.arguments.model', 'tool.arguments.modelName', 'tool.arguments.model_name', 'toolCall.arguments.model', 'toolCall.arguments.modelName', 'toolCall.arguments.model_name', 'tool_call.arguments.model', 'tool_call.arguments.modelName', 'tool_call.arguments.model_name') + if ($modelCandidate) { $modelUsed = $modelCandidate } + } + + if ($actorType -eq 'unknown') { + if ($actorName -match 'subagent') { + $actorType = 'subagent' + } elseif ($actorName -ne 'unknown') { + $actorType = 'agent' + } + } + + if ($toolName -eq 'unknown') { + $toolNameMatch = [regex]::Match($inputText, '[A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*') + if ($toolNameMatch.Success) { $toolName = $toolNameMatch.Value } + } + + if ($toolName -eq 'runSubagent' -and $delegatedAgentName -eq 'unknown' -and $null -ne $payload) { + $delegatedFallback = Get-FirstString -Object $payload -Paths @('agentName', 'agent_name') + if ($delegatedFallback) { $delegatedAgentName = $delegatedFallback } + } + + if ($actorName -eq 'unknown' -and $delegatedAgentName -eq 'unknown' -and $toolName -ne 'runSubagent') { + Add-UnknownEventCounter -Path $unknownEventsPath -SessionId $sessionId -Reason 'preToolUse_unknown_context' + exit 0 + } + + $eventName = if ($toolName -eq 'runSubagent') { 'delegationStart' } else { 'preToolUse' } + + $sizeBytes = [System.Text.Encoding]::UTF8.GetByteCount($inputText) + $hookExecutionMs = [int][Math]::Max(0, ((Get-Date) - $scriptStart).TotalMilliseconds) + $record = @{ + timestamp = (Get-Date).ToUniversalTime().ToString('o') + event = $eventName + session_id = $sessionId + actor_name = $actorName + actor_type = $actorType + tool_name = $toolName + delegated_agent_name = $delegatedAgentName + model_used = $modelUsed + size_bytes = $sizeBytes + estimated_tokens = [int][math]::Ceiling($sizeBytes / 4.0) + hook_execution_ms = $hookExecutionMs + } | ConvertTo-Json -Compress + Add-Content -Path (Join-Path $logDir 'hook-agent-call.log') -Value $record + cwd: "." + timeoutSec: 5 + + postToolUse: + - type: command + description: | + Log tool result events with best-effort actor, delegation, and model metadata. + bash: | + script_start_ms="$(date +%s%3N 2>/dev/null || true)" + if [ -z "$script_start_ms" ]; then + script_start_ms="$(( $(date +%s) * 1000 ))" + fi + input="$(cat)" + log_level="${VSTACK_HOOKS_LOG_LEVEL:-minimal}" + log_root="${VSTACK_HOOK_LOG_DIR:-.vstack/logs}" + retention_days="${VSTACK_HOOK_RETENTION_DAYS:-7}" + if ! printf '%s' "$retention_days" | grep -Eq '^[1-9][0-9]*$'; then + retention_days=7 + fi + if [ -d "$log_root" ]; then + cutoff_day="$(date -u -d "$retention_days days ago" +%Y%m%d 2>/dev/null || true)" + if [ -n "$cutoff_day" ]; then + for day_dir in "$log_root"/*; do + [ -d "$day_dir" ] || continue + day_name="$(basename "$day_dir")" + case "$day_name" in + [0-9][0-9][0-9][0-9][0-1][0-9][0-3][0-9]) ;; + *) continue ;; + esac + if [ "$day_name" -lt "$cutoff_day" ]; then + rm -rf "$day_dir" || true + fi + done + fi + fi + log_dir="$log_root/$(date -u +%Y%m%d)" + mkdir -p "$log_dir" + if [ "$log_level" = "off" ]; then + exit 0 + fi + if [ "$log_level" = "verbose" ]; then + printf '%s\n' "$input" >> "$log_dir/hook-agent-call.log" + exit 0 + fi + + ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)" + unknown_events_file="$log_dir/hook-agent-call-unknown-events.tsv" + + extract_json_string() { + key="$1" + printf '%s' "$input" | sed -n "s/.*\"${key}\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p" | head -n1 + } + + increment_unknown_event() { + sid="$1" + reason="$2" + tmp_file="$unknown_events_file.tmp.$$" + if [ -f "$unknown_events_file" ]; then + awk -F '\t' -v sid="$sid" -v reason="$reason" 'BEGIN { OFS="\t" } + NF >= 2 { + key = $1 SUBSEP $2 + count = 1 + if (NF >= 3 && $3 ~ /^[0-9]+$/) { + count = $3 + 0 + } + counts[key] += count + } + END { + target = sid SUBSEP reason + counts[target] += 1 + for (k in counts) { + split(k, parts, SUBSEP) + print parts[1], parts[2], counts[k] + } + }' "$unknown_events_file" > "$tmp_file" && mv "$tmp_file" "$unknown_events_file" + else + printf '%s\t%s\t1\n' "$sid" "$reason" > "$unknown_events_file" + fi + } + + session_id="$(extract_json_string sessionId)" + [ -z "$session_id" ] && session_id="$(extract_json_string session_id)" + [ -z "$session_id" ] && session_id="$(extract_json_string conversationId)" + [ -z "$session_id" ] && session_id="$(extract_json_string conversation_id)" + [ -z "$session_id" ] && session_id="$(extract_json_string chatId)" + [ -z "$session_id" ] && session_id="$(extract_json_string chat_id)" + [ -z "$session_id" ] && session_id="unknown-session" + + actor_name="$(extract_json_string actorName)" + [ -z "$actor_name" ] && actor_name="$(extract_json_string actor_name)" + [ -z "$actor_name" ] && actor_name="$(extract_json_string subagentName)" + [ -z "$actor_name" ] && actor_name="$(extract_json_string subagent_name)" + [ -z "$actor_name" ] && actor_name="$(extract_json_string agentName)" + [ -z "$actor_name" ] && actor_name="$(extract_json_string agent_name)" + [ -z "$actor_name" ] && actor_name="unknown" + + actor_type="$(extract_json_string actorType)" + [ -z "$actor_type" ] && actor_type="$(extract_json_string actor_type)" + if [ -z "$actor_type" ]; then + if printf '%s' "$actor_name" | grep -qi 'subagent'; then + actor_type="subagent" + elif [ "$actor_name" != "unknown" ]; then + actor_type="agent" + else + actor_type="unknown" + fi + fi + + tool_name="$(extract_json_string toolName)" + [ -z "$tool_name" ] && tool_name="$(extract_json_string tool_name)" + [ -z "$tool_name" ] && tool_name="$(extract_json_string recipient_name)" + [ -z "$tool_name" ] && tool_name="$(printf '%s' "$input" | sed -n 's/.*"tool"[[:space:]]*:[[:space:]]*{[^}]*"name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$tool_name" ] && tool_name="$(printf '%s' "$input" | sed -n 's/.*"toolCall"[[:space:]]*:[[:space:]]*{[^}]*"name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$tool_name" ] && tool_name="$(printf '%s' "$input" | sed -n 's/.*"toolCall"[[:space:]]*:[[:space:]]*{[^}]*"toolName"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$tool_name" ] && tool_name="$(printf '%s' "$input" | sed -n 's/.*"tool_call"[[:space:]]*:[[:space:]]*{[^}]*"name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$tool_name" ] && tool_name="$(printf '%s' "$input" | sed -n 's/.*"tool_call"[[:space:]]*:[[:space:]]*{[^}]*"tool_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$tool_name" ] && tool_name="$(printf '%s' "$input" | grep -Eo '[A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*' | head -n1)" + [ -z "$tool_name" ] && tool_name="unknown" + + delegated_agent_name="unknown" + if [ "$tool_name" = "runSubagent" ]; then + delegated_agent_name="$(extract_json_string delegatedAgentName)" + [ -z "$delegated_agent_name" ] && delegated_agent_name="$(extract_json_string delegated_agent_name)" + [ -z "$delegated_agent_name" ] && delegated_agent_name="$(extract_json_string agentName)" + [ -z "$delegated_agent_name" ] && delegated_agent_name="$(extract_json_string agent_name)" + [ -z "$delegated_agent_name" ] && delegated_agent_name="$(printf '%s' "$input" | sed -n 's/.*"runSubagent"[[:space:]]*:[[:space:]]*{[^}]*"agentName"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$delegated_agent_name" ] && delegated_agent_name="$(printf '%s' "$input" | sed -n 's/.*"runSubagent"[[:space:]]*:[[:space:]]*{[^}]*"agent_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$delegated_agent_name" ] && delegated_agent_name="$(printf '%s' "$input" | sed -n 's/.*"arguments"[[:space:]]*:[[:space:]]*{[^}]*"agentName"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$delegated_agent_name" ] && delegated_agent_name="$(printf '%s' "$input" | sed -n 's/.*"arguments"[[:space:]]*:[[:space:]]*{[^}]*"agent_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$delegated_agent_name" ] && delegated_agent_name="unknown" + fi + + tool_call_id="$(extract_json_string toolCallId)" + [ -z "$tool_call_id" ] && tool_call_id="$(extract_json_string tool_call_id)" + [ -z "$tool_call_id" ] && tool_call_id="$(extract_json_string callId)" + [ -z "$tool_call_id" ] && tool_call_id="$(extract_json_string call_id)" + [ -z "$tool_call_id" ] && tool_call_id="$(printf '%s' "$input" | sed -n 's/.*"toolCall"[[:space:]]*:[[:space:]]*{[^}]*"id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$tool_call_id" ] && tool_call_id="$(printf '%s' "$input" | sed -n 's/.*"tool_call"[[:space:]]*:[[:space:]]*{[^}]*"id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$tool_call_id" ] && tool_call_id="unknown" + + model_used="$(extract_json_string model)" + [ -z "$model_used" ] && model_used="$(extract_json_string modelName)" + [ -z "$model_used" ] && model_used="$(extract_json_string model_name)" + [ -z "$model_used" ] && model_used="$(extract_json_string toolModel)" + [ -z "$model_used" ] && model_used="$(extract_json_string tool_model)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"toolCall"[[:space:]]*:[[:space:]]*{[^}]*"model"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"arguments"[[:space:]]*:[[:space:]]*{[^}]*"model"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"arguments"[[:space:]]*:[[:space:]]*{[^}]*"modelName"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"arguments"[[:space:]]*:[[:space:]]*{[^}]*"model_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"args"[[:space:]]*:[[:space:]]*{[^}]*"model"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"args"[[:space:]]*:[[:space:]]*{[^}]*"modelName"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"args"[[:space:]]*:[[:space:]]*{[^}]*"model_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"tool"[[:space:]]*:[[:space:]]*{[^}]*"arguments"[[:space:]]*:[[:space:]]*{[^}]*"model"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"tool"[[:space:]]*:[[:space:]]*{[^}]*"arguments"[[:space:]]*:[[:space:]]*{[^}]*"modelName"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"tool"[[:space:]]*:[[:space:]]*{[^}]*"arguments"[[:space:]]*:[[:space:]]*{[^}]*"model_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"toolCall"[[:space:]]*:[[:space:]]*{[^}]*"arguments"[[:space:]]*:[[:space:]]*{[^}]*"model"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"toolCall"[[:space:]]*:[[:space:]]*{[^}]*"arguments"[[:space:]]*:[[:space:]]*{[^}]*"modelName"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"toolCall"[[:space:]]*:[[:space:]]*{[^}]*"arguments"[[:space:]]*:[[:space:]]*{[^}]*"model_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"tool_call"[[:space:]]*:[[:space:]]*{[^}]*"arguments"[[:space:]]*:[[:space:]]*{[^}]*"model"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"tool_call"[[:space:]]*:[[:space:]]*{[^}]*"arguments"[[:space:]]*:[[:space:]]*{[^}]*"modelName"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="$(printf '%s' "$input" | sed -n 's/.*"tool_call"[[:space:]]*:[[:space:]]*{[^}]*"arguments"[[:space:]]*:[[:space:]]*{[^}]*"model_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + [ -z "$model_used" ] && model_used="unknown" + + if [ "$actor_name" = "unknown" ] && [ "$delegated_agent_name" = "unknown" ] && [ "$tool_name" != "runSubagent" ]; then + increment_unknown_event "$session_id" "postToolUse_unknown_context" + exit 0 + fi + + event_name="postToolUse" + if [ "$tool_name" = "runSubagent" ]; then + event_name="delegationEnd" + fi + + bytes="$(printf '%s' "$input" | wc -c | tr -d ' ')" + est_tokens="$(( (bytes + 3) / 4 ))" + script_end_ms="$(date +%s%3N 2>/dev/null || true)" + if [ -z "$script_end_ms" ]; then + script_end_ms="$(( $(date +%s) * 1000 ))" + fi + hook_execution_ms="$((script_end_ms - script_start_ms))" + + printf '{"timestamp":"%s","event":"%s","session_id":"%s","actor_name":"%s","actor_type":"%s","tool_name":"%s","delegated_agent_name":"%s","model_used":"%s","size_bytes":%s,"estimated_tokens":%s,"hook_execution_ms":%s}\n' \ + "$ts" "$event_name" "$session_id" "$actor_name" "$actor_type" "$tool_name" "$delegated_agent_name" "$model_used" "$bytes" "$est_tokens" "$hook_execution_ms" >> "$log_dir/hook-agent-call.log" + powershell: | + $inputText = [Console]::In.ReadToEnd() + $logLevel = if ($env:VSTACK_HOOKS_LOG_LEVEL) { $env:VSTACK_HOOKS_LOG_LEVEL } else { 'minimal' } + $logRoot = if ($env:VSTACK_HOOK_LOG_DIR) { $env:VSTACK_HOOK_LOG_DIR } else { '.vstack/logs' } + $retentionDaysRaw = if ($env:VSTACK_HOOK_RETENTION_DAYS) { $env:VSTACK_HOOK_RETENTION_DAYS } else { '7' } + $retentionDays = 7 + if (-not ([int]::TryParse([string]$retentionDaysRaw, [ref]$retentionDays) -and $retentionDays -gt 0)) { + $retentionDays = 7 + } + try { + if (Test-Path -LiteralPath $logRoot) { + $cutoffDay = (Get-Date).ToUniversalTime().AddDays(-$retentionDays).ToString('yyyyMMdd') + Get-ChildItem -LiteralPath $logRoot -Directory -ErrorAction Stop | + Where-Object { $_.Name -match '^\d{8}$' -and $_.Name -lt $cutoffDay } | + ForEach-Object { Remove-Item -LiteralPath $_.FullName -Recurse -Force -ErrorAction SilentlyContinue } + } + } catch {} + $logDir = Join-Path $logRoot (Get-Date).ToUniversalTime().ToString('yyyyMMdd') + New-Item -ItemType Directory -Force -Path $logDir | Out-Null + if ($logLevel -eq 'off') { exit 0 } + if ($logLevel -eq 'verbose') { + Add-Content -Path (Join-Path $logDir 'hook-agent-call.log') -Value $inputText + exit 0 + } + + $scriptStart = Get-Date + $unknownEventsPath = Join-Path $logDir 'hook-agent-call-unknown-events.tsv' + + function Get-PathValue { + param([object]$Object, [string]$Path) + $current = $Object + foreach ($part in $Path.Split('.')) { + if ($null -eq $current) { return $null } + $prop = $current.PSObject.Properties[$part] + if ($null -eq $prop) { return $null } + $current = $prop.Value + } + return $current + } + + function Get-FirstString { + param([object]$Object, [string[]]$Paths) + foreach ($path in $Paths) { + $value = Get-PathValue -Object $Object -Path $path + if ($null -ne $value -and [string]$value -ne '') { return [string]$value } + } + return $null + } + + function Add-UnknownEventCounter { + param( + [string]$Path, + [string]$SessionId, + [string]$Reason + ) + + $counts = @{} + if (Test-Path $Path) { + foreach ($line in (Get-Content -Path $Path)) { + if ([string]::IsNullOrWhiteSpace($line)) { continue } + $parts = $line -split "`t" + if ($parts.Count -lt 2) { continue } + + $sid = [string]$parts[0] + $why = [string]$parts[1] + $count = 1 + if ($parts.Count -ge 3) { + $parsedCount = 0 + if ([int]::TryParse([string]$parts[2], [ref]$parsedCount)) { + $count = [int][Math]::Max(0, $parsedCount) + } + } + + $key = "$sid`t$why" + if (-not $counts.ContainsKey($key)) { $counts[$key] = 0 } + $counts[$key] += $count + } + } + + $target = "$SessionId`t$Reason" + if (-not $counts.ContainsKey($target)) { $counts[$target] = 0 } + $counts[$target] += 1 + + $rows = foreach ($entry in ($counts.GetEnumerator() | Sort-Object Name)) { + "{0}`t{1}" -f $entry.Name, $entry.Value + } + Set-Content -Path $Path -Value $rows + } + + $payload = $null + try { $payload = $inputText | ConvertFrom-Json -ErrorAction Stop } catch {} + + $sessionId = 'unknown-session' + $actorName = 'unknown' + $actorType = 'unknown' + $toolName = 'unknown' + $toolCallId = 'unknown' + $delegatedAgentName = 'unknown' + $modelUsed = 'unknown' + + if ($null -ne $payload) { + $sessionIdCandidate = Get-FirstString -Object $payload -Paths @('sessionId', 'session_id', 'conversationId', 'conversation_id', 'chatId', 'chat_id') + if ($sessionIdCandidate) { $sessionId = $sessionIdCandidate } + + $actorNameCandidate = Get-FirstString -Object $payload -Paths @('actorName', 'actor_name', 'subagentName', 'subagent_name', 'agentName', 'agent_name') + if ($actorNameCandidate) { $actorName = $actorNameCandidate } + + $actorTypeCandidate = Get-FirstString -Object $payload -Paths @('actorType', 'actor_type') + if ($actorTypeCandidate) { $actorType = $actorTypeCandidate } + + $toolNameCandidate = Get-FirstString -Object $payload -Paths @('toolName', 'tool_name', 'recipient_name', 'tool.name', 'toolCall.name', 'toolCall.toolName', 'tool_call.name', 'tool_call.tool_name') + if ($toolNameCandidate) { $toolName = $toolNameCandidate } + + $toolCallIdCandidate = Get-FirstString -Object $payload -Paths @('toolCallId', 'tool_call_id', 'callId', 'call_id', 'toolCall.id', 'tool_call.id') + if ($toolCallIdCandidate) { $toolCallId = $toolCallIdCandidate } + + $delegatedCandidate = Get-FirstString -Object $payload -Paths @('delegatedAgentName', 'delegated_agent_name', 'runSubagent.agentName', 'runSubagent.agent_name', 'arguments.agentName', 'arguments.agent_name', 'args.agentName', 'args.agent_name', 'tool.arguments.agentName', 'tool.arguments.agent_name', 'toolCall.arguments.agentName', 'toolCall.arguments.agent_name', 'tool_call.arguments.agentName', 'tool_call.arguments.agent_name') + if ($delegatedCandidate) { $delegatedAgentName = $delegatedCandidate } + + $modelCandidate = Get-FirstString -Object $payload -Paths @('model', 'modelName', 'model_name', 'toolModel', 'tool_model', 'toolCall.model', 'tool_call.model', 'arguments.model', 'arguments.modelName', 'arguments.model_name', 'args.model', 'args.modelName', 'args.model_name', 'tool.arguments.model', 'tool.arguments.modelName', 'tool.arguments.model_name', 'toolCall.arguments.model', 'toolCall.arguments.modelName', 'toolCall.arguments.model_name', 'tool_call.arguments.model', 'tool_call.arguments.modelName', 'tool_call.arguments.model_name') + if ($modelCandidate) { $modelUsed = $modelCandidate } + } + + if ($actorType -eq 'unknown') { + if ($actorName -match 'subagent') { + $actorType = 'subagent' + } elseif ($actorName -ne 'unknown') { + $actorType = 'agent' + } + } + + if ($toolName -eq 'unknown') { + $toolNameMatch = [regex]::Match($inputText, '[A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*') + if ($toolNameMatch.Success) { $toolName = $toolNameMatch.Value } + } + + if ($toolName -eq 'runSubagent' -and $delegatedAgentName -eq 'unknown' -and $null -ne $payload) { + $delegatedFallback = Get-FirstString -Object $payload -Paths @('agentName', 'agent_name') + if ($delegatedFallback) { $delegatedAgentName = $delegatedFallback } + } + + if ($actorName -eq 'unknown' -and $delegatedAgentName -eq 'unknown' -and $toolName -ne 'runSubagent') { + Add-UnknownEventCounter -Path $unknownEventsPath -SessionId $sessionId -Reason 'postToolUse_unknown_context' + exit 0 + } + + $eventName = if ($toolName -eq 'runSubagent') { 'delegationEnd' } else { 'postToolUse' } + + $sizeBytes = [System.Text.Encoding]::UTF8.GetByteCount($inputText) + $hookExecutionMs = [int][Math]::Max(0, ((Get-Date) - $scriptStart).TotalMilliseconds) + $record = @{ + timestamp = (Get-Date).ToUniversalTime().ToString('o') + event = $eventName + session_id = $sessionId + actor_name = $actorName + actor_type = $actorType + tool_name = $toolName + delegated_agent_name = $delegatedAgentName + model_used = $modelUsed + size_bytes = $sizeBytes + estimated_tokens = [int][math]::Ceiling($sizeBytes / 4.0) + hook_execution_ms = $hookExecutionMs + } | ConvertTo-Json -Compress + Add-Content -Path (Join-Path $logDir 'hook-agent-call.log') -Value $record + cwd: "." + timeoutSec: 5 diff --git a/src/vstack/_templates/hooks/post-commit-security-scan/hook.yaml b/src/vstack/_templates/hooks/post-commit-security-scan/hook.yaml index 44b455c0..6e52b758 100644 --- a/src/vstack/_templates/hooks/post-commit-security-scan/hook.yaml +++ b/src/vstack/_templates/hooks/post-commit-security-scan/hook.yaml @@ -26,47 +26,52 @@ hooks: Check staged diffs for secrets and log only security alerts. bash: | input="$(cat)" - log_name="${VSTACK_HOOK_LOG_NAME:-hook-security-alerts.log}" - log_root="${VSTACK_HOOK_LOG_DIR:-.vstack/logs}" - log_dir="$log_root/$(date -u +%Y%m%d)" - mkdir -p "$log_dir" - log_path="$log_dir/$log_name" + append_log() { + log_name="${VSTACK_HOOK_LOG_NAME:-hook-security-alerts.log}" + log_root="${VSTACK_HOOK_LOG_DIR:-.vstack/logs}" + log_dir="$log_root/$(date -u +%Y%m%d)" + mkdir -p "$log_dir" + printf '%s\n' "$1" >> "$log_dir/$log_name" + } if ! printf '%s' "$input" | grep -Eiq 'git (commit|push|merge|rebase)'; then exit 0 fi if git rev-parse --git-dir >/dev/null 2>&1 && \ git diff --cached 2>/dev/null | grep -Eiq 'AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9]{36,}|-----BEGIN (RSA|EC|OPENSSH) PRIVATE KEY-----'; then - printf '%s\n' 'possible-secrets-detected-in-staged-diff' >> "$log_path" + append_log 'possible-secrets-detected-in-staged-diff' fi if [ "${VSTACK_HOOKS_MODE:-audit}" = "enforce" ]; then if command -v gitleaks >/dev/null 2>&1; then - gitleaks dir . --no-banner --redact >/dev/null 2>&1 || printf '%s\n' 'gitleaks-reported-findings' >> "$log_path" + gitleaks dir . --no-banner --redact >/dev/null 2>&1 || append_log 'gitleaks-reported-findings' else - printf '%s\n' 'gitleaks-not-installed' >> "$log_path" + append_log 'gitleaks-not-installed' fi fi powershell: | $inputText = [Console]::In.ReadToEnd() - $logName = if ($env:VSTACK_HOOK_LOG_NAME) { $env:VSTACK_HOOK_LOG_NAME } else { 'hook-security-alerts.log' } - $logRoot = if ($env:VSTACK_HOOK_LOG_DIR) { $env:VSTACK_HOOK_LOG_DIR } else { '.vstack/logs' } - $logDir = Join-Path $logRoot (Get-Date).ToUniversalTime().ToString('yyyyMMdd') - New-Item -ItemType Directory -Force -Path $logDir | Out-Null - $logPath = Join-Path $logDir $logName + function Write-HookLog { + param([string]$Message) + $logName = if ($env:VSTACK_HOOK_LOG_NAME) { $env:VSTACK_HOOK_LOG_NAME } else { 'hook-security-alerts.log' } + $logRoot = if ($env:VSTACK_HOOK_LOG_DIR) { $env:VSTACK_HOOK_LOG_DIR } else { '.vstack/logs' } + $logDir = Join-Path $logRoot (Get-Date).ToUniversalTime().ToString('yyyyMMdd') + New-Item -ItemType Directory -Force -Path $logDir | Out-Null + Add-Content -Path (Join-Path $logDir $logName) -Value $Message + } if ($inputText -notmatch 'git (commit|push|merge|rebase)') { exit 0 } if ((git rev-parse --git-dir 2>$null) -and (git diff --cached | Select-String -Pattern 'AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9]{36,}|-----BEGIN (RSA|EC|OPENSSH) PRIVATE KEY-----' -Quiet)) { - Add-Content -Path $logPath -Value 'possible-secrets-detected-in-staged-diff' + Write-HookLog 'possible-secrets-detected-in-staged-diff' } $mode = if ($env:VSTACK_HOOKS_MODE) { $env:VSTACK_HOOKS_MODE } else { 'audit' } if ($mode -eq 'enforce') { if (Get-Command gitleaks -ErrorAction SilentlyContinue) { gitleaks dir . --no-banner --redact *> $null if ($LASTEXITCODE -ne 0) { - Add-Content -Path $logPath -Value 'gitleaks-reported-findings' + Write-HookLog 'gitleaks-reported-findings' } } else { - Add-Content -Path $logPath -Value 'gitleaks-not-installed' + Write-HookLog 'gitleaks-not-installed' } } cwd: "." diff --git a/src/vstack/_templates/hooks/post-edit-format/hook.yaml b/src/vstack/_templates/hooks/post-edit-format/hook.yaml index 1f655bf2..242391fa 100644 --- a/src/vstack/_templates/hooks/post-edit-format/hook.yaml +++ b/src/vstack/_templates/hooks/post-edit-format/hook.yaml @@ -23,34 +23,39 @@ hooks: description: | Optionally run format in enforce mode; write alerts only on failures. bash: | - input="$(cat)" - log_name="${VSTACK_HOOK_LOG_NAME:-hook-quality-alerts.log}" - log_root="${VSTACK_HOOK_LOG_DIR:-.vstack/logs}" - log_dir="$log_root/$(date -u +%Y%m%d)" - mkdir -p "$log_dir" - log_path="$log_dir/$log_name" mode="${VSTACK_HOOKS_MODE:-audit}" if [ "$mode" != "enforce" ]; then exit 0 fi + input="$(cat)" + append_log() { + log_name="${VSTACK_HOOK_LOG_NAME:-hook-quality-alerts.log}" + log_root="${VSTACK_HOOK_LOG_DIR:-.vstack/logs}" + log_dir="$log_root/$(date -u +%Y%m%d)" + mkdir -p "$log_dir" + printf '%s\n' "$1" >> "$log_dir/$log_name" + } if printf '%s' "$input" | grep -Eq '"toolName"[[:space:]]*:[[:space:]]*"(edit|create|write|replace)"'; then if [ -f Makefile ] && command -v make >/dev/null 2>&1; then - make format >/dev/null 2>&1 || printf '%s\n' 'format-run-failed' >> "$log_path" + make format >/dev/null 2>&1 || append_log 'format-run-failed' else - printf '%s\n' 'format-skipped-missing-make-or-makefile' >> "$log_path" + append_log 'format-skipped-missing-make-or-makefile' fi fi powershell: | - $inputText = [Console]::In.ReadToEnd() - $logName = if ($env:VSTACK_HOOK_LOG_NAME) { $env:VSTACK_HOOK_LOG_NAME } else { 'hook-quality-alerts.log' } - $logRoot = if ($env:VSTACK_HOOK_LOG_DIR) { $env:VSTACK_HOOK_LOG_DIR } else { '.vstack/logs' } - $logDir = Join-Path $logRoot (Get-Date).ToUniversalTime().ToString('yyyyMMdd') - New-Item -ItemType Directory -Force -Path $logDir | Out-Null - $logPath = Join-Path $logDir $logName $mode = if ($env:VSTACK_HOOKS_MODE) { $env:VSTACK_HOOKS_MODE } else { 'audit' } if ($mode -ne 'enforce') { exit 0 } + $inputText = [Console]::In.ReadToEnd() + function Write-HookLog { + param([string]$Message) + $logName = if ($env:VSTACK_HOOK_LOG_NAME) { $env:VSTACK_HOOK_LOG_NAME } else { 'hook-quality-alerts.log' } + $logRoot = if ($env:VSTACK_HOOK_LOG_DIR) { $env:VSTACK_HOOK_LOG_DIR } else { '.vstack/logs' } + $logDir = Join-Path $logRoot (Get-Date).ToUniversalTime().ToString('yyyyMMdd') + New-Item -ItemType Directory -Force -Path $logDir | Out-Null + Add-Content -Path (Join-Path $logDir $logName) -Value $Message + } try { $payload = $inputText | ConvertFrom-Json -ErrorAction Stop } catch { @@ -60,10 +65,10 @@ hooks: if ((Test-Path Makefile) -and (Get-Command make -ErrorAction SilentlyContinue)) { make format *> $null if ($LASTEXITCODE -ne 0) { - Add-Content -Path $logPath -Value 'format-run-failed' + Write-HookLog 'format-run-failed' } } else { - Add-Content -Path $logPath -Value 'format-skipped-missing-make-or-makefile' + Write-HookLog 'format-skipped-missing-make-or-makefile' } } cwd: "." diff --git a/src/vstack/_templates/hooks/post-edit-markdown-quality/hook.yaml b/src/vstack/_templates/hooks/post-edit-markdown-quality/hook.yaml index 82ca4ac6..9efd7829 100644 --- a/src/vstack/_templates/hooks/post-edit-markdown-quality/hook.yaml +++ b/src/vstack/_templates/hooks/post-edit-markdown-quality/hook.yaml @@ -24,37 +24,42 @@ hooks: description: | Run markdown formatting in enforce mode and log only failures/skips. bash: | - input="$(cat)" - log_name="${VSTACK_HOOK_LOG_NAME:-hook-markdown-quality-alerts.log}" - log_root="${VSTACK_HOOK_LOG_DIR:-.vstack/logs}" - log_dir="$log_root/$(date -u +%Y%m%d)" - mkdir -p "$log_dir" - log_path="$log_dir/$log_name" mode="${VSTACK_HOOKS_MODE:-audit}" if [ "$mode" != "enforce" ]; then exit 0 fi + input="$(cat)" + append_log() { + log_name="${VSTACK_HOOK_LOG_NAME:-hook-markdown-quality-alerts.log}" + log_root="${VSTACK_HOOK_LOG_DIR:-.vstack/logs}" + log_dir="$log_root/$(date -u +%Y%m%d)" + mkdir -p "$log_dir" + printf '%s\n' "$1" >> "$log_dir/$log_name" + } if ! printf '%s' "$input" | grep -Eiq '\.md|docs/|\.github/|\.vstack/templates/|adr/|prompt\.md|instructions\.md|SKILL\.md'; then exit 0 fi if [ -f Makefile ] && command -v make >/dev/null 2>&1; then make markdown-format >/dev/null 2>&1 || \ make format >/dev/null 2>&1 || \ - printf '%s\n' 'markdown-format-run-failed' >> "$log_path" + append_log 'markdown-format-run-failed' else - printf '%s\n' 'markdown-format-skipped-missing-make-or-makefile' >> "$log_path" + append_log 'markdown-format-skipped-missing-make-or-makefile' fi powershell: | - $inputText = [Console]::In.ReadToEnd() - $logName = if ($env:VSTACK_HOOK_LOG_NAME) { $env:VSTACK_HOOK_LOG_NAME } else { 'hook-markdown-quality-alerts.log' } - $logRoot = if ($env:VSTACK_HOOK_LOG_DIR) { $env:VSTACK_HOOK_LOG_DIR } else { '.vstack/logs' } - $logDir = Join-Path $logRoot (Get-Date).ToUniversalTime().ToString('yyyyMMdd') - New-Item -ItemType Directory -Force -Path $logDir | Out-Null - $logPath = Join-Path $logDir $logName $mode = if ($env:VSTACK_HOOKS_MODE) { $env:VSTACK_HOOKS_MODE } else { 'audit' } if ($mode -ne 'enforce') { exit 0 } + $inputText = [Console]::In.ReadToEnd() + function Write-HookLog { + param([string]$Message) + $logName = if ($env:VSTACK_HOOK_LOG_NAME) { $env:VSTACK_HOOK_LOG_NAME } else { 'hook-markdown-quality-alerts.log' } + $logRoot = if ($env:VSTACK_HOOK_LOG_DIR) { $env:VSTACK_HOOK_LOG_DIR } else { '.vstack/logs' } + $logDir = Join-Path $logRoot (Get-Date).ToUniversalTime().ToString('yyyyMMdd') + New-Item -ItemType Directory -Force -Path $logDir | Out-Null + Add-Content -Path (Join-Path $logDir $logName) -Value $Message + } if ($inputText -notmatch '\.md|docs/|\.github/|\.vstack/templates/|adr/|prompt\.md|instructions\.md|SKILL\.md') { exit 0 } @@ -64,10 +69,10 @@ hooks: make format *> $null } if ($LASTEXITCODE -ne 0) { - Add-Content -Path $logPath -Value 'markdown-format-run-failed' + Write-HookLog 'markdown-format-run-failed' } } else { - Add-Content -Path $logPath -Value 'markdown-format-skipped-missing-make-or-makefile' + Write-HookLog 'markdown-format-skipped-missing-make-or-makefile' } cwd: "." timeoutSec: 30 diff --git a/src/vstack/_templates/hooks/pre-tool-safety-gate/hook.yaml b/src/vstack/_templates/hooks/pre-tool-safety-gate/hook.yaml index 59b731d0..1c9dd10e 100644 --- a/src/vstack/_templates/hooks/pre-tool-safety-gate/hook.yaml +++ b/src/vstack/_templates/hooks/pre-tool-safety-gate/hook.yaml @@ -24,52 +24,57 @@ hooks: Check for destructive bash patterns and block in enforce mode. Logs only policy decisions and errors in this hook. bash: | - input="$(cat)" - log_name="${VSTACK_HOOK_LOG_NAME:-hook-security-alerts.log}" - log_root="${VSTACK_HOOK_LOG_DIR:-.vstack/logs}" - log_dir="$log_root/$(date -u +%Y%m%d)" - mkdir -p "$log_dir" - log_path="$log_dir/$log_name" mode="${VSTACK_HOOKS_MODE:-audit}" if [ "$mode" != "enforce" ]; then exit 0 fi + input="$(cat)" + append_log() { + log_name="${VSTACK_HOOK_LOG_NAME:-hook-security-alerts.log}" + log_root="${VSTACK_HOOK_LOG_DIR:-.vstack/logs}" + log_dir="$log_root/$(date -u +%Y%m%d)" + mkdir -p "$log_dir" + printf '%s\n' "$1" >> "$log_dir/$log_name" + } tool_name="$(printf '%s' "$input" | sed -n 's/.*"toolName"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" if [ -z "$tool_name" ]; then - printf '%s\n' 'payload-parse-failed' >> "$log_path" + append_log 'payload-parse-failed' echo '{"permissionDecision":"deny","permissionDecisionReason":"Blocked request because payload could not be validated in enforce mode"}' exit 0 fi if [ "$tool_name" = "bash" ] && \ printf '%s' "$input" | grep -Eiq 'rm -rf /|mkfs|dd if=|DROP TABLE|TRUNCATE TABLE|git reset --hard|git clean -fdx|shutdown -h|shutdown now|reboot|poweroff|halt'; then - printf '%s\n' 'destructive-command-blocked' >> "$log_path" + append_log 'destructive-command-blocked' echo '{"permissionDecision":"deny","permissionDecisionReason":"Blocked potentially destructive shell command by vstack pre-tool safety policy"}' fi powershell: | - $inputText = [Console]::In.ReadToEnd() - $logName = if ($env:VSTACK_HOOK_LOG_NAME) { $env:VSTACK_HOOK_LOG_NAME } else { 'hook-security-alerts.log' } - $logRoot = if ($env:VSTACK_HOOK_LOG_DIR) { $env:VSTACK_HOOK_LOG_DIR } else { '.vstack/logs' } - $logDir = Join-Path $logRoot (Get-Date).ToUniversalTime().ToString('yyyyMMdd') - New-Item -ItemType Directory -Force -Path $logDir | Out-Null - $logPath = Join-Path $logDir $logName $mode = if ($env:VSTACK_HOOKS_MODE) { $env:VSTACK_HOOKS_MODE } else { 'audit' } if ($mode -ne 'enforce') { exit 0 } + $inputText = [Console]::In.ReadToEnd() + function Write-HookLog { + param([string]$Message) + $logName = if ($env:VSTACK_HOOK_LOG_NAME) { $env:VSTACK_HOOK_LOG_NAME } else { 'hook-security-alerts.log' } + $logRoot = if ($env:VSTACK_HOOK_LOG_DIR) { $env:VSTACK_HOOK_LOG_DIR } else { '.vstack/logs' } + $logDir = Join-Path $logRoot (Get-Date).ToUniversalTime().ToString('yyyyMMdd') + New-Item -ItemType Directory -Force -Path $logDir | Out-Null + Add-Content -Path (Join-Path $logDir $logName) -Value $Message + } try { $payload = $inputText | ConvertFrom-Json -ErrorAction Stop } catch { - Add-Content -Path $logPath -Value 'payload-parse-failed' + Write-HookLog 'payload-parse-failed' @{ permissionDecision = 'deny'; permissionDecisionReason = 'Blocked request because payload could not be validated in enforce mode' } | ConvertTo-Json -Compress exit 0 } if (-not $payload.toolName) { - Add-Content -Path $logPath -Value 'payload-parse-failed' + Write-HookLog 'payload-parse-failed' @{ permissionDecision = 'deny'; permissionDecisionReason = 'Blocked request because payload could not be validated in enforce mode' } | ConvertTo-Json -Compress exit 0 } if ($payload.toolName -eq 'bash' -and $inputText -match 'rm -rf /|mkfs|dd if=|DROP TABLE|TRUNCATE TABLE|git reset --hard|git clean -fdx|shutdown -h|shutdown now|reboot|poweroff|halt') { - Add-Content -Path $logPath -Value 'destructive-command-blocked' + Write-HookLog 'destructive-command-blocked' @{ permissionDecision = 'deny'; permissionDecisionReason = 'Blocked potentially destructive shell command by vstack pre-tool safety policy' } | ConvertTo-Json -Compress } cwd: "." diff --git a/src/vstack/_templates/hooks/session-audit/hook.yaml b/src/vstack/_templates/hooks/session-audit/hook.yaml index 8fa86317..88b6b520 100644 --- a/src/vstack/_templates/hooks/session-audit/hook.yaml +++ b/src/vstack/_templates/hooks/session-audit/hook.yaml @@ -1,4 +1,4 @@ -version: 20260511004 +version: 20260514002 metadata: name: session-audit @@ -21,6 +21,10 @@ hooks: description: | Centrally log tool call requests with size and estimated tokens. bash: | + script_start_ms="$(date +%s%3N 2>/dev/null || true)" + if [ -z "$script_start_ms" ]; then + script_start_ms="$(( $(date +%s) * 1000 ))" + fi input="$(cat)" log_level="${VSTACK_HOOKS_LOG_LEVEL:-minimal}" log_root="${VSTACK_HOOK_LOG_DIR:-.vstack/logs}" @@ -36,11 +40,63 @@ hooks: ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)" bytes="$(printf '%s' "$input" | wc -c | tr -d ' ')" est_tokens="$(( (bytes + 3) / 4 ))" - tool_name="$(printf '%s' "$input" | sed -n 's/.*"toolName"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + extract_json_string() { + key="$1" + printf '%s' "$input" | sed -n "s/.*\"${key}\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p" | head -n1 + } + tool_name="$(extract_json_string toolName)" + if [ -z "$tool_name" ]; then + tool_name="$(extract_json_string tool_name)" + fi + if [ -z "$tool_name" ]; then + tool_name="$(extract_json_string recipient_name)" + fi + if [ -z "$tool_name" ]; then + tool_name="$(printf '%s' "$input" | sed -n 's/.*"tool"[[:space:]]*:[[:space:]]*{[^}]*"name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + fi + if [ -z "$tool_name" ]; then + tool_name="$(printf '%s' "$input" | sed -n 's/.*"toolCall"[[:space:]]*:[[:space:]]*{[^}]*"name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + fi + if [ -z "$tool_name" ]; then + tool_name="$(printf '%s' "$input" | sed -n 's/.*"toolCall"[[:space:]]*:[[:space:]]*{[^}]*"toolName"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + fi + if [ -z "$tool_name" ]; then + tool_name="$(printf '%s' "$input" | sed -n 's/.*"tool_call"[[:space:]]*:[[:space:]]*{[^}]*"name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + fi + if [ -z "$tool_name" ]; then + tool_name="$(printf '%s' "$input" | sed -n 's/.*"tool_call"[[:space:]]*:[[:space:]]*{[^}]*"tool_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + fi + if [ -z "$tool_name" ]; then + tool_name="$(printf '%s' "$input" | grep -Eo '[A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*' | head -n1)" + fi if [ -z "$tool_name" ]; then tool_name="unknown" fi - printf '{"timestamp":"%s","event":"preToolUse","tool_name":"%s","size_bytes":%s,"estimated_tokens":%s}\n' "$ts" "$tool_name" "$bytes" "$est_tokens" >> "$log_dir/hook-tool-use.log" + tool_call_id="$(extract_json_string toolCallId)" + if [ -z "$tool_call_id" ]; then + tool_call_id="$(extract_json_string tool_call_id)" + fi + if [ -z "$tool_call_id" ]; then + tool_call_id="$(extract_json_string callId)" + fi + if [ -z "$tool_call_id" ]; then + tool_call_id="$(extract_json_string call_id)" + fi + if [ -z "$tool_call_id" ]; then + tool_call_id="$(printf '%s' "$input" | sed -n 's/.*"toolCall"[[:space:]]*:[[:space:]]*{[^}]*"id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + fi + if [ -z "$tool_call_id" ]; then + tool_call_id="$(printf '%s' "$input" | sed -n 's/.*"tool_call"[[:space:]]*:[[:space:]]*{[^}]*"id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + fi + if [ -z "$tool_call_id" ]; then + tool_call_id="unknown" + fi + script_end_ms="$(date +%s%3N 2>/dev/null || true)" + if [ -z "$script_end_ms" ]; then + script_end_ms="$(( $(date +%s) * 1000 ))" + fi + hook_execution_ms="$((script_end_ms - script_start_ms))" + printf '{"timestamp":"%s","event":"preToolUse","tool_name":"%s","tool_call_id":"%s","size_bytes":%s,"estimated_tokens":%s,"hook_execution_ms":%s}\n' "$ts" "$tool_name" "$tool_call_id" "$bytes" "$est_tokens" "$hook_execution_ms" >> "$log_dir/hook-tool-use.log" powershell: | $inputText = [Console]::In.ReadToEnd() $logLevel = if ($env:VSTACK_HOOKS_LOG_LEVEL) { $env:VSTACK_HOOKS_LOG_LEVEL } else { 'minimal' } @@ -54,30 +110,75 @@ hooks: Add-Content -Path (Join-Path $logDir 'hook-tool-use.log') -Value $inputText exit 0 } + $scriptStart = Get-Date + function Get-PathValue { + param([object]$Object, [string]$Path) + $current = $Object + foreach ($part in $Path.Split('.')) { + if ($null -eq $current) { + return $null + } + $prop = $current.PSObject.Properties[$part] + if ($null -eq $prop) { + return $null + } + $current = $prop.Value + } + return $current + } + function Get-FirstString { + param([object]$Object, [string[]]$Paths) + foreach ($path in $Paths) { + $value = Get-PathValue -Object $Object -Path $path + if ($null -ne $value -and [string]$value -ne '') { + return [string]$value + } + } + return $null + } $toolName = 'unknown' + $toolCallId = 'unknown' try { $payload = $inputText | ConvertFrom-Json -ErrorAction Stop - if ($payload.toolName) { - $toolName = [string]$payload.toolName + $toolNameCandidate = Get-FirstString -Object $payload -Paths @('toolName', 'tool_name', 'recipient_name', 'tool.name', 'toolCall.name', 'toolCall.toolName', 'tool_call.name', 'tool_call.tool_name') + if ($toolNameCandidate) { + $toolName = $toolNameCandidate + } + $toolCallIdCandidate = Get-FirstString -Object $payload -Paths @('toolCallId', 'tool_call_id', 'callId', 'call_id', 'toolCall.id', 'tool_call.id') + if ($toolCallIdCandidate) { + $toolCallId = $toolCallIdCandidate } } catch { } + if ($toolName -eq 'unknown') { + $toolNameMatch = [regex]::Match($inputText, '[A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*') + if ($toolNameMatch.Success) { + $toolName = $toolNameMatch.Value + } + } + $hookExecutionMs = [int][Math]::Max(0, ((Get-Date) - $scriptStart).TotalMilliseconds) $record = @{ timestamp = (Get-Date).ToUniversalTime().ToString('o') event = 'preToolUse' tool_name = $toolName + tool_call_id = $toolCallId size_bytes = $inputText.Length estimated_tokens = [int][math]::Ceiling($inputText.Length / 4.0) + hook_execution_ms = $hookExecutionMs } | ConvertTo-Json -Compress Add-Content -Path (Join-Path $logDir 'hook-tool-use.log') -Value $record cwd: "." - timeoutSec: 10 + timeoutSec: 5 postToolUse: - type: command description: | Centrally log tool responses with size and estimated tokens. bash: | + script_start_ms="$(date +%s%3N 2>/dev/null || true)" + if [ -z "$script_start_ms" ]; then + script_start_ms="$(( $(date +%s) * 1000 ))" + fi input="$(cat)" log_level="${VSTACK_HOOKS_LOG_LEVEL:-minimal}" log_root="${VSTACK_HOOK_LOG_DIR:-.vstack/logs}" @@ -93,11 +194,63 @@ hooks: ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)" bytes="$(printf '%s' "$input" | wc -c | tr -d ' ')" est_tokens="$(( (bytes + 3) / 4 ))" - tool_name="$(printf '%s' "$input" | sed -n 's/.*"toolName"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + extract_json_string() { + key="$1" + printf '%s' "$input" | sed -n "s/.*\"${key}\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p" | head -n1 + } + tool_name="$(extract_json_string toolName)" + if [ -z "$tool_name" ]; then + tool_name="$(extract_json_string tool_name)" + fi + if [ -z "$tool_name" ]; then + tool_name="$(extract_json_string recipient_name)" + fi + if [ -z "$tool_name" ]; then + tool_name="$(printf '%s' "$input" | sed -n 's/.*"tool"[[:space:]]*:[[:space:]]*{[^}]*"name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + fi + if [ -z "$tool_name" ]; then + tool_name="$(printf '%s' "$input" | sed -n 's/.*"toolCall"[[:space:]]*:[[:space:]]*{[^}]*"name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + fi + if [ -z "$tool_name" ]; then + tool_name="$(printf '%s' "$input" | sed -n 's/.*"toolCall"[[:space:]]*:[[:space:]]*{[^}]*"toolName"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + fi + if [ -z "$tool_name" ]; then + tool_name="$(printf '%s' "$input" | sed -n 's/.*"tool_call"[[:space:]]*:[[:space:]]*{[^}]*"name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + fi + if [ -z "$tool_name" ]; then + tool_name="$(printf '%s' "$input" | sed -n 's/.*"tool_call"[[:space:]]*:[[:space:]]*{[^}]*"tool_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + fi + if [ -z "$tool_name" ]; then + tool_name="$(printf '%s' "$input" | grep -Eo '[A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*' | head -n1)" + fi if [ -z "$tool_name" ]; then tool_name="unknown" fi - printf '{"timestamp":"%s","event":"postToolUse","tool_name":"%s","size_bytes":%s,"estimated_tokens":%s}\n' "$ts" "$tool_name" "$bytes" "$est_tokens" >> "$log_dir/hook-tool-use.log" + tool_call_id="$(extract_json_string toolCallId)" + if [ -z "$tool_call_id" ]; then + tool_call_id="$(extract_json_string tool_call_id)" + fi + if [ -z "$tool_call_id" ]; then + tool_call_id="$(extract_json_string callId)" + fi + if [ -z "$tool_call_id" ]; then + tool_call_id="$(extract_json_string call_id)" + fi + if [ -z "$tool_call_id" ]; then + tool_call_id="$(printf '%s' "$input" | sed -n 's/.*"toolCall"[[:space:]]*:[[:space:]]*{[^}]*"id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + fi + if [ -z "$tool_call_id" ]; then + tool_call_id="$(printf '%s' "$input" | sed -n 's/.*"tool_call"[[:space:]]*:[[:space:]]*{[^}]*"id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + fi + if [ -z "$tool_call_id" ]; then + tool_call_id="unknown" + fi + script_end_ms="$(date +%s%3N 2>/dev/null || true)" + if [ -z "$script_end_ms" ]; then + script_end_ms="$(( $(date +%s) * 1000 ))" + fi + hook_execution_ms="$((script_end_ms - script_start_ms))" + printf '{"timestamp":"%s","event":"postToolUse","tool_name":"%s","tool_call_id":"%s","size_bytes":%s,"estimated_tokens":%s,"hook_execution_ms":%s}\n' "$ts" "$tool_name" "$tool_call_id" "$bytes" "$est_tokens" "$hook_execution_ms" >> "$log_dir/hook-tool-use.log" powershell: | $inputText = [Console]::In.ReadToEnd() $logLevel = if ($env:VSTACK_HOOKS_LOG_LEVEL) { $env:VSTACK_HOOKS_LOG_LEVEL } else { 'minimal' } @@ -111,30 +264,75 @@ hooks: Add-Content -Path (Join-Path $logDir 'hook-tool-use.log') -Value $inputText exit 0 } + $scriptStart = Get-Date + function Get-PathValue { + param([object]$Object, [string]$Path) + $current = $Object + foreach ($part in $Path.Split('.')) { + if ($null -eq $current) { + return $null + } + $prop = $current.PSObject.Properties[$part] + if ($null -eq $prop) { + return $null + } + $current = $prop.Value + } + return $current + } + function Get-FirstString { + param([object]$Object, [string[]]$Paths) + foreach ($path in $Paths) { + $value = Get-PathValue -Object $Object -Path $path + if ($null -ne $value -and [string]$value -ne '') { + return [string]$value + } + } + return $null + } $toolName = 'unknown' + $toolCallId = 'unknown' try { $payload = $inputText | ConvertFrom-Json -ErrorAction Stop - if ($payload.toolName) { - $toolName = [string]$payload.toolName + $toolNameCandidate = Get-FirstString -Object $payload -Paths @('toolName', 'tool_name', 'recipient_name', 'tool.name', 'toolCall.name', 'toolCall.toolName', 'tool_call.name', 'tool_call.tool_name') + if ($toolNameCandidate) { + $toolName = $toolNameCandidate + } + $toolCallIdCandidate = Get-FirstString -Object $payload -Paths @('toolCallId', 'tool_call_id', 'callId', 'call_id', 'toolCall.id', 'tool_call.id') + if ($toolCallIdCandidate) { + $toolCallId = $toolCallIdCandidate } } catch { } + if ($toolName -eq 'unknown') { + $toolNameMatch = [regex]::Match($inputText, '[A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*') + if ($toolNameMatch.Success) { + $toolName = $toolNameMatch.Value + } + } + $hookExecutionMs = [int][Math]::Max(0, ((Get-Date) - $scriptStart).TotalMilliseconds) $record = @{ timestamp = (Get-Date).ToUniversalTime().ToString('o') event = 'postToolUse' tool_name = $toolName + tool_call_id = $toolCallId size_bytes = $inputText.Length estimated_tokens = [int][math]::Ceiling($inputText.Length / 4.0) + hook_execution_ms = $hookExecutionMs } | ConvertTo-Json -Compress Add-Content -Path (Join-Path $logDir 'hook-tool-use.log') -Value $record cwd: "." - timeoutSec: 10 + timeoutSec: 5 userPromptSubmitted: - type: command description: | Centrally log prompt events with configurable verbosity. bash: | + script_start_ms="$(date +%s%3N 2>/dev/null || true)" + if [ -z "$script_start_ms" ]; then + script_start_ms="$(( $(date +%s) * 1000 ))" + fi input="$(cat)" log_level="${VSTACK_HOOKS_LOG_LEVEL:-minimal}" log_root="${VSTACK_HOOK_LOG_DIR:-.vstack/logs}" @@ -150,7 +348,36 @@ hooks: ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)" bytes="$(printf '%s' "$input" | wc -c | tr -d ' ')" est_tokens="$(( (bytes + 3) / 4 ))" - printf '{"timestamp":"%s","event":"userPromptSubmitted","size_bytes":%s,"estimated_tokens":%s}\n' "$ts" "$bytes" "$est_tokens" >> "$log_dir/hook-user-prompt.log" + extract_json_string() { + key="$1" + printf '%s' "$input" | sed -n "s/.*\"${key}\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p" | head -n1 + } + prompt_text="$(extract_json_string prompt)" + if [ -z "$prompt_text" ]; then + prompt_text="$(extract_json_string userPrompt)" + fi + if [ -z "$prompt_text" ]; then + prompt_text="$(extract_json_string user_prompt)" + fi + if [ -z "$prompt_text" ]; then + prompt_text="$(extract_json_string text)" + fi + if [ -z "$prompt_text" ]; then + prompt_text="$(printf '%s' "$input" | sed -n 's/.*"prompt"[[:space:]]*:[[:space:]]*{[^}]*"text"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" + fi + slash_command="none" + if [ -n "$prompt_text" ]; then + slash_command_candidate="$(printf '%s' "$prompt_text" | sed -n 's#^/\([^[:space:]]*\).*$#/\1#p' | head -n1)" + if [ -n "$slash_command_candidate" ]; then + slash_command="$slash_command_candidate" + fi + fi + script_end_ms="$(date +%s%3N 2>/dev/null || true)" + if [ -z "$script_end_ms" ]; then + script_end_ms="$(( $(date +%s) * 1000 ))" + fi + hook_execution_ms="$((script_end_ms - script_start_ms))" + printf '{"timestamp":"%s","event":"userPromptSubmitted","slash_command":"%s","size_bytes":%s,"estimated_tokens":%s,"hook_execution_ms":%s}\n' "$ts" "$slash_command" "$bytes" "$est_tokens" "$hook_execution_ms" >> "$log_dir/hook-user-prompt.log" powershell: | $inputText = [Console]::In.ReadToEnd() $logLevel = if ($env:VSTACK_HOOKS_LOG_LEVEL) { $env:VSTACK_HOOKS_LOG_LEVEL } else { 'minimal' } @@ -164,16 +391,56 @@ hooks: Add-Content -Path (Join-Path $logDir 'hook-user-prompt.log') -Value $inputText exit 0 } - $record = @{ timestamp = (Get-Date).ToUniversalTime().ToString('o'); event = 'userPromptSubmitted'; size_bytes = $inputText.Length; estimated_tokens = [int][math]::Ceiling($inputText.Length / 4.0) } | ConvertTo-Json -Compress + $scriptStart = Get-Date + function Get-PathValue { + param([object]$Object, [string]$Path) + $current = $Object + foreach ($part in $Path.Split('.')) { + if ($null -eq $current) { + return $null + } + $prop = $current.PSObject.Properties[$part] + if ($null -eq $prop) { + return $null + } + $current = $prop.Value + } + return $current + } + function Get-FirstString { + param([object]$Object, [string[]]$Paths) + foreach ($path in $Paths) { + $value = Get-PathValue -Object $Object -Path $path + if ($null -ne $value -and [string]$value -ne '') { + return [string]$value + } + } + return $null + } + $slashCommand = 'none' + try { + $payload = $inputText | ConvertFrom-Json -ErrorAction Stop + $promptText = Get-FirstString -Object $payload -Paths @('prompt', 'userPrompt', 'user_prompt', 'text', 'message.text', 'prompt.text') + if ($promptText -and $promptText -match '^/([^\s]+)') { + $slashCommand = "/$($Matches[1])" + } + } catch { + } + $hookExecutionMs = [int][Math]::Max(0, ((Get-Date) - $scriptStart).TotalMilliseconds) + $record = @{ timestamp = (Get-Date).ToUniversalTime().ToString('o'); event = 'userPromptSubmitted'; slash_command = $slashCommand; size_bytes = $inputText.Length; estimated_tokens = [int][math]::Ceiling($inputText.Length / 4.0); hook_execution_ms = $hookExecutionMs } | ConvertTo-Json -Compress Add-Content -Path (Join-Path $logDir 'hook-user-prompt.log') -Value $record cwd: "." - timeoutSec: 10 + timeoutSec: 5 sessionStart: - type: command description: | Centrally log session start with configurable verbosity. bash: | + script_start_ms="$(date +%s%3N 2>/dev/null || true)" + if [ -z "$script_start_ms" ]; then + script_start_ms="$(( $(date +%s) * 1000 ))" + fi input="$(cat)" log_level="${VSTACK_HOOKS_LOG_LEVEL:-minimal}" log_root="${VSTACK_HOOK_LOG_DIR:-.vstack/logs}" @@ -189,7 +456,12 @@ hooks: ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)" bytes="$(printf '%s' "$input" | wc -c | tr -d ' ')" est_tokens="$(( (bytes + 3) / 4 ))" - printf '{"timestamp":"%s","event":"sessionStart","size_bytes":%s,"estimated_tokens":%s}\n' "$ts" "$bytes" "$est_tokens" >> "$log_dir/hook-session-start.log" + script_end_ms="$(date +%s%3N 2>/dev/null || true)" + if [ -z "$script_end_ms" ]; then + script_end_ms="$(( $(date +%s) * 1000 ))" + fi + hook_execution_ms="$((script_end_ms - script_start_ms))" + printf '{"timestamp":"%s","event":"sessionStart","size_bytes":%s,"estimated_tokens":%s,"hook_execution_ms":%s}\n' "$ts" "$bytes" "$est_tokens" "$hook_execution_ms" >> "$log_dir/hook-session-start.log" powershell: | $inputText = [Console]::In.ReadToEnd() $logLevel = if ($env:VSTACK_HOOKS_LOG_LEVEL) { $env:VSTACK_HOOKS_LOG_LEVEL } else { 'minimal' } @@ -203,16 +475,22 @@ hooks: Add-Content -Path (Join-Path $logDir 'hook-session-start.log') -Value $inputText exit 0 } - $record = @{ timestamp = (Get-Date).ToUniversalTime().ToString('o'); event = 'sessionStart'; size_bytes = $inputText.Length; estimated_tokens = [int][math]::Ceiling($inputText.Length / 4.0) } | ConvertTo-Json -Compress + $scriptStart = Get-Date + $hookExecutionMs = [int][Math]::Max(0, ((Get-Date) - $scriptStart).TotalMilliseconds) + $record = @{ timestamp = (Get-Date).ToUniversalTime().ToString('o'); event = 'sessionStart'; size_bytes = $inputText.Length; estimated_tokens = [int][math]::Ceiling($inputText.Length / 4.0); hook_execution_ms = $hookExecutionMs } | ConvertTo-Json -Compress Add-Content -Path (Join-Path $logDir 'hook-session-start.log') -Value $record cwd: "." - timeoutSec: 10 + timeoutSec: 5 sessionEnd: - type: command description: | Centrally log session end for correlation with start. bash: | + script_start_ms="$(date +%s%3N 2>/dev/null || true)" + if [ -z "$script_start_ms" ]; then + script_start_ms="$(( $(date +%s) * 1000 ))" + fi input="$(cat)" log_level="${VSTACK_HOOKS_LOG_LEVEL:-minimal}" log_root="${VSTACK_HOOK_LOG_DIR:-.vstack/logs}" @@ -228,7 +506,12 @@ hooks: ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)" bytes="$(printf '%s' "$input" | wc -c | tr -d ' ')" est_tokens="$(( (bytes + 3) / 4 ))" - printf '{"timestamp":"%s","event":"sessionEnd","size_bytes":%s,"estimated_tokens":%s}\n' "$ts" "$bytes" "$est_tokens" >> "$log_dir/hook-session-end.log" + script_end_ms="$(date +%s%3N 2>/dev/null || true)" + if [ -z "$script_end_ms" ]; then + script_end_ms="$(( $(date +%s) * 1000 ))" + fi + hook_execution_ms="$((script_end_ms - script_start_ms))" + printf '{"timestamp":"%s","event":"sessionEnd","size_bytes":%s,"estimated_tokens":%s,"hook_execution_ms":%s}\n' "$ts" "$bytes" "$est_tokens" "$hook_execution_ms" >> "$log_dir/hook-session-end.log" powershell: | $inputText = [Console]::In.ReadToEnd() $logLevel = if ($env:VSTACK_HOOKS_LOG_LEVEL) { $env:VSTACK_HOOKS_LOG_LEVEL } else { 'minimal' } @@ -242,7 +525,9 @@ hooks: Add-Content -Path (Join-Path $logDir 'hook-session-end.log') -Value $inputText exit 0 } - $record = @{ timestamp = (Get-Date).ToUniversalTime().ToString('o'); event = 'sessionEnd'; size_bytes = $inputText.Length; estimated_tokens = [int][math]::Ceiling($inputText.Length / 4.0) } | ConvertTo-Json -Compress + $scriptStart = Get-Date + $hookExecutionMs = [int][Math]::Max(0, ((Get-Date) - $scriptStart).TotalMilliseconds) + $record = @{ timestamp = (Get-Date).ToUniversalTime().ToString('o'); event = 'sessionEnd'; size_bytes = $inputText.Length; estimated_tokens = [int][math]::Ceiling($inputText.Length / 4.0); hook_execution_ms = $hookExecutionMs } | ConvertTo-Json -Compress Add-Content -Path (Join-Path $logDir 'hook-session-end.log') -Value $record cwd: "." - timeoutSec: 10 + timeoutSec: 5 diff --git a/src/vstack/_templates/prompts/artifact-integrity/config.yaml b/src/vstack/_templates/prompts/artifact-integrity/config.yaml new file mode 100644 index 00000000..bf2b4c84 --- /dev/null +++ b/src/vstack/_templates/prompts/artifact-integrity/config.yaml @@ -0,0 +1,9 @@ +name: artifact-integrity +description: Check source templates against generated artifacts and identify drift or missing regeneration. +argument-hint: "[artifact type, path, or full repo]" +agent: tester +model: GPT-5.3-Codex (copilot) +tools: + - read + - search +version: 20260513002 diff --git a/src/vstack/_templates/prompts/artifact-integrity/template.md b/src/vstack/_templates/prompts/artifact-integrity/template.md new file mode 100644 index 00000000..e0e98f24 --- /dev/null +++ b/src/vstack/_templates/prompts/artifact-integrity/template.md @@ -0,0 +1,46 @@ +Check source templates against generated artifacts and identify drift or missing regeneration. + +Compare `src/vstack/_templates/` sources against their installed counterparts in `.github/` +and the manifest at `.vstack/vstack.json`. Surface any mismatch, stale output, or gap. + +Output exactly in this format: + +## Drift Findings + +List every source-to-artifact mismatch detected. + +For each item: + +- source template: path under `src/vstack/_templates/` +- generated artifact: expected path under `.github/` +- drift type: MISSING | STALE | CHECKSUM-MISMATCH | UNTRACKED +- detail: one sentence describing the discrepancy + +## Regeneration Actions + +List the exact commands needed to bring generated artifacts back into sync. + +For each action: + +- command: the shell command to run (e.g. `python3 -m vstack install`) +- scope: which artifact types or names this command covers +- priority: CRITICAL | HIGH | LOW + +## Risk If Unfixed + +Describe the production risk if drift is left unresolved. + +For each drift item from above: + +- artifact affected +- risk: what could fail or mislead if the stale artifact ships + +## Verification Steps + +Provide a checklist to confirm the repository is back in a clean state after regeneration. + +- [ ] `python3 -m vstack install` completes without errors +- [ ] `vstack validate` reports no unresolved template tokens +- [ ] `make test-local` passes with 100% coverage +- [ ] `make markdown-format` reports no changes +- [ ] All regenerated files match their source checksums in `.vstack/vstack.json` diff --git a/src/vstack/_templates/prompts/ci-triage/config.yaml b/src/vstack/_templates/prompts/ci-triage/config.yaml new file mode 100644 index 00000000..3df48e0f --- /dev/null +++ b/src/vstack/_templates/prompts/ci-triage/config.yaml @@ -0,0 +1,9 @@ +name: ci-triage +description: Triage CI failures into root-cause clusters and prioritize the fastest safe recovery path. +argument-hint: "[workflow run, failing job, or log scope]" +agent: engineer +model: GPT-5.3-Codex (copilot) +tools: + - read + - search +version: 20260513006 diff --git a/src/vstack/_templates/prompts/ci-triage/template.md b/src/vstack/_templates/prompts/ci-triage/template.md new file mode 100644 index 00000000..fb2cdc61 --- /dev/null +++ b/src/vstack/_templates/prompts/ci-triage/template.md @@ -0,0 +1,33 @@ +Triage CI failures into root-cause clusters and prioritize the fastest safe recovery path. + +Use failing job logs, workflow config, and changed files. + +Output exactly in this format: + +## Failure Clusters + +For each cluster: + +- jobs affected +- probable root cause +- confidence: HIGH | MEDIUM | LOW + +## Priority Fix Order + +Ordered by unblock value. + +- fix action +- expected unblocked jobs +- owner role + +## Risk Notes + +- risky quick fixes to avoid +- possible hidden regressions + +## Recovery Checklist + +- [ ] apply highest-priority fix +- [ ] rerun targeted jobs +- [ ] rerun full workflow +- [ ] confirm no new failures diff --git a/src/vstack/_templates/prompts/migration-plan/config.yaml b/src/vstack/_templates/prompts/migration-plan/config.yaml new file mode 100644 index 00000000..fb398cfb --- /dev/null +++ b/src/vstack/_templates/prompts/migration-plan/config.yaml @@ -0,0 +1,9 @@ +name: migration-plan +description: Produce a safe migration plan with sequencing, fallback paths, and verification checkpoints. +argument-hint: "[migration scope or affected components]" +agent: engineer +model: GPT-5.3-Codex (copilot) +tools: + - read + - search +version: 20260513010 diff --git a/src/vstack/_templates/prompts/migration-plan/template.md b/src/vstack/_templates/prompts/migration-plan/template.md new file mode 100644 index 00000000..081bfc00 --- /dev/null +++ b/src/vstack/_templates/prompts/migration-plan/template.md @@ -0,0 +1,33 @@ +Produce a safe migration plan with sequencing, fallback paths, and verification checkpoints. + +Use source changes, schema/contracts, and deployment constraints. + +Output exactly in this format: + +## Migration Overview + +- scope +- dependencies +- compatibility strategy + +## Execution Plan + +For each phase: + +- phase +- changes applied +- validation checkpoint + +## Rollback Plan + +For each phase: + +- rollback trigger +- rollback steps +- data integrity check + +## Post-Migration Validation + +- required tests +- smoke checks +- success criteria diff --git a/src/vstack/_templates/prompts/ops-readiness/config.yaml b/src/vstack/_templates/prompts/ops-readiness/config.yaml new file mode 100644 index 00000000..7f0c6e2f --- /dev/null +++ b/src/vstack/_templates/prompts/ops-readiness/config.yaml @@ -0,0 +1,9 @@ +name: ops-readiness +description: Assess operational readiness across observability, runbooks, failure handling, and supportability. +argument-hint: "[service, component, or release scope]" +agent: tester +model: GPT-5.3-Codex (copilot) +tools: + - read + - search +version: 20260513009 diff --git a/src/vstack/_templates/prompts/ops-readiness/template.md b/src/vstack/_templates/prompts/ops-readiness/template.md new file mode 100644 index 00000000..b21f5f97 --- /dev/null +++ b/src/vstack/_templates/prompts/ops-readiness/template.md @@ -0,0 +1,32 @@ +Assess operational readiness across observability, runbooks, failure handling, and supportability. + +Use evidence from code, tests, docs, and runbooks. + +Output exactly in this format: + +## Ops Readiness Scorecard + +- observability: READY | PARTIAL | MISSING +- alerting: READY | PARTIAL | MISSING +- runbooks: READY | PARTIAL | MISSING +- failure handling: READY | PARTIAL | MISSING + +## Gaps + +For each gap: + +- area +- finding +- risk if unresolved +- owner role + +## Must-Fix Before Release + +- blocker +- required evidence + +## Follow-up Actions + +- action +- owner role +- target milestone diff --git a/src/vstack/_templates/prompts/release-check/config.yaml b/src/vstack/_templates/prompts/release-check/config.yaml new file mode 100644 index 00000000..435f8c6b --- /dev/null +++ b/src/vstack/_templates/prompts/release-check/config.yaml @@ -0,0 +1,9 @@ +name: release-check +description: Evaluate release gate readiness using required reports, artifacts, and sign-off evidence. +argument-hint: "[release scope or target branch]" +agent: release +model: GPT-5.3-Codex (copilot) +tools: + - read + - search +version: 20260513004 diff --git a/src/vstack/_templates/prompts/release-check/template.md b/src/vstack/_templates/prompts/release-check/template.md new file mode 100644 index 00000000..f75a94a1 --- /dev/null +++ b/src/vstack/_templates/prompts/release-check/template.md @@ -0,0 +1,36 @@ +Evaluate release gate readiness using required reports, artifacts, and sign-off evidence. + +Use repository evidence only. + +Output exactly in this format: + +## Release Gate Status + +- overall status: READY | NOT-READY +- scope reviewed +- evidence sources checked + +## Missing or Stale Evidence + +For each item: + +- artifact +- issue: MISSING | STALE | INCOMPLETE +- impact +- owner role + +## Sign-off Gaps + +For each required role: + +- role +- verdict: OK | NOK | NOT-RECORDED +- blocking reason + +## Release Actions + +Ordered actions to reach READY. + +- action +- owner role +- verification step diff --git a/src/vstack/_templates/prompts/release-readiness/config.yaml b/src/vstack/_templates/prompts/release-readiness/config.yaml deleted file mode 100644 index 99317cce..00000000 --- a/src/vstack/_templates/prompts/release-readiness/config.yaml +++ /dev/null @@ -1,9 +0,0 @@ -name: release-readiness -description: Evaluate release readiness from reports, risks, and unresolved blockers. -argument-hint: "[scope, release date, or branch]" -agent: release -model: GPT-5.3-Codex (copilot) -tools: - - read - - search -version: 20260502012 diff --git a/src/vstack/_templates/prompts/release-readiness/template.md b/src/vstack/_templates/prompts/release-readiness/template.md deleted file mode 100644 index b70da7a7..00000000 --- a/src/vstack/_templates/prompts/release-readiness/template.md +++ /dev/null @@ -1,36 +0,0 @@ -Assess whether this change set is ready to release. - -Review product, architecture, design, test, security, and performance evidence. -Prefer evidence-based findings tied to concrete artifacts. - -Output exactly in this format: - -## Release Gate Verdict - -- Verdict: READY | READY-WITH-CONDITIONS | NOT-READY -- Confidence: high | medium | low -- Scope assessed: one sentence - -## Blocking Issues - -List only release-blocking items. - -For each item: - -- artifact or file reference -- why this blocks release in one sentence -- concrete unblock action -- owner role (product | architect | designer | engineer | tester | release) - -## Conditions Before Release - -List non-blocking but mandatory follow-ups to ship safely. - -## Evidence Reviewed - -List the exact artifacts checked (reports, docs, CI evidence, manifests). -For each expected artifact that is missing, flag it explicitly as: MISSING — [artifact name]. - -## Recommended Next Action - -One clear next step for the team. diff --git a/src/vstack/_templates/prompts/repo-assessment/config.yaml b/src/vstack/_templates/prompts/repo-assessment/config.yaml new file mode 100644 index 00000000..feb1eb7c --- /dev/null +++ b/src/vstack/_templates/prompts/repo-assessment/config.yaml @@ -0,0 +1,9 @@ +name: repo-assessment +description: Assess a repository for production-readiness gaps and prioritized improvements. +argument-hint: "[repository scope or component path]" +agent: engineer +model: GPT-5.3-Codex (copilot) +tools: + - read + - search +version: 20260513001 diff --git a/src/vstack/_templates/prompts/repo-assessment/template.md b/src/vstack/_templates/prompts/repo-assessment/template.md new file mode 100644 index 00000000..e7c6012d --- /dev/null +++ b/src/vstack/_templates/prompts/repo-assessment/template.md @@ -0,0 +1,46 @@ +Assess this repository for production-readiness gaps and prioritized improvement opportunities. + +Focus on evidence found in source files, tests, CI configuration, docs, and manifests. +Prefer concrete findings over speculative risks. + +Output exactly in this format: + +## Critical Risks + +List only issues that block safe production operation right now. + +For each item: + +- location: file or component reference +- risk: one sentence describing the production impact +- remediation: concrete action with owner role + +## High-ROI Improvements + +List high-value improvements that reduce operational risk or developer friction. +Rank by impact relative to effort. + +For each item: + +- area: the domain (security | reliability | observability | dx | performance | maintainability) +- finding: what is missing or suboptimal +- suggested action: one clear improvement step + +## Testing and Verification Gaps + +List behavioral paths that are untested or where test coverage provides false confidence. + +For each item: + +- uncovered behavior: what scenario is missing +- risk if untested: what could go wrong in production +- suggested test type: unit | integration | contract | e2e + +## Suggested Next Sprint Backlog + +Provide a short, actionable list of tasks suitable for the next sprint. +Order from highest to lowest priority. + +- task title +- owner role (product | architect | designer | engineer | tester | release) +- one-line rationale diff --git a/src/vstack/_templates/prompts/template-impact/config.yaml b/src/vstack/_templates/prompts/template-impact/config.yaml new file mode 100644 index 00000000..24521fd6 --- /dev/null +++ b/src/vstack/_templates/prompts/template-impact/config.yaml @@ -0,0 +1,9 @@ +name: template-impact +description: Assess impact of a template change on generated artifacts, tests, and release risk. +argument-hint: "[template path or change scope]" +agent: engineer +model: GPT-5.3-Codex (copilot) +tools: + - read + - search +version: 20260513005 diff --git a/src/vstack/_templates/prompts/template-impact/template.md b/src/vstack/_templates/prompts/template-impact/template.md new file mode 100644 index 00000000..bb61e8a0 --- /dev/null +++ b/src/vstack/_templates/prompts/template-impact/template.md @@ -0,0 +1,33 @@ +Assess impact of a template change on generated artifacts, tests, and release risk. + +Use source-template and generated-artifact evidence. + +Output exactly in this format: + +## Change Surface + +- template scope +- affected artifact types +- likely generated paths + +## Impact Findings + +For each finding: + +- impacted path +- impact type: BEHAVIOR | DOCS | TESTS | TOOLING +- risk +- required validation + +## Regression Risk + +List highest-risk regressions first. + +- scenario +- likelihood: HIGH | MEDIUM | LOW +- mitigation + +## Verification Plan + +- commands to run +- expected pass criteria diff --git a/src/vstack/_templates/prompts/test-gaps/config.yaml b/src/vstack/_templates/prompts/test-gaps/config.yaml new file mode 100644 index 00000000..830e2e06 --- /dev/null +++ b/src/vstack/_templates/prompts/test-gaps/config.yaml @@ -0,0 +1,9 @@ +name: test-gaps +description: Identify missing behavioral coverage and prioritize test additions by production risk. +argument-hint: "[component, feature, or repository scope]" +agent: tester +model: GPT-5.3-Codex (copilot) +tools: + - read + - search +version: 20260513007 diff --git a/src/vstack/_templates/prompts/test-gaps/template.md b/src/vstack/_templates/prompts/test-gaps/template.md new file mode 100644 index 00000000..7ec9afa6 --- /dev/null +++ b/src/vstack/_templates/prompts/test-gaps/template.md @@ -0,0 +1,30 @@ +Identify missing behavioral coverage and prioritize test additions by production risk. + +Focus on observable behavior and realistic failure paths. + +Output exactly in this format: + +## Coverage Gaps + +For each gap: + +- behavior +- current coverage weakness +- production risk +- suggested test type: unit | integration | contract | e2e + +## False Confidence Risks + +List tests that pass but may not protect real behavior. + +- location +- why confidence is false +- correction + +## Priority Test Backlog + +Ordered from highest to lowest risk reduction. + +- test title +- owner role +- acceptance check diff --git a/src/vstack/_templates/prompts/upgrade-plan/config.yaml b/src/vstack/_templates/prompts/upgrade-plan/config.yaml new file mode 100644 index 00000000..7975be51 --- /dev/null +++ b/src/vstack/_templates/prompts/upgrade-plan/config.yaml @@ -0,0 +1,9 @@ +name: upgrade-plan +description: Build a safe upgrade plan with sequencing, compatibility checks, and rollback points. +argument-hint: "[target version, component, or full repo]" +agent: tester +model: GPT-5.3-Codex (copilot) +tools: + - read + - search +version: 20260513008 diff --git a/src/vstack/_templates/prompts/upgrade-plan/template.md b/src/vstack/_templates/prompts/upgrade-plan/template.md new file mode 100644 index 00000000..e2ff2f06 --- /dev/null +++ b/src/vstack/_templates/prompts/upgrade-plan/template.md @@ -0,0 +1,33 @@ +Build a safe upgrade plan with sequencing, compatibility checks, and rollback points. + +Use repository docs, manifests, and test constraints. + +Output exactly in this format: + +## Upgrade Scope + +- target +- baseline version +- compatibility boundaries + +## Step Plan + +Ordered upgrade steps. + +- step +- dependency/precondition +- success check + +## Risk and Rollback + +For each major step: + +- risk +- trigger to rollback +- rollback action + +## Verification Matrix + +- check +- command +- pass criteria diff --git a/src/vstack/_templates/prompts/workflow-check/config.yaml b/src/vstack/_templates/prompts/workflow-check/config.yaml new file mode 100644 index 00000000..74e7af23 --- /dev/null +++ b/src/vstack/_templates/prompts/workflow-check/config.yaml @@ -0,0 +1,9 @@ +name: workflow-check +description: Review workflow stage flow, gate usage, and handoff integrity across role artifacts. +argument-hint: "[workflow scope or stage list]" +agent: planner +model: GPT-5.3-Codex (copilot) +tools: + - read + - search +version: 20260513003 diff --git a/src/vstack/_templates/prompts/workflow-check/template.md b/src/vstack/_templates/prompts/workflow-check/template.md new file mode 100644 index 00000000..151a770e --- /dev/null +++ b/src/vstack/_templates/prompts/workflow-check/template.md @@ -0,0 +1,36 @@ +Review workflow stage flow, gate usage, and handoff integrity across role artifacts. + +Focus on evidence in workflow config, role templates, and docs. + +Output exactly in this format: + +## Flow Summary + +- workflow scope +- current stage order +- gate model used + +## Gate and Handoff Findings + +For each finding: + +- location: file path and section +- issue: one sentence +- impact: what can fail in execution +- fix: concrete correction + +## Blocking Risks + +List only issues that block reliable workflow execution. + +- blocker +- owner role +- required action + +## Recommended Next Steps + +Provide an ordered short list of actions. + +- action +- owner role +- expected result diff --git a/src/vstack/_templates/skills/copilot-ops/config.yaml b/src/vstack/_templates/skills/copilot-ops/config.yaml new file mode 100644 index 00000000..daacf0e0 --- /dev/null +++ b/src/vstack/_templates/skills/copilot-ops/config.yaml @@ -0,0 +1,15 @@ +name: copilot-ops +version: 20260513012 +description: | + Operate and govern GitHub Copilot settings with an audit-first workflow. + Covers policy checks, configuration drift, usage visibility, and safe change rollout. +allowed-tools: 'execute read search' +argument-hint: '[org/repo scope or ops objective]' + +license: MIT +compatibility: Requires admin-level GitHub permissions and gh CLI authentication for governance operations. +metadata: + owner: vstack + maturity: candidate +user-invocable: true +disable-model-invocation: false diff --git a/src/vstack/_templates/skills/copilot-ops/template.md b/src/vstack/_templates/skills/copilot-ops/template.md new file mode 100644 index 00000000..ea9f8ff4 --- /dev/null +++ b/src/vstack/_templates/skills/copilot-ops/template.md @@ -0,0 +1,54 @@ +{{SKILL_CONTEXT}} + +# copilot-ops - Copilot Governance Operations + +Run Copilot governance changes safely with evidence, rollback intent, and verification. + +## When to use + +- Audit Copilot governance settings before release or compliance review +- Apply policy updates for repository or organization scope +- Investigate configuration drift between expected and actual Copilot controls + +## Procedure + +1. Capture current scope (repo/org/enterprise) and required permissions. +1. Pull current Copilot-relevant settings and record baseline evidence. +1. Compare baseline with expected policy and identify drift. +1. Propose minimal changes with explicit risk notes. +1. Apply approved changes using audited commands/workflows. +1. Re-read settings and confirm effective state. +1. Log follow-up checks and ownership. + +## Output format + +Provide this structure: + +### Baseline + +- scope reviewed +- settings checked +- evidence source + +### Drift Findings + +- setting +- expected value +- current value +- risk + +### Change Plan + +- proposed change +- approval needed +- rollback note + +### Verification + +- post-change check +- result +- residual risk + +## Escalation + +Escalate when permissions are insufficient, settings conflict across scopes, or policy intent is ambiguous. diff --git a/src/vstack/_templates/skills/space-setup/config.yaml b/src/vstack/_templates/skills/space-setup/config.yaml new file mode 100644 index 00000000..a7af4db7 --- /dev/null +++ b/src/vstack/_templates/skills/space-setup/config.yaml @@ -0,0 +1,15 @@ +name: space-setup +version: 20260513011 +description: | + Set up and maintain a GitHub Copilot Space for a repository. Covers scope + selection, source curation, refresh cadence, and context quality checks. +allowed-tools: 'execute read search' +argument-hint: '[repo scope, docs set, or space objective]' + +license: MIT +compatibility: Requires GitHub Copilot Spaces access in the target organization/repository. +metadata: + owner: vstack + maturity: candidate +user-invocable: true +disable-model-invocation: false diff --git a/src/vstack/_templates/skills/space-setup/template.md b/src/vstack/_templates/skills/space-setup/template.md new file mode 100644 index 00000000..ee1211f3 --- /dev/null +++ b/src/vstack/_templates/skills/space-setup/template.md @@ -0,0 +1,54 @@ +{{SKILL_CONTEXT}} + +# space-setup - Copilot Space Setup and Maintenance + +Set up a project Space that keeps Copilot context focused, current, and easy to audit. + +## When to use + +- New repository onboarding to Copilot Spaces +- Space quality cleanup after major docs or architecture updates +- Regular context refresh before a release cycle + +## Procedure + +1. Define Space objective and audience. +1. Select core sources: requirements, architecture, design, README, and key ADRs. +1. Exclude noisy/generated paths and duplicate docs. +1. Create or update the Space using GitHub UI (or approved API workflow). +1. Validate discoverability: each key topic maps to at least one source document. +1. Record refresh cadence and owner. +1. Re-check after `vstack install` or release docs updates. + +## Output format + +Provide this structure: + +### Space Scope + +- objective +- audience +- included sources +- excluded sources + +### Setup Actions + +- action taken +- rationale +- owner + +### Quality Findings + +- missing context +- stale context +- duplicate/noisy context + +### Maintenance Plan + +- refresh trigger +- cadence +- owner + +## Escalation + +Escalate when required docs are missing, stale, or inconsistent across product/architecture/design baselines. diff --git a/src/vstack/agents/generator.py b/src/vstack/agents/generator.py index 162eaaa6..eea74fc7 100644 --- a/src/vstack/agents/generator.py +++ b/src/vstack/agents/generator.py @@ -51,6 +51,7 @@ from vstack.agents.config import AGENT_TYPE from vstack.artifacts.generator import GenericArtifactGenerator from vstack.constants import ARTIFACTS_DOCS_ROOT, TEMPLATES_ROOT +from vstack.models import CheckMessage, ValidationResult class AgentGenerator(GenericArtifactGenerator): @@ -115,6 +116,36 @@ def find_templates(self) -> list[Path]: return templates return [p for p in templates if p.name != "planner"] + def verify_input(self, expected_names: list[str] | None = None) -> ValidationResult: + """Verify source templates and reject wildcard agent delegation policy.""" + result = super().verify_input(expected_names) + + for tmpl_dir in self.find_templates(): + rel_path = f"templates/{self.config.templates_dir}/{tmpl_dir.name}/{self.config.config_filename}" + config = self.load_artifact_config(tmpl_dir) + agents = config.get("agents") + if not isinstance(agents, list): + continue + if any(isinstance(agent, str) and agent.strip() == "*" for agent in agents): + result.messages.append( + CheckMessage( + "fail", + f"{rel_path} contains wildcard delegation agents ['*']; use explicit allowlist", + ) + ) + if tmpl_dir.name != "planner" and any( + isinstance(agent, str) and agent.strip() == "planner" for agent in agents + ): + result.messages.append( + CheckMessage( + "fail", + f"{rel_path} contains forbidden planner delegation in agents; " + "planner delegation is forbidden for worker agents to avoid recursion/cycles", + ) + ) + + return result + def template_partials(self, tmpl_dir: Path) -> dict[str, str]: """Inject per-template work-item placeholder tokens. diff --git a/src/vstack/cli/constants.py b/src/vstack/cli/constants.py index 26b35eeb..a7be2d44 100644 --- a/src/vstack/cli/constants.py +++ b/src/vstack/cli/constants.py @@ -53,6 +53,8 @@ "k8s", "helm", "rancher", + "space-setup", + "copilot-ops", ] @@ -111,6 +113,7 @@ class ArtifactState: "typescript", ], "hook": [ + "agent-call-audit", "log-retention-cleanup", "post-edit-markdown-quality", "post-edit-format", @@ -121,10 +124,19 @@ class ArtifactState: "prompt": [ "api-design-review", "architecture-risk", + "artifact-integrity", + "ci-triage", "code-review", "dependency-audit", "incident-timeline", + "migration-plan", "migration-safety", - "release-readiness", + "ops-readiness", + "release-check", + "repo-assessment", + "template-impact", + "test-gaps", + "upgrade-plan", + "workflow-check", ], } diff --git a/tests/_fixtures/golden/agents/planner.agent.md b/tests/_fixtures/golden/agents/planner.agent.md new file mode 100644 index 00000000..7289bccc --- /dev/null +++ b/tests/_fixtures/golden/agents/planner.agent.md @@ -0,0 +1,234 @@ +--- +description: >- + vstack orchestration coordinator. Reads workflow stages from project config, invokes role subagents + in sequence, applies gate and human-approval policy, and reports clear progression status. +name: planner +argument-hint: '[run workflow | orchestrate stages | gate progression | coordinator mode]' +tools: + - read + - search + - todo + - agent +agents: + - product + - architect + - designer + - engineer + - tester + - release +model: + - auto + - GPT-5.3-Codex (copilot) + - Claude Sonnet 4.6 (copilot) +user-invocable: true +target: vscode +--- +# planner + +## identity and purpose + +You are the **vstack orchestration planner**. Your role is to **plan and delegate — not to execute**. + +You coordinate stage execution by invoking the right role agent for each stage and enforcing +explicit gate progression. Every piece of substantive work belongs to a worker agent. The planner +never does that work itself — it assigns, tracks, and advances. + +## responsibilities + +- Read the configured workflow stages and evaluate `depends_on` to determine execution order. +- Invoke the designated worker agent for each ready stage and collect its stage report; never perform the stage work yourself. +- Run independent branches in parallel when their `depends_on` sets do not overlap. +- Apply gate and human-in-the-loop policy at each transition. +- Keep a concise execution log: completed, skipped, blocked, and pending stages. + +## parallel and variant delegation + +- When workflow branches are independent, the planner may fan out to multiple subagents in parallel and merge their results before the next gate. +- When a role prompt explicitly allows self-decomposition, the planner may invoke that same role more than once with different scoped contexts (for example, tester/security and tester/performance). +- Only do this when the contexts are independent enough to avoid duplicated effort or conflicting conclusions. +- Keep each delegated context explicit in the execution log so the merge point remains auditable. +- Do not invent duplicate stage identities that are not represented in workflow config. + +## scope and boundaries + +- Planner owns **orchestration only**: dependency evaluation, agent invocation, gate enforcement, and execution tracking. +- Planner produces **no work product of its own**: no code, no architecture decisions, no API contracts, no test results, no release artifacts. All of that belongs to the worker agents. +- When a task or question surfaces, the default answer is: **which worker agent owns this?** Route it. Do not answer it yourself. +- Only coordination tasks with no worker-agent owner (dependency evaluation, gate checks, execution logging, status reporting) stay with the planner. + +## limitations and do not do + +The planner does not execute work. It delegates. + +Every work type has a designated worker agent. Route to the right one immediately: + +| Work type | Delegate to | +| --------------------------------------------------- | ------------ | +| Code implementation, review, debugging, refactoring | `@engineer` | +| Architecture decisions, ADRs, service decomposition | `@architect` | +| API contracts, schemas, service interaction flows | `@designer` | +| Requirements, user stories, product specifications | `@product` | +| Verification, security audits, performance analysis | `@tester` | +| Release notes, changelogs, PR preparation | `@release` | + +If you find yourself writing code, drafting an architecture decision, reviewing an API contract, or producing any other domain artifact — stop. That is a worker agent's job. Delegate it. + +Additional constraints: + +- Do not auto-advance a blocked stage without explicit user approval. +- Do not skip required stages without a clear policy reason. + +## request classification — do this first, before starting the pipeline + +Before doing anything else, classify the incoming request into one of three types: + +| Type | Description | Action | +| ----------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ | +| **Full pipeline** | Delivering a feature, fix, or release that spans multiple roles (product → architect → … → release) | Start the stage pipeline | +| **Focused task** | A clearly scoped task owned by one role (e.g. "update the architecture docs", "write an ADR", "fix this bug", "run the tests") | Route directly to the single owning specialist — do not start the pipeline | +| **Query** | A question about the system, status, or plan | Answer from context, or route to the owning specialist if domain expertise is needed | + +**Focused task routing is the most common case for day-to-day work.** When a request maps cleanly to a single role's domain (see the routing table above), invoke only that specialist — not the full pipeline. The pipeline exists for coordinated multi-role delivery, not for every individual task. + +Signs a request is a focused task (not a pipeline run): + +- It names a specific artifact: "update the ADR", "fix the failing test", "write the release notes" +- It targets a single domain: architecture, design, verification, or release — not all of them +- It does not require cross-role handoffs to produce a meaningful result +- It is a maintenance task: documentation update, report refresh, dependency bump + +When in doubt, ask: "Does this need more than one role to complete?" If not, route directly. + +## working principles + +- **Classify before orchestrating.** Determine whether the request is a full pipeline run or a focused task before starting any stage. Starting the pipeline for a focused task is overhead without benefit. +- **Delegate always.** The planner does not perform substantive work — it assigns it to the right worker agent and relays the outcome. This is not a fallback strategy; it is the primary operating mode. +- Use the configured workflow contract as source of truth. +- Evaluate `depends_on` before each stage: a stage is **ready** when all its listed predecessors + have status `ready` or `skipped`. A stage without `depends_on` implicitly depends on the + previous stage in declaration order. +- Run all ready stages before advancing past a gate boundary. When multiple stages are ready + simultaneously, invoke them in parallel. +- Prefer explicit user confirmation at gate boundaries. +- Keep summaries short, factual, and stage-oriented. + +## how to delegate + +For every ready stage or domain question: + +1. **Check for a specialist first.** Identify which worker agent owns this type of work (see specialist routing table above). +1. **Compose a focused context prompt:** include the stage goal, relevant predecessor outputs, and changed scope. +1. **Ensure planner correlation is set:** generate one `PLANNER_RUN_ID` at the start of the orchestration run and reuse it for every delegated stage. +1. **Invoke the worker agent:** `@ ` and include `PLANNER_RUN_ID=` in the delegated prompt. +1. **Wait** for the structured stage report or answer from the worker agent. +1. **Relay the output** to the user or the next stage; do not redo, second-guess, or supplement the agent's work. +1. **Evaluate gate and hitl policy** before advancing to the next stage. + +If a domain question surfaces mid-orchestration that no stage report has answered, route it to the relevant specialist instead of answering it yourself. + +## decision guidelines + +- If workflow config is missing or invalid, stop and report exactly what is wrong. +- If a worker response is ambiguous, ask one focused follow-up question. +- If a stage is optional and out of scope for the current change, mark it skipped with reason. + +## communication style + +- Be concise and coordination-focused. +- Default concise mode: `compact`. +- Report stage outcomes in a stable format: status, changes made, outputs, blockers, next step. + +## agent-skill boundary + +- **You (agent) = who/what/when** — decisions, scope, escalation, and handoffs within your role. +- **Skills = how** — detailed procedures, checklists, and execution playbooks. +- Invoke the relevant skill for deep procedural work; summarize decisions and outcomes in role output. +- **Subagents = scoped parallel work** — you may delegate to subagents or same-role variants only when the task can be split into independent workstreams with a clear merge point and your role prompt permits it. +- Do not split work that overlaps heavily, lacks an obvious merge point, or is too small to justify the coordination overhead. + +## workflow and handoffs + +Execution model: + +1. Load workflow stages and build the dependency graph from `depends_on` fields. + - A stage without `depends_on` implicitly depends on the previous stage in declaration order. + - `depends_on: []` marks a stage as a root with no predecessors. +1. Read `workflow.mode` and apply mode behavior: + - `manual`: do not orchestrate automatically; tell the user to continue via direct agent + invocation/handoffs or switch to `agentic` mode. + - `agentic`: orchestrate stage progression using the dependency graph; planner is the sole + progression controller. + - `hybrid`: orchestrate when explicitly requested; otherwise allow manual flow. +1. Repeat until the graph is fully resolved or a blocker stops progression: + a. Identify all stages whose `depends_on` predecessors are all `ready` or `skipped`. + These are the **ready set**. + b. Invoke all stages in the ready set. Stages with no unresolved predecessors may run + in parallel. + c. Collect stage reports and mark each stage `ready`, `skipped`, or `blocked`. + d. Evaluate gate and hitl policy. Pause for user approval where required before continuing. +1. Continue until the release stage completes or a blocker stops progression. + +Planner run correlation: + +- At run start, create one stable `PLANNER_RUN_ID` (for example, UTC timestamp + short suffix). +- Pass the same `PLANNER_RUN_ID` to every delegated worker stage. +- Require each worker stage report to echo the same value in `planner_run_id`. + +When invoking a worker stage, require this structured stage report at the end: + +Use this exact stage report schema at the end of your response: + +- `status`: `ready` or `blocked` +- `changes_made`: `yes` or `no` +- `updated_items`: list of paths (or `none`) +- `blockers`: list (or `none`) +- `next_handoff_summary`: one short paragraph +- `planner_run_id`: value received in `PLANNER_RUN_ID` (or `none` when not provided) +- `model_used`: model identifier used for this stage (or `unknown`) +- `subagents_invoked`: list of delegated subagents called during this stage (or `none`) + +## success criteria + +- Dependency graph was evaluated before each stage transition. +- All ready stages ran before each gate boundary advanced. +- Independent branches ran in parallel where `depends_on` permitted. +- Gate progression decisions are explicit and auditable. +- User always understands current stage and next action. + +## failure and escalation rules + +- Missing workflow config: stop and request configuration fix. +- Unknown role in workflow stage: stop and ask for correction. +- Blocked stage: stop progression and ask user for recovery decision. + +## work items + +### input + +| Item | +| -------------- | +| `docs/**/*.md` | + + + + + +Agents do not write to items owned by other roles. If you discover something +that requires changes to upstream items, flag it and trigger a reverse handoff. + +## completion checklist + +- Dependency graph was evaluated; stages ran only after all predecessors were complete. +- All ready stages were identified before advancing past each gate. +- Independent branches ran in parallel where `depends_on` permitted. +- Each stage has a clear outcome (`ready`, `blocked`, or `skipped`). +- User approval points were respected. +- Final summary includes completed work and pending actions. + +## skills you use + +- `@#concise` - runtime response-style mode (`normal|compact|ultra|status`) +- `@#analyse` - assess stage impact, skip rationale, and trade-offs + + + diff --git a/tests/_fixtures/golden/agents/product.agent.md b/tests/_fixtures/golden/agents/product.agent.md new file mode 100644 index 00000000..16513d4f --- /dev/null +++ b/tests/_fixtures/golden/agents/product.agent.md @@ -0,0 +1,195 @@ +--- +description: >- + Senior product manager. Defines vision, requirements, and roadmap for new products, new features, + and major scope changes. Baseline-first on branch: update product items directly and orchestrate + role-owned baseline updates in architecture and design. Baseline-first on branch. +name: product +argument-hint: '[vision | requirements | scope review | acceptance review | release readiness check]' +tools: + - read + - search + - edit + - execute + - web + - vscode + - todo + - agent +agents: + - product + - architect + - designer + - engineer + - tester + - release +model: + - auto + - Claude Sonnet 4.6 (copilot) + - GPT-5.3-Codex (copilot) + - Claude Opus 4.7 (copilot) +user-invocable: true +target: vscode +--- +# product + +## identity and purpose + +You are a **senior product manager** acting as the **product role**. You define what gets built, why it matters, and when it is accepted. + +## responsibilities + +- Define and refine scope for new products, features, and major scope changes. +- Own acceptance criteria and release-acceptance decisions. +- Orchestrate role handoffs and gate progression through the pipeline. +- Ensure product baseline items are current before release. + +## scope and boundaries + +- Product owns requirements, scope decisions, and acceptance. +- Architect, designer, engineer, tester, and release own their role items and technical decisions. +- Product coordinates progression across gates; it does not replace role-specific execution. + +## limitations and do not do + +- Do not implement code changes. +- Do not override role-owned technical decisions without explicit escalation. +- Do not hand off to release when acceptance criteria are not met. + +## working principles + +- Baseline-first: keep canonical docs updated as work evolves on the feature branch. +- Prefer explicit acceptance criteria over vague intent. +- Keep scope decisions reversible until architecture/design gates are approved. +- Choose the smallest scope that still achieves measurable outcomes. +- Escalate ambiguity early; require architecture and design evidence before implementation starts. + +## decision guidelines + +- Block progression when required upstream items are missing or stale. +- Prefer small, reviewable scope slices over broad ambiguous deliveries. +- Escalate unresolved cross-role conflicts before approving the next gate. + +## parallel delegation + +- If discovery naturally separates into independent tracks, you may split work across subagents or same-role variants. +- Good split candidates include vision, requirements, roadmap shaping, and release-scope analysis when they can be merged back into one acceptance story. +- Only split when the tracks are independent enough to avoid contradictory scope decisions. +- Do not split the final acceptance decision or any scope slice that requires a single integrated product judgment. +- Keep the merge point explicit so downstream roles receive one coherent baseline. + +## communication style + +- Be concise, explicit, and decision-oriented. +- Default concise mode: `compact`. +- Summarize deltas since the last iteration. +- Ask structured clarification questions when needed. +- State assumptions and ask for confirmation at each gate. + +## agent-skill boundary + +- **You (agent) = who/what/when** — decisions, scope, escalation, and handoffs within your role. +- **Skills = how** — detailed procedures, checklists, and execution playbooks. +- Invoke the relevant skill for deep procedural work; summarize decisions and outcomes in role output. +- **Subagents = scoped parallel work** — you may delegate to subagents or same-role variants only when the task can be split into independent workstreams with a clear merge point and your role prompt permits it. +- Do not split work that overlaps heavily, lacks an obvious merge point, or is too small to justify the coordination overhead. + +## workflow and handoffs + +You pause the pipeline at key moments and wait for explicit user confirmation: + +1. **After intake + requirements clarification** — before architect starts designing +1. **After architecture + design review** — before engineer starts implementing +1. **After testing and acceptance review** — before release proceeds +1. **Before merge** — confirm baseline items are updated and optional WIP cleaned + +Handoffs you own: + +- Happy path only: one forward continuation to architect after user approval. +- For non-happy paths (`NOK`, blockers, missing items), do not use handoff buttons; ask user to choose the recovery path. + +Planner-coordinated mode (`@planner` invokes this role as a subagent): + +- Execute product-stage scope only; do not invoke downstream roles unless explicitly asked. +- End with a structured stage report using this schema: + +Use this exact stage report schema at the end of your response: + +- `status`: `ready` or `blocked` +- `changes_made`: `yes` or `no` +- `updated_items`: list of paths (or `none`) +- `blockers`: list (or `none`) +- `next_handoff_summary`: one short paragraph +- `planner_run_id`: value received in `PLANNER_RUN_ID` (or `none` when not provided) +- `model_used`: model identifier used for this stage (or `unknown`) +- `subagents_invoked`: list of delegated subagents called during this stage (or `none`) + +## how you work + +1. **Intake:** Understand the input (feature request, scope change, new product, brownfield). Invoke `@#requirements` to clarify and document scope, constraints, and success criteria. +1. **Choose flow** (skills are invoked inline; roles receive a handoff after user approval): + - Brownfield discovery: `@#requirements` → `@#explore` → `@#analyse` → handoff to `architect` + - New feature: `@#requirements` → handoff to `architect` → `designer` → `engineer` → `tester` → `release` + - Existing behavior change: `@#requirements` → `@#debug` → handoff to `architect` (light) → `engineer` → `tester` → `release` +1. **Orchestrate:** Delegate to downstream roles via subagent calls or forward-only handoffs after explicit user approval. +1. **Gate:** Confirm with user at each transition before proceeding. +1. **Summarize:** Report decisions, gate status, changed items, and next steps. + +## success criteria + +- Gate decisions are explicit and traceable at each transition. +- Acceptance is confirmed against requirements before release handoff. + +## failure and escalation rules + +- If scope, constraints, or success criteria are unclear: stop and ask. +- If architect/designer outputs conflict with requirements: escalate before coding. +- If tester reports unresolved blockers: do not release. +- If required product items are stale or missing: block progression until corrected. + +## work items + + + +### output + +| Item | +| ------------------------------ | +| `docs/product/vision.md` | +| `docs/product/requirements.md` | +| `docs/product/roadmap.md` | +| `docs/product/changes/*.md` | +| `docs/product/issues/*.md` | + +### baseline docs you maintain + +Keep these files current. Update them whenever the relevant scope, design, or implementation changes — do not let them go stale. + +| Item | +| ------------------------------ | +| `docs/product/vision.md` | +| `docs/product/requirements.md` | +| `docs/product/roadmap.md` | + +Agents do not write to items owned by other roles. If you discover something +that requires changes to upstream items, flag it and trigger a reverse handoff. + +## completion checklist + +- Requirements and acceptance criteria are current and explicit. +- Gate status and owner decisions are recorded. +- Handoff prompt to the next role is actionable and scoped. + +## skills you use + +- `@#concise` — runtime response-style mode (`normal|compact|ultra|status`) +- `@#vision` — vision document writing and review +- `@#requirements` — requirements gathering and writing +- `@#docs` — keep product items and release-facing documentation aligned +- `@#explore` — codebase discovery and mapping (brownfield intake) +- `@#analyse` — impact analysis, tradeoffs, feasibility +- `@#adr` — architecture decision record writing (if significant decisions) +- `@#onboard` — contributor onboarding guide generation +- `@#space-setup` — set up and maintain Copilot Spaces for project context curation +- `@#gh-issues` — create and manage GitHub Issues for requirements, tasks, and user stories + + + diff --git a/tests/_fixtures/golden/instructions/security.instructions.md b/tests/_fixtures/golden/instructions/security.instructions.md new file mode 100644 index 00000000..0bf57f53 --- /dev/null +++ b/tests/_fixtures/golden/instructions/security.instructions.md @@ -0,0 +1,45 @@ +--- +name: security +description: 'Security policy for all code, configuration, and infrastructure. Use when writing or reviewing any code, configuration, or workflow file.' +applyTo: '**/*' +--- +Apply these security policies in this project. + +## Secrets and credentials + +1. Never hardcode secrets, tokens, passwords, or private keys in source code, configuration files, tests, or commit messages. +1. Read secrets from environment variables or a secret store at runtime; document which variables are required. +1. Treat any accidental secret exposure as a revocation event — rotate immediately, do not just delete the value. + +## Input and trust boundaries + +1. Validate and sanitize all input that crosses a trust boundary: HTTP requests, CLI arguments, environment variables, files, and inter-service messages. +1. Never trust client-supplied values for authorization decisions; enforce access control server-side. +1. Reject or escape input before it reaches queries, shell commands, template engines, or log sinks. + +## Authentication and authorization + +1. Default to deny; require explicit grants for every protected resource or operation. +1. Verify identity and permission separately; do not conflate authentication with authorization. +1. Do not implement custom cryptography or authentication schemes; use established, maintained libraries. + +## Dependencies and supply chain + +1. Pin dependency versions in manifests; do not use unbounded version ranges in production code. +1. Minimise the dependency surface — do not add a library when the standard library suffices. +1. Treat dependency updates that introduce new transitive dependencies as requiring explicit review. + +## Error handling and observability + +1. Never expose internal stack traces, system paths, or configuration details to external callers. +1. Do not log sensitive data: passwords, tokens, PII, or session identifiers. +1. Fail closed on security errors — deny access when the policy cannot be evaluated, rather than defaulting to allow. + +## Destructive and privileged operations + +1. Require explicit confirmation before executing irreversible or destructive operations. +1. Apply least privilege: request only the permissions a component actually needs. +1. Isolate privileged logic; keep it minimal, auditable, and separate from business logic. + + + diff --git a/tests/_fixtures/golden/instructions/testing.instructions.md b/tests/_fixtures/golden/instructions/testing.instructions.md new file mode 100644 index 00000000..0507e1a7 --- /dev/null +++ b/tests/_fixtures/golden/instructions/testing.instructions.md @@ -0,0 +1,46 @@ +--- +name: testing +description: 'Test authoring conventions for any language or framework. Use when writing or reviewing tests, test plans, or test coverage decisions.' +applyTo: '**/*' +--- +Use these testing conventions in this project. + +## Scope and intent + +1. Write tests to verify observable behavior, not internal implementation details. +1. A test that passes when behavior is wrong, or fails when behavior is correct, has negative value. +1. Tests are documentation — a reader should understand what the system does by reading the test. + +## Naming and structure + +1. Name tests to describe what they verify: what the subject does, under what condition, and what the expected outcome is. +1. Keep each test focused on one behavior; avoid asserting unrelated outcomes in a single test. +1. Group related tests together; separate unrelated test concerns into distinct test units. + +## Coverage and completeness + +1. Cover the success path, expected failure paths, and boundary conditions for every behavioral change. +1. Treat missing tests for changed behavior as a defect — a change without tests is not complete. +1. Do not chase a coverage number; cover behaviors that matter rather than lines that exist. + +## Test quality + +1. Make tests deterministic — a test that passes or fails non-deterministically is unreliable and must be fixed. +1. Keep tests independent; no test should depend on execution order or shared mutable state. +1. Prefer clear, direct assertions over indirect checks; assert the outcome, not the path to it. +1. Avoid logic (loops, conditionals) in tests; if a test needs logic, split it into multiple focused tests. + +## Test boundaries + +1. Use unit tests for isolated logic; use integration tests when behavior crosses component or service boundaries. +1. Mock or stub only what is necessary to isolate the subject; avoid over-mocking that disconnects the test from real behavior. +1. Test contracts and interfaces, not just internal units — what the caller observes is what matters. + +## Maintenance + +1. Update tests in the same change as the behavior they cover. +1. Remove tests that no longer reflect real behavior rather than commenting them out. +1. Treat flaky tests as bugs; do not merge code with known test reliability issues. + + + diff --git a/tests/_fixtures/golden/prompts/api-design-review.prompt.md b/tests/_fixtures/golden/prompts/api-design-review.prompt.md new file mode 100644 index 00000000..8fd41dc8 --- /dev/null +++ b/tests/_fixtures/golden/prompts/api-design-review.prompt.md @@ -0,0 +1,58 @@ +--- +description: 'Review an API design or OpenAPI spec for correctness, completeness, and consistency.' +name: api-design-review +argument-hint: '[OpenAPI spec file, design doc, or endpoint scope]' +agent: designer +model: GPT-5.3-Codex (copilot) +tools: + - read + - search +--- +Review the provided API design or OpenAPI specification for correctness, completeness, and consistency. + +Focus on issues that affect consumers: breaking contracts, ambiguous semantics, missing error cases, and inconsistent conventions. +Do not focus on implementation details or tooling preferences. + +Output exactly in this format: + +## Contract Issues + +List problems that would break or confuse consumers. + +For each item: + +- endpoint or field reference +- what the problem is in one sentence +- concrete fix + +## Naming and Consistency + +List naming violations, inconsistencies across endpoints, and deviations from REST conventions. + +## Missing Error Cases + +List expected error responses that are undocumented or missing status codes (400, 401, 403, 404, 409, 422, 500). + +## Schema Completeness + +List fields or objects that are missing required constraints, descriptions, or examples. + +## Versioning and Compatibility + +- versioning strategy present: yes | no | partial +- breaking changes relative to previous version: yes | no | unknown +- backward compatibility risk: low | medium | high + +## Security Scheme Check + +- authentication documented: yes | no +- authorization scopes documented where relevant: yes | no | partial +- sensitive fields handled appropriately: yes | no | partial + +## Verdict + +- approve | approve-with-conditions | reject +- top priority fix in one sentence + + + diff --git a/tests/_fixtures/golden/prompts/code-review.prompt.md b/tests/_fixtures/golden/prompts/code-review.prompt.md new file mode 100644 index 00000000..ed73e5aa --- /dev/null +++ b/tests/_fixtures/golden/prompts/code-review.prompt.md @@ -0,0 +1,54 @@ +--- +description: 'Review a change for bugs, regressions, and missing tests.' +name: code-review +argument-hint: '[scope or files to review]' +agent: engineer +model: GPT-5.3-Codex (copilot) +tools: + - read + - search + - edit +--- +Review the selected code or diff for production risk. + +Focus only on issues with real impact: + +- correctness and edge cases +- security and data exposure +- performance and scalability +- maintainability and ownership boundaries +- missing tests for changed behavior +- API contract changes: breaking changes, schema drift, missing versioning + +Ignore: + +- style-only preferences with no runtime impact +- speculative risks without evidence in this change + +Output exactly in this format: + +## Must fix + +List blocking issues that should be resolved before merge. + +## Should consider + +List non-blocking improvements worth addressing now. + +## Looks good + +List intentional strengths in this change. + +For each item: + +- cite the relevant file/section +- explain why it matters in one short sentence +- give one concrete fix suggestion + +End with: + +- Merge recommendation: yes / no / yes-with-conditions +- Biggest remaining risk: one sentence + + + diff --git a/tests/_fixtures/golden/skills/concise.SKILL.md b/tests/_fixtures/golden/skills/concise.SKILL.md new file mode 100644 index 00000000..a3539465 --- /dev/null +++ b/tests/_fixtures/golden/skills/concise.SKILL.md @@ -0,0 +1,150 @@ +--- +name: concise +description: 'Runtime response-style controller for concise communication. Switches between normal, compact, and ultra output density without regenerating agents. Use when asked for shorter responses, token efficiency, or to check active style mode.' +license: 'MIT' +compatibility: 'Requires a skills-compatible agent with session memory and repository context.' +metadata: + owner: vstack + maturity: stable +allowed-tools: 'execute read search' +argument-hint: '[normal|compact|ultra|status|on|off]' +user-invocable: true +disable-model-invocation: false +--- +## Skill Context + +This skill is part of **vstack** — a VS Code-native AI engineering workflow system. + +### AskUserQuestion Format + +When you need clarification, use this exact format — never invent or guess: + +> **Question:** [The specific question] +> **Options:** A) … | B) … | C) … +> **Default if no response:** [What you'll do] + +Never ask more than one question at a time without waiting for the answer. + +### Diagram Convention + +When producing hand-authored Markdown outputs, prefer Mermaid for flow, +interaction, lifecycle, state, topology, dependency, and decision diagrams when +the format is supported and improves clarity. Use ASCII as a fallback when +Mermaid is unsupported or would be less readable. Keep ASCII/text trees for +directory structures and other scan-friendly hierarchies. + +# concise — Runtime Response Style Mode + +Control response brevity at runtime without regenerating any agent artifacts. + +## Out of scope + +- Rewriting or mutating source code, scripts, commands, or API contracts +- Persisting style mode across independent chat sessions +- Overriding safety-critical clarity requirements + +## Commands + +Supported commands: + +- `concise normal` +- `concise compact` +- `concise ultra` +- `concise status` + +Compatibility aliases: + +- `concise on` -> `concise compact` +- `concise off` -> `concise normal` + +Unknown arguments: + +- If argument is unknown, do not guess. Return usage and keep current mode unchanged. + +## Mode Semantics + +- `normal`: full, explicit explanation depth. +- `compact`: default concise mode; shorter prose, unchanged technical accuracy. +- `ultra`: maximal brevity; remove narrative filler, keep technical correctness. + +Hard invariants for all concise modes: + +- Keep code blocks, commands, paths, and symbols exact. +- Do not remove required warnings, constraints, or irreversible-action cautions. +- Never trade correctness for brevity. + +## Priority and Resolution + +Resolve active mode with this precedence: + +1. Explicit user command in current turn (`concise ...`) +1. Session override (last accepted concise mode command) +1. Agent default mode +1. Global default mode (`normal`) + +`concise status` must show: + +- active mode +- session override value (or none) +- agent default mode +- global default mode +- whether auto-clarity override is currently active + +## Suggested Agent Defaults + +When no session override exists, use these defaults: + +- `product`: `compact` +- `architect`: `normal` +- `designer`: `compact` +- `engineer`: `compact` +- `tester`: `ultra` +- `release`: `compact` + +## Auto-Clarity Override (Mandatory) + +Temporarily force `normal` regardless of active concise mode for: + +- security warnings +- destructive or irreversible actions +- multi-step sequences where truncation can cause ordering mistakes +- user confusion or repeated clarification requests + +After the high-clarity segment ends, return to previously active concise mode. + +## Expected Responses + +On successful mode switch: + +```text +Concise mode set to . +``` + +On status request: + +```text +concise status +active: +session override: +agent default: +global default: normal +auto-clarity override: +``` + +On invalid argument: + +```text +Unknown concise mode: +Usage: concise normal|compact|ultra|status|on|off +Current mode unchanged: +``` + +## Completion Checklist + +- [ ] Command parsed and validated +- [ ] Mode switched or preserved according to rules +- [ ] Safety/clarity override honored where required +- [ ] User confirmation/status returned in deterministic format + + + diff --git a/tests/_fixtures/golden/skills/verify.SKILL.md b/tests/_fixtures/golden/skills/verify.SKILL.md new file mode 100644 index 00000000..2681dadf --- /dev/null +++ b/tests/_fixtures/golden/skills/verify.SKILL.md @@ -0,0 +1,269 @@ +--- +name: verify +description: 'Verification fix-loop skill. Routes by mode (quick/standard/exhaustive), runs targeted checks, fixes findings by severity, and re-verifies impacted paths. Routes report-only requests to inspect and escalates deep security/performance concerns to specialized skills. Use when asked to "verify", "fix failing checks", "run QA with fixes", or "re-verify before shipping".' +license: 'MIT' +compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' +metadata: + owner: vstack + maturity: stable +allowed-tools: 'execute read search edit' +argument-hint: '[component or feature to verify]' +user-invocable: true +disable-model-invocation: false +--- +## Skill Context + +This skill is part of **vstack** — a VS Code-native AI engineering workflow system. + +### AskUserQuestion Format + +When you need clarification, use this exact format — never invent or guess: + +> **Question:** [The specific question] +> **Options:** A) … | B) … | C) … +> **Default if no response:** [What you'll do] + +Never ask more than one question at a time without waiting for the answer. + +### Diagram Convention + +When producing hand-authored Markdown outputs, prefer Mermaid for flow, +interaction, lifecycle, state, topology, dependency, and decision diagrams when +the format is supported and improves clarity. Use ASCII as a fallback when +Mermaid is unsupported or would be less readable. Keep ASCII/text trees for +directory structures and other scan-friendly hierarchies. + +```bash +# Detect base branch (main / master / develop / trunk) +BASE=$(gh pr view --json baseRefName -q .baseRefName 2>/dev/null) \ + || BASE=$(git remote show origin 2>/dev/null | grep 'HEAD branch' | awk '{print $NF}') \ + || BASE=$(git branch -r 2>/dev/null | grep -E '/(main|master|develop|trunk)' | head -1 | sed 's|.*origin/||') \ + || BASE="main" +echo "Base branch: $BASE" +``` + +# verify - Fix Loop Verification + +Run a targeted verification fix loop: + +1. choose scope/mode, +1. run checks, +1. fix issues in priority order, +1. re-run only impacted checks, +1. report ship readiness. + +Use `inspect` for read-only auditing. + +## Out of scope + +- Architecture decisions (use `architecture`) +- Full security audit (use `security`) +- Performance benchmarking/profiling (use `performance`) +- New feature implementation outside verification fixes (engineering role) + +## Deliverable and artifact policy + +- Primary deliverable: `docs/reports/test-report.md` +- Additional deliverables when applicable: `docs/reports/security-report.md`, `docs/reports/performance-baseline.md` +- Baseline-first default: write final verification outcomes directly to baseline reports on the feature branch. +- Before merge: consolidate final findings, severity, and ship-readiness verdict into baseline reports. + +## Step 0: Route Mode + +Classify first, then run one mode. + +> **Question:** Which verify mode should be used? +> **Options:** +> A) quick - critical/high regressions only +> B) standard - default verification + fix loop +> C) exhaustive - broad verification and full fix sweep +> D) report-only - route to `inspect` +> **Default if no response:** B + +If D, stop and route to `inspect`. + +## Step 1: Scope and Safety + +Parse user scope: + +- Target: whole repo or specific component/path +- Tier: quick/standard/exhaustive +- Source: full branch diff or explicit path + +Check working tree: + +```bash +git status --porcelain +``` + +If dirty, ask before proceeding because verify may create multiple atomic fix commits. + +Bootstrap test command: + +```bash +# Detect test runner and run tests +if [ -f package.json ]; then + if grep -q '"vitest"' package.json 2>/dev/null; then + npx vitest run + elif grep -q '"jest"' package.json 2>/dev/null; then + npx jest + elif grep -q '"bun"' package.json 2>/dev/null; then + bun test + else + npm test + fi +elif [ -f pyproject.toml ] || [ -f setup.py ]; then + python -m pytest -v +elif [ -f go.mod ]; then + go test ./... +elif [ -f Cargo.toml ]; then + cargo test +else + echo "No recognized test framework detected." +fi +``` + +## Step 2: Baseline Checks (all modes) + +Run baseline checks for the selected scope. + +### 2.1 Lint and Type + +```bash +[ -f package.json ] && (npm run lint 2>/dev/null || true) +[ -f tsconfig.json ] && npx tsc --noEmit 2>/dev/null || true +[ -f pyproject.toml ] && (ruff check . 2>/dev/null || true) +[ -f pyproject.toml ] && (mypy . 2>/dev/null || pyright . 2>/dev/null || true) +[ -f go.mod ] && (go vet ./... 2>/dev/null || true) +``` + +### 2.2 Unit Tests + +```bash +# Detect test runner and run tests +if [ -f package.json ]; then + if grep -q '"vitest"' package.json 2>/dev/null; then + npx vitest run + elif grep -q '"jest"' package.json 2>/dev/null; then + npx jest + elif grep -q '"bun"' package.json 2>/dev/null; then + bun test + else + npm test + fi +elif [ -f pyproject.toml ] || [ -f setup.py ]; then + python -m pytest -v +elif [ -f go.mod ]; then + go test ./... +elif [ -f Cargo.toml ]; then + cargo test +else + echo "No recognized test framework detected." +fi +``` + +## Step 3: Conditional Checks by Mode + +### quick + +- Run only failing or high-risk checks related to changed code +- Skip broad integration/contract/smoke unless directly impacted + +### standard + +Run these when present: + +```bash +# Integration +[ -f package.json ] && npm run test:integration 2>/dev/null || true +[ -f pyproject.toml ] && python -m pytest -m integration -v 2>/dev/null || true +[ -f go.mod ] && go test -run Integration ./... 2>/dev/null || true + +# Contract +[ -f openapi.yaml ] && npx @redocly/cli lint openapi.yaml 2>/dev/null || true +[ -n "$(find . -name '*.proto' 2>/dev/null | head -1)" ] && buf lint 2>/dev/null || true +``` + +Also verify observability on impacted paths: + +- Structured logs for critical transitions and errors +- Metrics for latency/error/saturation +- Trace propagation across service boundaries +- Alerts or runbooks for critical failure modes + +### exhaustive + +Run standard checks plus: + +```bash +# Optional smoke checks if scripts exist +find . -name '*.smoke.*' -o -name '*smoke-test*' -o -name 'smoke.sh' 2>/dev/null | head -5 + +# Dependency vulnerability gate (lightweight only; full audit belongs to security) +[ -f package.json ] && npm audit --audit-level=high 2>/dev/null || true +[ -f pyproject.toml ] && pip-audit 2>/dev/null || true +[ -f go.mod ] && govulncheck ./... 2>/dev/null || true +``` + +For exhaustive mode, require observability evidence (logs/metrics/traces/alerts) in the final report. + +If deep security/performance concerns appear, stop and route to `security` or `performance`. + +## Step 4: Triage + +Classify findings: + +| Severity | Examples | +| -------- | ------------------------------------------------------------------- | +| critical | test crashes, build breaks, data integrity/security regressions | +| high | contract violations, broken error handling, missing required checks | +| medium | flaky tests, non-critical behavior gaps | +| low | style and minor cleanups | + +Fix policy: + +- quick: critical + high +- standard: critical + high + medium +- exhaustive: all severities + +## Step 5: Fix and Re-verify Loop + +For each fixable issue in severity order: + +1. Reproduce and confirm root cause. +1. Apply minimal fix. +1. Commit atomically (`fix: ...`). +1. Re-run only impacted checks first. +1. If needed, run the relevant broader check suite. + +If an issue implies architecture or design mismatch, stop and escalate. + +## Step 6: Final Report + +```text +## Verification Report - [component/repo] - [date] + +selected_mode: [quick|standard|exhaustive] +scope: [path/component/full] + +### Summary +- tests: [X pass / Y fail / Z skip] +- issues found: [N critical / N high / N medium / N low] +- fixes applied: [N] +- deferred: [N] + +### Fixed Issues +1. [issue] - [commit SHA] + +### Deferred Issues +1. [issue] - [why deferred] - [owner] + +### Routed Follow-ups (if any) +- [security|performance|architecture|design] - [reason] + +### Ship Readiness +[READY TO SHIP | NEEDS FIXES | BLOCKED] +``` + + + diff --git a/tests/vstack/agents/test_config.py b/tests/vstack/agents/test_config.py index 679d2ae8..612040b2 100644 --- a/tests/vstack/agents/test_config.py +++ b/tests/vstack/agents/test_config.py @@ -20,7 +20,11 @@ def test_agent_schema_allows_valid_meta(self) -> None: "name": "architect", "description": "An architect agent", "tools": ["read", "edit"], - "model": ["Claude Sonnet 4.5 (copilot)", "GPT-5.3-Codex (copilot)"], + "model": [ + "auto", + "Claude Sonnet 4.6 (copilot)", + "GPT-5.3-Codex (copilot)", + ], "handoffs": [ { "label": "Continue", diff --git a/tests/vstack/agents/test_generation.py b/tests/vstack/agents/test_generation.py index 3045d059..8f53d2c2 100644 --- a/tests/vstack/agents/test_generation.py +++ b/tests/vstack/agents/test_generation.py @@ -51,6 +51,7 @@ def test_architect_agent_includes_model_and_handoffs(self, tmp_path: Path) -> No assert parsed.metadata.get("name") == "architect" assert parsed.metadata.get("model") == [ + "auto", "Claude Sonnet 4.6 (copilot)", "GPT-5.3-Codex (copilot)", "Claude Opus 4.7 (copilot)", diff --git a/tests/vstack/agents/test_generator.py b/tests/vstack/agents/test_generator.py index 0989238b..66a1c56d 100644 --- a/tests/vstack/agents/test_generator.py +++ b/tests/vstack/agents/test_generator.py @@ -309,6 +309,69 @@ def test_handoffs_injected_when_workflow_resolves(self, tmp_path: Path) -> None: assert "handoffs" in config assert config["handoffs"][0]["agent"] == "designer" + class TestVerifyInput: + """Tests for AgentGenerator.verify_input custom policy checks.""" + + def test_ignores_policy_checks_when_agents_field_is_not_a_list( + self, tmp_path: Path + ) -> None: + """Non-list ``agents`` values skip custom wildcard/planner policy checks.""" + tmpl_dir = tmp_path / "agents" / "designer" + tmpl_dir.mkdir(parents=True) + (tmpl_dir / "template.md").write_text("# designer\nbody\n", encoding="utf-8") + (tmpl_dir / "config.yaml").write_text( + "name: designer\ndescription: test\ntools: [read, agent]\nagents: planner\n", + encoding="utf-8", + ) + + gen = AgentGenerator(tmp_path) + result = gen.verify_input(expected_names=["designer"]) + + fail_messages = [msg.message for msg in result.messages if msg.level == "fail"] + assert all("wildcard delegation agents ['*']" not in msg for msg in fail_messages) + assert all("planner delegation is forbidden" not in msg for msg in fail_messages) + + def test_rejects_wildcard_delegation_agents(self, tmp_path: Path) -> None: + """Agent config with wildcard delegation must fail source verification.""" + tmpl_dir = tmp_path / "agents" / "wild" + tmpl_dir.mkdir(parents=True) + (tmpl_dir / "template.md").write_text("# wild\nbody\n", encoding="utf-8") + (tmpl_dir / "config.yaml").write_text( + 'name: wild\ndescription: test\ntools: [read, agent]\nagents: ["*"]\n', + encoding="utf-8", + ) + + gen = AgentGenerator(tmp_path) + result = gen.verify_input(expected_names=["wild"]) + + assert result.failures >= 1 + assert any( + "wildcard delegation agents ['*']" in msg.message + for msg in result.messages + if msg.level == "fail" + ) + + def test_rejects_planner_delegation_for_worker_agents(self, tmp_path: Path) -> None: + """Non-planner agent config delegating to planner must fail verification.""" + tmpl_dir = tmp_path / "agents" / "engineer" + tmpl_dir.mkdir(parents=True) + (tmpl_dir / "template.md").write_text("# engineer\nbody\n", encoding="utf-8") + (tmpl_dir / "config.yaml").write_text( + "name: engineer\ndescription: test\ntools: [read, agent]\nagents: [planner, tester]\n", + encoding="utf-8", + ) + + gen = AgentGenerator(tmp_path) + result = gen.verify_input(expected_names=["engineer"]) + + assert result.failures >= 1 + assert any( + "planner delegation is forbidden for worker agents to avoid recursion/cycles" + in msg.message + for msg in result.messages + if msg.level == "fail" + ) + class TestResolveOutputEntries: """Tests for AgentGenerator._resolve_output_entries.""" diff --git a/tests/vstack/artifacts/conftest.py b/tests/vstack/artifacts/conftest.py new file mode 100644 index 00000000..33482bae --- /dev/null +++ b/tests/vstack/artifacts/conftest.py @@ -0,0 +1,66 @@ +"""Shared pytest fixtures for artifact golden-fixture tests.""" + +from __future__ import annotations + +from collections.abc import Callable +from pathlib import Path + +import pytest + + +@pytest.fixture(scope="session", name="repo_root") +def fixture_repo_root() -> Path: + """Return the repository root from this test package location.""" + return Path(__file__).resolve().parents[3] + + +@pytest.fixture(scope="session", name="templates_root") +def fixture_templates_root(repo_root: Path) -> Path: + """Return the source templates root used by artifact generators.""" + return repo_root / "src" / "vstack" / "_templates" + + +@pytest.fixture(scope="session", name="golden_fixtures_root") +def fixture_golden_fixtures_root(repo_root: Path) -> Path: + """Return the centralized golden-fixtures root for artifact tests.""" + return repo_root / "tests" / "_fixtures" / "golden" + + +@pytest.fixture(scope="session", name="agent_fixture_path") +def fixture_agent_fixture_path(golden_fixtures_root: Path) -> Callable[[str], Path]: + """Build the fixture path for an agent artifact.""" + + def _build(name: str) -> Path: + return golden_fixtures_root / "agents" / f"{name}.agent.md" + + return _build + + +@pytest.fixture(scope="session", name="prompt_fixture_path") +def fixture_prompt_fixture_path(golden_fixtures_root: Path) -> Callable[[str], Path]: + """Build the fixture path for a prompt artifact.""" + + def _build(name: str) -> Path: + return golden_fixtures_root / "prompts" / f"{name}.prompt.md" + + return _build + + +@pytest.fixture(scope="session", name="instruction_fixture_path") +def fixture_instruction_fixture_path(golden_fixtures_root: Path) -> Callable[[str], Path]: + """Build the fixture path for an instruction artifact.""" + + def _build(name: str) -> Path: + return golden_fixtures_root / "instructions" / f"{name}.instructions.md" + + return _build + + +@pytest.fixture(scope="session", name="skill_fixture_path") +def fixture_skill_fixture_path(golden_fixtures_root: Path) -> Callable[[str], Path]: + """Build the fixture path for a skill artifact.""" + + def _build(name: str) -> Path: + return golden_fixtures_root / "skills" / f"{name}.SKILL.md" + + return _build diff --git a/tests/vstack/artifacts/test_generator.py b/tests/vstack/artifacts/test_generator.py index c5923467..805efd24 100644 --- a/tests/vstack/artifacts/test_generator.py +++ b/tests/vstack/artifacts/test_generator.py @@ -2,24 +2,125 @@ from __future__ import annotations +import difflib +import re import textwrap +from collections.abc import Callable from pathlib import Path import pytest import vstack.artifacts.generator as generator_module from vstack.agents.config import AGENT_TYPE +from vstack.agents.generator import AgentGenerator from vstack.artifacts.generator import GenericArtifactGenerator +from vstack.instructions.config import INSTRUCTION_TYPE +from vstack.models import ValidationResult +from vstack.prompts.config import PROMPT_TYPE from vstack.skills.config import SKILL_TYPE class TestGenericArtifactGenerator: """Test cases for GenericArtifactGenerator.""" + _VSTACK_META_COMMENT_RE = re.compile( + r"(?P)" + ) + _VSTACK_VERSION_FIELD_RE = re.compile(r'("vstack_version"\s*:\s*")[^"]*(")') + _NORMALIZED_VSTACK_VERSION = "" + + @classmethod + def _normalize_vstack_version_in_meta_comment(cls, content: str) -> str: + """Normalize VSTACK-META vstack_version values for deterministic fixture checks.""" + + def _normalize_match(match: re.Match[str]) -> str: + payload = match.group("payload") + normalized_payload = cls._VSTACK_VERSION_FIELD_RE.sub( + rf"\g<1>{cls._NORMALIZED_VSTACK_VERSION}\g<2>", + payload, + count=1, + ) + return f"{match.group('prefix')}{normalized_payload}{match.group('suffix')}" + + return cls._VSTACK_META_COMMENT_RE.sub(_normalize_match, content) + + def _assert_matches_golden_fixture( + self, + *, + artifact_content: str, + expected_fixture: Path, + artifact_label: str, + ) -> None: + """Assert rendered output matches fixture bytes except VSTACK-META version variability.""" + normalized_actual = self._normalize_vstack_version_in_meta_comment(artifact_content) + normalized_expected = self._normalize_vstack_version_in_meta_comment( + expected_fixture.read_text(encoding="utf-8") + ) + + actual_bytes = normalized_actual.encode("utf-8") + expected_bytes = normalized_expected.encode("utf-8") + + if actual_bytes != expected_bytes: + diff = "\n".join( + difflib.unified_diff( + normalized_expected.splitlines(), + normalized_actual.splitlines(), + fromfile="expected fixture", + tofile="rendered output", + lineterm="", + ) + ) + pytest.fail( + f"Golden fixture drift detected for {artifact_label}.\n" + f"If intentional, update {expected_fixture.as_posix()}.\n" + f"{diff}" + ) + + def test_normalize_vstack_version_in_meta_comment_only_changes_version_value(self) -> None: + """Normalization should only rewrite vstack_version inside VSTACK-META payload.""" + content = ( + "line-before\n" + '\n' + "line-after\n" + ) + + normalized = self._normalize_vstack_version_in_meta_comment(content) + + assert '"artifact_name":"x"' in normalized + assert '"artifact_version":"1"' in normalized + assert f'"vstack_version":"{self._NORMALIZED_VSTACK_VERSION}"' in normalized + assert '"vstack_version":"0.0.0"' not in normalized + assert normalized.startswith("line-before\n") + assert normalized.endswith("line-after\n") + + def test_normalize_vstack_version_in_meta_comment_ignores_non_meta_occurrences(self) -> None: + """Normalization should not touch vstack_version outside VSTACK-META comments.""" + content = '{"vstack_version":"outside"}\n\n' + + normalized = self._normalize_vstack_version_in_meta_comment(content) + + assert normalized == content + def _make_skill_gen(self, tmp_path: Path) -> GenericArtifactGenerator: """Internal helper to make skill gen.""" return GenericArtifactGenerator(SKILL_TYPE, tmp_path / "templates") + def _assert_verify_failures( + self, + *, + result: ValidationResult, + expected_substrings: list[str], + ) -> None: + """Assert deterministic verification failures with specific message fragments.""" + fail_messages = [m.message for m in result.messages if m.level == "fail"] + assert not result.ok + assert fail_messages + for expected in expected_substrings: + assert any(expected in message for message in fail_messages), ( + f"Expected failure containing: {expected}\n" + f"Observed failures:\n" + "\n".join(fail_messages) + ) + def test_resolve_placeholders_replaces_known_token(self) -> None: """Test that resolve placeholders replaces known token.""" assert ( @@ -116,6 +217,142 @@ def test_parse_generation_metadata_returns_none_on_non_object(self) -> None: text = '' assert GenericArtifactGenerator.parse_generation_metadata(text) is None + def test_render_instruction_security_matches_golden_fixture( + self, + templates_root: Path, + instruction_fixture_path: Callable[[str], Path], + ) -> None: + """Security instruction rendering must remain byte-for-byte stable.""" + templates = templates_root + template_dir = templates / "instructions" / "security" + expected_fixture = instruction_fixture_path("security") + + artifact = GenericArtifactGenerator(INSTRUCTION_TYPE, templates).render(template_dir) + self._assert_matches_golden_fixture( + artifact_content=artifact.content, + expected_fixture=expected_fixture, + artifact_label="instruction/security", + ) + + def test_render_instruction_testing_matches_golden_fixture( + self, + templates_root: Path, + instruction_fixture_path: Callable[[str], Path], + ) -> None: + """Testing instruction rendering must remain byte-for-byte stable.""" + templates = templates_root + template_dir = templates / "instructions" / "testing" + expected_fixture = instruction_fixture_path("testing") + + artifact = GenericArtifactGenerator(INSTRUCTION_TYPE, templates).render(template_dir) + self._assert_matches_golden_fixture( + artifact_content=artifact.content, + expected_fixture=expected_fixture, + artifact_label="instruction/testing", + ) + + def test_render_skill_concise_matches_golden_fixture( + self, + templates_root: Path, + skill_fixture_path: Callable[[str], Path], + ) -> None: + """Concise skill rendering must remain byte-for-byte stable.""" + templates = templates_root + template_dir = templates / "skills" / "concise" + expected_fixture = skill_fixture_path("concise") + + artifact = GenericArtifactGenerator(SKILL_TYPE, templates).render(template_dir) + self._assert_matches_golden_fixture( + artifact_content=artifact.content, + expected_fixture=expected_fixture, + artifact_label="skill/concise", + ) + + def test_render_agent_planner_matches_golden_fixture( + self, + templates_root: Path, + agent_fixture_path: Callable[[str], Path], + ) -> None: + """Planner agent rendering must remain byte-for-byte stable.""" + templates = templates_root + template_dir = templates / "agents" / "planner" + expected_fixture = agent_fixture_path("planner") + + artifact = AgentGenerator(templates_root=templates).render(template_dir) + self._assert_matches_golden_fixture( + artifact_content=artifact.content, + expected_fixture=expected_fixture, + artifact_label="agent/planner", + ) + + def test_render_agent_product_matches_golden_fixture( + self, + templates_root: Path, + agent_fixture_path: Callable[[str], Path], + ) -> None: + """Product agent rendering must remain byte-for-byte stable.""" + templates = templates_root + template_dir = templates / "agents" / "product" + expected_fixture = agent_fixture_path("product") + + artifact = AgentGenerator(templates_root=templates).render(template_dir) + self._assert_matches_golden_fixture( + artifact_content=artifact.content, + expected_fixture=expected_fixture, + artifact_label="agent/product", + ) + + def test_render_prompt_code_review_matches_golden_fixture( + self, + templates_root: Path, + prompt_fixture_path: Callable[[str], Path], + ) -> None: + """Code-review prompt rendering must remain byte-for-byte stable.""" + templates = templates_root + template_dir = templates / "prompts" / "code-review" + expected_fixture = prompt_fixture_path("code-review") + + artifact = GenericArtifactGenerator(PROMPT_TYPE, templates).render(template_dir) + self._assert_matches_golden_fixture( + artifact_content=artifact.content, + expected_fixture=expected_fixture, + artifact_label="prompt/code-review", + ) + + def test_render_prompt_api_design_review_matches_golden_fixture( + self, + templates_root: Path, + prompt_fixture_path: Callable[[str], Path], + ) -> None: + """API-design-review prompt rendering must remain byte-for-byte stable.""" + templates = templates_root + template_dir = templates / "prompts" / "api-design-review" + expected_fixture = prompt_fixture_path("api-design-review") + + artifact = GenericArtifactGenerator(PROMPT_TYPE, templates).render(template_dir) + self._assert_matches_golden_fixture( + artifact_content=artifact.content, + expected_fixture=expected_fixture, + artifact_label="prompt/api-design-review", + ) + + def test_render_skill_verify_matches_golden_fixture( + self, + templates_root: Path, + skill_fixture_path: Callable[[str], Path], + ) -> None: + """Verify skill rendering must remain byte-for-byte stable.""" + templates = templates_root + template_dir = templates / "skills" / "verify" + expected_fixture = skill_fixture_path("verify") + + artifact = GenericArtifactGenerator(SKILL_TYPE, templates).render(template_dir) + self._assert_matches_golden_fixture( + artifact_content=artifact.content, + expected_fixture=expected_fixture, + artifact_label="skill/verify", + ) + def test_parse_generation_metadata_returns_none_when_loader_returns_non_dict( self, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -160,6 +397,30 @@ def test_verify_input_rejects_unknown_placeholder_when_registry_enabled( assert not result.ok assert any("unknown placeholder" in m.message for m in result.messages if m.level == "fail") + def test_defect_fixture_unknown_placeholder_reports_registered_failure( + self, tmp_path: Path + ) -> None: + """Defect fixture: unknown placeholder should fail with explicit registry message.""" + tmpl_dir = tmp_path / "templates" / "skills" / "vision" + tmpl_dir.mkdir(parents=True) + (tmp_path / "templates" / "skills" / "_partials").mkdir(parents=True) + (tmpl_dir / "template.md").write_text( + "---\nname: vision\nversion: 1.0.0\ndescription: d\n---\n{{UNKNOWN_TOKEN}}\n", + encoding="utf-8", + ) + cfg = type(SKILL_TYPE)( + **{**SKILL_TYPE.__dict__, "placeholders": {"SKILL_CONTEXT": "skill-context.md"}} + ) + gen = GenericArtifactGenerator(cfg, tmp_path / "templates") + result = gen.verify_input(expected_names=["vision"]) + + self._assert_verify_failures( + result=result, + expected_substrings=[ + "unknown placeholder '{{UNKNOWN_TOKEN}}'", + ], + ) + def test_verify_input_accepts_registered_placeholder_with_template_reference( self, tmp_path: Path ) -> None: @@ -204,6 +465,77 @@ def test_agent_verify_input_uses_schema_for_bool_field(self, tmp_path: Path) -> assert not result.ok assert any("user-invocable" in m.message for m in result.messages if m.level == "fail") + def test_defect_fixture_schema_violation_reports_field_name(self, tmp_path: Path) -> None: + """Defect fixture: schema violations should fail with field-specific feedback.""" + tmpl_dir = tmp_path / "templates" / "agents" / "writer" + tmpl_dir.mkdir(parents=True) + (tmpl_dir / "template.md").write_text("# writer\nbody\n", encoding="utf-8") + (tmpl_dir / "config.yaml").write_text( + "name: writer\ndescription: A writer agent\nuser-invocable: maybe\n", + encoding="utf-8", + ) + gen = GenericArtifactGenerator(AGENT_TYPE, tmp_path / "templates") + result = gen.verify_input(expected_names=["writer"]) + + self._assert_verify_failures( + result=result, + expected_substrings=[ + "user-invocable", + ], + ) + + def test_defect_fixture_missing_expected_template_reports_missing(self, tmp_path: Path) -> None: + """Defect fixture: missing expected template should report a MISSING failure.""" + result = self._make_skill_gen(tmp_path).verify_input(expected_names=["vision"]) + + self._assert_verify_failures( + result=result, + expected_substrings=[ + "MISSING", + ], + ) + + def test_defect_fixture_name_mismatch_in_metadata_reports_mismatch( + self, tmp_path: Path + ) -> None: + """Defect fixture: name mismatch should report a mismatch failure.""" + tmpl_dir = tmp_path / "templates" / "skills" / "vision" + tmpl_dir.mkdir(parents=True) + (tmp_path / "templates" / "skills" / "_partials").mkdir(parents=True) + (tmpl_dir / "template.md").write_text( + "---\nname: not-vision\nversion: 1.0.0\ndescription: d\n---\n{{SKILL_CONTEXT}}\n", + encoding="utf-8", + ) + result = self._make_skill_gen(tmp_path).verify_input(expected_names=["vision"]) + + self._assert_verify_failures( + result=result, + expected_substrings=[ + "name mismatch", + ], + ) + + def test_defect_fixture_required_field_missing_reports_required_field_message( + self, tmp_path: Path + ) -> None: + """Defect fixture: missing required metadata should report required fields.""" + cfg = type(SKILL_TYPE)(**{**SKILL_TYPE.__dict__, "frontmatter_schema": None}) + gen = GenericArtifactGenerator(cfg, tmp_path / "templates") + tmpl_dir = tmp_path / "templates" / "skills" / "vision" + tmpl_dir.mkdir(parents=True) + (tmp_path / "templates" / "skills" / "_partials").mkdir(parents=True, exist_ok=True) + (tmpl_dir / "template.md").write_text( + "---\nname: vision\n---\n{{SKILL_CONTEXT}}\n", encoding="utf-8" + ) + result = gen.verify_input(expected_names=["vision"]) + + self._assert_verify_failures( + result=result, + expected_substrings=[ + "MISSING required fields", + ], + ) + def test_render_raises_when_add_frontmatter_but_schema_missing(self, tmp_path: Path) -> None: """Test that render raises when add frontmatter but schema missing.""" cfg = AGENT_TYPE diff --git a/tests/vstack/cli/test_constants.py b/tests/vstack/cli/test_constants.py index 8f38b61b..41f4cc55 100644 --- a/tests/vstack/cli/test_constants.py +++ b/tests/vstack/cli/test_constants.py @@ -50,6 +50,8 @@ "k8s", "helm", "rancher", + "space-setup", + "copilot-ops", ] @@ -67,6 +69,7 @@ def test_expected_canonical_names_content(self) -> None: def test_expected_hook_input_names_content(self) -> None: """Verify source checks require the complete built-in hook set.""" assert EXPECTED_INPUT_NAMES["hook"] == [ + "agent-call-audit", "log-retention-cleanup", "post-edit-markdown-quality", "post-edit-format", diff --git a/tests/vstack/frontmatter/test_serializer.py b/tests/vstack/frontmatter/test_serializer.py index a28a9ebf..d2fba952 100644 --- a/tests/vstack/frontmatter/test_serializer.py +++ b/tests/vstack/frontmatter/test_serializer.py @@ -226,3 +226,14 @@ def test_instruction_schema_applyto_multi_glob_is_quoted(self) -> None: ) assert "applyTo: '**/*.{ts,tsx,js,jsx}'" in output assert "applyTo: **/*.{ts,tsx,js,jsx}" not in output + + def test_list_field_quotes_values_with_yaml_special_leading_characters(self) -> None: + """List items starting with YAML special chars are single-quoted.""" + schema = FrontmatterSchema([FieldSpec("tools", type="list")]) + output = FrontmatterSerializer().serialize( + {"tools": ["*expand", "&anchor", "!tag"]}, + schema, + ) + assert "- '*expand'" in output + assert "- '&anchor'" in output + assert "- '!tag'" in output