contractor-bid 0.2.0__py3-none-any.whl

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 (137) hide show
  1. contractor_bid/.claude-plugin/marketplace.json +13 -0
  2. contractor_bid/.claude-plugin/plugin.json +19 -0
  3. contractor_bid/.cursor-plugin/plugin.json +30 -0
  4. contractor_bid/.mcp.json +8 -0
  5. contractor_bid/AGENTS.md +66 -0
  6. contractor_bid/CLAUDE.md +5 -0
  7. contractor_bid/__init__.py +5 -0
  8. contractor_bid/__main__.py +4 -0
  9. contractor_bid/cli.py +506 -0
  10. contractor_bid/codex-marketplace.json +18 -0
  11. contractor_bid/commands/check.md +7 -0
  12. contractor_bid/commands/new-bid.md +11 -0
  13. contractor_bid/commands/triage.md +10 -0
  14. contractor_bid/csi_starters.py +1269 -0
  15. contractor_bid/docs/CSI_DIVISIONS.md +65 -0
  16. contractor_bid/docs/MCP_PLUGIN.md +129 -0
  17. contractor_bid/docs/SELF_LEARNING.md +36 -0
  18. contractor_bid/docs/WHAT_YOU_GET.md +43 -0
  19. contractor_bid/docs/WORKFLOW.md +63 -0
  20. contractor_bid/docs/assets/contractor-bid-flow.svg +118 -0
  21. contractor_bid/doctor.py +100 -0
  22. contractor_bid/examples/agent-session.md +102 -0
  23. contractor_bid/examples/profiles/concrete-flatwork.json +16 -0
  24. contractor_bid/examples/profiles/division-03-concrete.json +68 -0
  25. contractor_bid/examples/profiles/division-04-masonry.json +64 -0
  26. contractor_bid/examples/profiles/division-05-metals.json +68 -0
  27. contractor_bid/examples/profiles/division-06-wood-plastics-composites.json +64 -0
  28. contractor_bid/examples/profiles/division-07-thermal-moisture-protection.json +68 -0
  29. contractor_bid/examples/profiles/division-08-openings.json +67 -0
  30. contractor_bid/examples/profiles/division-09-finishes.json +66 -0
  31. contractor_bid/examples/profiles/division-10-specialties.json +65 -0
  32. contractor_bid/examples/profiles/division-11-equipment.json +64 -0
  33. contractor_bid/examples/profiles/division-12-furnishings.json +64 -0
  34. contractor_bid/examples/profiles/division-13-special-construction.json +65 -0
  35. contractor_bid/examples/profiles/division-14-conveying-equipment.json +63 -0
  36. contractor_bid/examples/profiles/division-21-fire-suppression.json +63 -0
  37. contractor_bid/examples/profiles/division-22-plumbing.json +64 -0
  38. contractor_bid/examples/profiles/division-23-hvac.json +69 -0
  39. contractor_bid/examples/profiles/division-25-integrated-automation.json +64 -0
  40. contractor_bid/examples/profiles/division-26-electrical.json +66 -0
  41. contractor_bid/examples/profiles/division-27-communications.json +65 -0
  42. contractor_bid/examples/profiles/division-28-electronic-safety-security.json +66 -0
  43. contractor_bid/examples/profiles/division-31-earthwork.json +67 -0
  44. contractor_bid/examples/profiles/division-32-exterior-improvements.json +69 -0
  45. contractor_bid/examples/profiles/division-33-utilities.json +68 -0
  46. contractor_bid/examples/profiles/drywall-framing.json +16 -0
  47. contractor_bid/examples/profiles/electrical.json +16 -0
  48. contractor_bid/examples/profiles/fences-gates.json +16 -0
  49. contractor_bid/examples/profiles/hvac.json +70 -0
  50. contractor_bid/examples/profiles/plumbing.json +65 -0
  51. contractor_bid/examples/profiles/roofing.json +68 -0
  52. contractor_bid/learning.py +42 -0
  53. contractor_bid/mcp_server.py +845 -0
  54. contractor_bid/packets.py +326 -0
  55. contractor_bid/profile.py +185 -0
  56. contractor_bid/profiles/README.md +27 -0
  57. contractor_bid/profiles/concrete-flatwork.json +16 -0
  58. contractor_bid/profiles/division-03-concrete.json +68 -0
  59. contractor_bid/profiles/division-04-masonry.json +64 -0
  60. contractor_bid/profiles/division-05-metals.json +68 -0
  61. contractor_bid/profiles/division-06-wood-plastics-composites.json +64 -0
  62. contractor_bid/profiles/division-07-thermal-moisture-protection.json +68 -0
  63. contractor_bid/profiles/division-08-openings.json +67 -0
  64. contractor_bid/profiles/division-09-finishes.json +66 -0
  65. contractor_bid/profiles/division-10-specialties.json +65 -0
  66. contractor_bid/profiles/division-11-equipment.json +64 -0
  67. contractor_bid/profiles/division-12-furnishings.json +64 -0
  68. contractor_bid/profiles/division-13-special-construction.json +65 -0
  69. contractor_bid/profiles/division-14-conveying-equipment.json +63 -0
  70. contractor_bid/profiles/division-21-fire-suppression.json +63 -0
  71. contractor_bid/profiles/division-22-plumbing.json +64 -0
  72. contractor_bid/profiles/division-23-hvac.json +69 -0
  73. contractor_bid/profiles/division-25-integrated-automation.json +64 -0
  74. contractor_bid/profiles/division-26-electrical.json +66 -0
  75. contractor_bid/profiles/division-27-communications.json +65 -0
  76. contractor_bid/profiles/division-28-electronic-safety-security.json +66 -0
  77. contractor_bid/profiles/division-31-earthwork.json +67 -0
  78. contractor_bid/profiles/division-32-exterior-improvements.json +69 -0
  79. contractor_bid/profiles/division-33-utilities.json +68 -0
  80. contractor_bid/profiles/drywall-framing.json +16 -0
  81. contractor_bid/profiles/electrical.json +16 -0
  82. contractor_bid/profiles/fences-gates.json +16 -0
  83. contractor_bid/profiles/hvac.json +16 -0
  84. contractor_bid/profiles/plumbing.json +16 -0
  85. contractor_bid/profiles/roofing.json +16 -0
  86. contractor_bid/project.py +101 -0
  87. contractor_bid/scripts/generate-csi-starters.py +94 -0
  88. contractor_bid/scripts/install.ps1 +78 -0
  89. contractor_bid/scripts/install.sh +123 -0
  90. contractor_bid/sendoff.py +76 -0
  91. contractor_bid/skills/README.md +13 -0
  92. contractor_bid/skills/bid-tracker/SKILL.md +99 -0
  93. contractor_bid/skills/concrete-flatwork-bid-scope/SKILL.md +62 -0
  94. contractor_bid/skills/division-03-concrete-bid-scope/SKILL.md +67 -0
  95. contractor_bid/skills/division-04-masonry-bid-scope/SKILL.md +65 -0
  96. contractor_bid/skills/division-05-metals-bid-scope/SKILL.md +66 -0
  97. contractor_bid/skills/division-06-wood-plastics-composites-bid-scope/SKILL.md +65 -0
  98. contractor_bid/skills/division-07-thermal-moisture-protection-bid-scope/SKILL.md +66 -0
  99. contractor_bid/skills/division-08-openings-bid-scope/SKILL.md +66 -0
  100. contractor_bid/skills/division-09-finishes-bid-scope/SKILL.md +65 -0
  101. contractor_bid/skills/division-10-specialties-bid-scope/SKILL.md +65 -0
  102. contractor_bid/skills/division-11-equipment-bid-scope/SKILL.md +65 -0
  103. contractor_bid/skills/division-12-furnishings-bid-scope/SKILL.md +64 -0
  104. contractor_bid/skills/division-13-special-construction-bid-scope/SKILL.md +65 -0
  105. contractor_bid/skills/division-14-conveying-equipment-bid-scope/SKILL.md +64 -0
  106. contractor_bid/skills/division-21-fire-suppression-bid-scope/SKILL.md +64 -0
  107. contractor_bid/skills/division-22-plumbing-bid-scope/SKILL.md +65 -0
  108. contractor_bid/skills/division-23-hvac-bid-scope/SKILL.md +66 -0
  109. contractor_bid/skills/division-25-integrated-automation-bid-scope/SKILL.md +65 -0
  110. contractor_bid/skills/division-26-electrical-bid-scope/SKILL.md +66 -0
  111. contractor_bid/skills/division-27-communications-bid-scope/SKILL.md +65 -0
  112. contractor_bid/skills/division-28-electronic-safety-security-bid-scope/SKILL.md +66 -0
  113. contractor_bid/skills/division-31-earthwork-bid-scope/SKILL.md +66 -0
  114. contractor_bid/skills/division-32-exterior-improvements-bid-scope/SKILL.md +66 -0
  115. contractor_bid/skills/division-33-utilities-bid-scope/SKILL.md +65 -0
  116. contractor_bid/skills/drywall-framing-bid-scope/SKILL.md +62 -0
  117. contractor_bid/skills/electrical-bid-scope/SKILL.md +63 -0
  118. contractor_bid/skills/fences-gates-bid-scope/SKILL.md +64 -0
  119. contractor_bid/skills/hvac-bid-scope/SKILL.md +72 -0
  120. contractor_bid/skills/plumbing-bid-scope/SKILL.md +69 -0
  121. contractor_bid/skills/roofing-bid-scope/SKILL.md +71 -0
  122. contractor_bid/templates/00-scope-reference-index-template.md +45 -0
  123. contractor_bid/templates/02-proposal-letter-template.md +48 -0
  124. contractor_bid/templates/project-readme-template.md +38 -0
  125. contractor_bid/templates/review-pages-template.md +7 -0
  126. contractor_bid/templates/scope-pages-sources-template.json +19 -0
  127. contractor_bid/templates/takeoff-template.json +48 -0
  128. contractor_bid/tracker.py +382 -0
  129. contractor_bid/triage.py +483 -0
  130. contractor_bid/util.py +90 -0
  131. contractor_bid/validate.py +251 -0
  132. contractor_bid/workbook.py +328 -0
  133. contractor_bid-0.2.0.dist-info/METADATA +402 -0
  134. contractor_bid-0.2.0.dist-info/RECORD +137 -0
  135. contractor_bid-0.2.0.dist-info/WHEEL +4 -0
  136. contractor_bid-0.2.0.dist-info/entry_points.txt +3 -0
  137. contractor_bid-0.2.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "contractor-bid",
3
+ "owner": {
4
+ "name": "ContractorKeith"
5
+ },
6
+ "plugins": [
7
+ {
8
+ "name": "contractor-bid",
9
+ "source": "./",
10
+ "description": "AI-ready bid workspaces for commercial subcontractors."
11
+ }
12
+ ]
13
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "contractor-bid",
3
+ "version": "0.2.0",
4
+ "description": "AI-ready bid workspaces for commercial subcontractors. Triage plan sets, build scope/spec packets, takeoff workbooks, alerts, and supplier sendoffs.",
5
+ "author": {
6
+ "name": "ContractorKeith",
7
+ "url": "https://github.com/ContractorKeith"
8
+ },
9
+ "repository": "https://github.com/ContractorKeith/contractor-bid",
10
+ "license": "MIT",
11
+ "keywords": [
12
+ "construction",
13
+ "estimating",
14
+ "bidding",
15
+ "csi",
16
+ "subcontractor",
17
+ "takeoff"
18
+ ]
19
+ }
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "contractor-bid",
3
+ "version": "0.2.0",
4
+ "description": "AI-ready bid workspaces for commercial subcontractors.",
5
+ "author": {
6
+ "name": "ContractorKeith",
7
+ "url": "https://github.com/ContractorKeith"
8
+ },
9
+ "repository": "https://github.com/ContractorKeith/contractor-bid",
10
+ "license": "MIT",
11
+ "skills": [
12
+ "skills"
13
+ ],
14
+ "commands": [
15
+ "commands"
16
+ ],
17
+ "mcpServers": {
18
+ "contractor-bid": {
19
+ "command": "contractor-bid-mcp",
20
+ "args": []
21
+ }
22
+ },
23
+ "keywords": [
24
+ "construction",
25
+ "estimating",
26
+ "bidding",
27
+ "csi",
28
+ "subcontractor"
29
+ ]
30
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "mcpServers": {
3
+ "contractor-bid": {
4
+ "command": "contractor-bid-mcp",
5
+ "args": []
6
+ }
7
+ }
8
+ }
@@ -0,0 +1,66 @@
1
+ # Agent Instructions
2
+
3
+ This repo builds AI-ready commercial subcontractor bid projects.
4
+
5
+ ## Operating Rules
6
+
7
+ - Read `profiles/<profile>.json` and `skills/<profile>-bid-scope/SKILL.md` before making scope calls.
8
+ - Built-in starter profiles include canonical `division-XX-*` profiles for every active CSI MasterFormat division from 03 through 33. Read `docs/CSI_DIVISIONS.md` when selecting a starter.
9
+ - Trade-specific examples such as `fences-gates`, `concrete-flatwork`, `drywall-framing`, `electrical`, `plumbing`, `hvac`, and `roofing` are narrower examples, not the full CSI coverage set.
10
+ - If none of the starter profiles fit, run `contractor-bid init` to create a custom profile and matching skill.
11
+ - Source PDFs and bid forms belong in `bid-docs/`; generated artifacts belong in `bid-package-working/`.
12
+ - Treat `takeoff/*.json` as the source of truth for workbook generation.
13
+ - Do not silently include excluded or review-only adjacent scopes in base bid.
14
+ - Carry the same scope boundary through the summary, reference index, workbook, proposal letter, alerts, and sendoff.
15
+ - Record user corrections with `contractor-bid learn`; only update durable profile rules when the user confirms the correction should persist.
16
+ - Keep the workspace bid tracker current with the `track-*` commands, but ALWAYS confirm with the user and show a one-line change summary before writing (see `skills/bid-tracker/SKILL.md`).
17
+
18
+ ## Standard Pipeline
19
+
20
+ ```bash
21
+ contractor-bid doctor
22
+ contractor-bid triage <project> --profile <profile> --render
23
+ # Human review: open candidate-pages.md and scope-pages-sources.suggested.json.
24
+ # Copy/merge approved pages into takeoff/scope-pages-sources.json.
25
+ contractor-bid build-packets <project>
26
+ # Human review: fill the takeoff/BOM JSON from source-backed quantities and quotes.
27
+ contractor-bid build-workbook <project> --profile <profile>
28
+ contractor-bid check <project> --profile <profile>
29
+ contractor-bid package-sendoff <project>
30
+ ```
31
+
32
+ Use `contractor-bid status <project> --profile <profile>` for a non-writing readiness check,
33
+ or `contractor-bid run <project> --profile <profile>` after the two human-fill steps are done.
34
+
35
+ ## Bid Tracker
36
+
37
+ Maintain a workspace-wide pipeline view with the `track-*` commands. Source of truth is
38
+ `.contractor-bid/bid-tracker.json`; the readable sheet is `Bid-Tracker.xlsx` (Active Bids +
39
+ Archived & Completed). Read `skills/bid-tracker/SKILL.md` first.
40
+
41
+ Hard rule: never write to the tracker silently. Before any `track-add`, `track-update`,
42
+ `track-move`, or `track-reopen`, show the user a one-line summary of the change and wait for
43
+ confirmation. `track-list` and `track-build` are read-only.
44
+
45
+ ```bash
46
+ contractor-bid track-add bids/<project> --progress Triage # add a bid (pulls project.json)
47
+ contractor-bid track-update "<bid>" --progress Submitted --next "Follow up Friday"
48
+ contractor-bid track-move "<bid>" --outcome won # moves it to Archived & Completed
49
+ contractor-bid track-list # read-only
50
+ ```
51
+
52
+ ## Review Before Pricing
53
+
54
+ - Latest addendum/revision basis.
55
+ - Manual measurements and quantity basis.
56
+ - Scope exclusions and review-only terms.
57
+ - Alternates versus base bid.
58
+ - GC bid-form requirements.
59
+ - Supplier quote inputs and lead times.
60
+
61
+ ## Installation Assumptions
62
+
63
+ - Normal users install with `scripts/install.sh` or `scripts/install.ps1`.
64
+ - Python package dependencies are installed into the contractor-bid virtualenv.
65
+ - Poppler is recommended for PDF extraction/rendering; without it, `pypdf` fallback works for some PDFs but page images are unavailable.
66
+ - GitHub CLI is not required for normal use.
@@ -0,0 +1,5 @@
1
+ # Claude Compatibility
2
+
3
+ Read `AGENTS.md` first. It is the canonical agent workflow for this repository.
4
+
5
+ Claude-specific reminder: do not invent final quantities from PDF text hits. Text hits are triage evidence only; quantities require source-backed measurement or a stated manual placeholder.
@@ -0,0 +1,5 @@
1
+ """AI-ready commercial subcontractor bid-project toolkit."""
2
+
3
+ __all__ = ["__version__"]
4
+
5
+ __version__ = "0.2.0"
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
contractor_bid/cli.py ADDED
@@ -0,0 +1,506 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import sys
5
+ from datetime import date
6
+ from importlib.metadata import PackageNotFoundError, version
7
+ from pathlib import Path
8
+
9
+ from . import __version__
10
+ from .doctor import format_doctor, run_doctor
11
+ from .learning import record_feedback
12
+ from .packets import build_packets
13
+ from .profile import build_profile, list_available_profiles, load_profile, parse_csv, write_profile
14
+ from .project import create_project, ensure_workspace
15
+ from .sendoff import package_sendoff
16
+ from .tracker import (
17
+ OUTCOMES,
18
+ PROGRESS_STAGES,
19
+ active_bids,
20
+ add_or_update,
21
+ archived_bids,
22
+ change_summary,
23
+ load_tracker,
24
+ move_entry,
25
+ reopen_entry,
26
+ render_tracker,
27
+ )
28
+ from .triage import triage_project
29
+ from .util import markdown_table
30
+ from .validate import deliverable_checklist, validate_project
31
+ from .workbook import build_workbook
32
+
33
+
34
+ def package_version() -> str:
35
+ try:
36
+ return version("contractor-bid")
37
+ except PackageNotFoundError:
38
+ return __version__
39
+
40
+
41
+ def ask(prompt: str, default: str = "") -> str:
42
+ suffix = f" [{default}]" if default else ""
43
+ value = input(f"{prompt}{suffix}: ").strip()
44
+ return value or default
45
+
46
+
47
+ def ask_list(prompt: str, default: list[str] | None = None) -> list[str]:
48
+ default = default or []
49
+ rendered = ", ".join(default)
50
+ value = ask(prompt, rendered)
51
+ return parse_csv(value)
52
+
53
+
54
+ def command_init(args: argparse.Namespace) -> int:
55
+ root = args.root.resolve()
56
+ ensure_workspace(root)
57
+ if args.non_interactive:
58
+ company = args.company or "Your Company"
59
+ trade = args.trade or "Your Scope"
60
+ scope_rule = args.scope_rule
61
+ divisions = parse_csv(args.divisions)
62
+ base_scope = parse_csv(args.base_scope)
63
+ include_terms = parse_csv(args.include_terms)
64
+ spec_sections = parse_csv(args.spec_sections)
65
+ quantity_units = parse_csv(args.quantity_units)
66
+ review_terms = parse_csv(args.review_terms)
67
+ exclude_terms = parse_csv(args.exclude_terms)
68
+ proposal_exclusions = parse_csv(args.proposal_exclusions)
69
+ else:
70
+ print("Create a reusable scope profile for your commercial bid workflow.\n")
71
+ company = ask("Company name", args.company or "Your Company")
72
+ trade = ask("Trade / scope name", args.trade or "Fences and Gates")
73
+ scope_rule = ask(
74
+ "Scope rule",
75
+ args.scope_rule
76
+ or (
77
+ f"Base bid is limited to {trade}. Adjacent scopes must be explicitly "
78
+ "included, excluded, or flagged before pricing."
79
+ ),
80
+ )
81
+ divisions = ask_list("CSI division(s), comma-separated", parse_csv(args.divisions) or ["32"])
82
+ base_scope = ask_list("Work you usually carry in base bid")
83
+ include_terms = ask_list("Keywords/spec terms that identify your scope")
84
+ spec_sections = ask_list("CSI spec section hints")
85
+ quantity_units = ask_list("Quantity units you measure", ["EA", "LF", "SF", "CY"])
86
+ review_terms = ask_list("Adjacent scope terms to flag before pricing")
87
+ exclude_terms = ask_list("Adjacent scope terms to exclude by default")
88
+ proposal_exclusions = ask_list("Standard proposal exclusions")
89
+
90
+ profile = build_profile(
91
+ company_name=company,
92
+ trade_name=trade,
93
+ profile_id=args.profile,
94
+ scope_rule=scope_rule,
95
+ divisions=divisions,
96
+ base_scope=base_scope,
97
+ include_terms=include_terms,
98
+ spec_sections=spec_sections,
99
+ quantity_units=quantity_units,
100
+ review_terms=review_terms,
101
+ exclude_terms=exclude_terms,
102
+ proposal_exclusions=proposal_exclusions,
103
+ )
104
+ profile_file, skill_file = write_profile(root, profile)
105
+ print(f"Wrote profile: {profile_file}")
106
+ print(f"Wrote skill: {skill_file}")
107
+ return 0
108
+
109
+
110
+ def command_new(args: argparse.Namespace) -> int:
111
+ root = args.root.resolve()
112
+ ensure_workspace(root)
113
+ profile = load_profile(args.profile, root)
114
+ created = create_project(
115
+ project_dir=args.project.resolve(),
116
+ profile=profile,
117
+ project_name=args.project_name,
118
+ bid_due=args.bid_due,
119
+ gc=args.gc,
120
+ address=args.address,
121
+ )
122
+ print(f"Created bid project: {args.project.resolve()}")
123
+ for path in created:
124
+ print(f"- {path}")
125
+ return 0
126
+
127
+
128
+ def command_triage(args: argparse.Namespace) -> int:
129
+ root = args.root.resolve()
130
+ profile = load_profile(args.profile, root)
131
+ hits = triage_project(
132
+ args.project.resolve(),
133
+ profile,
134
+ render=args.render,
135
+ max_render=args.max_render,
136
+ write_sources=args.write_sources,
137
+ )
138
+ print(f"Triage complete: {len(hits)} candidate page(s)")
139
+ print(f"- {args.project.resolve() / 'bid-package-working' / 'takeoff' / 'candidate-pages.md'}")
140
+ print(f"- {args.project.resolve() / 'bid-package-working' / 'takeoff' / 'triage-scope-signals.md'}")
141
+ return 0
142
+
143
+
144
+ def command_build_packets(args: argparse.Namespace) -> int:
145
+ result = build_packets(args.project.resolve(), sources=args.sources)
146
+ print("Built page-packet artifacts")
147
+ print(f"- Summary: {result['summary']}")
148
+ print(f"- Scope pages: {result['scope_pages']}")
149
+ print(f"- Spec pages: {result['spec_pages']}")
150
+ return 0
151
+
152
+
153
+ def command_build_workbook(args: argparse.Namespace) -> int:
154
+ profile = load_profile(args.profile, args.root.resolve()) if args.profile else None
155
+ out = build_workbook(args.project.resolve(), profile=profile, config=args.config, out=args.out)
156
+ print(f"Built workbook: {out}")
157
+ return 0
158
+
159
+
160
+ def command_check(args: argparse.Namespace) -> int:
161
+ profile = load_profile(args.profile, args.root.resolve())
162
+ today = date.fromisoformat(args.today) if args.today else None
163
+ out, exit_code, warnings, errors = validate_project(
164
+ args.project.resolve(),
165
+ profile,
166
+ today=today,
167
+ write=not args.no_write,
168
+ )
169
+ print(f"Wrote alerts: {out}")
170
+ print(f"Warnings: {len(warnings)}")
171
+ print(f"Hard errors: {len(errors)}")
172
+ return exit_code
173
+
174
+
175
+ def command_status(args: argparse.Namespace) -> int:
176
+ profile = load_profile(args.profile, args.root.resolve())
177
+ today = date.fromisoformat(args.today) if args.today else None
178
+ project = args.project.resolve()
179
+ rows, _alerts, _errors = deliverable_checklist(project / "bid-package-working")
180
+ _out, exit_code, warnings, errors = validate_project(
181
+ project,
182
+ profile,
183
+ today=today,
184
+ write=False,
185
+ )
186
+ print(f"Status for: {project}")
187
+ print("")
188
+ print("\n".join(markdown_table(["Status", "Deliverable", "Path"], rows)))
189
+ print("")
190
+ print(f"Warnings: {len(warnings)}")
191
+ print(f"Hard errors: {len(errors)}")
192
+ return exit_code
193
+
194
+
195
+ def command_package(args: argparse.Namespace) -> int:
196
+ out_dir, zip_path = package_sendoff(args.project.resolve(), name=args.name)
197
+ print(f"Built sendoff folder: {out_dir}")
198
+ print(f"Built sendoff zip: {zip_path}")
199
+ return 0
200
+
201
+
202
+ def command_learn(args: argparse.Namespace) -> int:
203
+ path = record_feedback(
204
+ args.root.resolve(),
205
+ note=args.note,
206
+ project=str(args.project) if args.project else None,
207
+ profile_id=args.profile,
208
+ category=args.category,
209
+ )
210
+ print(f"Recorded feedback: {path}")
211
+ return 0
212
+
213
+
214
+ def command_doctor(args: argparse.Namespace) -> int:
215
+ checks, exit_code = run_doctor()
216
+ print(format_doctor(checks))
217
+ return exit_code
218
+
219
+
220
+ def command_list_profiles(args: argparse.Namespace) -> int:
221
+ rows = list_available_profiles(args.root.resolve())
222
+ if not rows:
223
+ print("No profiles found.")
224
+ return 1
225
+ for profile_id, trade_name, source in rows:
226
+ print(f"{profile_id} - {trade_name} ({source})")
227
+ return 0
228
+
229
+
230
+ def command_run(args: argparse.Namespace) -> int:
231
+ profile = load_profile(args.profile, args.root.resolve())
232
+ project = args.project.resolve()
233
+
234
+ packets = build_packets(project)
235
+ print(f"Built page-packet artifacts: {packets['summary']}")
236
+
237
+ workbook = build_workbook(project, profile=profile)
238
+ print(f"Built workbook: {workbook}")
239
+
240
+ today = date.fromisoformat(args.today) if args.today else None
241
+ alerts, exit_code, warnings, errors = validate_project(project, profile, today=today)
242
+ print(f"Wrote alerts: {alerts}")
243
+ print(f"Warnings: {len(warnings)}")
244
+ print(f"Hard errors: {len(errors)}")
245
+
246
+ out_dir, zip_path = package_sendoff(project, name=args.name)
247
+ print(f"Built sendoff folder: {out_dir}")
248
+ print(f"Built sendoff zip: {zip_path}")
249
+ return exit_code
250
+
251
+
252
+ def _render_and_report(root: Path) -> None:
253
+ out = render_tracker(root)
254
+ if out:
255
+ print(f"Tracker spreadsheet: {out}")
256
+ else:
257
+ print("Tracker JSON updated. Install openpyxl to render Bid-Tracker.xlsx.")
258
+
259
+
260
+ def command_track_add(args: argparse.Namespace) -> int:
261
+ root = args.root.resolve()
262
+ ensure_workspace(root)
263
+ project_path = args.project.resolve() if args.project else None
264
+ entry, created, changed = add_or_update(
265
+ root,
266
+ project_path=project_path,
267
+ id=args.id,
268
+ name=args.name,
269
+ location=args.location,
270
+ due_date=args.due,
271
+ progress=args.progress,
272
+ next_action=args.next_action,
273
+ client_gc=args.gc,
274
+ profile=args.profile,
275
+ note=args.note,
276
+ )
277
+ print(change_summary(entry, created, changed))
278
+ _render_and_report(root)
279
+ return 0
280
+
281
+
282
+ def command_track_update(args: argparse.Namespace) -> int:
283
+ root = args.root.resolve()
284
+ ensure_workspace(root)
285
+ entry, created, changed = add_or_update(
286
+ root,
287
+ id=args.bid,
288
+ location=args.location,
289
+ due_date=args.due,
290
+ progress=args.progress,
291
+ next_action=args.next_action,
292
+ client_gc=args.gc,
293
+ note=args.note,
294
+ )
295
+ print(change_summary(entry, created, changed))
296
+ _render_and_report(root)
297
+ return 0
298
+
299
+
300
+ def command_track_move(args: argparse.Namespace) -> int:
301
+ root = args.root.resolve()
302
+ ensure_workspace(root)
303
+ entry = move_entry(root, args.bid, outcome=args.outcome)
304
+ print(f"Moved '{entry.get('project')}' to Archived/Completed (outcome={entry.get('outcome')}).")
305
+ _render_and_report(root)
306
+ return 0
307
+
308
+
309
+ def command_track_reopen(args: argparse.Namespace) -> int:
310
+ root = args.root.resolve()
311
+ ensure_workspace(root)
312
+ entry = reopen_entry(root, args.bid)
313
+ print(f"Reopened '{entry.get('project')}' back to Active Bids.")
314
+ _render_and_report(root)
315
+ return 0
316
+
317
+
318
+ def command_track_list(args: argparse.Namespace) -> int:
319
+ root = args.root.resolve()
320
+ data = load_tracker(root)
321
+ active = active_bids(data)
322
+ print(f"Active bids ({len(active)}):")
323
+ if active:
324
+ rows = [
325
+ [b.get("project", ""), b.get("due_date", "") or "TBD", b.get("progress", ""), b.get("next_action", "")]
326
+ for b in active
327
+ ]
328
+ print("\n".join(markdown_table(["Project", "Due", "Progress", "Next Action"], rows)))
329
+ else:
330
+ print("- none")
331
+ if args.all:
332
+ archived = archived_bids(data)
333
+ print(f"\nArchived/completed bids ({len(archived)}):")
334
+ if archived:
335
+ rows = [
336
+ [b.get("project", ""), b.get("outcome", ""), b.get("closed", "")] for b in archived
337
+ ]
338
+ print("\n".join(markdown_table(["Project", "Outcome", "Closed"], rows)))
339
+ else:
340
+ print("- none")
341
+ return 0
342
+
343
+
344
+ def command_track_build(args: argparse.Namespace) -> int:
345
+ root = args.root.resolve()
346
+ ensure_workspace(root)
347
+ out = render_tracker(root)
348
+ if out:
349
+ print(f"Built tracker spreadsheet: {out}")
350
+ return 0
351
+ print("openpyxl is required to render Bid-Tracker.xlsx. Install with: pip install openpyxl")
352
+ return 1
353
+
354
+
355
+ def build_parser() -> argparse.ArgumentParser:
356
+ parser = argparse.ArgumentParser(
357
+ prog="contractor-bid",
358
+ description="Create and maintain AI-ready commercial subcontractor bid projects.",
359
+ )
360
+ parser.add_argument("--root", type=Path, default=Path.cwd(), help="Workspace root containing profiles/ and skills/.")
361
+ parser.add_argument(
362
+ "--version",
363
+ action="version",
364
+ version=f"%(prog)s {package_version()}",
365
+ )
366
+ sub = parser.add_subparsers(dest="command", required=True)
367
+
368
+ list_profiles = sub.add_parser("list-profiles", help="List built-in and workspace profiles.")
369
+ list_profiles.set_defaults(func=command_list_profiles)
370
+
371
+ init = sub.add_parser("init", help="Create a reusable scope profile and generated agent skill.")
372
+ init.add_argument("--profile", default=None, help="Profile id, e.g. division-32-exterior-improvements.")
373
+ init.add_argument("--company", default=None)
374
+ init.add_argument("--trade", default=None)
375
+ init.add_argument("--divisions", default="")
376
+ init.add_argument("--base-scope", default="")
377
+ init.add_argument("--include-terms", default="")
378
+ init.add_argument("--spec-sections", default="")
379
+ init.add_argument("--quantity-units", default="")
380
+ init.add_argument("--review-terms", default="")
381
+ init.add_argument("--exclude-terms", default="")
382
+ init.add_argument("--proposal-exclusions", default="")
383
+ init.add_argument("--scope-rule", default=None)
384
+ init.add_argument("--non-interactive", action="store_true")
385
+ init.set_defaults(func=command_init)
386
+
387
+ new = sub.add_parser("new", help="Create a bid project folder from a scope profile.")
388
+ new.add_argument("project", type=Path)
389
+ new.add_argument("--profile", required=True, help="Profile id or path to profile JSON.")
390
+ new.add_argument("--project-name", default=None)
391
+ new.add_argument("--bid-due", default=None)
392
+ new.add_argument("--gc", default=None)
393
+ new.add_argument("--address", default=None)
394
+ new.set_defaults(func=command_new)
395
+
396
+ triage = sub.add_parser("triage", help="Extract PDF text and score candidate scope pages.")
397
+ triage.add_argument("project", type=Path)
398
+ triage.add_argument("--profile", required=True)
399
+ triage.add_argument("--render", action="store_true", help="Render top candidate pages with pdftoppm when available.")
400
+ triage.add_argument("--max-render", type=int, default=20)
401
+ triage.add_argument(
402
+ "--write-sources",
403
+ action="store_true",
404
+ help="Copy suggested scope pages into the canonical sources JSON only if it is empty.",
405
+ )
406
+ triage.set_defaults(func=command_triage)
407
+
408
+ packets = sub.add_parser("build-packets", help="Build scope/spec page PDFs and quick-read summary.")
409
+ packets.add_argument("project", type=Path)
410
+ packets.add_argument("--sources", type=Path, default=None)
411
+ packets.set_defaults(func=command_build_packets)
412
+
413
+ workbook = sub.add_parser("build-workbook", help="Build takeoff/BOM workbook from JSON.")
414
+ workbook.add_argument("project", type=Path)
415
+ workbook.add_argument("--profile", default=None)
416
+ workbook.add_argument("--config", type=Path, default=None)
417
+ workbook.add_argument("--out", type=Path, default=None)
418
+ workbook.set_defaults(func=command_build_workbook)
419
+
420
+ check = sub.add_parser("check", help="Validate required bid package artifacts and scope guardrails.")
421
+ check.add_argument("project", type=Path)
422
+ check.add_argument("--profile", required=True)
423
+ check.add_argument("--today", default=None, help="Override date for due-date math, YYYY-MM-DD.")
424
+ check.add_argument("--no-write", action="store_true")
425
+ check.set_defaults(func=command_check)
426
+
427
+ status = sub.add_parser("status", help="Show bid package status without writing ALERTS.md.")
428
+ status.add_argument("project", type=Path)
429
+ status.add_argument("--profile", required=True)
430
+ status.add_argument("--today", default=None, help="Override date for due-date math, YYYY-MM-DD.")
431
+ status.set_defaults(func=command_status)
432
+
433
+ package = sub.add_parser("package-sendoff", help="Create supplier/partner sendoff folder and zip.")
434
+ package.add_argument("project", type=Path)
435
+ package.add_argument("--name", default=None)
436
+ package.set_defaults(func=command_package)
437
+
438
+ run = sub.add_parser("run", help="Run packets, workbook, validation, and sendoff packaging.")
439
+ run.add_argument("project", type=Path)
440
+ run.add_argument("--profile", required=True)
441
+ run.add_argument("--today", default=None, help="Override date for due-date math, YYYY-MM-DD.")
442
+ run.add_argument("--name", default=None, help="Optional sendoff package name.")
443
+ run.set_defaults(func=command_run)
444
+
445
+ learn = sub.add_parser("learn", help="Record a correction or reusable lesson for future bids.")
446
+ learn.add_argument("--note", required=True)
447
+ learn.add_argument("--project", type=Path, default=None)
448
+ learn.add_argument("--profile", default=None)
449
+ learn.add_argument("--category", default="correction")
450
+ learn.set_defaults(func=command_learn)
451
+
452
+ def add_field_flags(p: argparse.ArgumentParser) -> None:
453
+ p.add_argument("--location", default=None, help="Project location / address.")
454
+ p.add_argument("--due", default=None, help="Bid due date, e.g. 2026-07-01 or '2026-07-01 14:00'.")
455
+ p.add_argument("--progress", default=None, help="Stage, e.g. " + ", ".join(PROGRESS_STAGES) + ".")
456
+ p.add_argument("--next", dest="next_action", default=None, help="Next action to take.")
457
+ p.add_argument("--gc", default=None, help="Client / GC contact info.")
458
+ p.add_argument("--note", default=None, help="Append a dated note to the bid.")
459
+
460
+ track_add = sub.add_parser("track-add", help="Add a bid to the tracker (Active sheet).")
461
+ track_add.add_argument(
462
+ "project", type=Path, nargs="?", help="Optional bid project folder to pull project.json from."
463
+ )
464
+ track_add.add_argument("--id", default=None, help="Explicit bid id (slug).")
465
+ track_add.add_argument("--name", default=None, help="Project name when not using a project folder.")
466
+ track_add.add_argument("--profile", default=None, help="Scope profile id.")
467
+ add_field_flags(track_add)
468
+ track_add.set_defaults(func=command_track_add)
469
+
470
+ track_update = sub.add_parser("track-update", help="Update fields on an existing bid.")
471
+ track_update.add_argument("bid", help="Bid id or project name.")
472
+ add_field_flags(track_update)
473
+ track_update.set_defaults(func=command_track_update)
474
+
475
+ track_move = sub.add_parser("track-move", help="Move a bid to the Archived/Completed sheet.")
476
+ track_move.add_argument("bid", help="Bid id or project name.")
477
+ track_move.add_argument("--outcome", choices=OUTCOMES, default="completed")
478
+ track_move.set_defaults(func=command_track_move)
479
+
480
+ track_reopen = sub.add_parser("track-reopen", help="Move an archived bid back to Active.")
481
+ track_reopen.add_argument("bid", help="Bid id or project name.")
482
+ track_reopen.set_defaults(func=command_track_reopen)
483
+
484
+ track_list = sub.add_parser("track-list", help="List tracked bids in the terminal.")
485
+ track_list.add_argument("--all", action="store_true", help="Include archived/completed bids.")
486
+ track_list.set_defaults(func=command_track_list)
487
+
488
+ track_build = sub.add_parser("track-build", help="Regenerate Bid-Tracker.xlsx from the JSON.")
489
+ track_build.set_defaults(func=command_track_build)
490
+
491
+ doctor = sub.add_parser("doctor", help="Check local dependencies and optional PDF tools.")
492
+ doctor.set_defaults(func=command_doctor)
493
+
494
+ return parser
495
+
496
+
497
+ def main(argv: list[str] | None = None) -> int:
498
+ parser = build_parser()
499
+ args = parser.parse_args(argv)
500
+ try:
501
+ return args.func(args)
502
+ except KeyboardInterrupt:
503
+ return 130
504
+ except Exception as exc:
505
+ print(f"error: {exc}", file=sys.stderr)
506
+ return 1
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "contractor-bid",
3
+ "owner": {
4
+ "name": "ContractorKeith"
5
+ },
6
+ "plugins": [
7
+ {
8
+ "name": "contractor-bid",
9
+ "source": "./",
10
+ "description": "AI-ready bid workspaces for commercial subcontractors.",
11
+ "category": "productivity",
12
+ "policy": {
13
+ "installation": "AVAILABLE",
14
+ "authentication": "NONE"
15
+ }
16
+ }
17
+ ]
18
+ }