@elvis1513/auto-coding-skill 1.0.0 → 1.0.1

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 CHANGED
@@ -7,6 +7,8 @@ Engineering workflow skill for:
7
7
 
8
8
  This skill targets Go backend + frontend monorepo projects that rely on Jenkins for build and deployment. The optimized default flow is lightweight locally, then Jenkins-first and target-environment-first for real verification.
9
9
 
10
+ `docs/ENGINEERING.md` is intentionally Git-tracked. The environment fields kept in that file are mandatory, must be filled with real values, and are committed as part of project maintenance. Unused environment items should be removed instead of being kept as placeholders.
11
+
10
12
  ## Install
11
13
 
12
14
  ```bash
@@ -108,6 +110,7 @@ python3 .claude/skills/auto-coding-skill/scripts/ap.py --repo . install
108
110
  - `docs/ENGINEERING.md` frontmatter
109
111
 
110
112
  This frontmatter is the only manual config source.
113
+ It must be committed to Git. Do not add it to `.gitignore`.
111
114
 
112
115
  重点字段:
113
116
  - `commands.*`
@@ -121,13 +124,24 @@ This frontmatter is the only manual config source.
121
124
  - `commands.quick_test` 或 `commands.test`
122
125
  - `commands.lint` 或 `commands.typecheck`
123
126
  - `target_env.name`
127
+ - `target_env.frontend_base_url`
128
+ - `target_env.frontend_username`
129
+ - `target_env.frontend_password`
130
+ - `target_env.backend_base_url`
131
+ - `target_env.backend_username`
132
+ - `target_env.backend_password`
124
133
  - `target_env.health_base_url`
125
134
  - `target_env.health_path`
135
+ - `jenkins.base_url`
136
+ - `jenkins.ui_username`
137
+ - `jenkins.ui_password`
138
+ - `jenkins.api_user`
139
+ - `jenkins.api_password`
126
140
  - `jenkins.trigger_branch`
127
141
  - `jenkins.image_repository`
128
142
  - `jenkins.image_tag_strategy`
129
143
  - `jenkins.deploy_env`
130
- - `jenkins.job_url` 或 `jenkins.base_url + jenkins.job_name` 或 `jenkins.base_url + jenkins.multibranch_root_job`
144
+ - `jenkins.job_url`
131
145
 
132
146
  4. Start AI development by constraints:
133
147
 
@@ -185,7 +199,9 @@ This frontmatter is the only manual config source.
185
199
  - Purpose: single source of project config + workflow rules.
186
200
  - How to record:
187
201
  - Fill YAML frontmatter once.
188
- - Keep target env accounts, Jenkins UI/API accounts, commands, docs paths here only.
202
+ - Keep target env front/backend usernames and passwords, Jenkins UI/API usernames and passwords, commands, docs paths here only.
203
+ - This file is expected to be committed to Git and maintained in plaintext for this workflow.
204
+ - Remaining environment keys are all mandatory; blank values, TODO-like placeholders, and incorrect URL/path formats are treated as blocking errors by `doctor`.
189
205
  - Do not duplicate config elsewhere.
190
206
 
191
207
  ### 2) docs/tasks/taskbook.md
@@ -7,6 +7,8 @@ description: Use for a lightweight Jenkins-first engineering workflow in Claude/
7
7
 
8
8
  This skill is for Go backend + frontend monorepo projects that rely on Jenkins to build and deploy after push. It supports both Claude and Codex. The default workflow is lightweight locally, then uses Jenkins and the real target environment as the authoritative verification path.
9
9
 
10
+ `docs/ENGINEERING.md` is intentionally Git-tracked in this workflow. The remaining environment fields in that file are mandatory, must be filled with real values, and are committed as part of the project baseline. Unused environment keys should be removed from the template instead of being left as placeholders.
11
+
10
12
  Prefer already available MCP servers, installed skills, plugins, and app connectors over ad-hoc manual work whenever they can complete the task reliably.
11
13
 
12
14
  Default to multi-agent execution when the client supports it. Break work into independent design, research, implementation, validation, and documentation subtasks so Claude/Codex can run them in parallel whenever that reduces cycle time without weakening control of the main task.
@@ -71,6 +73,7 @@ This contains all manual fields:
71
73
  - `docs.*`
72
74
 
73
75
  Do not duplicate config in other md/yaml files.
76
+ Do not hide `docs/ENGINEERING.md` in `.gitignore`.
74
77
 
75
78
  Minimum required config for the default flow:
76
79
  - `project.name`
@@ -78,13 +81,24 @@ Minimum required config for the default flow:
78
81
  - `commands.quick_test` or `commands.test`
79
82
  - `commands.lint` or `commands.typecheck`
80
83
  - `target_env.name`
84
+ - `target_env.frontend_base_url`
85
+ - `target_env.frontend_username`
86
+ - `target_env.frontend_password`
87
+ - `target_env.backend_base_url`
88
+ - `target_env.backend_username`
89
+ - `target_env.backend_password`
81
90
  - `target_env.health_base_url`
82
91
  - `target_env.health_path`
92
+ - `jenkins.base_url`
93
+ - `jenkins.ui_username`
94
+ - `jenkins.ui_password`
95
+ - `jenkins.api_user`
96
+ - `jenkins.api_password`
83
97
  - `jenkins.trigger_branch`
84
98
  - `jenkins.image_repository`
85
99
  - `jenkins.image_tag_strategy`
86
100
  - `jenkins.deploy_env`
87
- - `jenkins.job_url` or `jenkins.base_url + jenkins.job_name` or `jenkins.base_url + jenkins.multibranch_root_job`
101
+ - `jenkins.job_url`
88
102
 
89
103
  ## Branch policy
90
104
 
@@ -134,8 +148,9 @@ python3 docs/tools/autopipeline/ap.py gen-summary <TASK_ID>
134
148
  ## Quality policy
135
149
 
136
150
  - Default local gate is lightweight only: build, unit/quick test, lint, typecheck, API docs, Jenkinsfile / script syntax, `git diff --check`.
137
- - `doctor` should be used early to catch missing config before the first implementation loop.
151
+ - `doctor` should be used early to catch missing or invalid config before the first implementation loop.
138
152
  - `light-gate` now fails if the required default commands are not configured.
153
+ - `doctor`, `light-gate`, and `commit-push` all fail when required environment fields are missing, placeholder-like, or syntactically invalid.
139
154
  - Do not require local Docker Compose or full local regression for every small change.
140
155
  - Jenkins and target environment verification are more valuable than repeated local simulation of deploy-only problems.
141
156
  - `verify-target` should be used for real target-environment API/page checks when the task touches user-visible or deploy-sensitive behavior.
@@ -45,28 +45,19 @@ target_env:
45
45
  backend_password: ""
46
46
  health_base_url: ""
47
47
  health_path: ""
48
- verify_notes: ""
49
48
 
50
49
  jenkins:
51
50
  base_url: ""
52
51
  ui_username: ""
53
52
  ui_password: ""
54
- crumb_url: ""
55
- job_name: ""
56
53
  job_url: ""
57
- multibranch_root_job: ""
58
- branch_name: ""
59
54
  trigger_branch: ""
60
55
  image_repository: ""
61
56
  image_tag_strategy: ""
62
57
  deploy_env: ""
63
58
  deploy_timeout_sec: 1800
64
59
  api_user: ""
65
- api_token: ""
66
60
  api_password: ""
67
- api_user_env: "JENKINS_USER"
68
- api_token_env: "JENKINS_TOKEN"
69
- api_password_env: "JENKINS_PASSWORD"
70
61
 
71
62
  docs:
72
63
  taskbook: "docs/tasks/taskbook.md"
@@ -97,6 +88,9 @@ docs:
97
88
  补充规则:
98
89
  - 每次任务闭环后,必须清理临时文件、临时目录、日志、截图、构建缓存等非必要产物;仅明确需要保留的本地诊断目录允许保留。
99
90
  - 所有手工填写信息,只维护在本文件 frontmatter 中,其他文档不得重复配置。
91
+ - `docs/ENGINEERING.md` 必须提交到 Git 管理,不允许写入 `.gitignore`。
92
+ - 本 workflow 明确允许在 `docs/ENGINEERING.md` 中明文维护平台账号、密码,并随 Git 一起版本化。
93
+ - 未参与默认流程的环境项不要保留占位;模板中未保留的字段视为已清理,不再额外配置。
100
94
 
101
95
  ---
102
96
 
@@ -104,14 +98,14 @@ docs:
104
98
 
105
99
  先填写 `docs/ENGINEERING.md` frontmatter 中的所有空值。重点包括:
106
100
  - `commands.*`:本地轻量校验命令
107
- - `target_env.*`:目标环境地址、后台账号、前端验证信息
108
- - `jenkins.*`:Jenkins UI/API 账号、Job、分支、镜像、部署环境
101
+ - `target_env.*`:目标环境前端 / 后端地址、用户名、密码,必须全部填写且真实可用
102
+ - `jenkins.*`:Jenkins UI/API 用户名、密码、Job、分支、镜像、部署环境,必须全部填写且真实可用
109
103
 
110
104
  字段说明:
111
105
  - `target_env.backend_username` / `target_env.backend_password`:目标环境后台账号
112
- - `target_env.frontend_username` / `target_env.frontend_password`:目标环境前端登录账号(如需要)
106
+ - `target_env.frontend_username` / `target_env.frontend_password`:目标环境前端登录账号
113
107
  - `jenkins.ui_username` / `jenkins.ui_password`:Jenkins 页面登录账号
114
- - `jenkins.api_user` + `jenkins.api_token` / `jenkins.api_password`:Jenkins API 校验凭据
108
+ - `jenkins.api_user` / `jenkins.api_password`:Jenkins API 用户名 / 密码
115
109
 
116
110
  默认必填:
117
111
  - `project.name`
@@ -119,18 +113,27 @@ docs:
119
113
  - `commands.quick_test` 或 `commands.test`
120
114
  - `commands.lint` 或 `commands.typecheck`
121
115
  - `target_env.name`
116
+ - `target_env.frontend_base_url`
117
+ - `target_env.frontend_username`
118
+ - `target_env.frontend_password`
119
+ - `target_env.backend_base_url`
120
+ - `target_env.backend_username`
121
+ - `target_env.backend_password`
122
122
  - `target_env.health_base_url`
123
123
  - `target_env.health_path`
124
+ - `jenkins.ui_username`
125
+ - `jenkins.ui_password`
126
+ - `jenkins.api_user`
127
+ - `jenkins.api_password`
124
128
  - `jenkins.trigger_branch`
125
129
  - `jenkins.image_repository`
126
130
  - `jenkins.image_tag_strategy`
127
131
  - `jenkins.deploy_env`
128
- - `jenkins.job_url` 或 `jenkins.base_url + jenkins.job_name` 或 `jenkins.base_url + jenkins.multibranch_root_job`
132
+ - `jenkins.job_url`
129
133
 
130
134
  按需填写:
131
135
  - `runtime.*`:仅在本地运行诊断时使用
132
136
  - `commands.compose_up` / `commands.compose_down` / `commands.smoke` / `commands.regression`
133
- - `target_env.frontend_*` / `target_env.backend_*`:仅在需要额外页面/API验证或受保护资源校验时使用
134
137
 
135
138
  ---
136
139
 
@@ -217,6 +220,9 @@ docs:
217
220
  9. 闭环记录
218
221
  每个任务必须留下轻量闭环记录:任务 ID、提交号、Jenkins Build URL、目标环境验证结果、是否通过、遗留问题。
219
222
 
223
+ 10. 配置入库
224
+ `docs/ENGINEERING.md` 中保留下来的环境信息、前端/后端账号、Jenkins 账号与密码必须 100% 填写、正确填写,并提交 Git 作为项目权威配置持续维护。
225
+
220
226
  ---
221
227
 
222
228
  ## 3. 高风险变更(必须补强验证)
@@ -8,7 +8,6 @@ import argparse
8
8
  import base64
9
9
  import datetime as _dt
10
10
  import json
11
- import os
12
11
  import time
13
12
  import urllib.parse
14
13
  import urllib.error
@@ -20,6 +19,7 @@ from core import APError, ensure_git_repo, copy_tree, run, load_yaml, find_confi
20
19
 
21
20
 
22
21
  _JENKINS_CRUMB_CACHE: dict[str, dict[str, str]] = {}
22
+ _INVALID_PLACEHOLDERS = {"N/A", "TODO", "TBD", "CHANGEME", "CHANGE_ME", "FILL_ME", "FILL-ME", "PLACEHOLDER", "XXX"}
23
23
 
24
24
 
25
25
  def _skill_root() -> Path:
@@ -60,27 +60,21 @@ def _run_configured_command(repo: Path, cfg: dict, name: str) -> bool:
60
60
 
61
61
  def _jenkins_basic_auth_headers(cfg: dict) -> dict:
62
62
  jenkins_cfg = (cfg.get("jenkins") or {})
63
- direct_user = str(jenkins_cfg.get("api_user") or jenkins_cfg.get("ui_username") or "").strip()
64
- direct_token = str(jenkins_cfg.get("api_token") or "").strip()
65
- direct_password = str(jenkins_cfg.get("api_password") or jenkins_cfg.get("ui_password") or "").strip()
66
- user_env = str(jenkins_cfg.get("api_user_env") or "JENKINS_USER").strip() or "JENKINS_USER"
67
- token_env = str(jenkins_cfg.get("api_token_env") or "JENKINS_TOKEN").strip() or "JENKINS_TOKEN"
68
- password_env = str(jenkins_cfg.get("api_password_env") or "JENKINS_PASSWORD").strip() or "JENKINS_PASSWORD"
69
- user = direct_user or os.getenv(user_env) or os.getenv("JENKINS_USER")
70
- token = (
71
- direct_token
72
- or direct_password
73
- or os.getenv(token_env)
74
- or os.getenv(password_env)
75
- or os.getenv("JENKINS_TOKEN")
76
- or os.getenv("JENKINS_PASSWORD")
77
- )
78
- if not user or not token:
63
+ user_candidates = [
64
+ jenkins_cfg.get("api_user"),
65
+ jenkins_cfg.get("ui_username"),
66
+ ]
67
+ secret_candidates = [
68
+ jenkins_cfg.get("api_password"),
69
+ jenkins_cfg.get("ui_password"),
70
+ ]
71
+ user = next((_text(v) for v in user_candidates if _is_explicit_fill(v)), "")
72
+ secret = next((_text(v) for v in secret_candidates if _is_explicit_fill(v)), "")
73
+ if not user or not secret:
79
74
  raise APError(
80
- "Missing Jenkins API credentials. Fill jenkins.api_user / jenkins.api_token / jenkins.api_password "
81
- f"in docs/ENGINEERING.md, or set env vars {user_env}, {token_env}, {password_env}."
75
+ "Missing Jenkins API credentials. Fill jenkins.api_user and jenkins.api_password in docs/ENGINEERING.md."
82
76
  )
83
- raw = f"{user}:{token}".encode("utf-8")
77
+ raw = f"{user}:{secret}".encode("utf-8")
84
78
  auth = base64.b64encode(raw).decode("ascii")
85
79
  return {"Authorization": f"Basic {auth}"}
86
80
 
@@ -122,10 +116,6 @@ def _jenkins_root_url(cfg: dict, job_url: str = "") -> str:
122
116
 
123
117
 
124
118
  def _jenkins_crumb_api_url(cfg: dict, job_url: str = "") -> str:
125
- jenkins_cfg = (cfg.get("jenkins") or {})
126
- explicit = str(jenkins_cfg.get("crumb_url") or "").strip()
127
- if explicit:
128
- return explicit
129
119
  root = _jenkins_root_url(cfg, job_url=job_url)
130
120
  if not root:
131
121
  return ""
@@ -203,12 +193,12 @@ def _jenkins_api_get_json(url: str, cfg: dict, timeout_s: int = 15, allow_404: b
203
193
  except Exception as retry_exc:
204
194
  raise APError(f"Jenkins API request failed after crumb retry: {url}\n{retry_exc}") from retry_exc
205
195
  else:
206
- raise APError(
207
- f"Jenkins API request failed: {url}\n"
208
- f"HTTP 403\n{body or '(empty response body)'}\n"
209
- "Jenkins may require crumb/CSRF handling, but no crumb issuer endpoint was available. "
210
- "Fill jenkins.base_url or jenkins.crumb_url in docs/ENGINEERING.md if needed."
211
- ) from exc
196
+ raise APError(
197
+ f"Jenkins API request failed: {url}\n"
198
+ f"HTTP 403\n{body or '(empty response body)'}\n"
199
+ "Jenkins may require crumb/CSRF handling, but no crumb issuer endpoint was available. "
200
+ "Fill jenkins.base_url in docs/ENGINEERING.md if needed."
201
+ ) from exc
212
202
  else:
213
203
  raise APError(
214
204
  f"Jenkins API request failed: {url}\n"
@@ -296,15 +286,12 @@ def _resolve_jenkins_job_url(cfg: dict, job_name: str = "", job_url: str = "") -
296
286
  jenkins_cfg = (cfg.get("jenkins") or {})
297
287
  explicit_url = str(job_url or "").strip()
298
288
  requested_name = str(job_name or "").strip()
299
- configured_name = str(jenkins_cfg.get("job_name") or "").strip()
300
289
  configured_url = str(jenkins_cfg.get("job_url") or "").strip()
301
290
  base_url = str(jenkins_cfg.get("base_url") or "").strip()
302
291
 
303
292
  if explicit_url:
304
293
  return explicit_url.rstrip("/")
305
294
  if requested_name:
306
- if configured_name and requested_name == configured_name and configured_url:
307
- return configured_url.rstrip("/")
308
295
  if base_url:
309
296
  return _jenkins_job_url_from_name(base_url, requested_name)
310
297
  raise APError(
@@ -313,11 +300,9 @@ def _resolve_jenkins_job_url(cfg: dict, job_name: str = "", job_url: str = "") -
313
300
  )
314
301
  if configured_url:
315
302
  return configured_url.rstrip("/")
316
- if configured_name and base_url:
317
- return _jenkins_job_url_from_name(base_url, configured_name)
318
303
  raise APError(
319
- "Missing Jenkins job location. Fill jenkins.job_url, or fill both "
320
- "jenkins.base_url and jenkins.job_name in docs/ENGINEERING.md."
304
+ "Missing Jenkins job location. Fill jenkins.job_url in docs/ENGINEERING.md, "
305
+ "or pass --job-url / --job-name explicitly."
321
306
  )
322
307
 
323
308
 
@@ -331,17 +316,16 @@ def _resolve_jenkins_job_candidates(
331
316
  branch_name: str = "",
332
317
  ) -> List[str]:
333
318
  jenkins_cfg = (cfg.get("jenkins") or {})
334
- effective_branch = str(branch_name or jenkins_cfg.get("branch_name") or "").strip()
319
+ effective_branch = str(branch_name or "").strip()
335
320
  if not effective_branch:
336
321
  inferred_branch = _resolve_git_branch_name(repo, git_ref or "HEAD")
337
322
  if inferred_branch:
338
323
  effective_branch = inferred_branch
339
324
 
340
- effective_root = str(multibranch_root_job or jenkins_cfg.get("multibranch_root_job") or "").strip()
325
+ effective_root = str(multibranch_root_job or "").strip()
341
326
  explicit_url = str(job_url or "").strip()
342
327
  explicit_name = str(job_name or "").strip()
343
328
  configured_url = str(jenkins_cfg.get("job_url") or "").strip()
344
- configured_name = str(jenkins_cfg.get("job_name") or "").strip()
345
329
 
346
330
  if effective_branch:
347
331
  if explicit_url:
@@ -354,12 +338,9 @@ def _resolve_jenkins_job_candidates(
354
338
  return _jenkins_branch_job_urls(_jenkins_job_url_from_name(base_url, explicit_name), effective_branch)
355
339
  if configured_url:
356
340
  return _jenkins_branch_job_urls(configured_url, effective_branch)
357
- if configured_name:
358
- base_url = str(jenkins_cfg.get("base_url") or "").strip()
359
- return _jenkins_branch_job_urls(_jenkins_job_url_from_name(base_url, configured_name), effective_branch)
360
341
  raise APError(
361
- "Missing Jenkins multibranch root job location. Fill jenkins.multibranch_root_job + jenkins.base_url, "
362
- "or pass --job-url / --job-name together with --branch-name."
342
+ "Missing Jenkins multibranch root job location. Pass --job-url / --job-name together with "
343
+ "--branch-name, or pass --multibranch-root-job with jenkins.base_url."
363
344
  )
364
345
 
365
346
  return [_resolve_jenkins_job_url(cfg, job_name=job_name, job_url=job_url)]
@@ -404,17 +385,8 @@ def cmd_install(args: argparse.Namespace) -> None:
404
385
  copy_tree(Path(__file__).resolve().parent / "core.py", tools_dir / "core.py")
405
386
  copy_tree(Path(__file__).resolve().parent / "http_checks.py", tools_dir / "http_checks.py")
406
387
 
407
- gi = repo / ".gitignore"
408
- secret_line = "docs/ENGINEERING.md"
409
- if gi.exists():
410
- txt = gi.read_text(encoding="utf-8")
411
- if secret_line not in txt:
412
- gi.write_text(txt.rstrip() + "\n" + secret_line + "\n", encoding="utf-8")
413
- else:
414
- gi.write_text(secret_line + "\n", encoding="utf-8")
415
-
416
388
  print(f"[install] OK: scaffold installed into {repo}")
417
- print("[install] Next: edit docs/ENGINEERING.md frontmatter and fill project/commands/target_env/jenkins fields")
389
+ print("[install] Next: edit docs/ENGINEERING.md frontmatter, fill all platform credentials, and commit that file into Git.")
418
390
 
419
391
 
420
392
  def _infer_title(taskbook: Path, task_id: str) -> str:
@@ -568,12 +540,35 @@ def _load_cfg(repo: Path) -> dict:
568
540
  return load_yaml(cfg_path)
569
541
 
570
542
 
571
- def _pair_state(left: str, right: str) -> str:
572
- if left and right:
573
- return "complete"
574
- if not left and not right:
575
- return "empty"
576
- return "partial"
543
+ def _text(value: object) -> str:
544
+ return str(value or "").strip()
545
+
546
+
547
+ def _is_placeholder(value: object) -> bool:
548
+ return _text(value).upper() in _INVALID_PLACEHOLDERS
549
+
550
+
551
+ def _is_explicit_fill(value: object) -> bool:
552
+ return bool(_text(value)) and not _is_placeholder(value)
553
+
554
+
555
+ def _validate_url_field(errors: List[str], field: str, value: object) -> None:
556
+ raw = _text(value)
557
+ parsed = urllib.parse.urlparse(raw)
558
+ if parsed.scheme not in {"http", "https"} or not parsed.netloc:
559
+ errors.append(f"{field} must be a valid http/https URL")
560
+
561
+
562
+ def _validate_path_field(errors: List[str], field: str, value: object) -> None:
563
+ raw = _text(value)
564
+ if not raw.startswith("/"):
565
+ errors.append(f"{field} must start with '/'")
566
+
567
+
568
+ def _require_explicit_field(missing: List[str], field: str, value: object) -> None:
569
+ raw = _text(value)
570
+ if not _is_explicit_fill(raw):
571
+ missing.append(f"{field} (must be explicitly filled, not blank/TODO)")
577
572
 
578
573
 
579
574
  def _run_git_diff_check(repo: Path, cfg: dict) -> None:
@@ -611,6 +606,7 @@ def cmd_run(args: argparse.Namespace) -> None:
611
606
 
612
607
  def cmd_light_gate(args: argparse.Namespace) -> None:
613
608
  repo = Path(args.repo).resolve()
609
+ cmd_doctor(argparse.Namespace(repo=str(repo)))
614
610
  cfg = _load_cfg(repo)
615
611
  commands = (cfg.get("commands") or {})
616
612
 
@@ -770,10 +766,7 @@ def cmd_verify_target(args: argparse.Namespace) -> None:
770
766
  raise APError(f"Frontend target verification failed: {url} -> {status}\n{body[:400]}")
771
767
  checks.append(f"frontend:{url}->{status}")
772
768
 
773
- note = str(target_cfg.get("verify_notes") or "").strip()
774
769
  summary = ", ".join(checks) if checks else "health-only"
775
- if note:
776
- summary = summary + f" | notes={note}"
777
770
  print(f"[verify-target] OK: {summary}")
778
771
 
779
772
 
@@ -788,6 +781,8 @@ def cmd_verify_jenkins(args: argparse.Namespace) -> None:
788
781
  raise APError(f"Jenkinsfile not found: {jenkinsfile}")
789
782
 
790
783
  required = [
784
+ ("jenkins.base_url", jenkins_cfg.get("base_url")),
785
+ ("jenkins.job_url", jenkins_cfg.get("job_url")),
791
786
  ("jenkins.trigger_branch", jenkins_cfg.get("trigger_branch")),
792
787
  ("jenkins.image_repository", jenkins_cfg.get("image_repository")),
793
788
  ("jenkins.image_tag_strategy", jenkins_cfg.get("image_tag_strategy")),
@@ -796,15 +791,6 @@ def cmd_verify_jenkins(args: argparse.Namespace) -> None:
796
791
  ("target_env.health_path", target_cfg.get("health_path")),
797
792
  ]
798
793
  missing = [name for name, value in required if not str(value or "").strip()]
799
- if not str(jenkins_cfg.get("job_url") or "").strip():
800
- base_url = str(jenkins_cfg.get("base_url") or "").strip()
801
- job_name = str(jenkins_cfg.get("job_name") or "").strip()
802
- multibranch_root = str(jenkins_cfg.get("multibranch_root_job") or "").strip()
803
- if not (base_url and job_name) and not (base_url and multibranch_root):
804
- missing.append(
805
- "jenkins.job_url (or jenkins.base_url + jenkins.job_name, "
806
- "or jenkins.base_url + jenkins.multibranch_root_job)"
807
- )
808
794
  if missing:
809
795
  raise APError("Missing Jenkins config: " + ", ".join(missing))
810
796
  print(f"[verify-jenkins] OK: {jenkinsfile}")
@@ -831,27 +817,26 @@ def cmd_doctor(args: argparse.Namespace) -> None:
831
817
  missing.append("commands.quick_test or commands.test")
832
818
  if not (str(commands.get("lint") or "").strip() or str(commands.get("typecheck") or "").strip()):
833
819
  missing.append("commands.lint or commands.typecheck")
834
- if not str(target_cfg.get("name") or "").strip():
835
- missing.append("target_env.name")
836
- if not str(target_cfg.get("health_base_url") or "").strip():
837
- missing.append("target_env.health_base_url")
838
- if not str(target_cfg.get("health_path") or "").strip():
839
- missing.append("target_env.health_path")
840
- if not str(jenkins_cfg.get("trigger_branch") or "").strip():
841
- missing.append("jenkins.trigger_branch")
842
- if not str(jenkins_cfg.get("image_repository") or "").strip():
843
- missing.append("jenkins.image_repository")
844
- if not str(jenkins_cfg.get("image_tag_strategy") or "").strip():
845
- missing.append("jenkins.image_tag_strategy")
846
- if not str(jenkins_cfg.get("deploy_env") or "").strip():
847
- missing.append("jenkins.deploy_env")
848
-
849
- base_url = str(jenkins_cfg.get("base_url") or "").strip()
850
- job_name = str(jenkins_cfg.get("job_name") or "").strip()
851
- job_url = str(jenkins_cfg.get("job_url") or "").strip()
852
- multibranch_root = str(jenkins_cfg.get("multibranch_root_job") or "").strip()
853
- if not job_url and not (base_url and job_name) and not (base_url and multibranch_root):
854
- missing.append("jenkins.job_url or (jenkins.base_url + jenkins.job_name) or (jenkins.base_url + jenkins.multibranch_root_job)")
820
+ _require_explicit_field(missing, "target_env.name", target_cfg.get("name"))
821
+ _require_explicit_field(missing, "target_env.frontend_base_url", target_cfg.get("frontend_base_url"))
822
+ _require_explicit_field(missing, "target_env.frontend_username", target_cfg.get("frontend_username"))
823
+ _require_explicit_field(missing, "target_env.frontend_password", target_cfg.get("frontend_password"))
824
+ _require_explicit_field(missing, "target_env.backend_base_url", target_cfg.get("backend_base_url"))
825
+ _require_explicit_field(missing, "target_env.backend_username", target_cfg.get("backend_username"))
826
+ _require_explicit_field(missing, "target_env.backend_password", target_cfg.get("backend_password"))
827
+ _require_explicit_field(missing, "target_env.health_base_url", target_cfg.get("health_base_url"))
828
+ _require_explicit_field(missing, "target_env.health_path", target_cfg.get("health_path"))
829
+
830
+ _require_explicit_field(missing, "jenkins.base_url", jenkins_cfg.get("base_url"))
831
+ _require_explicit_field(missing, "jenkins.ui_username", jenkins_cfg.get("ui_username"))
832
+ _require_explicit_field(missing, "jenkins.ui_password", jenkins_cfg.get("ui_password"))
833
+ _require_explicit_field(missing, "jenkins.job_url", jenkins_cfg.get("job_url"))
834
+ _require_explicit_field(missing, "jenkins.trigger_branch", jenkins_cfg.get("trigger_branch"))
835
+ _require_explicit_field(missing, "jenkins.image_repository", jenkins_cfg.get("image_repository"))
836
+ _require_explicit_field(missing, "jenkins.image_tag_strategy", jenkins_cfg.get("image_tag_strategy"))
837
+ _require_explicit_field(missing, "jenkins.deploy_env", jenkins_cfg.get("deploy_env"))
838
+ _require_explicit_field(missing, "jenkins.api_user", jenkins_cfg.get("api_user"))
839
+ _require_explicit_field(missing, "jenkins.api_password", jenkins_cfg.get("api_password"))
855
840
 
856
841
  repo_docs = {
857
842
  "docs.taskbook": Path(repo, str(docs_cfg.get("taskbook", "docs/tasks/taskbook.md"))),
@@ -863,30 +848,31 @@ def cmd_doctor(args: argparse.Namespace) -> None:
863
848
  if not path.exists():
864
849
  warnings.append(f"{key} missing on disk: {path}")
865
850
 
866
- if _pair_state(str(target_cfg.get("backend_username") or "").strip(), str(target_cfg.get("backend_password") or "").strip()) == "partial":
867
- warnings.append("target_env.backend_username / target_env.backend_password is partial")
868
- if _pair_state(str(target_cfg.get("frontend_username") or "").strip(), str(target_cfg.get("frontend_password") or "").strip()) == "partial":
869
- warnings.append("target_env.frontend_username / target_env.frontend_password is partial")
870
- if _pair_state(str(jenkins_cfg.get("ui_username") or "").strip(), str(jenkins_cfg.get("ui_password") or "").strip()) == "partial":
871
- warnings.append("jenkins.ui_username / jenkins.ui_password is partial")
872
-
873
- api_user = str(jenkins_cfg.get("api_user") or "").strip()
874
- api_secret = str(jenkins_cfg.get("api_token") or jenkins_cfg.get("api_password") or "").strip()
875
- if _pair_state(api_user, api_secret) == "partial":
876
- warnings.append("jenkins.api_user with jenkins.api_token/api_password is partial")
851
+ _validate_url_field(warnings, "target_env.frontend_base_url", target_cfg.get("frontend_base_url"))
852
+ _validate_url_field(warnings, "target_env.backend_base_url", target_cfg.get("backend_base_url"))
853
+ _validate_url_field(warnings, "target_env.health_base_url", target_cfg.get("health_base_url"))
854
+ _validate_path_field(warnings, "target_env.health_path", target_cfg.get("health_path"))
855
+ _validate_url_field(warnings, "jenkins.base_url", jenkins_cfg.get("base_url"))
856
+ _validate_url_field(warnings, "jenkins.job_url", jenkins_cfg.get("job_url"))
877
857
 
878
858
  runtime_enabled = any(str(runtime_cfg.get(key) or "").strip() for key in ["docker_compose_file", "docker_service", "health_base_url", "health_path"])
879
859
  if runtime_enabled and not (str(commands.get("compose_up") or "").strip() or str(runtime_cfg.get("docker_compose_file") or "").strip()):
880
860
  warnings.append("runtime config is partially enabled but compose_up or docker_compose_file is missing")
881
861
 
862
+ try:
863
+ timeout_s = int(jenkins_cfg.get("deploy_timeout_sec") or 0)
864
+ if timeout_s <= 0:
865
+ warnings.append("jenkins.deploy_timeout_sec must be a positive integer")
866
+ except Exception:
867
+ warnings.append("jenkins.deploy_timeout_sec must be a positive integer")
868
+
869
+ if warnings:
870
+ missing.extend([f"invalid {item}" for item in warnings])
871
+
882
872
  if missing:
883
- raise APError("Doctor found blocking config issues:\n- " + "\n- ".join(missing) + (("\nWarnings:\n- " + "\n- ".join(warnings)) if warnings else ""))
873
+ raise APError("Doctor found blocking config issues:\n- " + "\n- ".join(missing))
884
874
 
885
875
  print("[doctor] OK")
886
- if warnings:
887
- print("[doctor] Warnings:")
888
- for item in warnings:
889
- print(f"- {item}")
890
876
 
891
877
 
892
878
  def cmd_verify_jenkins_build(args: argparse.Namespace) -> None:
@@ -908,12 +894,10 @@ def cmd_verify_jenkins_build(args: argparse.Namespace) -> None:
908
894
  timeout_s = int(args.timeout_sec or 300)
909
895
  poll_s = int(args.poll_sec or 5)
910
896
  inferred_branch = _resolve_git_branch_name(repo, git_ref)
911
- branch_hint = str(args.branch_name or jenkins_cfg.get("branch_name") or inferred_branch or "").strip()
897
+ branch_hint = str(args.branch_name or inferred_branch or "").strip()
912
898
  root_hint = str(
913
899
  args.multibranch_root_job
914
- or jenkins_cfg.get("multibranch_root_job")
915
900
  or args.job_name
916
- or jenkins_cfg.get("job_name")
917
901
  or args.job_url
918
902
  or jenkins_cfg.get("job_url")
919
903
  or ""
@@ -1064,6 +1048,7 @@ def cmd_record_closure(args: argparse.Namespace) -> None:
1064
1048
  def cmd_commit_push(args: argparse.Namespace) -> None:
1065
1049
  repo = Path(args.repo).resolve()
1066
1050
  ensure_git_repo(repo)
1051
+ cmd_doctor(argparse.Namespace(repo=str(repo)))
1067
1052
 
1068
1053
  msg = args.msg
1069
1054
 
package/cli/src/index.js CHANGED
@@ -62,19 +62,6 @@ function resolveTargetDir(ai, mode, destOverride){
62
62
  die(`unknown ai: ${ai}`);
63
63
  }
64
64
 
65
- function ensureGitignore(projectDir){
66
- const gi = path.join(projectDir, ".gitignore");
67
- const line = "docs/ENGINEERING.md";
68
- if (!exists(gi)) {
69
- fs.writeFileSync(gi, `${line}\n`, "utf-8");
70
- return;
71
- }
72
- const txt = fs.readFileSync(gi, "utf-8");
73
- if (!txt.includes(line)) {
74
- fs.appendFileSync(gi, (txt.endsWith("\n") ? "" : "\n") + line + "\n");
75
- }
76
- }
77
-
78
65
  function main(){
79
66
  const args = parseArgs(process.argv);
80
67
 
@@ -104,8 +91,6 @@ Examples:
104
91
  const assetSkill = path.resolve(here, "..", "assets", "skill");
105
92
  if (!exists(assetSkill)) die(`missing assets at ${assetSkill}`);
106
93
 
107
- const proj = projectRoot();
108
-
109
94
  for (const t of targets) {
110
95
  const dstOverride = args.dest
111
96
  ? (targets.length > 1 ? path.join(args.dest, t) : args.dest)
@@ -119,8 +104,6 @@ Examples:
119
104
  console.log(`[autocoding] installed skill to: ${dst}`);
120
105
  }
121
106
 
122
- if (args.mode === "project") ensureGitignore(proj);
123
-
124
107
  console.log("[autocoding] done.");
125
108
  }
126
109
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elvis1513/auto-coding-skill",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "CLI installer for auto-coding-skill (Claude Code + Codex CLI) with Go fullstack + Jenkins workflow support.",
5
5
  "type": "module",
6
6
  "bin": {