@haystackeditor/cli 0.15.14 → 0.15.15

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/dist/index.js CHANGED
@@ -41,6 +41,7 @@ import { setupCommand } from './commands/setup.js';
41
41
  import { schemaCommand, listSchemas } from './commands/schema-cmd.js';
42
42
  import { registerWebhook, listWebhooks, rotateWebhookSecret, setWebhookEnabled, listDeliveries, replayDelivery, } from './commands/webhooks.js';
43
43
  import { editRulesCommand, validateRulesCommand } from './commands/rules.js';
44
+ import { verificationInitCommand, verificationValidateCommand, verificationPreflightCommand, verificationRunCommand, verificationCollectCommand, verificationCheckSafetyCommand, } from './commands/verification.js';
44
45
  import { headlessLoginCommand, headlessLogoutCommand, listTokensCommand, revokeTokenCommand, } from './commands/tokens.js';
45
46
  // `mcp` is imported lazily inside its command action (below), NOT at the top level.
46
47
  // It is the only module that pulls in `@modelcontextprotocol/sdk`; keeping it out of
@@ -817,6 +818,135 @@ rules
817
818
  process.exit(1);
818
819
  }
819
820
  });
821
+ // ─── verification ────────────────────────────────────────────────────────────
822
+ //
823
+ // Repo-local verification contract (.haystack/verification.yml): how to boot
824
+ // the app, prove it's ready, log in as test personas, run scenarios, and
825
+ // collect evidence into a standard artifact manifest + review packet.
826
+ // See docs/VERIFICATION-CONTRACT.md.
827
+ const verification = program
828
+ .command('verification')
829
+ .description('Repo verification contract — validate, run scenarios, collect evidence');
830
+ verification
831
+ .command('init')
832
+ .description('Scaffold .haystack/verification.yml, preflight/smoke scripts, and the agent skill')
833
+ .option('-f, --force', 'Overwrite existing files')
834
+ .option('--skill', 'Only write the install-verification agent skill (hand the integration to a coding agent)')
835
+ .addHelpText('after', `
836
+ Creates:
837
+ .haystack/verification.yml the verification contract
838
+ scripts/haystack-preflight app-is-ready check
839
+ scripts/haystack-smoke boot + capture-evidence smoke scenario
840
+ .haystack/AGENTS.md how agents should use the contract
841
+ .haystack/skills/install-verification/SKILL.md skill for completing the integration
842
+
843
+ Detection fills in framework, package manager, dev command, port, and any
844
+ auth-bypass env var found in the repo's own env examples. Review the output —
845
+ detection is a starting point, not truth.
846
+ `)
847
+ .action(async (opts) => {
848
+ try {
849
+ await verificationInitCommand(opts);
850
+ }
851
+ catch (err) {
852
+ console.error(chalk.red('init failed:'), err instanceof Error ? err.message : err);
853
+ process.exit(1);
854
+ }
855
+ });
856
+ verification
857
+ .command('validate')
858
+ .description('Check whether the repo satisfies the verification contract')
859
+ .option('--json', 'Machine-readable report')
860
+ .addHelpText('after', `
861
+ Readiness verdicts:
862
+ ready all checks pass (exit 0)
863
+ partially_ready usable, with gaps (exit 0)
864
+ not_ready contract missing/invalid or commands broken (exit 1)
865
+ unsafe contract permits real side effects, or login helpers
866
+ lack a production-disabled proof (exit 2)
867
+ `)
868
+ .action(async (opts) => {
869
+ try {
870
+ await verificationValidateCommand(opts);
871
+ }
872
+ catch (err) {
873
+ console.error(chalk.red('validate failed:'), err instanceof Error ? err.message : err);
874
+ process.exit(1);
875
+ }
876
+ });
877
+ verification
878
+ .command('preflight')
879
+ .description('Run the contract preflight — is the app up and answering?')
880
+ .action(async () => {
881
+ try {
882
+ await verificationPreflightCommand();
883
+ }
884
+ catch (err) {
885
+ console.error(chalk.red('preflight failed:'), err instanceof Error ? err.message : err);
886
+ process.exit(1);
887
+ }
888
+ });
889
+ verification
890
+ .command('run <scenario>')
891
+ .description('Run a contract scenario and write the artifact manifest + review packet')
892
+ .option('--json', 'Print the manifest as JSON')
893
+ .option('--setup', 'Run environment.setup first (clean-clone mode)')
894
+ .option('--skip-preflight', 'Skip the preflight check')
895
+ .option('--timeout <seconds>', 'Override the scenario timeout', (v) => parseInt(v, 10))
896
+ .addHelpText('after', `
897
+ Runs preflight, then the scenario command, capturing stdout/stderr to the
898
+ artifact directory. Ends with a status:
899
+ verified scenario passed (exit 0)
900
+ failed scenario failed (exit 1)
901
+ environment_not_ready setup/preflight failed before the scenario ran (exit 1)
902
+ unsafe_to_verify safety policy permits real side effects (exit 2)
903
+
904
+ Evidence lands in the contract's artifact_dir:
905
+ manifest-<scenario>.json machine-readable evidence manifest
906
+ review-packet-<scenario>.md human-readable review packet
907
+ logs/ captured command output
908
+
909
+ Examples:
910
+ haystack verification run smoke
911
+ haystack verification run billing_upgrade --json
912
+ haystack verification run smoke --setup # clean-clone mode (CI)
913
+ `)
914
+ .action(async (scenario, opts) => {
915
+ try {
916
+ await verificationRunCommand(scenario, opts);
917
+ }
918
+ catch (err) {
919
+ console.error(chalk.red('run failed:'), err instanceof Error ? err.message : err);
920
+ process.exit(1);
921
+ }
922
+ });
923
+ verification
924
+ .command('collect-artifacts')
925
+ .description('Rebuild an evidence manifest from artifacts already on disk (status: inconclusive)')
926
+ .option('--scenario <name>', 'Scenario name to record in the manifest')
927
+ .option('--json', 'Print the manifest as JSON')
928
+ .action(async (opts) => {
929
+ try {
930
+ await verificationCollectCommand(opts);
931
+ }
932
+ catch (err) {
933
+ console.error(chalk.red('collect-artifacts failed:'), err instanceof Error ? err.message : err);
934
+ process.exit(1);
935
+ }
936
+ });
937
+ verification
938
+ .command('check-safety')
939
+ .description('Verify the verification setup is safe (policy, prod-disabled proof, dev-route gating, secret scan)')
940
+ .option('--json', 'Machine-readable report')
941
+ .action(async (opts) => {
942
+ try {
943
+ await verificationCheckSafetyCommand(opts);
944
+ }
945
+ catch (err) {
946
+ console.error(chalk.red('check-safety failed:'), err instanceof Error ? err.message : err);
947
+ process.exit(1);
948
+ }
949
+ });
820
950
  // ─── mcp ─────────────────────────────────────────────────────────────────────
821
951
  program
822
952
  .command('mcp')
@@ -0,0 +1,240 @@
1
+ import { z } from 'zod';
2
+ export declare const CONTRACT_RELATIVE_PATH = ".haystack/verification.yml";
3
+ export declare const DEFAULT_ARTIFACT_DIR = ".haystack/artifacts";
4
+ declare const scenarioSchema: z.ZodObject<{
5
+ command: z.ZodString;
6
+ description: z.ZodOptional<z.ZodString>;
7
+ /** Free-form labels of what the scenario captures (screenshots, server_logs, ...). Informational. */
8
+ artifacts: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
9
+ /** Wall-clock cap for the scenario command. Default applied by the runner. */
10
+ timeout_seconds: z.ZodOptional<z.ZodNumber>;
11
+ /**
12
+ * Whether `run` gates this scenario on environment.preflight. Set false for
13
+ * self-contained scenarios that boot the app themselves (like the scaffolded
14
+ * smoke script) — preflight against a cold environment would always fail.
15
+ */
16
+ preflight: z.ZodDefault<z.ZodBoolean>;
17
+ }, "strip", z.ZodTypeAny, {
18
+ command: string;
19
+ preflight: boolean;
20
+ description?: string | undefined;
21
+ artifacts?: string[] | undefined;
22
+ timeout_seconds?: number | undefined;
23
+ }, {
24
+ command: string;
25
+ description?: string | undefined;
26
+ artifacts?: string[] | undefined;
27
+ timeout_seconds?: number | undefined;
28
+ preflight?: boolean | undefined;
29
+ }>;
30
+ declare const safetySchema: z.ZodObject<{
31
+ /** Command proving dev-only helpers are disabled in production builds. */
32
+ production_disabled_check: z.ZodOptional<z.ZodString>;
33
+ external_services: z.ZodOptional<z.ZodEnum<["sandbox_only", "mocked", "none"]>>;
34
+ allow_real_email: z.ZodDefault<z.ZodBoolean>;
35
+ allow_real_payments: z.ZodDefault<z.ZodBoolean>;
36
+ allow_customer_data: z.ZodDefault<z.ZodBoolean>;
37
+ }, "strip", z.ZodTypeAny, {
38
+ allow_real_email: boolean;
39
+ allow_real_payments: boolean;
40
+ allow_customer_data: boolean;
41
+ production_disabled_check?: string | undefined;
42
+ external_services?: "none" | "sandbox_only" | "mocked" | undefined;
43
+ }, {
44
+ production_disabled_check?: string | undefined;
45
+ external_services?: "none" | "sandbox_only" | "mocked" | undefined;
46
+ allow_real_email?: boolean | undefined;
47
+ allow_real_payments?: boolean | undefined;
48
+ allow_customer_data?: boolean | undefined;
49
+ }>;
50
+ export declare const verificationContractSchema: z.ZodObject<{
51
+ version: z.ZodEffects<z.ZodUnion<[z.ZodLiteral<1>, z.ZodLiteral<"1">]>, 1, 1 | "1">;
52
+ app: z.ZodObject<{
53
+ name: z.ZodString;
54
+ framework: z.ZodOptional<z.ZodString>;
55
+ }, "strip", z.ZodTypeAny, {
56
+ name: string;
57
+ framework?: string | undefined;
58
+ }, {
59
+ name: string;
60
+ framework?: string | undefined;
61
+ }>;
62
+ environment: z.ZodObject<{
63
+ setup: z.ZodOptional<z.ZodString>;
64
+ start: z.ZodOptional<z.ZodString>;
65
+ preflight: z.ZodString;
66
+ reset: z.ZodOptional<z.ZodString>;
67
+ artifact_dir: z.ZodDefault<z.ZodString>;
68
+ log_paths: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
69
+ }, "strip", z.ZodTypeAny, {
70
+ preflight: string;
71
+ artifact_dir: string;
72
+ log_paths: string[];
73
+ start?: string | undefined;
74
+ setup?: string | undefined;
75
+ reset?: string | undefined;
76
+ }, {
77
+ preflight: string;
78
+ start?: string | undefined;
79
+ setup?: string | undefined;
80
+ reset?: string | undefined;
81
+ artifact_dir?: string | undefined;
82
+ log_paths?: string[] | undefined;
83
+ }>;
84
+ personas: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
85
+ login: z.ZodString;
86
+ description: z.ZodOptional<z.ZodString>;
87
+ }, "strip", z.ZodTypeAny, {
88
+ login: string;
89
+ description?: string | undefined;
90
+ }, {
91
+ login: string;
92
+ description?: string | undefined;
93
+ }>>>;
94
+ scenarios: z.ZodEffects<z.ZodRecord<z.ZodString, z.ZodObject<{
95
+ command: z.ZodString;
96
+ description: z.ZodOptional<z.ZodString>;
97
+ /** Free-form labels of what the scenario captures (screenshots, server_logs, ...). Informational. */
98
+ artifacts: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
99
+ /** Wall-clock cap for the scenario command. Default applied by the runner. */
100
+ timeout_seconds: z.ZodOptional<z.ZodNumber>;
101
+ /**
102
+ * Whether `run` gates this scenario on environment.preflight. Set false for
103
+ * self-contained scenarios that boot the app themselves (like the scaffolded
104
+ * smoke script) — preflight against a cold environment would always fail.
105
+ */
106
+ preflight: z.ZodDefault<z.ZodBoolean>;
107
+ }, "strip", z.ZodTypeAny, {
108
+ command: string;
109
+ preflight: boolean;
110
+ description?: string | undefined;
111
+ artifacts?: string[] | undefined;
112
+ timeout_seconds?: number | undefined;
113
+ }, {
114
+ command: string;
115
+ description?: string | undefined;
116
+ artifacts?: string[] | undefined;
117
+ timeout_seconds?: number | undefined;
118
+ preflight?: boolean | undefined;
119
+ }>>, Record<string, {
120
+ command: string;
121
+ preflight: boolean;
122
+ description?: string | undefined;
123
+ artifacts?: string[] | undefined;
124
+ timeout_seconds?: number | undefined;
125
+ }>, Record<string, {
126
+ command: string;
127
+ description?: string | undefined;
128
+ artifacts?: string[] | undefined;
129
+ timeout_seconds?: number | undefined;
130
+ preflight?: boolean | undefined;
131
+ }>>;
132
+ safety: z.ZodObject<{
133
+ /** Command proving dev-only helpers are disabled in production builds. */
134
+ production_disabled_check: z.ZodOptional<z.ZodString>;
135
+ external_services: z.ZodOptional<z.ZodEnum<["sandbox_only", "mocked", "none"]>>;
136
+ allow_real_email: z.ZodDefault<z.ZodBoolean>;
137
+ allow_real_payments: z.ZodDefault<z.ZodBoolean>;
138
+ allow_customer_data: z.ZodDefault<z.ZodBoolean>;
139
+ }, "strip", z.ZodTypeAny, {
140
+ allow_real_email: boolean;
141
+ allow_real_payments: boolean;
142
+ allow_customer_data: boolean;
143
+ production_disabled_check?: string | undefined;
144
+ external_services?: "none" | "sandbox_only" | "mocked" | undefined;
145
+ }, {
146
+ production_disabled_check?: string | undefined;
147
+ external_services?: "none" | "sandbox_only" | "mocked" | undefined;
148
+ allow_real_email?: boolean | undefined;
149
+ allow_real_payments?: boolean | undefined;
150
+ allow_customer_data?: boolean | undefined;
151
+ }>;
152
+ }, "strip", z.ZodTypeAny, {
153
+ version: 1;
154
+ app: {
155
+ name: string;
156
+ framework?: string | undefined;
157
+ };
158
+ environment: {
159
+ preflight: string;
160
+ artifact_dir: string;
161
+ log_paths: string[];
162
+ start?: string | undefined;
163
+ setup?: string | undefined;
164
+ reset?: string | undefined;
165
+ };
166
+ personas: Record<string, {
167
+ login: string;
168
+ description?: string | undefined;
169
+ }>;
170
+ scenarios: Record<string, {
171
+ command: string;
172
+ preflight: boolean;
173
+ description?: string | undefined;
174
+ artifacts?: string[] | undefined;
175
+ timeout_seconds?: number | undefined;
176
+ }>;
177
+ safety: {
178
+ allow_real_email: boolean;
179
+ allow_real_payments: boolean;
180
+ allow_customer_data: boolean;
181
+ production_disabled_check?: string | undefined;
182
+ external_services?: "none" | "sandbox_only" | "mocked" | undefined;
183
+ };
184
+ }, {
185
+ version: 1 | "1";
186
+ app: {
187
+ name: string;
188
+ framework?: string | undefined;
189
+ };
190
+ environment: {
191
+ preflight: string;
192
+ start?: string | undefined;
193
+ setup?: string | undefined;
194
+ reset?: string | undefined;
195
+ artifact_dir?: string | undefined;
196
+ log_paths?: string[] | undefined;
197
+ };
198
+ scenarios: Record<string, {
199
+ command: string;
200
+ description?: string | undefined;
201
+ artifacts?: string[] | undefined;
202
+ timeout_seconds?: number | undefined;
203
+ preflight?: boolean | undefined;
204
+ }>;
205
+ safety: {
206
+ production_disabled_check?: string | undefined;
207
+ external_services?: "none" | "sandbox_only" | "mocked" | undefined;
208
+ allow_real_email?: boolean | undefined;
209
+ allow_real_payments?: boolean | undefined;
210
+ allow_customer_data?: boolean | undefined;
211
+ };
212
+ personas?: Record<string, {
213
+ login: string;
214
+ description?: string | undefined;
215
+ }> | undefined;
216
+ }>;
217
+ export type VerificationContract = z.infer<typeof verificationContractSchema>;
218
+ export type VerificationScenario = z.infer<typeof scenarioSchema>;
219
+ export type VerificationSafety = z.infer<typeof safetySchema>;
220
+ export type ContractLoadResult = {
221
+ status: 'ok';
222
+ contract: VerificationContract;
223
+ path: string;
224
+ } | {
225
+ status: 'missing';
226
+ path: string;
227
+ } | {
228
+ status: 'invalid';
229
+ path: string;
230
+ errors: string[];
231
+ };
232
+ /**
233
+ * Load and validate the contract from `<rootDir>/.haystack/verification.yml`.
234
+ * Never throws — parse/schema failures come back as `status: 'invalid'` so
235
+ * callers can map them onto readiness / manifest statuses.
236
+ */
237
+ export declare function loadContract(rootDir?: string): Promise<ContractLoadResult>;
238
+ /** Resolve the artifact directory to an absolute path, creating it if needed. */
239
+ export declare function ensureArtifactDir(contract: VerificationContract, rootDir?: string): Promise<string>;
240
+ export {};
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Haystack Verification Contract — `.haystack/verification.yml`
3
+ *
4
+ * The contract is the repo-local answer to five questions:
5
+ * 1. How do I set up and start the app?
6
+ * 2. How do I know the app is ready? (preflight)
7
+ * 3. How do I log in as useful test users? (personas)
8
+ * 4. Which verification flows can I run? (scenarios)
9
+ * 5. Where do logs, screenshots, and artifacts go?
10
+ *
11
+ * This is deliberately separate from `.haystack.json`: that file configures
12
+ * Haystack's own sandbox verification (services/flows/fixtures the sandbox
13
+ * drives). The contract instead describes commands the REPO owns — anything
14
+ * (Haystack, CI, a coding agent, a human) can run them and get the same
15
+ * evidence. See docs/VERIFICATION-CONTRACT.md.
16
+ */
17
+ import * as fs from 'node:fs/promises';
18
+ import * as path from 'node:path';
19
+ import { parse as parseYaml } from 'yaml';
20
+ import { z } from 'zod';
21
+ export const CONTRACT_RELATIVE_PATH = '.haystack/verification.yml';
22
+ export const DEFAULT_ARTIFACT_DIR = '.haystack/artifacts';
23
+ const commandString = z.string().min(1, 'command must be a non-empty string');
24
+ const personaSchema = z.object({
25
+ login: commandString.describe('Command that logs the given persona in (or prints credentials)'),
26
+ description: z.string().optional(),
27
+ });
28
+ const scenarioSchema = z.object({
29
+ command: commandString,
30
+ description: z.string().optional(),
31
+ /** Free-form labels of what the scenario captures (screenshots, server_logs, ...). Informational. */
32
+ artifacts: z.array(z.string()).optional(),
33
+ /** Wall-clock cap for the scenario command. Default applied by the runner. */
34
+ timeout_seconds: z.number().int().positive().max(7200).optional(),
35
+ /**
36
+ * Whether `run` gates this scenario on environment.preflight. Set false for
37
+ * self-contained scenarios that boot the app themselves (like the scaffolded
38
+ * smoke script) — preflight against a cold environment would always fail.
39
+ */
40
+ preflight: z.boolean().default(true),
41
+ });
42
+ const safetySchema = z.object({
43
+ /** Command proving dev-only helpers are disabled in production builds. */
44
+ production_disabled_check: commandString.optional(),
45
+ external_services: z.enum(['sandbox_only', 'mocked', 'none']).optional(),
46
+ allow_real_email: z.boolean().default(false),
47
+ allow_real_payments: z.boolean().default(false),
48
+ allow_customer_data: z.boolean().default(false),
49
+ });
50
+ export const verificationContractSchema = z.object({
51
+ // Accept 1 or "1" — YAML authors will write both.
52
+ version: z
53
+ .union([z.literal(1), z.literal('1')])
54
+ .transform(() => 1),
55
+ app: z.object({
56
+ name: commandString,
57
+ framework: z.string().optional(),
58
+ }),
59
+ environment: z.object({
60
+ setup: commandString.optional(),
61
+ start: commandString.optional(),
62
+ preflight: commandString,
63
+ reset: commandString.optional(),
64
+ artifact_dir: z.string().default(DEFAULT_ARTIFACT_DIR),
65
+ log_paths: z.array(z.string()).default([]),
66
+ }),
67
+ personas: z.record(personaSchema).default({}),
68
+ scenarios: z
69
+ .record(scenarioSchema)
70
+ .refine((s) => Object.keys(s).length > 0, {
71
+ message: 'at least one scenario is required',
72
+ }),
73
+ safety: safetySchema,
74
+ });
75
+ /**
76
+ * Load and validate the contract from `<rootDir>/.haystack/verification.yml`.
77
+ * Never throws — parse/schema failures come back as `status: 'invalid'` so
78
+ * callers can map them onto readiness / manifest statuses.
79
+ */
80
+ export async function loadContract(rootDir = process.cwd()) {
81
+ const contractPath = path.join(rootDir, CONTRACT_RELATIVE_PATH);
82
+ let raw;
83
+ try {
84
+ raw = await fs.readFile(contractPath, 'utf-8');
85
+ }
86
+ catch {
87
+ return { status: 'missing', path: contractPath };
88
+ }
89
+ let parsed;
90
+ try {
91
+ parsed = parseYaml(raw);
92
+ }
93
+ catch (err) {
94
+ return {
95
+ status: 'invalid',
96
+ path: contractPath,
97
+ errors: [`YAML parse error: ${err instanceof Error ? err.message : String(err)}`],
98
+ };
99
+ }
100
+ const result = verificationContractSchema.safeParse(parsed);
101
+ if (!result.success) {
102
+ return {
103
+ status: 'invalid',
104
+ path: contractPath,
105
+ errors: result.error.issues.map((issue) => `${issue.path.length ? issue.path.join('.') : '(root)'}: ${issue.message}`),
106
+ };
107
+ }
108
+ return { status: 'ok', contract: result.data, path: contractPath };
109
+ }
110
+ /** Resolve the artifact directory to an absolute path, creating it if needed. */
111
+ export async function ensureArtifactDir(contract, rootDir = process.cwd()) {
112
+ const dir = path.resolve(rootDir, contract.environment.artifact_dir);
113
+ await fs.mkdir(path.join(dir, 'logs'), { recursive: true });
114
+ return dir;
115
+ }
@@ -0,0 +1,10 @@
1
+ export interface InitResult {
2
+ created: string[];
3
+ skipped: string[];
4
+ }
5
+ export interface InitOptions {
6
+ force?: boolean;
7
+ /** Only write the agent skill file (for handing the integration to a coding agent). */
8
+ skillOnly?: boolean;
9
+ }
10
+ export declare function initVerification(rootDir?: string, options?: InitOptions): Promise<InitResult>;