hunter-harness 0.2.4 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/bin.js +537 -179
  2. package/package.json +4 -4
package/dist/bin.js CHANGED
@@ -140,8 +140,18 @@ var baselineManifestSchema = z2.object({
140
140
  var finalizeProposalSchema = z2.object({
141
141
  schema_version: z2.literal(1),
142
142
  manifest_sha256: sha256Schema,
143
- base_artifact_id: z2.string().regex(/^art_/).nullable()
144
- }).strict();
143
+ base_artifact_id: z2.string().regex(/^art_/).nullable(),
144
+ sensitive_scan_skip: z2.literal(true).optional(),
145
+ sensitive_scan_skip_reason: z2.string().max(500).optional()
146
+ }).strict().superRefine((value, ctx) => {
147
+ if (value.sensitive_scan_skip_reason !== void 0 && value.sensitive_scan_skip !== true) {
148
+ ctx.addIssue({
149
+ code: "custom",
150
+ message: "sensitive_scan_skip_reason requires sensitive_scan_skip",
151
+ path: ["sensitive_scan_skip_reason"]
152
+ });
153
+ }
154
+ });
145
155
  var requestMetadataSchema = z2.object({
146
156
  request_id: z2.uuid(),
147
157
  idempotency_key: z2.uuid(),
@@ -3009,7 +3019,7 @@ function isBenignHighEntropyLookalike(value) {
3009
3019
  return false;
3010
3020
  }
3011
3021
  function highEntropyCandidates(content) {
3012
- const matches = content.matchAll(/\b[A-Za-z0-9_+.=\-]{24,}\b/g);
3022
+ const matches = content.matchAll(/\b[A-Za-z0-9_+.=-]{24,}\b/g);
3013
3023
  return [...matches].map((match) => ({
3014
3024
  value: match[0],
3015
3025
  offset: match.index,
@@ -3100,8 +3110,9 @@ function scanSensitiveFiles(files, options = {}) {
3100
3110
  const path = normalizeManagedPath(inputPath);
3101
3111
  const ignores = parseInlineIgnores(content);
3102
3112
  const knowledgeMetadata = path === ".harness/knowledge" || path.startsWith(".harness/knowledge/");
3113
+ const archiveMetadata = path === ".harness/archive" || path.startsWith(".harness/archive/");
3103
3114
  for (const raw of rawFindings(content)) {
3104
- if (knowledgeMetadata && raw.ruleId === "HH_WINDOWS_ABSOLUTE_PATH") {
3115
+ if ((knowledgeMetadata || archiveMetadata) && raw.ruleId === "HH_WINDOWS_ABSOLUTE_PATH") {
3105
3116
  continue;
3106
3117
  }
3107
3118
  const position = location(content, raw.offset);
@@ -3172,29 +3183,147 @@ function generateProposalPreview(input, scanOptions = {}) {
3172
3183
  };
3173
3184
  }
3174
3185
 
3175
- // ../core/dist/push/push.js
3176
- import { lstat as lstat2, readFile as readFile8, readdir as readdir4, rm as rm4 } from "node:fs/promises";
3177
- import { join as join9, relative, resolve as resolve5 } from "node:path";
3186
+ // ../core/dist/push/credentials.js
3187
+ import { readFile as readFile6, writeFile as writeFile3 } from "node:fs/promises";
3188
+ import { join as join7 } from "node:path";
3178
3189
  import { parse as parseYaml4, stringify as stringifyYaml3 } from "yaml";
3190
+ var CREDENTIALS_LOCAL_RELATIVE = ".harness/credentials.local.yaml";
3191
+ var CREDENTIALS_GITIGNORE_LINES = [
3192
+ ".harness/credentials.local.yaml",
3193
+ ".harness/credentials.local.*"
3194
+ ];
3195
+ var InvalidCredentialsError = class extends Error {
3196
+ constructor(message) {
3197
+ super(message);
3198
+ this.name = "InvalidCredentialsError";
3199
+ }
3200
+ };
3201
+ function assertHttpsServerUrl(url) {
3202
+ try {
3203
+ const parsed = new URL(url.trim());
3204
+ if (parsed.protocol !== "https:") {
3205
+ throw new InvalidCredentialsError("server_url must use HTTPS");
3206
+ }
3207
+ return parsed.toString().replace(/\/$/, "");
3208
+ } catch (error) {
3209
+ if (error instanceof InvalidCredentialsError) {
3210
+ throw error;
3211
+ }
3212
+ throw new InvalidCredentialsError("server_url is invalid");
3213
+ }
3214
+ }
3215
+ function validateLocalCredentials(credentials) {
3216
+ const token = typeof credentials.token === "string" && credentials.token.trim().length > 0 ? credentials.token.trim() : void 0;
3217
+ const serverUrl = typeof credentials.server_url === "string" && credentials.server_url.trim().length > 0 ? assertHttpsServerUrl(credentials.server_url) : void 0;
3218
+ if (token === void 0 && serverUrl === void 0) {
3219
+ throw new InvalidCredentialsError("credentials.local must include token and/or server_url");
3220
+ }
3221
+ return {
3222
+ ...token === void 0 ? {} : { token },
3223
+ ...serverUrl === void 0 ? {} : { server_url: serverUrl }
3224
+ };
3225
+ }
3226
+ function parseLocalCredentials(raw) {
3227
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
3228
+ return null;
3229
+ }
3230
+ const record = raw;
3231
+ const token = typeof record.token === "string" && record.token.trim().length > 0 ? record.token.trim() : void 0;
3232
+ const serverUrlRaw = typeof record.server_url === "string" && record.server_url.trim().length > 0 ? record.server_url.trim() : void 0;
3233
+ let serverUrl;
3234
+ if (serverUrlRaw !== void 0) {
3235
+ try {
3236
+ serverUrl = assertHttpsServerUrl(serverUrlRaw);
3237
+ } catch {
3238
+ return null;
3239
+ }
3240
+ }
3241
+ if (token === void 0 && serverUrl === void 0) {
3242
+ return null;
3243
+ }
3244
+ return {
3245
+ ...token === void 0 ? {} : { token },
3246
+ ...serverUrl === void 0 ? {} : { server_url: serverUrl }
3247
+ };
3248
+ }
3249
+ function mergeLocalCredentials(existing, patch) {
3250
+ return validateLocalCredentials({
3251
+ ...existing?.token === void 0 ? {} : { token: existing.token },
3252
+ ...existing?.server_url === void 0 ? {} : { server_url: existing.server_url },
3253
+ ...patch
3254
+ });
3255
+ }
3256
+ async function readLocalCredentials(projectRoot) {
3257
+ try {
3258
+ const raw = await readFile6(join7(projectRoot, CREDENTIALS_LOCAL_RELATIVE), "utf8");
3259
+ return parseLocalCredentials(parseYaml4(raw));
3260
+ } catch (error) {
3261
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
3262
+ return null;
3263
+ }
3264
+ throw error;
3265
+ }
3266
+ }
3267
+ async function writeLocalCredentials(projectRoot, credentials) {
3268
+ const normalized = validateLocalCredentials(credentials);
3269
+ await writeFile3(join7(projectRoot, CREDENTIALS_LOCAL_RELATIVE), stringifyYaml3(normalized, { sortMapEntries: true }) + "\n", "utf8");
3270
+ }
3271
+ async function ensureCredentialsGitignore(projectRoot) {
3272
+ const gitignorePath = join7(projectRoot, ".gitignore");
3273
+ let content = "";
3274
+ try {
3275
+ content = await readFile6(gitignorePath, "utf8");
3276
+ } catch (error) {
3277
+ if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) {
3278
+ throw error;
3279
+ }
3280
+ }
3281
+ const existing = new Set(content.split("\n").map((line) => line.trim()));
3282
+ const missing = CREDENTIALS_GITIGNORE_LINES.filter((line) => !existing.has(line));
3283
+ if (missing.length === 0) {
3284
+ return;
3285
+ }
3286
+ const suffix = content.length === 0 || content.endsWith("\n") ? "" : "\n";
3287
+ await writeFile3(gitignorePath, content + suffix + missing.join("\n") + "\n", "utf8");
3288
+ }
3289
+ function resolvePushAuth(input) {
3290
+ const tokenEnv = input.tokenEnv ?? input.projectTokenEnv;
3291
+ const envToken = input.env[tokenEnv]?.trim();
3292
+ const localToken = input.local?.token?.trim();
3293
+ const token = envToken && envToken.length > 0 ? envToken : localToken && localToken.length > 0 ? localToken : void 0;
3294
+ const serverUrl = input.serverUrlFlag ?? input.local?.server_url ?? input.projectUrl ?? void 0;
3295
+ if (serverUrl === void 0 || serverUrl === null || serverUrl.trim() === "") {
3296
+ return { code: "SERVER_URL_REQUIRED" };
3297
+ }
3298
+ if (token === void 0) {
3299
+ return { code: "TOKEN_INVALID" };
3300
+ }
3301
+ return { serverUrl, token };
3302
+ }
3303
+
3304
+ // ../core/dist/push/push.js
3305
+ import { lstat as lstat2, readFile as readFile9, readdir as readdir4, rm as rm4 } from "node:fs/promises";
3306
+ import { join as join10, relative, resolve as resolve5 } from "node:path";
3307
+ import { parse as parseYaml5, stringify as stringifyYaml4 } from "yaml";
3179
3308
 
3180
3309
  // ../core/dist/state/baseline.js
3181
- import { readFile as readFile6 } from "node:fs/promises";
3182
- import { join as join7 } from "node:path";
3310
+ import { readFile as readFile7 } from "node:fs/promises";
3311
+ import { join as join8 } from "node:path";
3183
3312
  async function readBaseline(projectRoot) {
3184
- const content = await readFile6(join7(stateLayout(projectRoot).baseline, "manifest.json"), "utf8");
3313
+ const content = await readFile7(join8(stateLayout(projectRoot).baseline, "manifest.json"), "utf8");
3185
3314
  return baselineManifestSchema.parse(JSON.parse(content));
3186
3315
  }
3187
3316
 
3188
3317
  // ../core/dist/state/locks.js
3189
3318
  import { randomUUID as randomUUID3 } from "node:crypto";
3190
- import { readFile as readFile7, rename as rename3, rm as rm3, writeFile as writeFile3 } from "node:fs/promises";
3191
- import { join as join8 } from "node:path";
3319
+ import { readFile as readFile8, rename as rename3, rm as rm3, writeFile as writeFile4 } from "node:fs/promises";
3320
+ import { join as join9 } from "node:path";
3192
3321
  async function readLock(path) {
3193
- return JSON.parse(await readFile7(path, "utf8"));
3322
+ return JSON.parse(await readFile8(path, "utf8"));
3194
3323
  }
3195
3324
  async function acquireProtocolLock(projectRoot, operation, options = {}) {
3196
3325
  const layout = await ensureStateLayout(projectRoot);
3197
- const lockPath = join8(layout.locks, "protocol.lock");
3326
+ const lockPath = join9(layout.locks, "protocol.lock");
3198
3327
  const now = options.now ?? Date.now();
3199
3328
  const staleAfterMs = options.staleAfterMs ?? 15 * 60 * 1e3;
3200
3329
  const record = {
@@ -3207,7 +3336,7 @@ async function acquireProtocolLock(projectRoot, operation, options = {}) {
3207
3336
  };
3208
3337
  for (let attempt = 0; attempt < 2; attempt += 1) {
3209
3338
  try {
3210
- await writeFile3(lockPath, JSON.stringify(record, null, 2), { flag: "wx" });
3339
+ await writeFile4(lockPath, JSON.stringify(record, null, 2), { flag: "wx" });
3211
3340
  return {
3212
3341
  path: lockPath,
3213
3342
  operation,
@@ -3239,11 +3368,15 @@ async function acquireProtocolLock(projectRoot, operation, options = {}) {
3239
3368
  var PushWorkflowError = class extends Error {
3240
3369
  exitCode;
3241
3370
  code;
3242
- constructor(message, exitCode, code) {
3371
+ details;
3372
+ constructor(message, exitCode, code, details) {
3243
3373
  super(message);
3244
3374
  this.name = "PushWorkflowError";
3245
3375
  this.exitCode = exitCode;
3246
3376
  this.code = code;
3377
+ if (details !== void 0) {
3378
+ this.details = details;
3379
+ }
3247
3380
  }
3248
3381
  };
3249
3382
  var SHARED_MANAGED_ROOTS = [
@@ -3271,7 +3404,7 @@ async function walkFiles(root, current, output) {
3271
3404
  return;
3272
3405
  }
3273
3406
  for (const item2 of await readdir4(current, { withFileTypes: true })) {
3274
- const path = join9(current, item2.name);
3407
+ const path = join10(current, item2.name);
3275
3408
  if (item2.isSymbolicLink()) {
3276
3409
  throw new PushWorkflowError("symlink is not pushable", 6, "UNSAFE_SYMLINK");
3277
3410
  }
@@ -3293,7 +3426,7 @@ async function walkHarnessEntries(root, directory, output) {
3293
3426
  return;
3294
3427
  for (const item2 of await readdir4(directory, { withFileTypes: true })) {
3295
3428
  if (item2.name.startsWith("harness-")) {
3296
- const path = join9(directory, item2.name);
3429
+ const path = join10(directory, item2.name);
3297
3430
  if (item2.isDirectory()) {
3298
3431
  await walkFiles(root, path, output);
3299
3432
  } else if (item2.isFile()) {
@@ -3312,25 +3445,25 @@ async function managedFiles(projectRoot, project) {
3312
3445
  ...adapters.some((adapter) => adapter.name === "codebuddy") ? ["CODEBUDDY.md"] : []
3313
3446
  ];
3314
3447
  for (const path of managedFiles2) {
3315
- if (await exists3(join9(root, path))) {
3448
+ if (await exists3(join10(root, path))) {
3316
3449
  paths.push(path);
3317
3450
  }
3318
3451
  }
3319
3452
  for (const path of SHARED_MANAGED_ROOTS) {
3320
- await walkFiles(root, join9(root, path), paths);
3453
+ await walkFiles(root, join10(root, path), paths);
3321
3454
  }
3322
3455
  for (const adapter of adapters) {
3323
3456
  if (adapter.rulesRoot !== null) {
3324
- await walkFiles(root, join9(root, adapter.rulesRoot), paths);
3457
+ await walkFiles(root, join10(root, adapter.rulesRoot), paths);
3325
3458
  }
3326
- await walkHarnessEntries(root, join9(root, adapter.skillsRoot), paths);
3459
+ await walkHarnessEntries(root, join10(root, adapter.skillsRoot), paths);
3327
3460
  if (adapter.agentsRoot !== null) {
3328
- await walkHarnessEntries(root, join9(root, adapter.agentsRoot), paths);
3461
+ await walkHarnessEntries(root, join10(root, adapter.agentsRoot), paths);
3329
3462
  }
3330
3463
  }
3331
3464
  const result = {};
3332
3465
  for (const path of [...new Set(paths)].sort()) {
3333
- result[path] = await readFile8(join9(root, path), "utf8");
3466
+ result[path] = await readFile9(join10(root, path), "utf8");
3334
3467
  }
3335
3468
  return result;
3336
3469
  }
@@ -3339,14 +3472,14 @@ function proposalBaseline(baseline) {
3339
3472
  }
3340
3473
  async function readProject(root) {
3341
3474
  try {
3342
- return projectConfigSchema.parse(parseYaml4(await readFile8(join9(root, ".harness", "project.yaml"), "utf8")));
3475
+ return projectConfigSchema.parse(parseYaml5(await readFile9(join10(root, ".harness", "project.yaml"), "utf8")));
3343
3476
  } catch {
3344
3477
  throw new PushWorkflowError("project configuration is missing or invalid", 3, "PROJECT_CONFIG_INVALID");
3345
3478
  }
3346
3479
  }
3347
3480
  async function readOptionalJson(path) {
3348
3481
  try {
3349
- return JSON.parse(await readFile8(path, "utf8"));
3482
+ return JSON.parse(await readFile9(path, "utf8"));
3350
3483
  } catch (error) {
3351
3484
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
3352
3485
  return null;
@@ -3358,7 +3491,7 @@ async function clientIdFor(root, explicit) {
3358
3491
  if (explicit !== void 0) {
3359
3492
  return explicit;
3360
3493
  }
3361
- const path = join9(root, ".harness", "state", "local", "client.json");
3494
+ const path = join10(root, ".harness", "state", "local", "client.json");
3362
3495
  const existing = await readOptionalJson(path);
3363
3496
  if (typeof existing?.client_id === "string" && /^cli_[A-Za-z0-9_-]+$/.test(existing.client_id)) {
3364
3497
  return existing.client_id;
@@ -3403,6 +3536,50 @@ function resetSession(state, proposalManifestHash) {
3403
3536
  chunk_idempotency_keys: {}
3404
3537
  };
3405
3538
  }
3539
+ function summarizeFindings(security) {
3540
+ const blocked = security.findings.filter((finding) => finding.disposition === "blocked");
3541
+ return {
3542
+ findings: blocked.map((finding) => ({
3543
+ path: finding.path,
3544
+ rule_id: finding.rule_id,
3545
+ severity: finding.severity,
3546
+ overridable: finding.overridable,
3547
+ fingerprint: finding.fingerprint,
3548
+ line: finding.line,
3549
+ column: finding.column
3550
+ })),
3551
+ finding_count: blocked.length,
3552
+ scanner_version: security.scanner_version
3553
+ };
3554
+ }
3555
+ function sensitiveBlockedError(preview) {
3556
+ return new PushWorkflowError("sensitive information scan blocked the proposal", 6, "SENSITIVE_CONTENT_BLOCKED", summarizeFindings(preview.security));
3557
+ }
3558
+ async function resolveSensitiveScanSkip(preview, options) {
3559
+ if (!preview.blocked) {
3560
+ return { skip: false };
3561
+ }
3562
+ if (options.sensitiveScanSkip === true) {
3563
+ return {
3564
+ skip: true,
3565
+ ...options.sensitiveScanSkipReason === void 0 ? {} : { reason: options.sensitiveScanSkipReason }
3566
+ };
3567
+ }
3568
+ if (options.confirmSensitiveScanSkip !== void 0) {
3569
+ const answer = await options.confirmSensitiveScanSkip(preview);
3570
+ if (answer === "cancelled") {
3571
+ return { skip: false, cancelled: true };
3572
+ }
3573
+ return answer.skip ? { skip: true, ...answer.reason === void 0 ? {} : { reason: answer.reason } } : { skip: false };
3574
+ }
3575
+ throw sensitiveBlockedError(preview);
3576
+ }
3577
+ function assertPreviewAllowed(preview, skip) {
3578
+ if (preview.blocked && !skip) {
3579
+ throw sensitiveBlockedError(preview);
3580
+ }
3581
+ }
3582
+ var CREDENTIALS_HINT = "\u53EF\u5728\u4EA4\u4E92\u6A21\u5F0F\u4E0B\u5F55\u5165\uFF0C\u6216\u5199\u5165 .harness/credentials.local.yaml\uFF08\u52FF\u63D0\u4EA4 git\uFF09";
3406
3583
  function makePreview(baseline, files, confirmedProjectLocal, ignorePaths, deletedAt = (/* @__PURE__ */ new Date()).toISOString()) {
3407
3584
  const filteredFiles = {};
3408
3585
  for (const [path, content] of Object.entries(files)) {
@@ -3430,7 +3607,7 @@ async function bindProject(root, project, baseline, projectId) {
3430
3607
  {
3431
3608
  operation: "modify",
3432
3609
  path: ".harness/project.yaml",
3433
- content: stringifyYaml3(nextProject, { sortMapEntries: true })
3610
+ content: stringifyYaml4(nextProject, { sortMapEntries: true })
3434
3611
  },
3435
3612
  {
3436
3613
  operation: "modify",
@@ -3447,24 +3624,38 @@ async function pushProject(options) {
3447
3624
  const profile = parseHarnessProfile(project.project.profiles[0]);
3448
3625
  const installedPaths = profile === null ? /* @__PURE__ */ new Set() : new Set(await Promise.all(enabledHarnessAgents(project).map((agent) => managedBundleTargets(options.resourcesRoot, profile, agent))).then((targets) => targets.flatMap((target) => [...target])));
3449
3626
  let preview = makePreview(baseline, await managedFiles(root, project), options.confirmedProjectLocal ?? [], installedPaths);
3450
- if (preview.blocked) {
3451
- throw new PushWorkflowError("sensitive information scan blocked the proposal", 6, "SENSITIVE_CONTENT_BLOCKED");
3627
+ const initialSkip = await resolveSensitiveScanSkip(preview, options);
3628
+ if (initialSkip.cancelled === true) {
3629
+ return { preview, proposalId: null, projectId: project.project.project_id, cancelled: true };
3452
3630
  }
3631
+ let sensitiveScanSkip = initialSkip.skip;
3632
+ let sensitiveScanSkipReason = initialSkip.reason;
3633
+ assertPreviewAllowed(preview, sensitiveScanSkip);
3453
3634
  if (options.dryRun) {
3454
3635
  return { preview, proposalId: null, projectId: project.project.project_id };
3455
3636
  }
3456
3637
  if (options.confirmProposal !== void 0 && !await options.confirmProposal(preview)) {
3457
3638
  return { preview, proposalId: null, projectId: project.project.project_id, cancelled: true };
3458
3639
  }
3459
- const serverUrl = options.serverUrl ?? project.server.url;
3460
- if (serverUrl === null || serverUrl === void 0) {
3461
- throw new PushWorkflowError("server_url is required", 3, "SERVER_URL_REQUIRED");
3640
+ const localCredentials = await readLocalCredentials(root);
3641
+ const auth = resolvePushAuth({
3642
+ ...options.serverUrl === void 0 ? {} : { serverUrlFlag: options.serverUrl },
3643
+ ...options.tokenEnv === void 0 ? {} : { tokenEnv: options.tokenEnv },
3644
+ env: options.env,
3645
+ local: localCredentials,
3646
+ projectUrl: project.server.url,
3647
+ projectTokenEnv: project.server.token_env
3648
+ });
3649
+ if ("code" in auth) {
3650
+ if (auth.code === "SERVER_URL_REQUIRED") {
3651
+ throw new PushWorkflowError("server_url is required; use --server-url, project.yaml server.url, or " + CREDENTIALS_HINT, 3, "SERVER_URL_REQUIRED");
3652
+ }
3653
+ const tokenEnv2 = options.tokenEnv ?? project.server.token_env;
3654
+ throw new PushWorkflowError("API token is unset; set " + tokenEnv2 + " or " + CREDENTIALS_HINT, 8, "TOKEN_INVALID");
3462
3655
  }
3656
+ const serverUrl = auth.serverUrl;
3657
+ const token = auth.token;
3463
3658
  const tokenEnv = options.tokenEnv ?? project.server.token_env;
3464
- const token = options.env[tokenEnv];
3465
- if (token === void 0 || token.trim() === "") {
3466
- throw new PushWorkflowError("API token environment variable is unset", 8, "TOKEN_INVALID");
3467
- }
3468
3659
  if (!/^[A-Z_][A-Z0-9_]*$/.test(tokenEnv)) {
3469
3660
  throw new PushWorkflowError("token_env is invalid", 3, "TOKEN_ENV_INVALID");
3470
3661
  }
@@ -3477,7 +3668,7 @@ async function pushProject(options) {
3477
3668
  if (parsedServerUrl.protocol !== "https:") {
3478
3669
  throw new PushWorkflowError("server_url must use HTTPS", 3, "SERVER_URL_INVALID");
3479
3670
  }
3480
- const workflowPath = join9(root, ".harness", "state", "local", "push-workflow.json");
3671
+ const workflowPath = join10(root, ".harness", "state", "local", "push-workflow.json");
3481
3672
  const priorWorkflow = await readOptionalJson(workflowPath);
3482
3673
  const provisionalRequestId = priorWorkflow?.local_project_key === project.project.local_project_key ? priorWorkflow.request_id : uuidV7();
3483
3674
  const lock = await acquireProtocolLock(root, "push", { requestId: provisionalRequestId });
@@ -3505,9 +3696,15 @@ async function pushProject(options) {
3505
3696
  workflow.project_id = resolved.project_id;
3506
3697
  await atomicWriteJson(workflowPath, workflow);
3507
3698
  preview = makePreview(baseline, await managedFiles(root, project), options.confirmedProjectLocal ?? [], installedPaths, workflow.created_at);
3508
- if (preview.blocked) {
3509
- throw new PushWorkflowError("sensitive information scan blocked the proposal", 6, "SENSITIVE_CONTENT_BLOCKED");
3699
+ if (!sensitiveScanSkip) {
3700
+ const reboundSkip = await resolveSensitiveScanSkip(preview, options);
3701
+ if (reboundSkip.cancelled === true) {
3702
+ return { preview, proposalId: null, projectId: project.project.project_id, cancelled: true };
3703
+ }
3704
+ sensitiveScanSkip = reboundSkip.skip;
3705
+ sensitiveScanSkipReason = reboundSkip.reason;
3510
3706
  }
3707
+ assertPreviewAllowed(preview, sensitiveScanSkip);
3511
3708
  }
3512
3709
  const projectId = project.project.project_id;
3513
3710
  if (projectId === null) {
@@ -3570,9 +3767,13 @@ async function pushProject(options) {
3570
3767
  const finalized = await client.finalizeProposal(session.session_id, {
3571
3768
  schema_version: 1,
3572
3769
  manifest_sha256: proposalManifestHash,
3573
- base_artifact_id: baseline.latest_artifact_id ?? null
3770
+ base_artifact_id: baseline.latest_artifact_id ?? null,
3771
+ ...sensitiveScanSkip ? {
3772
+ sensitive_scan_skip: true,
3773
+ ...sensitiveScanSkipReason === void 0 ? {} : { sensitive_scan_skip_reason: sensitiveScanSkipReason }
3774
+ } : {}
3574
3775
  }, requestId, workflow.finalize_idempotency_key);
3575
- await atomicWriteJson(join9(root, ".harness", "state", "local", "push-results", finalized.proposal_id + ".json"), {
3776
+ await atomicWriteJson(join10(root, ".harness", "state", "local", "push-results", finalized.proposal_id + ".json"), {
3576
3777
  schema_version: 1,
3577
3778
  request_id: requestId,
3578
3779
  project_id: projectId,
@@ -3592,6 +3793,14 @@ async function pushProject(options) {
3592
3793
  if (error.code === "STALE_PUSH") {
3593
3794
  throw new PushWorkflowError("\u670D\u52A1\u7AEF\u5DF2\u6709\u66F4\u65B0\u7684\u63A8\u9001\uFF0C\u8BF7\u5148 git pull \u540E\u6267\u884C npx hunter-harness update \u518D\u63A8", 5, "STALE_PUSH");
3594
3795
  }
3796
+ if (error.code === "SENSITIVE_CONTENT_BLOCKED") {
3797
+ const details = error.details;
3798
+ throw new PushWorkflowError(error.message, 6, "SENSITIVE_CONTENT_BLOCKED", details === null || typeof details !== "object" ? void 0 : {
3799
+ ...typeof details.finding_count === "number" ? { finding_count: details.finding_count } : {},
3800
+ ...typeof details.scanner_version === "string" ? { scanner_version: details.scanner_version } : {},
3801
+ ...Array.isArray(details.findings) ? { findings: details.findings } : {}
3802
+ });
3803
+ }
3595
3804
  const exitCode = error.status === 401 ? 8 : error.status === 409 ? 5 : 4;
3596
3805
  throw new PushWorkflowError(error.message, exitCode, error.code);
3597
3806
  }
@@ -3602,8 +3811,8 @@ async function pushProject(options) {
3602
3811
  }
3603
3812
 
3604
3813
  // ../core/dist/state/cleanup.js
3605
- import { readFile as readFile9, readdir as readdir5, rm as rm5 } from "node:fs/promises";
3606
- import { join as join10 } from "node:path";
3814
+ import { readFile as readFile10, readdir as readdir5, rm as rm5 } from "node:fs/promises";
3815
+ import { join as join11 } from "node:path";
3607
3816
  function isSafeEntryName(name) {
3608
3817
  return name.length > 0 && !name.includes("/") && !name.includes("\\") && name !== "." && name !== ".." && !name.includes("\0");
3609
3818
  }
@@ -3627,7 +3836,7 @@ async function cleanupProject(options) {
3627
3836
  continue;
3628
3837
  let journal;
3629
3838
  try {
3630
- journal = JSON.parse(await readFile9(join10(layout.transactions, name, "journal.json"), "utf8"));
3839
+ journal = JSON.parse(await readFile10(join11(layout.transactions, name, "journal.json"), "utf8"));
3631
3840
  } catch {
3632
3841
  continue;
3633
3842
  }
@@ -3645,7 +3854,7 @@ async function cleanupProject(options) {
3645
3854
  continue;
3646
3855
  pruned.push(entry.id);
3647
3856
  if (!options.dryRun) {
3648
- await rm5(join10(layout.transactions, entry.id), { recursive: true, force: true });
3857
+ await rm5(join11(layout.transactions, entry.id), { recursive: true, force: true });
3649
3858
  }
3650
3859
  }
3651
3860
  }
@@ -3654,7 +3863,7 @@ async function cleanupProject(options) {
3654
3863
  continue;
3655
3864
  removedCache.push(name);
3656
3865
  if (!options.dryRun) {
3657
- await rm5(join10(layout.serverArtifacts, name), { recursive: true, force: true });
3866
+ await rm5(join11(layout.serverArtifacts, name), { recursive: true, force: true });
3658
3867
  }
3659
3868
  }
3660
3869
  return {
@@ -3665,11 +3874,11 @@ async function cleanupProject(options) {
3665
3874
  }
3666
3875
 
3667
3876
  // ../core/dist/skill/frontmatter.js
3668
- import { parse as parseYaml5 } from "yaml";
3877
+ import { parse as parseYaml6 } from "yaml";
3669
3878
  import { z as z14 } from "zod";
3670
3879
 
3671
3880
  // ../core/dist/skill/fixer.js
3672
- import { parse as parseYaml6, stringify as stringifyYaml4 } from "yaml";
3881
+ import { parse as parseYaml7, stringify as stringifyYaml5 } from "yaml";
3673
3882
 
3674
3883
  // ../core/dist/skill/agents.js
3675
3884
  var AGENT_DESCRIPTORS = {
@@ -3708,10 +3917,10 @@ var AGENT_DESCRIPTORS = {
3708
3917
  var INSTALLABLE_AGENTS = Object.keys(AGENT_DESCRIPTORS).filter((agent) => AGENT_DESCRIPTORS[agent]?.installable === true);
3709
3918
 
3710
3919
  // ../core/dist/transaction/recovery.js
3711
- import { readFile as readFile10, readdir as readdir6, rm as rm6, stat as stat3 } from "node:fs/promises";
3712
- import { join as join11 } from "node:path";
3920
+ import { readFile as readFile11, readdir as readdir6, rm as rm6, stat as stat3 } from "node:fs/promises";
3921
+ import { join as join12 } from "node:path";
3713
3922
  async function recoverTransaction(projectRoot, transactionId) {
3714
- const journal = JSON.parse(await readFile10(join11(stateLayout(projectRoot).transactions, transactionId, "journal.json"), "utf8"));
3923
+ const journal = JSON.parse(await readFile11(join12(stateLayout(projectRoot).transactions, transactionId, "journal.json"), "utf8"));
3715
3924
  if (journal.state === "committed") {
3716
3925
  return { transactionId, status: "committed" };
3717
3926
  }
@@ -3738,7 +3947,7 @@ async function listTransactions(projectRoot) {
3738
3947
  const transactions = [];
3739
3948
  for (const transactionId of names) {
3740
3949
  try {
3741
- const journal = JSON.parse(await readFile10(join11(root, transactionId, "journal.json"), "utf8"));
3950
+ const journal = JSON.parse(await readFile11(join12(root, transactionId, "journal.json"), "utf8"));
3742
3951
  transactions.push({
3743
3952
  transactionId,
3744
3953
  kind: journal.kind,
@@ -3769,11 +3978,11 @@ async function rollbackLatestCommittedUpdate(projectRoot) {
3769
3978
  if (latest === void 0) {
3770
3979
  throw new Error("no committed update transaction is available for rollback");
3771
3980
  }
3772
- const transactionRoot = join11(stateLayout(projectRoot).transactions, latest.transactionId);
3773
- const journal = JSON.parse(await readFile10(join11(transactionRoot, "journal.json"), "utf8"));
3774
- const after = JSON.parse(await readFile10(join11(transactionRoot, "after", "manifest.json"), "utf8"));
3981
+ const transactionRoot = join12(stateLayout(projectRoot).transactions, latest.transactionId);
3982
+ const journal = JSON.parse(await readFile11(join12(transactionRoot, "journal.json"), "utf8"));
3983
+ const after = JSON.parse(await readFile11(join12(transactionRoot, "after", "manifest.json"), "utf8"));
3775
3984
  for (const entry of after) {
3776
- const target = join11(projectRoot, entry.path);
3985
+ const target = join12(projectRoot, entry.path);
3777
3986
  const exists5 = await pathExists(target);
3778
3987
  if (exists5 !== entry.exists || exists5 && await sha256File(target) !== entry.hash) {
3779
3988
  throw new Error("cannot rollback dirty path: " + entry.path);
@@ -3786,10 +3995,10 @@ async function rollbackLatestCommittedUpdate(projectRoot) {
3786
3995
  continue;
3787
3996
  }
3788
3997
  seen.add(snapshot.path);
3789
- const target = join11(projectRoot, snapshot.path);
3998
+ const target = join12(projectRoot, snapshot.path);
3790
3999
  const exists5 = await pathExists(target);
3791
4000
  if (snapshot.existed && snapshot.snapshot_name !== null) {
3792
- const content = await readFile10(join11(transactionRoot, "before", snapshot.snapshot_name));
4001
+ const content = await readFile11(join12(transactionRoot, "before", snapshot.snapshot_name));
3793
4002
  operations.push({
3794
4003
  operation: exists5 ? "modify" : "add",
3795
4004
  path: snapshot.path,
@@ -3818,7 +4027,7 @@ async function cleanupOldTransactions(projectRoot, now = /* @__PURE__ */ new Dat
3818
4027
  if (!removable) {
3819
4028
  continue;
3820
4029
  }
3821
- await rm6(join11(stateLayout(projectRoot).transactions, item2.transactionId), {
4030
+ await rm6(join12(stateLayout(projectRoot).transactions, item2.transactionId), {
3822
4031
  recursive: true,
3823
4032
  force: true
3824
4033
  });
@@ -3850,9 +4059,9 @@ function managedBlockDirty(currentContent, managedBlockHash) {
3850
4059
  }
3851
4060
 
3852
4061
  // ../core/dist/update/update.js
3853
- import { lstat as lstat3, readFile as readFile11, rm as rm7 } from "node:fs/promises";
3854
- import { join as join12, resolve as resolve6 } from "node:path";
3855
- import { parse as parseYaml7 } from "yaml";
4062
+ import { lstat as lstat3, readFile as readFile12, rm as rm7 } from "node:fs/promises";
4063
+ import { join as join13, resolve as resolve6 } from "node:path";
4064
+ import { parse as parseYaml8 } from "yaml";
3856
4065
  var UpdateWorkflowError = class extends Error {
3857
4066
  exitCode;
3858
4067
  code;
@@ -3875,7 +4084,7 @@ async function pathExists2(path) {
3875
4084
  }
3876
4085
  }
3877
4086
  async function optionalContent(path) {
3878
- return await pathExists2(path) ? readFile11(path, "utf8") : null;
4087
+ return await pathExists2(path) ? readFile12(path, "utf8") : null;
3879
4088
  }
3880
4089
  function manifestPayloadHash(manifest) {
3881
4090
  const payload = { ...manifest };
@@ -3893,10 +4102,10 @@ async function loadBlob(root, client, artifactId, operation, requestId, dryRun)
3893
4102
  return null;
3894
4103
  }
3895
4104
  const hash = operation.content_sha256;
3896
- const cacheRoot = join12(root, ".harness", "cache", "server-artifacts", artifactId);
3897
- const cachePath = join12(cacheRoot, "blobs", hash.replace(":", "_"));
4105
+ const cacheRoot = join13(root, ".harness", "cache", "server-artifacts", artifactId);
4106
+ const cachePath = join13(cacheRoot, "blobs", hash.replace(":", "_"));
3898
4107
  if (await pathExists2(cachePath) && await sha256File(cachePath) === hash) {
3899
- return readFile11(cachePath, "utf8");
4108
+ return readFile12(cachePath, "utf8");
3900
4109
  }
3901
4110
  const bytes = await client.downloadArtifactBlob(artifactId, hash, requestId);
3902
4111
  if (bytes.byteLength !== operation.size_bytes || sha256Bytes(bytes) !== hash) {
@@ -3946,7 +4155,7 @@ async function updateProject(options) {
3946
4155
  const root = resolve6(options.projectRoot);
3947
4156
  let project;
3948
4157
  try {
3949
- project = projectConfigSchema.parse(parseYaml7(await readFile11(join12(root, ".harness", "project.yaml"), "utf8")));
4158
+ project = projectConfigSchema.parse(parseYaml8(await readFile12(join13(root, ".harness", "project.yaml"), "utf8")));
3950
4159
  } catch {
3951
4160
  throw new UpdateWorkflowError("project configuration is invalid", 3, "PROJECT_CONFIG_INVALID");
3952
4161
  }
@@ -4031,8 +4240,8 @@ async function updateProject(options) {
4031
4240
  skipped.push({ path: target, operation: operation.operation, reason: "baseline-diverged" });
4032
4241
  continue;
4033
4242
  }
4034
- const sourceContent = await optionalContent(join12(root, source));
4035
- const targetContent = target === source ? sourceContent : await optionalContent(join12(root, target));
4243
+ const sourceContent = await optionalContent(join13(root, source));
4244
+ const targetContent = target === source ? sourceContent : await optionalContent(join13(root, target));
4036
4245
  const incoming = blobs.get(operation) ?? null;
4037
4246
  const incomingHash = contentHash(operation);
4038
4247
  let equivalent = incomingHash !== null && targetContent !== null && sha256Bytes(targetContent) === incomingHash;
@@ -4092,7 +4301,7 @@ async function updateProject(options) {
4092
4301
  dryRun: true
4093
4302
  };
4094
4303
  }
4095
- await atomicWriteJson(join12(root, ".harness", "cache", "server-artifacts", manifest.artifact_id, "manifest.json"), manifest);
4304
+ await atomicWriteJson(join13(root, ".harness", "cache", "server-artifacts", manifest.artifact_id, "manifest.json"), manifest);
4096
4305
  const lock = await acquireProtocolLock(root, "update", { requestId });
4097
4306
  try {
4098
4307
  const nextBaseline = baselineManifestSchema.parse(structuredClone(baseline));
@@ -4174,8 +4383,8 @@ async function updateProject(options) {
4174
4383
  }
4175
4384
 
4176
4385
  // src/config/init-config.ts
4177
- import { isAbsolute as isAbsolute2, join as join13 } from "node:path";
4178
- import { readFile as readFile12 } from "node:fs/promises";
4386
+ import { isAbsolute as isAbsolute2, join as join14 } from "node:path";
4387
+ import { readFile as readFile13 } from "node:fs/promises";
4179
4388
  var InitConfigurationError = class extends Error {
4180
4389
  exitCode;
4181
4390
  code;
@@ -4256,9 +4465,9 @@ function hasOwn(record, key) {
4256
4465
  async function resolveInitConfig(cwd, flags, prompts = {}, warnings = []) {
4257
4466
  let fileConfig = {};
4258
4467
  if (flags.config !== void 0) {
4259
- const path = isAbsolute2(flags.config) ? flags.config : join13(cwd, flags.config);
4468
+ const path = isAbsolute2(flags.config) ? flags.config : join14(cwd, flags.config);
4260
4469
  try {
4261
- fileConfig = JSON.parse(await readFile12(path, "utf8"));
4470
+ fileConfig = JSON.parse(await readFile13(path, "utf8"));
4262
4471
  } catch (error) {
4263
4472
  throw new InitConfigurationError(
4264
4473
  "unable to read init config: " + (error instanceof Error ? error.message : String(error)),
@@ -4340,20 +4549,20 @@ function serializeCliResult(result) {
4340
4549
  }
4341
4550
 
4342
4551
  // src/commands/refresh.ts
4343
- import { readFile as readFile13 } from "node:fs/promises";
4344
- import { join as join14 } from "node:path";
4345
- import { parse as parseYaml8 } from "yaml";
4552
+ import { readFile as readFile14 } from "node:fs/promises";
4553
+ import { join as join15 } from "node:path";
4554
+ import { parse as parseYaml9 } from "yaml";
4346
4555
  async function detectProject(root) {
4347
4556
  let content;
4348
4557
  try {
4349
- content = await readFile13(join14(root, ".harness", "project.yaml"), "utf8");
4558
+ content = await readFile14(join15(root, ".harness", "project.yaml"), "utf8");
4350
4559
  } catch (error) {
4351
4560
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
4352
4561
  return { status: "absent" };
4353
4562
  }
4354
4563
  throw error;
4355
4564
  }
4356
- const parsed = projectConfigSchema.safeParse(parseYaml8(content));
4565
+ const parsed = projectConfigSchema.safeParse(parseYaml9(content));
4357
4566
  if (!parsed.success) {
4358
4567
  return { status: "invalid" };
4359
4568
  }
@@ -4733,82 +4942,192 @@ async function runCleanup(options, dependencies) {
4733
4942
  }
4734
4943
 
4735
4944
  // src/commands/push.ts
4945
+ function formatFindings(details) {
4946
+ if (details?.findings === void 0 || details.findings.length === 0) {
4947
+ return "";
4948
+ }
4949
+ const lines = [
4950
+ "\u654F\u611F\u4FE1\u606F\u626B\u63CF\u53D1\u73B0 " + details.findings.length + " \u4E2A\u95EE\u9898\uFF1A"
4951
+ ];
4952
+ for (const finding of details.findings) {
4953
+ lines.push(
4954
+ " - " + finding.path + ":" + finding.line + " " + finding.rule_id + " (" + finding.severity + (finding.overridable ? ", \u53EF\u8986\u76D6" : ", \u4E0D\u53EF\u8986\u76D6") + ")"
4955
+ );
4956
+ }
4957
+ return lines.join("\n") + "\n";
4958
+ }
4959
+ function errorPayload(error) {
4960
+ const exitCode = error instanceof PushWorkflowError ? error.exitCode : 1;
4961
+ const message = error instanceof Error ? error.message : String(error);
4962
+ const code = error instanceof PushWorkflowError ? error.code : "GENERAL_FAILURE";
4963
+ if (error instanceof PushWorkflowError && error.details !== void 0) {
4964
+ return { exitCode, message, code, details: error.details };
4965
+ }
4966
+ return { exitCode, message, code };
4967
+ }
4968
+ async function promptForCredentials(dependencies, missing) {
4969
+ const serverUrl = missing === "token" ? void 0 : (await dependencies.prompt("\u670D\u52A1\u7AEF URL (https://...): ")).trim();
4970
+ const token = missing === "url" ? void 0 : (await dependencies.prompt("API Token: ")).trim();
4971
+ if ((missing === "url" || missing === "both") && (serverUrl === void 0 || serverUrl === "")) {
4972
+ return null;
4973
+ }
4974
+ if ((missing === "token" || missing === "both") && (token === void 0 || token === "")) {
4975
+ return null;
4976
+ }
4977
+ if (serverUrl !== void 0 && serverUrl !== "") {
4978
+ try {
4979
+ assertHttpsServerUrl(serverUrl);
4980
+ } catch (error) {
4981
+ if (error instanceof InvalidCredentialsError) {
4982
+ dependencies.stderr(error.message + "\n");
4983
+ return null;
4984
+ }
4985
+ throw error;
4986
+ }
4987
+ }
4988
+ return {
4989
+ ...serverUrl === void 0 ? {} : { serverUrl },
4990
+ ...token === void 0 ? {} : { token }
4991
+ };
4992
+ }
4736
4993
  async function runPush(options, dependencies) {
4737
4994
  const requestId = uuidV7();
4738
4995
  if (options.nonInteractive === true && options.yes !== true && options.dryRun !== true) {
4739
4996
  dependencies.stderr("\u975E\u4EA4\u4E92\u6A21\u5F0F\u63A8\u9001\u9700\u8981 --yes\n");
4740
4997
  return 2;
4741
4998
  }
4742
- try {
4743
- const result = await pushProject({
4744
- projectRoot: dependencies.cwd,
4745
- resourcesRoot: dependencies.resourcesRoot,
4746
- ...options.serverUrl === void 0 ? {} : { serverUrl: options.serverUrl },
4747
- ...options.tokenEnv === void 0 ? {} : { tokenEnv: options.tokenEnv },
4748
- env: dependencies.env,
4749
- dryRun: options.dryRun === true,
4750
- fetch: dependencies.fetch,
4751
- ...options.yes === true || options.nonInteractive === true ? {} : { confirmProposal: async () => {
4752
- const answer = await dependencies.prompt("Create this proposal? [y/N]: ");
4753
- return /^(?:y|yes)$/i.test(answer.trim());
4754
- } }
4755
- });
4756
- if ("cancelled" in result && result.cancelled === true) {
4757
- return 2;
4758
- }
4759
- const items = result.preview.operations.map((operation) => ({
4760
- path: operation.operation === "rename" ? operation.to_path : operation.path,
4761
- operation: operation.operation,
4762
- file_kind: operation.file_kind,
4763
- status: options.dryRun === true ? "planned" : "submitted",
4764
- reason: null,
4765
- size_bytes: "size_bytes" in operation ? operation.size_bytes : 0,
4766
- content_sha256: "content_sha256" in operation ? operation.content_sha256 : operation.tombstone.previous_sha256
4767
- }));
4768
- const output = {
4769
- schema_version: 1,
4770
- command: "push",
4771
- request_id: requestId,
4772
- dry_run: options.dryRun === true,
4773
- ok: true,
4774
- exit_code: 0,
4775
- project_id: result.projectId,
4776
- summary: {
4777
- planned: result.preview.operations.length,
4778
- submitted: options.dryRun === true ? 0 : result.preview.operations.length,
4779
- skipped: result.preview.skipped.length,
4780
- findings: result.preview.security.findings.length
4781
- },
4782
- items,
4783
- warnings: result.preview.skipped,
4784
- errors: []
4785
- };
4786
- dependencies.stdout(options.json === true ? serializeCliResult(output) : options.dryRun === true ? "Push preview contains " + items.length + " operations.\n" : "Pushed artifact " + result.artifactId + " (proposal " + result.proposalId + ").\n");
4787
- return 0;
4788
- } catch (error) {
4789
- const exitCode = error instanceof PushWorkflowError ? error.exitCode : 1;
4790
- const message = error instanceof Error ? error.message : String(error);
4791
- dependencies.stderr(message + "\n");
4792
- if (options.json === true) {
4793
- dependencies.stdout(serializeCliResult({
4999
+ if (options.nonInteractive === true && options.skipSensitiveScan === true && options.yes !== true) {
5000
+ dependencies.stderr("\u975E\u4EA4\u4E92\u8DF3\u8FC7\u654F\u611F\u626B\u63CF\u9700\u8981 --yes\n");
5001
+ return 2;
5002
+ }
5003
+ async function executePush(attempt = 0) {
5004
+ try {
5005
+ const result = await pushProject({
5006
+ projectRoot: dependencies.cwd,
5007
+ resourcesRoot: dependencies.resourcesRoot,
5008
+ ...options.serverUrl === void 0 ? {} : { serverUrl: options.serverUrl },
5009
+ ...options.tokenEnv === void 0 ? {} : { tokenEnv: options.tokenEnv },
5010
+ env: dependencies.env,
5011
+ dryRun: options.dryRun === true,
5012
+ fetch: dependencies.fetch,
5013
+ ...options.skipSensitiveScan === true ? { sensitiveScanSkip: true } : {},
5014
+ ...options.yes === true || options.nonInteractive === true ? {} : { confirmProposal: async () => {
5015
+ const answer = await dependencies.prompt("Create this proposal? [y/N]: ");
5016
+ return /^(?:y|yes)$/i.test(answer.trim());
5017
+ } },
5018
+ ...options.yes === true || options.nonInteractive === true || options.skipSensitiveScan === true ? {} : { confirmSensitiveScanSkip: async (preview) => {
5019
+ dependencies.stderr(formatFindings({
5020
+ findings: preview.security.findings.filter((finding) => finding.disposition === "blocked").map((finding) => ({
5021
+ path: finding.path,
5022
+ rule_id: finding.rule_id,
5023
+ severity: finding.severity,
5024
+ overridable: finding.overridable,
5025
+ fingerprint: finding.fingerprint,
5026
+ line: finding.line,
5027
+ column: finding.column
5028
+ })),
5029
+ finding_count: preview.security.findings.filter(
5030
+ (finding) => finding.disposition === "blocked"
5031
+ ).length,
5032
+ scanner_version: preview.security.scanner_version
5033
+ }));
5034
+ const answer = await dependencies.prompt(
5035
+ "\u654F\u611F\u626B\u63CF\u5DF2\u963B\u65AD\u63A8\u9001\u3002\u662F\u5426\u663E\u5F0F\u8DF3\u8FC7\u5E76\u7EE7\u7EED\uFF1F[y/N]: "
5036
+ );
5037
+ if (!/^(?:y|yes)$/i.test(answer.trim())) {
5038
+ return "cancelled";
5039
+ }
5040
+ const reasonAnswer = await dependencies.prompt("\u8DF3\u8FC7\u539F\u56E0\uFF08\u53EF\u9009\uFF0C\u56DE\u8F66\u8DF3\u8FC7\uFF09: ");
5041
+ const reason = reasonAnswer.trim();
5042
+ return reason === "" ? { skip: true } : { skip: true, reason };
5043
+ } }
5044
+ });
5045
+ if ("cancelled" in result && result.cancelled === true) {
5046
+ return 2;
5047
+ }
5048
+ const items = result.preview.operations.map((operation) => ({
5049
+ path: operation.operation === "rename" ? operation.to_path : operation.path,
5050
+ operation: operation.operation,
5051
+ file_kind: operation.file_kind,
5052
+ status: options.dryRun === true ? "planned" : "submitted",
5053
+ reason: null,
5054
+ size_bytes: "size_bytes" in operation ? operation.size_bytes : 0,
5055
+ content_sha256: "content_sha256" in operation ? operation.content_sha256 : operation.tombstone.previous_sha256
5056
+ }));
5057
+ const output = {
4794
5058
  schema_version: 1,
4795
5059
  command: "push",
4796
5060
  request_id: requestId,
4797
5061
  dry_run: options.dryRun === true,
4798
- ok: false,
4799
- exit_code: exitCode,
4800
- project_id: null,
4801
- summary: { planned: 0, submitted: 0 },
4802
- items: [],
4803
- warnings: [],
4804
- errors: [{
4805
- code: error instanceof PushWorkflowError ? error.code : "GENERAL_FAILURE",
4806
- message
4807
- }]
4808
- }));
5062
+ ok: true,
5063
+ exit_code: 0,
5064
+ project_id: result.projectId,
5065
+ summary: {
5066
+ planned: result.preview.operations.length,
5067
+ submitted: options.dryRun === true ? 0 : result.preview.operations.length,
5068
+ skipped: result.preview.skipped.length,
5069
+ findings: result.preview.security.findings.length
5070
+ },
5071
+ items,
5072
+ warnings: result.preview.skipped,
5073
+ errors: []
5074
+ };
5075
+ dependencies.stdout(options.json === true ? serializeCliResult(output) : options.dryRun === true ? "Push preview contains " + items.length + " operations.\n" : "Pushed artifact " + result.artifactId + " (proposal " + result.proposalId + ").\n");
5076
+ return 0;
5077
+ } catch (error) {
5078
+ if (attempt === 0 && options.nonInteractive !== true && options.dryRun !== true && error instanceof PushWorkflowError && (error.code === "SERVER_URL_REQUIRED" || error.code === "TOKEN_INVALID")) {
5079
+ const missing = error.code === "SERVER_URL_REQUIRED" ? "url" : error.code === "TOKEN_INVALID" ? "token" : "both";
5080
+ dependencies.stderr(
5081
+ error.message + "\n\u53EF\u5728\u4E0B\u65B9\u5F55\u5165\u5E76\u5199\u5165 .harness/credentials.local.yaml\u3002\n"
5082
+ );
5083
+ const entered = await promptForCredentials(dependencies, missing);
5084
+ if (entered !== null) {
5085
+ const existing = await readLocalCredentials(dependencies.cwd);
5086
+ try {
5087
+ await writeLocalCredentials(dependencies.cwd, mergeLocalCredentials(existing, {
5088
+ ...entered.serverUrl === void 0 ? {} : { server_url: entered.serverUrl },
5089
+ ...entered.token === void 0 ? {} : { token: entered.token }
5090
+ }));
5091
+ } catch (error2) {
5092
+ if (error2 instanceof InvalidCredentialsError) {
5093
+ dependencies.stderr(error2.message + "\n");
5094
+ return 3;
5095
+ }
5096
+ throw error2;
5097
+ }
5098
+ await ensureCredentialsGitignore(dependencies.cwd);
5099
+ dependencies.stderr(
5100
+ "\u5DF2\u5199\u5165 .harness/credentials.local.yaml\uFF1Bupload session \u7531 CLI \u81EA\u52A8\u7BA1\u7406\uFF0C\u65E0\u9700\u624B\u914D\u3002\n"
5101
+ );
5102
+ return executePush(attempt + 1);
5103
+ }
5104
+ }
5105
+ const payload = errorPayload(error);
5106
+ dependencies.stderr(payload.message + "\n");
5107
+ dependencies.stderr(formatFindings(payload.details));
5108
+ if (options.json === true) {
5109
+ dependencies.stdout(serializeCliResult({
5110
+ schema_version: 1,
5111
+ command: "push",
5112
+ request_id: requestId,
5113
+ dry_run: options.dryRun === true,
5114
+ ok: false,
5115
+ exit_code: payload.exitCode,
5116
+ project_id: null,
5117
+ summary: { planned: 0, submitted: 0 },
5118
+ items: [],
5119
+ warnings: [],
5120
+ errors: [{
5121
+ code: payload.code,
5122
+ message: payload.message,
5123
+ ...payload.details === void 0 ? {} : { details: payload.details }
5124
+ }]
5125
+ }));
5126
+ }
5127
+ return payload.exitCode;
4809
5128
  }
4810
- return exitCode;
4811
5129
  }
5130
+ return executePush();
4812
5131
  }
4813
5132
 
4814
5133
  // src/commands/update.ts
@@ -4904,7 +5223,7 @@ async function runUpdate(options, dependencies) {
4904
5223
 
4905
5224
  // src/commands/recovery.ts
4906
5225
  import { stat as stat4 } from "node:fs/promises";
4907
- import { join as join15 } from "node:path";
5226
+ import { join as join16 } from "node:path";
4908
5227
  async function exists4(path) {
4909
5228
  try {
4910
5229
  await stat4(path);
@@ -4964,7 +5283,7 @@ async function runRecoveryMenuIfApplicable(options, dependencies) {
4964
5283
  return 5;
4965
5284
  }
4966
5285
  }
4967
- const initialized = await exists4(join15(dependencies.cwd, ".harness", "project.yaml"));
5286
+ const initialized = await exists4(join16(dependencies.cwd, ".harness", "project.yaml"));
4968
5287
  if (!initialized || options.nonInteractive === true || explicitConfigure(options)) {
4969
5288
  return null;
4970
5289
  }
@@ -5007,8 +5326,8 @@ async function runRecoveryMenuIfApplicable(options, dependencies) {
5007
5326
  }
5008
5327
 
5009
5328
  // src/workflow-data/resolve.ts
5010
- import { cp, mkdir as mkdir4, readdir as readdir7, readFile as readFile14, stat as stat5 } from "node:fs/promises";
5011
- import { dirname as dirname4, join as join16, relative as relative2 } from "node:path";
5329
+ import { cp, mkdir as mkdir4, readdir as readdir7, readFile as readFile15, rm as rm8, stat as stat5 } from "node:fs/promises";
5330
+ import { dirname as dirname4, join as join17, relative as relative2 } from "node:path";
5012
5331
  import { fileURLToPath } from "node:url";
5013
5332
  var WorkflowDataResolutionError = class extends Error {
5014
5333
  code;
@@ -5045,14 +5364,14 @@ async function listFilesRecursive(root, base = root) {
5045
5364
  const entries = await readdir7(root, { withFileTypes: true });
5046
5365
  const files = [];
5047
5366
  for (const entry of entries) {
5048
- const absolute = join16(root, entry.name);
5367
+ const absolute = join17(root, entry.name);
5049
5368
  if (entry.isDirectory()) {
5050
5369
  files.push(...await listFilesRecursive(absolute, base));
5051
5370
  continue;
5052
5371
  }
5053
5372
  if (!entry.isFile()) continue;
5054
5373
  const rel = relative2(base, absolute).replaceAll("\\", "/");
5055
- files.push({ path: rel, content: await readFile14(absolute, "utf8") });
5374
+ files.push({ path: rel, content: await readFile15(absolute, "utf8") });
5056
5375
  }
5057
5376
  return files;
5058
5377
  }
@@ -5065,7 +5384,7 @@ async function verifyWorkflowPackageIntegrity(resourcesRoot) {
5065
5384
  const manifest = await readWorkflowFamilyManifest(resourcesRoot);
5066
5385
  const expected = manifest.content_sha256;
5067
5386
  if (typeof expected !== "string" || expected.length === 0) return;
5068
- const harnessRoot = join16(resourcesRoot, "harness");
5387
+ const harnessRoot = join17(resourcesRoot, "harness");
5069
5388
  if (!await pathExists3(harnessRoot)) {
5070
5389
  throw new WorkflowDataResolutionError(
5071
5390
  "\u5DE5\u4F5C\u6D41\u6570\u636E\u5305\u7F3A\u5C11 harness/\uFF0C\u65E0\u6CD5\u6821\u9A8C SHA-256",
@@ -5084,26 +5403,68 @@ async function verifyWorkflowPackageIntegrity(resourcesRoot) {
5084
5403
  }
5085
5404
  }
5086
5405
  async function monorepoResourcesRoot() {
5406
+ const here = dirname4(fileURLToPath(import.meta.url));
5087
5407
  const candidates = [
5088
- fileURLToPath(new URL("../../../resources", import.meta.url)),
5089
- fileURLToPath(new URL("../../../workflow-data-harness", import.meta.url)),
5090
- fileURLToPath(new URL("../../../../resources", import.meta.url))
5408
+ join17(here, "../../../../resources"),
5409
+ join17(here, "../../../../../resources")
5091
5410
  ];
5092
5411
  for (const candidate of candidates) {
5093
- if (await pathExists3(join16(candidate, "harness", "manifests"))) return candidate;
5412
+ if (await pathExists3(join17(candidate, "harness", "manifests"))) return candidate;
5094
5413
  }
5095
5414
  return null;
5096
5415
  }
5097
5416
  async function siblingWorkflowPackage(cwd) {
5098
5417
  const candidates = [
5099
- join16(cwd, "node_modules", "@hunter-harness", "workflow-harness"),
5100
- join16(dirname4(fileURLToPath(import.meta.url)), "..", "..", "workflow-data-harness")
5418
+ join17(cwd, "node_modules", "@hunter-harness", "workflow-harness"),
5419
+ join17(dirname4(fileURLToPath(import.meta.url)), "..", "..", "workflow-data-harness")
5101
5420
  ];
5102
5421
  for (const candidate of candidates) {
5103
- if (await pathExists3(join16(candidate, "harness", "manifests"))) return candidate;
5422
+ if (await pathExists3(join17(candidate, "harness", "manifests"))) return candidate;
5104
5423
  }
5105
5424
  return null;
5106
5425
  }
5426
+ async function readCachedNpmPackageVersion(cacheRoot) {
5427
+ const manifestPath = join17(cacheRoot, "package.json");
5428
+ if (!await pathExists3(manifestPath)) return null;
5429
+ try {
5430
+ const pkg = JSON.parse(await readFile15(manifestPath, "utf8"));
5431
+ return typeof pkg.version === "string" && pkg.version.length > 0 ? pkg.version : null;
5432
+ } catch {
5433
+ return null;
5434
+ }
5435
+ }
5436
+ async function latestWorkflowCacheIsStale(cacheRoot, packageName, resolveManifest) {
5437
+ const cachedVersion = await readCachedNpmPackageVersion(cacheRoot);
5438
+ if (cachedVersion === null) return true;
5439
+ try {
5440
+ const manifest = resolveManifest ?? (async (spec) => {
5441
+ const pacote = await import("pacote");
5442
+ const result = await pacote.default.manifest(spec);
5443
+ return { version: String(result.version) };
5444
+ });
5445
+ const remote = await manifest(packageName);
5446
+ return remote.version !== cachedVersion;
5447
+ } catch {
5448
+ return false;
5449
+ }
5450
+ }
5451
+ async function materializeWorkflowPackageCache(cacheRoot, packageSpec, extract) {
5452
+ await mkdir4(cacheRoot, { recursive: true });
5453
+ const staging = join17(cacheRoot, ".extract");
5454
+ await rm8(staging, { recursive: true, force: true });
5455
+ await mkdir4(staging, { recursive: true });
5456
+ await extract(packageSpec, staging);
5457
+ const packageDir = await pathExists3(join17(staging, "harness", "manifests")) ? staging : join17(staging, "package");
5458
+ if (!await pathExists3(join17(packageDir, "harness", "manifests"))) {
5459
+ throw new WorkflowDataResolutionError(
5460
+ "\u5DE5\u4F5C\u6D41\u6570\u636E\u5305\u5185\u5BB9\u65E0\u6548\uFF1A\u7F3A\u5C11 harness/manifests",
5461
+ "WORKFLOW_DATA_INVALID",
5462
+ 7
5463
+ );
5464
+ }
5465
+ await cp(packageDir, cacheRoot, { recursive: true });
5466
+ await verifyWorkflowPackageIntegrity(cacheRoot);
5467
+ }
5107
5468
  async function resolveWorkflowResourcesRoot(options, argv = []) {
5108
5469
  if (options.override !== void 0) return options.override;
5109
5470
  const envRoot = options.env.HUNTER_HARNESS_RESOURCES_ROOT?.trim();
@@ -5123,30 +5484,27 @@ async function resolveWorkflowResourcesRoot(options, argv = []) {
5123
5484
  const packageName = workflowPackageName(family, options.env);
5124
5485
  const packageSpec = version === "latest" ? packageName : `${packageName}@${version}`;
5125
5486
  const cacheKey = packageSpec.replace("/", "+");
5126
- const cacheRoot = join16(options.cwd, ".harness", "cache", "workflow-packages", cacheKey);
5127
- if (await pathExists3(join16(cacheRoot, "harness", "manifests"))) {
5128
- await verifyWorkflowPackageIntegrity(cacheRoot);
5129
- return cacheRoot;
5487
+ const cacheRoot = join17(options.cwd, ".harness", "cache", "workflow-packages", cacheKey);
5488
+ if (await pathExists3(join17(cacheRoot, "harness", "manifests"))) {
5489
+ if (version === "latest") {
5490
+ const stale = await latestWorkflowCacheIsStale(cacheRoot, packageName, options.pacoteManifest);
5491
+ if (stale) {
5492
+ await rm8(cacheRoot, { recursive: true, force: true });
5493
+ } else {
5494
+ await verifyWorkflowPackageIntegrity(cacheRoot);
5495
+ return cacheRoot;
5496
+ }
5497
+ } else {
5498
+ await verifyWorkflowPackageIntegrity(cacheRoot);
5499
+ return cacheRoot;
5500
+ }
5130
5501
  }
5131
5502
  try {
5132
- await mkdir4(cacheRoot, { recursive: true });
5133
5503
  const extract = options.pacoteExtract ?? (async (spec, destination) => {
5134
5504
  const pacote = await import("pacote");
5135
5505
  await pacote.default.extract(spec, destination);
5136
5506
  });
5137
- const staging = join16(cacheRoot, ".extract");
5138
- await mkdir4(staging, { recursive: true });
5139
- await extract(packageSpec, staging);
5140
- const packageDir = await pathExists3(join16(staging, "harness", "manifests")) ? staging : join16(staging, "package");
5141
- if (!await pathExists3(join16(packageDir, "harness", "manifests"))) {
5142
- throw new WorkflowDataResolutionError(
5143
- "\u5DE5\u4F5C\u6D41\u6570\u636E\u5305\u5185\u5BB9\u65E0\u6548\uFF1A\u7F3A\u5C11 harness/manifests",
5144
- "WORKFLOW_DATA_INVALID",
5145
- 7
5146
- );
5147
- }
5148
- await cp(packageDir, cacheRoot, { recursive: true });
5149
- await verifyWorkflowPackageIntegrity(cacheRoot);
5507
+ await materializeWorkflowPackageCache(cacheRoot, packageSpec, extract);
5150
5508
  return cacheRoot;
5151
5509
  } catch (error) {
5152
5510
  if (error instanceof WorkflowDataResolutionError) throw error;
@@ -5173,9 +5531,9 @@ function describeWorkflowDataFetchFailure(error, packageSpec) {
5173
5531
  return `\u65E0\u6CD5\u83B7\u53D6\u5DE5\u4F5C\u6D41\u6570\u636E\u5305 ${packageSpec}\uFF1A\u672C\u5730\u7F13\u5B58\u4E0D\u5B58\u5728\u4E14\u83B7\u53D6\u5931\u8D25\u3002\u53EF${hintRoot}\u3002\u539F\u56E0\uFF1A${code !== "" ? code + " " : ""}${detail}`;
5174
5532
  }
5175
5533
  async function readWorkflowFamilyManifest(resourcesRoot) {
5176
- const manifestPath = join16(resourcesRoot, "hunter-workflow-family.json");
5534
+ const manifestPath = join17(resourcesRoot, "hunter-workflow-family.json");
5177
5535
  if (!await pathExists3(manifestPath)) return {};
5178
- return JSON.parse(await readFile14(manifestPath, "utf8"));
5536
+ return JSON.parse(await readFile15(manifestPath, "utf8"));
5179
5537
  }
5180
5538
 
5181
5539
  // src/bin.ts
@@ -5244,7 +5602,7 @@ async function runCli(argv, overrides = {}) {
5244
5602
  dependencies
5245
5603
  );
5246
5604
  });
5247
- addCommonOptions(program.command("push")).description("\u521B\u5EFA\u53D7\u6CBB\u7406\u7684\u53D8\u66F4\u63D0\u6848").action(async (options) => {
5605
+ addCommonOptions(program.command("push")).description("\u521B\u5EFA\u53D7\u6CBB\u7406\u7684\u53D8\u66F4\u63D0\u6848").option("--skip-sensitive-scan", "\u663E\u5F0F\u8DF3\u8FC7\u654F\u611F\u626B\u63CF\u963B\u65AD\uFF08\u975E\u4EA4\u4E92\u9700\u914D\u5408 --yes\uFF09").action(async (options) => {
5248
5606
  exitCode = await runPush({ ...program.opts(), ...options }, dependencies);
5249
5607
  });
5250
5608
  addCommonOptions(program.command("cleanup")).description("\u6E05\u7406\u5DF2\u5B8C\u6210\u4E8B\u52A1\u548C\u8FC7\u671F\u670D\u52A1\u7AEF\u7F13\u5B58").action(async (options) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hunter-harness",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Local-first, server-governed agent harness",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -14,9 +14,9 @@
14
14
  "dist/bin.js"
15
15
  ],
16
16
  "scripts": {
17
- "build": "tsc -p tsconfig.json && npm run bundle",
18
- "bundle": "esbuild src/bin.ts --bundle --platform=node --format=esm --target=node24 --external:commander --external:yaml --external:zod --external:pacote --outfile=dist/bin.js",
19
- "typecheck": "tsc -p tsconfig.json --noEmit",
17
+ "build": "node ../../node_modules/typescript/bin/tsc -p tsconfig.json && npm run bundle",
18
+ "bundle": "node ../../node_modules/esbuild/bin/esbuild src/bin.ts --bundle --platform=node --format=esm --target=node24 --external:commander --external:yaml --external:zod --external:pacote --outfile=dist/bin.js",
19
+ "typecheck": "node ../../node_modules/typescript/bin/tsc -p tsconfig.json --noEmit",
20
20
  "prepack": "npm run build"
21
21
  },
22
22
  "dependencies": {