@haystackeditor/cli 0.25.1 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -455
- package/dist/capture/app-config.js +25 -1
- package/dist/commands/capture-brief.js +41 -32
- package/dist/commands/feedback.js +66 -0
- package/dist/commands/init-telemetry.js +53 -7
- package/dist/commands/init.js +7 -2
- package/dist/commands/lockfile-pin.js +307 -0
- package/dist/commands/verify.js +68 -13
- package/dist/index.js +27 -923
- package/dist/schema.js +4 -10
- package/dist/utils/haystack-api.js +0 -36
- package/package.json +1 -5
- package/schemas/feedback.v1.json +13 -0
- package/schemas/pre-verify.v2.json +240 -0
- package/schemas/verify-raw.v1.json +1132 -0
- package/schemas/verify.v2.json +655 -0
- package/dist/assets/hooks/agent-context/detect.ts +0 -316
- package/dist/assets/hooks/agent-context/format.ts +0 -100
- package/dist/assets/hooks/agent-context/index.ts +0 -41
- package/dist/assets/hooks/agent-context/parsers/claude.ts +0 -262
- package/dist/assets/hooks/agent-context/parsers/codex.ts +0 -416
- package/dist/assets/hooks/agent-context/parsers/gemini.ts +0 -155
- package/dist/assets/hooks/agent-context/parsers/opencode.ts +0 -174
- package/dist/assets/hooks/agent-context/tsconfig.json +0 -14
- package/dist/assets/hooks/agent-context/types.ts +0 -58
- package/dist/assets/hooks/llm-rules-template.md +0 -59
- package/dist/assets/hooks/package-lock.json +0 -598
- package/dist/assets/hooks/package.json +0 -12
- package/dist/assets/hooks/scripts/commit-msg.sh +0 -5
- package/dist/assets/hooks/scripts/post-commit.sh +0 -5
- package/dist/assets/hooks/scripts/pre-commit.sh +0 -175
- package/dist/assets/hooks/scripts/pre-push.sh +0 -25
- package/dist/assets/hooks/scripts/prepare-commit-msg.sh +0 -5
- package/dist/assets/hooks/truncation-checker/ast-analyzer.ts +0 -528
- package/dist/assets/hooks/truncation-checker/index.ts +0 -595
- package/dist/assets/hooks/truncation-checker/tsconfig.json +0 -13
- package/dist/assets/skills/map-cloud-verifier-universe/SKILL.md +0 -2051
- package/dist/assets/skills/map-cloud-verifier-universe/agents/openai.yaml +0 -4
- package/dist/assets/skills/map-cloud-verifier-universe/references/output-contract.md +0 -3411
- package/dist/assets/skills/map-your-system.md +0 -143
- package/dist/assets/skills/submit.md +0 -200
- package/dist/commands/ask.js +0 -20
- package/dist/commands/cloud-verifier-behaviors.js +0 -218
- package/dist/commands/cloud-verifier-data-store-census.js +0 -539
- package/dist/commands/cloud-verifier-data-store-drift.js +0 -158
- package/dist/commands/cloud-verifier-identity-census.js +0 -4060
- package/dist/commands/cloud-verifier-materialization.js +0 -704
- package/dist/commands/cloud-verifier-pascal-selector-census.js +0 -1382
- package/dist/commands/cloud-verifier-python-manifest-selector-census.js +0 -2015
- package/dist/commands/cloud-verifier-specialized-operational-census.js +0 -11432
- package/dist/commands/cloud-verifier-universe.js +0 -10178
- package/dist/commands/config.js +0 -549
- package/dist/commands/design-verify.js +0 -311
- package/dist/commands/dismiss.js +0 -159
- package/dist/commands/hooks.js +0 -226
- package/dist/commands/inbox.js +0 -137
- package/dist/commands/mcp.js +0 -201
- package/dist/commands/policy.js +0 -371
- package/dist/commands/pr-status.js +0 -207
- package/dist/commands/pr.js +0 -105
- package/dist/commands/prepare-universe-review.js +0 -1092
- package/dist/commands/production-source-deny-policy.js +0 -100
- package/dist/commands/request-review.js +0 -74
- package/dist/commands/review.js +0 -191
- package/dist/commands/rules.js +0 -98
- package/dist/commands/scaffold-provisional-universe.js +0 -806
- package/dist/commands/setup.js +0 -1170
- package/dist/commands/skills.js +0 -447
- package/dist/commands/submit.js +0 -745
- package/dist/commands/system-map.js +0 -228
- package/dist/commands/triage.js +0 -598
- package/dist/commands/webhooks.js +0 -241
- package/dist/states.js +0 -46
- package/dist/tools/detect.js +0 -832
- package/dist/triage/astra.js +0 -202
- package/dist/triage/prompts.js +0 -188
- package/dist/triage/runner.js +0 -200
- package/dist/triage/types.js +0 -7
- package/dist/utils/action-output.js +0 -26
- package/dist/utils/analysis-api.js +0 -416
- package/dist/utils/design-verifier-api.js +0 -294
- package/dist/utils/design-verifier-history.js +0 -79
- package/dist/utils/design-verifier-result.js +0 -424
- package/dist/utils/github-api.js +0 -324
- package/dist/utils/pending-state.js +0 -86
- package/dist/utils/pr-ref.js +0 -56
- package/dist/utils/prompter.js +0 -328
- package/schemas/action.v1.json +0 -22
- package/schemas/ask.v1.json +0 -40
- package/schemas/inbox.v1.json +0 -27
- package/schemas/pr-status.v1.json +0 -61
- package/schemas/pr.v1.json +0 -97
- package/schemas/pr.v3.json +0 -45
- package/schemas/setup.v1.json +0 -75
- package/schemas/submit.v1.json +0 -90
- package/schemas/triage.v1.json +0 -103
- package/schemas/triage.v2.json +0 -64
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
# Map Your System for Haystack QA
|
|
2
|
-
|
|
3
|
-
Create `.haystack/system.yml` — a short file of facts about how this
|
|
4
|
-
repository's system works, so Haystack QA can run it.
|
|
5
|
-
|
|
6
|
-
## Why this file exists
|
|
7
|
-
|
|
8
|
-
When Haystack reviews a risky PR, it doesn't just read the diff — it runs the
|
|
9
|
-
affected code in a sandbox and captures evidence. Haystack decides **what** to
|
|
10
|
-
test from each PR on its own. What it can't always infer is **how your system
|
|
11
|
-
works**: how to install and build it, how services boot, how to know they're
|
|
12
|
-
ready, how to seed data, how to act as a logged-in test user. That is what
|
|
13
|
-
this file provides. Every fact you add converts risks Haystack identified but
|
|
14
|
-
couldn't execute into checks that actually run.
|
|
15
|
-
|
|
16
|
-
## Hard rules
|
|
17
|
-
|
|
18
|
-
1. **Facts only — never test selection.** The file describes how the system
|
|
19
|
-
works. Keys like `scenarios`, `tests`, `skip`, or `focus` are rejected;
|
|
20
|
-
Haystack alone decides what to test.
|
|
21
|
-
2. **Only verified facts.** Run every command yourself before declaring it.
|
|
22
|
-
A wrong fact is worse than a missing one — Haystack tracks which declared
|
|
23
|
-
facts fail and will flag them on PRs.
|
|
24
|
-
3. **No secrets.** Reference environment variable *names* if needed, never
|
|
25
|
-
values. Anything that looks like a credential is rejected.
|
|
26
|
-
4. **Personas are seeded FAKE users** reachable through dev/test-only login
|
|
27
|
-
paths. Never real accounts, never customer data. If a dev login helper
|
|
28
|
-
exists, it must be disabled in production builds.
|
|
29
|
-
5. **Small and true beats big and aspirational.** Ten verified lines are more
|
|
30
|
-
useful than sixty guesses. Omit anything you could not verify.
|
|
31
|
-
|
|
32
|
-
## Steps
|
|
33
|
-
|
|
34
|
-
### 1. Inspect the repository
|
|
35
|
-
|
|
36
|
-
Determine, with evidence from the tree (lockfiles, manifests, scripts,
|
|
37
|
-
docker-compose, CI config, READMEs):
|
|
38
|
-
|
|
39
|
-
- package manager, install command, and any required build/codegen steps
|
|
40
|
-
- each runnable service: start command, port, and how "ready" is observable
|
|
41
|
-
(health endpoint, log line, or a command that exits 0)
|
|
42
|
-
- how to seed and reset local/test data
|
|
43
|
-
- how existing tests authenticate — is there a seeded test user or dev login?
|
|
44
|
-
- the test runner and any test helpers/factories generated tests should reuse
|
|
45
|
-
- external services the code calls, and how development runs without hitting
|
|
46
|
-
them for real (mock fixture, log driver, sandbox mode)
|
|
47
|
-
- where runtime logs are written
|
|
48
|
-
|
|
49
|
-
### 2. Verify each fact by running it
|
|
50
|
-
|
|
51
|
-
From a clean state where possible: run the install, run the build, boot each
|
|
52
|
-
service and confirm its readiness signal, run the seed command, run the login
|
|
53
|
-
helper. Record the exact commands that worked. Drop anything you could not
|
|
54
|
-
make work — do not declare it with a caveat.
|
|
55
|
-
|
|
56
|
-
### 3. Ask the developer only what you cannot determine
|
|
57
|
-
|
|
58
|
-
Typical questions worth asking a human: "Is there a dev-only way to log in as
|
|
59
|
-
a test user?", "Which services must be running for the API to work?", "Is
|
|
60
|
-
there a canonical seed script?". Keep it to the few facts you genuinely
|
|
61
|
-
cannot infer or verify.
|
|
62
|
-
|
|
63
|
-
### 4. Write `.haystack/system.yml`
|
|
64
|
-
|
|
65
|
-
```yaml
|
|
66
|
-
version: 1
|
|
67
|
-
|
|
68
|
-
environment:
|
|
69
|
-
setup:
|
|
70
|
-
- run: "pnpm install --frozen-lockfile"
|
|
71
|
-
- run: "pnpm codegen"
|
|
72
|
-
rationale: "generated types are imported throughout"
|
|
73
|
-
toolchain: { node: "20", pnpm: "10" }
|
|
74
|
-
|
|
75
|
-
services:
|
|
76
|
-
api:
|
|
77
|
-
root: "apps/api" # optional
|
|
78
|
-
boot: "pnpm dev"
|
|
79
|
-
ready: { http: "http://localhost:8080/health" } # or { log_pattern: "listening" } or { command: "pg_isready" }
|
|
80
|
-
env: { APP_ENV: "test" }
|
|
81
|
-
depends_on: [postgres]
|
|
82
|
-
|
|
83
|
-
data:
|
|
84
|
-
seed: "pnpm db:seed --profile=qa"
|
|
85
|
-
reset: "pnpm db:reset"
|
|
86
|
-
personas:
|
|
87
|
-
admin:
|
|
88
|
-
login: "pnpm test:login admin"
|
|
89
|
-
yields: "prints an Authorization header on stdout"
|
|
90
|
-
|
|
91
|
-
observability:
|
|
92
|
-
logs: ["apps/api/log/dev.log"]
|
|
93
|
-
inspect_db: "pnpm db:console --readonly"
|
|
94
|
-
|
|
95
|
-
external_services:
|
|
96
|
-
stripe:
|
|
97
|
-
offline: "repo ships a stripe-mock fixture used by tests"
|
|
98
|
-
must_not_touch_live: true
|
|
99
|
-
|
|
100
|
-
testing:
|
|
101
|
-
runner: "pnpm vitest"
|
|
102
|
-
probe_helpers: "tests/helpers/ has request builders and DB factories"
|
|
103
|
-
|
|
104
|
-
constraints:
|
|
105
|
-
- "full build takes ~6 min; apps/api boots standalone without the web build"
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
Every section is optional — include only what you verified. All commands run
|
|
109
|
-
from the repo root unless a `cwd`/`root` says otherwise.
|
|
110
|
-
|
|
111
|
-
### 5. Validate before opening the PR
|
|
112
|
-
|
|
113
|
-
Run the validator — it applies the same rules Haystack QA applies:
|
|
114
|
-
|
|
115
|
-
```bash
|
|
116
|
-
npx @haystackeditor/cli system-map validate
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
Fix every reported error, then confirm the things the validator cannot check:
|
|
120
|
-
|
|
121
|
-
- [ ] every `run`/`boot`/`login`/`seed` command was executed and worked
|
|
122
|
-
- [ ] every service has a `ready` signal you observed
|
|
123
|
-
- [ ] personas are seeded fake users via dev/test-only paths
|
|
124
|
-
|
|
125
|
-
Haystack re-validates the file on its next QA run and reports precise errors
|
|
126
|
-
if something is wrong.
|
|
127
|
-
|
|
128
|
-
### 6. Open a small, reviewable PR
|
|
129
|
-
|
|
130
|
-
Add only `.haystack/system.yml` (plus a dev login helper if one was genuinely
|
|
131
|
-
needed — dev/test-gated, with a test proving it's off in production). In the
|
|
132
|
-
PR description, list which facts you verified by running them and which came
|
|
133
|
-
from the developer's answers.
|
|
134
|
-
|
|
135
|
-
## What happens next
|
|
136
|
-
|
|
137
|
-
Haystack treats each entry as a claim. When a QA run executes a declared
|
|
138
|
-
command successfully, the fact is marked **verified** with the run's receipt;
|
|
139
|
-
if it fails at a newer commit it is marked **broken**, and Haystack will ask
|
|
140
|
-
about it on a PR instead of trusting it. When QA hits something the map
|
|
141
|
-
doesn't cover, it asks at most a couple of targeted questions on the affected
|
|
142
|
-
PR, each with a ready-to-paste snippet — answering them grows this file over
|
|
143
|
-
time.
|
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
# Submit PR via Haystack
|
|
2
|
-
|
|
3
|
-
**Your job**: Submit your changes as a PR using `haystack submit` instead of `gh pr create` or GitHub API.
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## Why Use Haystack Submit?
|
|
8
|
-
|
|
9
|
-
When you create PRs through Haystack, the CLI keeps PR creation inside Haystack's analysis workflow:
|
|
10
|
-
|
|
11
|
-
1. **Runs pre-PR triage** on your current branch
|
|
12
|
-
2. **Pushes that branch** to origin
|
|
13
|
-
3. **Creates the PR** on GitHub
|
|
14
|
-
4. **Waits for Haystack analysis** by default
|
|
15
|
-
5. **Applies auto-merge** only if the repo has it enabled in `.haystack.json`
|
|
16
|
-
|
|
17
|
-
---
|
|
18
|
-
|
|
19
|
-
## Command Reference
|
|
20
|
-
|
|
21
|
-
```bash
|
|
22
|
-
# Default: Submit current branch and create the PR
|
|
23
|
-
haystack submit
|
|
24
|
-
|
|
25
|
-
# Optional alpha auto-fix for straightforward mechanical issues
|
|
26
|
-
haystack submit --auto-fix
|
|
27
|
-
|
|
28
|
-
# Request Haystack review (for complex/risky changes)
|
|
29
|
-
haystack submit --review
|
|
30
|
-
|
|
31
|
-
# With custom title
|
|
32
|
-
haystack submit --title "Fix authentication bug"
|
|
33
|
-
|
|
34
|
-
# Target different base branch
|
|
35
|
-
haystack submit --base develop
|
|
36
|
-
|
|
37
|
-
# With markdown PR body (write to file first to preserve formatting)
|
|
38
|
-
cat > /tmp/pr-body.md << 'EOF'
|
|
39
|
-
## Summary
|
|
40
|
-
- Fixed authentication bug in session handling
|
|
41
|
-
- Added integration tests for token refresh
|
|
42
|
-
|
|
43
|
-
## Test plan
|
|
44
|
-
- [x] Auth flow works end-to-end
|
|
45
|
-
EOF
|
|
46
|
-
haystack submit --body-file /tmp/pr-body.md
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
> **Tip**: Always use `--body-file` instead of `--body` when the description contains markdown.
|
|
50
|
-
> The `--body` flag is a single CLI argument that mangles newlines and special characters (`#`, `*`, etc.).
|
|
51
|
-
|
|
52
|
-
---
|
|
53
|
-
|
|
54
|
-
## When to Use Each Mode
|
|
55
|
-
|
|
56
|
-
### Default Submit
|
|
57
|
-
|
|
58
|
-
Use for straightforward changes that don't need explicit human review:
|
|
59
|
-
|
|
60
|
-
- Bug fixes with clear scope
|
|
61
|
-
- Small features with tests
|
|
62
|
-
- Refactoring with no behavior change
|
|
63
|
-
- Documentation updates
|
|
64
|
-
- Dependency updates
|
|
65
|
-
|
|
66
|
-
```bash
|
|
67
|
-
haystack submit
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
If the repo has `preferences.auto_merge` enabled in `.haystack.json`, the PR will also enter the auto-merge queue.
|
|
71
|
-
|
|
72
|
-
### Auto-Fix (Alpha, Discouraged)
|
|
73
|
-
|
|
74
|
-
> **Do NOT default to `--auto-fix`.** It is an alpha feature.
|
|
75
|
-
> Use it only when the user explicitly wants to opt in, or when they are
|
|
76
|
-
> comfortable using the sandbox fixer for clearly straightforward mechanical issues.
|
|
77
|
-
|
|
78
|
-
Use `--auto-fix` only for issues like:
|
|
79
|
-
|
|
80
|
-
- Missing null checks
|
|
81
|
-
- Obvious typos or lint fixes
|
|
82
|
-
- Simple deterministic bugs with one clear fix
|
|
83
|
-
|
|
84
|
-
```bash
|
|
85
|
-
haystack submit --auto-fix
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
Repos that want auto-fix on by default for every `haystack submit` can opt
|
|
89
|
-
in via `.haystack.json`:
|
|
90
|
-
|
|
91
|
-
```json
|
|
92
|
-
{
|
|
93
|
-
"preferences": {
|
|
94
|
-
"auto_fix": true
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
When set, `haystack submit` applies the `haystack:auto-fix` label without
|
|
100
|
-
needing the explicit flag. Auto-fix is still alpha — use the repo-level
|
|
101
|
-
opt-in only when the team is comfortable with the sandbox fixer running on
|
|
102
|
-
mechanical findings. `haystack submit --no-auto-fix` overrides the repo
|
|
103
|
-
default for a single submission.
|
|
104
|
-
|
|
105
|
-
### Review Mode
|
|
106
|
-
|
|
107
|
-
> **Do NOT use `--review` unless the user explicitly asks for human review.**
|
|
108
|
-
> Plain `haystack submit` already runs full Haystack analysis. Using `--review`
|
|
109
|
-
> blocks auto-merge when auto-merge is enabled, which delays merging unnecessarily.
|
|
110
|
-
|
|
111
|
-
Use `--review` only when the user specifically requests human review:
|
|
112
|
-
|
|
113
|
-
- User says "get someone to review this" or "needs human eyes"
|
|
114
|
-
- User explicitly passes `--review` in their instructions
|
|
115
|
-
|
|
116
|
-
```bash
|
|
117
|
-
haystack submit --review
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
---
|
|
121
|
-
|
|
122
|
-
## Workflow
|
|
123
|
-
|
|
124
|
-
1. **Make your changes** on a feature branch
|
|
125
|
-
2. **Commit** your changes: `git add . && git commit -m "..."`
|
|
126
|
-
3. **Submit** via Haystack: `haystack submit`
|
|
127
|
-
|
|
128
|
-
Haystack will:
|
|
129
|
-
- Push your current branch to origin
|
|
130
|
-
- Create the PR with appropriate labels
|
|
131
|
-
- Trigger analysis automatically
|
|
132
|
-
|
|
133
|
-
---
|
|
134
|
-
|
|
135
|
-
## Prerequisites
|
|
136
|
-
|
|
137
|
-
Before using `haystack submit`, ensure you're logged in:
|
|
138
|
-
|
|
139
|
-
```bash
|
|
140
|
-
haystack login
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
This uses GitHub device flow - no tokens to copy/paste.
|
|
144
|
-
|
|
145
|
-
---
|
|
146
|
-
|
|
147
|
-
## Important Notes
|
|
148
|
-
|
|
149
|
-
- **Always use `haystack submit`** instead of `gh pr create` when working in Haystack-enabled repos
|
|
150
|
-
- Prefer plain `haystack submit` by default; use `--auto-fix` only as an explicit alpha opt-in for straightforward mechanical issues
|
|
151
|
-
- The command uses your current branch as the PR head
|
|
152
|
-
- Labels are applied automatically based on your chosen mode
|
|
153
|
-
- You don't need to push manually - the command handles it
|
|
154
|
-
|
|
155
|
-
## Close the Loop After Submitting
|
|
156
|
-
|
|
157
|
-
After `haystack submit` creates the PR, **run `haystack triage <pr>` before ending your turn.**
|
|
158
|
-
It blocks until Haystack's full analysis completes (waking instantly on the completion event),
|
|
159
|
-
then prints the rating, findings, and a per-finding `agentFixPrompt`.
|
|
160
|
-
|
|
161
|
-
- **Findings marked auto-fixing**: leave them alone — Haystack's fixer is already on them
|
|
162
|
-
- **Actionable findings**: address them (the `agentFixPrompt` is written for you), commit, and
|
|
163
|
-
push — analysis re-runs on the new commit
|
|
164
|
-
- **Good to merge**: report the rating to your user and stop
|
|
165
|
-
|
|
166
|
-
Don't end the session right after submit without checking — the verdict usually lands within a
|
|
167
|
-
few minutes, and acting on it immediately is much cheaper than a human bouncing the PR back.
|
|
168
|
-
|
|
169
|
-
---
|
|
170
|
-
|
|
171
|
-
## Example Session
|
|
172
|
-
|
|
173
|
-
```
|
|
174
|
-
Agent: I've fixed the authentication bug. Let me submit this as a PR.
|
|
175
|
-
|
|
176
|
-
$ haystack submit --title "Fix session expiry handling"
|
|
177
|
-
|
|
178
|
-
Haystack Submit
|
|
179
|
-
|
|
180
|
-
✓ Authenticated
|
|
181
|
-
✓ Repository: acme/webapp
|
|
182
|
-
Current branch: fix-auth
|
|
183
|
-
Base branch: main
|
|
184
|
-
✓ Branch pushed
|
|
185
|
-
✓ Pull request created: #142
|
|
186
|
-
✓ Auto-merge enabled for this PR
|
|
187
|
-
|
|
188
|
-
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
189
|
-
|
|
190
|
-
Pull Request Created!
|
|
191
|
-
|
|
192
|
-
URL: https://github.com/acme/webapp/pull/142
|
|
193
|
-
Title: Fix session expiry handling
|
|
194
|
-
Branch: fix-auth → main
|
|
195
|
-
Merge: Auto-merge if safe
|
|
196
|
-
|
|
197
|
-
Haystack will analyze this PR and:
|
|
198
|
-
• Auto-merge if approved
|
|
199
|
-
• Route for review if issues found
|
|
200
|
-
```
|
package/dist/commands/ask.js
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
import { loadToken } from '../utils/auth.js';
|
|
3
|
-
import { postAskHaystack } from '../utils/haystack-api.js';
|
|
4
|
-
import { parsePrRef } from '../utils/pr-ref.js';
|
|
5
|
-
export async function fetchAskPayload(ref, question, options) {
|
|
6
|
-
const pr = parsePrRef(ref, 'ask');
|
|
7
|
-
const token = await loadToken({ owner: pr.owner, repo: pr.repo });
|
|
8
|
-
if (!token)
|
|
9
|
-
throw new Error('Not authenticated. Run `haystack login` first.');
|
|
10
|
-
return postAskHaystack(pr.owner, pr.repo, pr.prNumber, question, options.session, token);
|
|
11
|
-
}
|
|
12
|
-
export async function askHaystackCommand(ref, question, options) {
|
|
13
|
-
const response = await fetchAskPayload(ref, question, options);
|
|
14
|
-
if (options.json) {
|
|
15
|
-
console.log(JSON.stringify(response, null, 2));
|
|
16
|
-
return;
|
|
17
|
-
}
|
|
18
|
-
console.log(`\n${response.answer}\n`);
|
|
19
|
-
console.log(chalk.dim(`${response.evidence.length} source${response.evidence.length === 1 ? '' : 's'} consulted · session ${response.session_id}\n`));
|
|
20
|
-
}
|
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
import { lstatSync, readFileSync } from 'node:fs';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import chalk from 'chalk';
|
|
5
|
-
import { parseDocument } from 'yaml';
|
|
6
|
-
import { findGitRoot } from '../utils/hooks.js';
|
|
7
|
-
// Keep behavior validation on the same bounded artifact envelope as universe
|
|
8
|
-
// validation. Large maps must not need a lossy reference projection merely to
|
|
9
|
-
// validate their exact interface and evidence joins.
|
|
10
|
-
export const MAX_BEHAVIOR_ARTIFACT_BYTES = 32 * 1024 * 1024;
|
|
11
|
-
const GIT_COMMIT = /^[0-9a-f]{40}$/;
|
|
12
|
-
const STABLE_UNIVERSE_ID = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
13
|
-
const REPOSITORY = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
|
|
14
|
-
const RUNTIME_ID = /^(?:action|symbol|boundary)-[0-9a-f]{24}$/;
|
|
15
|
-
const BOUNDARY_KINDS = new Set(['response', 'state', 'event', 'external', 'ui', 'cli', 'job']);
|
|
16
|
-
const TRIGGER_KINDS = new Set(['http', 'event', 'job', 'cli', 'ui']);
|
|
17
|
-
function stableIdentity(prefix, parts) {
|
|
18
|
-
const framed = parts.map(part => `${Buffer.byteLength(part, 'utf8')}:${part}`).join('|');
|
|
19
|
-
return `${prefix}-${createHash('sha256').update(framed).digest('hex').slice(0, 24)}`;
|
|
20
|
-
}
|
|
21
|
-
function normalizeRepository(repository) {
|
|
22
|
-
if (!REPOSITORY.test(repository))
|
|
23
|
-
throw new Error('must be a canonical owner/repository identity');
|
|
24
|
-
return repository.toLowerCase();
|
|
25
|
-
}
|
|
26
|
-
function normalizePath(sourcePath) {
|
|
27
|
-
const normalized = sourcePath.replace(/\\/g, '/').replace(/^\.\//, '').replace(/^\/+/, '');
|
|
28
|
-
if (!normalized || normalized.split('/').some(part => !part || part === '..')) {
|
|
29
|
-
throw new Error('must be a normalized repository-relative source path');
|
|
30
|
-
}
|
|
31
|
-
return normalized;
|
|
32
|
-
}
|
|
33
|
-
export function derivedBehaviorActionId(repository, trigger) {
|
|
34
|
-
let identity;
|
|
35
|
-
if (trigger?.kind === 'http') {
|
|
36
|
-
if (typeof trigger.method !== 'string' || typeof trigger.route_template !== 'string') {
|
|
37
|
-
throw new Error('HTTP trigger requires method and route_template');
|
|
38
|
-
}
|
|
39
|
-
identity = `${trigger.method.toUpperCase()} ${trigger.route_template}`;
|
|
40
|
-
}
|
|
41
|
-
else if (trigger?.kind === 'event') {
|
|
42
|
-
if (typeof trigger.channel_identity !== 'string' || typeof trigger.event_type !== 'string') {
|
|
43
|
-
throw new Error('event trigger requires channel_identity and event_type');
|
|
44
|
-
}
|
|
45
|
-
identity = `${trigger.channel_identity}\0${trigger.event_type}`;
|
|
46
|
-
}
|
|
47
|
-
else if (trigger?.kind === 'job')
|
|
48
|
-
identity = trigger.job_identity;
|
|
49
|
-
else if (trigger?.kind === 'cli')
|
|
50
|
-
identity = trigger.command_identity;
|
|
51
|
-
else if (trigger?.kind === 'ui')
|
|
52
|
-
identity = trigger.action_identity;
|
|
53
|
-
else
|
|
54
|
-
throw new Error('trigger kind must be http, event, job, cli, or ui');
|
|
55
|
-
if (typeof identity !== 'string' || !identity.trim())
|
|
56
|
-
throw new Error('trigger identity must be non-empty');
|
|
57
|
-
return stableIdentity('action', [normalizeRepository(repository), trigger.kind, identity]);
|
|
58
|
-
}
|
|
59
|
-
export function derivedBehaviorSymbolId(repository, sourcePath, qualifiedName) {
|
|
60
|
-
if (typeof qualifiedName !== 'string' || !qualifiedName.trim())
|
|
61
|
-
throw new Error('qualified_name must be non-empty');
|
|
62
|
-
return stableIdentity('symbol', [normalizeRepository(repository), normalizePath(sourcePath), qualifiedName.trim()]);
|
|
63
|
-
}
|
|
64
|
-
export function derivedBehaviorBoundaryId(repository, kind, identityKey) {
|
|
65
|
-
if (!BOUNDARY_KINDS.has(kind))
|
|
66
|
-
throw new Error('boundary kind is unsupported');
|
|
67
|
-
if (typeof identityKey !== 'string' || !identityKey.trim())
|
|
68
|
-
throw new Error('identity_key must be non-empty');
|
|
69
|
-
return stableIdentity('boundary', [normalizeRepository(repository), kind, identityKey.trim()]);
|
|
70
|
-
}
|
|
71
|
-
function parseYaml(text, name) {
|
|
72
|
-
const document = parseDocument(text, { prettyErrors: false });
|
|
73
|
-
if (document.errors.length)
|
|
74
|
-
throw new Error(`${name} is malformed YAML: ${document.errors[0].message}`);
|
|
75
|
-
const value = document.toJS({ maxAliasCount: 50 });
|
|
76
|
-
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
77
|
-
throw new Error(`${name} must contain a mapping`);
|
|
78
|
-
return value;
|
|
79
|
-
}
|
|
80
|
-
export function validateBehaviorArtifact(value, references) {
|
|
81
|
-
const issues = [];
|
|
82
|
-
const fail = (path, message) => issues.push({ path, message });
|
|
83
|
-
if (value?.version !== 1)
|
|
84
|
-
fail('version', 'must equal 1');
|
|
85
|
-
if (value?.schema_version !== 'haystack-behavior-map-v1') {
|
|
86
|
-
fail('schema_version', 'must equal haystack-behavior-map-v1');
|
|
87
|
-
}
|
|
88
|
-
let repository = '';
|
|
89
|
-
try {
|
|
90
|
-
repository = normalizeRepository(value?.repository);
|
|
91
|
-
}
|
|
92
|
-
catch (error) {
|
|
93
|
-
fail('repository', error.message);
|
|
94
|
-
}
|
|
95
|
-
if (!GIT_COMMIT.test(value?.source_commit || ''))
|
|
96
|
-
fail('source_commit', 'must be an exact Git commit');
|
|
97
|
-
const universeCommit = references.universe?.map_revision?.source_commit;
|
|
98
|
-
if (value?.source_commit !== universeCommit)
|
|
99
|
-
fail('source_commit', 'must exactly match universe.yml map_revision.source_commit');
|
|
100
|
-
const interfaceIds = new Set((Array.isArray(references.universe?.interfaces)
|
|
101
|
-
? references.universe.interfaces : []).map((item) => item?.id));
|
|
102
|
-
const evidenceIds = new Set((Array.isArray(references.evidence?.evidence)
|
|
103
|
-
? references.evidence.evidence : []).map((item) => item?.id));
|
|
104
|
-
if (!Array.isArray(value?.actions) || value.actions.length === 0) {
|
|
105
|
-
fail('actions', 'must contain at least one meaningful customer action');
|
|
106
|
-
return { version: 'haystack-behavior-validation-v1', status: 'invalid', issues, summary: { actions: 0, boundaries: 0 } };
|
|
107
|
-
}
|
|
108
|
-
const actionIds = new Set();
|
|
109
|
-
const triggerIds = new Set();
|
|
110
|
-
let boundaryCount = 0;
|
|
111
|
-
for (const [index, action] of value.actions.entries()) {
|
|
112
|
-
const path = `actions[${index}]`;
|
|
113
|
-
if (!action || typeof action !== 'object' || Array.isArray(action)) {
|
|
114
|
-
fail(path, 'must be a mapping');
|
|
115
|
-
continue;
|
|
116
|
-
}
|
|
117
|
-
let expectedAction = '';
|
|
118
|
-
try {
|
|
119
|
-
expectedAction = derivedBehaviorActionId(repository || value.repository, action.trigger);
|
|
120
|
-
}
|
|
121
|
-
catch (error) {
|
|
122
|
-
fail(`${path}.trigger`, error.message);
|
|
123
|
-
}
|
|
124
|
-
if (!RUNTIME_ID.test(action.id || '') || action.id !== expectedAction)
|
|
125
|
-
fail(`${path}.id`, `must equal derived exact id ${expectedAction}`);
|
|
126
|
-
if (actionIds.has(action.id))
|
|
127
|
-
fail(`${path}.id`, 'must be unique');
|
|
128
|
-
actionIds.add(action.id);
|
|
129
|
-
if (!TRIGGER_KINDS.has(action.trigger?.kind))
|
|
130
|
-
fail(`${path}.trigger.kind`, 'is unsupported');
|
|
131
|
-
if (triggerIds.has(expectedAction))
|
|
132
|
-
fail(`${path}.trigger`, 'duplicates another structured trigger');
|
|
133
|
-
triggerIds.add(expectedAction);
|
|
134
|
-
if (!STABLE_UNIVERSE_ID.test(action.interface_id || '') || !interfaceIds.has(action.interface_id)) {
|
|
135
|
-
fail(`${path}.interface_id`, 'must reference an exact universe.yml interface id');
|
|
136
|
-
}
|
|
137
|
-
let expectedSymbol = '';
|
|
138
|
-
try {
|
|
139
|
-
expectedSymbol = derivedBehaviorSymbolId(repository || value.repository, action.handler?.source_path, action.handler?.qualified_name);
|
|
140
|
-
}
|
|
141
|
-
catch (error) {
|
|
142
|
-
fail(`${path}.handler`, error.message);
|
|
143
|
-
}
|
|
144
|
-
if (action.handler?.symbol_id !== expectedSymbol)
|
|
145
|
-
fail(`${path}.handler.symbol_id`, `must equal derived exact id ${expectedSymbol}`);
|
|
146
|
-
if (!Array.isArray(action.observable_boundaries) || action.observable_boundaries.length === 0) {
|
|
147
|
-
fail(`${path}.observable_boundaries`, 'must contain a customer-relevant boundary');
|
|
148
|
-
}
|
|
149
|
-
else {
|
|
150
|
-
const boundaryIds = new Set();
|
|
151
|
-
for (const [boundaryIndex, boundary] of action.observable_boundaries.entries()) {
|
|
152
|
-
boundaryCount += 1;
|
|
153
|
-
const boundaryPath = `${path}.observable_boundaries[${boundaryIndex}]`;
|
|
154
|
-
let expectedBoundary = '';
|
|
155
|
-
try {
|
|
156
|
-
expectedBoundary = derivedBehaviorBoundaryId(repository || value.repository, boundary?.kind, boundary?.identity_key);
|
|
157
|
-
}
|
|
158
|
-
catch (error) {
|
|
159
|
-
fail(boundaryPath, error.message);
|
|
160
|
-
}
|
|
161
|
-
if (boundary?.id !== expectedBoundary)
|
|
162
|
-
fail(`${boundaryPath}.id`, `must equal derived exact id ${expectedBoundary}`);
|
|
163
|
-
if (boundaryIds.has(boundary?.id))
|
|
164
|
-
fail(`${boundaryPath}.id`, 'must be unique in the action');
|
|
165
|
-
boundaryIds.add(boundary?.id);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
if (!Array.isArray(action.evidence_ids) || action.evidence_ids.length === 0) {
|
|
169
|
-
fail(`${path}.evidence_ids`, 'must contain exact repository evidence');
|
|
170
|
-
}
|
|
171
|
-
else {
|
|
172
|
-
for (const evidenceId of action.evidence_ids) {
|
|
173
|
-
if (!evidenceIds.has(evidenceId))
|
|
174
|
-
fail(`${path}.evidence_ids`, `references unknown evidence id ${String(evidenceId)}`);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
return {
|
|
179
|
-
version: 'haystack-behavior-validation-v1',
|
|
180
|
-
status: issues.length ? 'invalid' : 'valid',
|
|
181
|
-
issues,
|
|
182
|
-
summary: { actions: value.actions.length, boundaries: boundaryCount },
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
function readArtifact(root, name) {
|
|
186
|
-
const path = join(root, '.haystack', 'cloud-verifier', name);
|
|
187
|
-
const metadata = lstatSync(path, { throwIfNoEntry: false });
|
|
188
|
-
if (!metadata || !metadata.isFile() || metadata.isSymbolicLink())
|
|
189
|
-
throw new Error(`${name} must be a regular non-symlink file`);
|
|
190
|
-
if (metadata.size > MAX_BEHAVIOR_ARTIFACT_BYTES) {
|
|
191
|
-
throw new Error(`${name} exceeds ${MAX_BEHAVIOR_ARTIFACT_BYTES} bytes`);
|
|
192
|
-
}
|
|
193
|
-
return readFileSync(path, 'utf8');
|
|
194
|
-
}
|
|
195
|
-
export function resolveBehaviorArtifactRoot(gitRoot, currentDirectory = process.cwd()) {
|
|
196
|
-
return gitRoot ?? currentDirectory;
|
|
197
|
-
}
|
|
198
|
-
export async function cloudVerifierBehaviorsValidateCommand(options) {
|
|
199
|
-
// Source-only evaluation archives deliberately omit Git metadata. The
|
|
200
|
-
// behavior validator only reads verifier artifacts, so current-directory
|
|
201
|
-
// fallback is both sufficient and consistent with validate-universe.
|
|
202
|
-
const root = resolveBehaviorArtifactRoot(findGitRoot());
|
|
203
|
-
const value = parseYaml(readArtifact(root, 'behaviors.yml'), 'behaviors.yml');
|
|
204
|
-
const universe = parseYaml(readArtifact(root, 'universe.yml'), 'universe.yml');
|
|
205
|
-
const evidence = parseYaml(readArtifact(root, 'evidence.yml'), 'evidence.yml');
|
|
206
|
-
const result = validateBehaviorArtifact(value, { universe, evidence });
|
|
207
|
-
if (options.json)
|
|
208
|
-
console.log(JSON.stringify(result, null, 2));
|
|
209
|
-
else if (result.status === 'valid') {
|
|
210
|
-
console.log(chalk.green(`Behavior map valid: ${result.summary.actions} actions, ${result.summary.boundaries} boundaries`));
|
|
211
|
-
}
|
|
212
|
-
else {
|
|
213
|
-
for (const issue of result.issues)
|
|
214
|
-
console.error(chalk.red(`behaviors.yml:${issue.path}: ${issue.message}`));
|
|
215
|
-
}
|
|
216
|
-
if (result.status !== 'valid')
|
|
217
|
-
process.exitCode = 1;
|
|
218
|
-
}
|