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.
- contractor_bid/.claude-plugin/marketplace.json +13 -0
- contractor_bid/.claude-plugin/plugin.json +19 -0
- contractor_bid/.cursor-plugin/plugin.json +30 -0
- contractor_bid/.mcp.json +8 -0
- contractor_bid/AGENTS.md +66 -0
- contractor_bid/CLAUDE.md +5 -0
- contractor_bid/__init__.py +5 -0
- contractor_bid/__main__.py +4 -0
- contractor_bid/cli.py +506 -0
- contractor_bid/codex-marketplace.json +18 -0
- contractor_bid/commands/check.md +7 -0
- contractor_bid/commands/new-bid.md +11 -0
- contractor_bid/commands/triage.md +10 -0
- contractor_bid/csi_starters.py +1269 -0
- contractor_bid/docs/CSI_DIVISIONS.md +65 -0
- contractor_bid/docs/MCP_PLUGIN.md +129 -0
- contractor_bid/docs/SELF_LEARNING.md +36 -0
- contractor_bid/docs/WHAT_YOU_GET.md +43 -0
- contractor_bid/docs/WORKFLOW.md +63 -0
- contractor_bid/docs/assets/contractor-bid-flow.svg +118 -0
- contractor_bid/doctor.py +100 -0
- contractor_bid/examples/agent-session.md +102 -0
- contractor_bid/examples/profiles/concrete-flatwork.json +16 -0
- contractor_bid/examples/profiles/division-03-concrete.json +68 -0
- contractor_bid/examples/profiles/division-04-masonry.json +64 -0
- contractor_bid/examples/profiles/division-05-metals.json +68 -0
- contractor_bid/examples/profiles/division-06-wood-plastics-composites.json +64 -0
- contractor_bid/examples/profiles/division-07-thermal-moisture-protection.json +68 -0
- contractor_bid/examples/profiles/division-08-openings.json +67 -0
- contractor_bid/examples/profiles/division-09-finishes.json +66 -0
- contractor_bid/examples/profiles/division-10-specialties.json +65 -0
- contractor_bid/examples/profiles/division-11-equipment.json +64 -0
- contractor_bid/examples/profiles/division-12-furnishings.json +64 -0
- contractor_bid/examples/profiles/division-13-special-construction.json +65 -0
- contractor_bid/examples/profiles/division-14-conveying-equipment.json +63 -0
- contractor_bid/examples/profiles/division-21-fire-suppression.json +63 -0
- contractor_bid/examples/profiles/division-22-plumbing.json +64 -0
- contractor_bid/examples/profiles/division-23-hvac.json +69 -0
- contractor_bid/examples/profiles/division-25-integrated-automation.json +64 -0
- contractor_bid/examples/profiles/division-26-electrical.json +66 -0
- contractor_bid/examples/profiles/division-27-communications.json +65 -0
- contractor_bid/examples/profiles/division-28-electronic-safety-security.json +66 -0
- contractor_bid/examples/profiles/division-31-earthwork.json +67 -0
- contractor_bid/examples/profiles/division-32-exterior-improvements.json +69 -0
- contractor_bid/examples/profiles/division-33-utilities.json +68 -0
- contractor_bid/examples/profiles/drywall-framing.json +16 -0
- contractor_bid/examples/profiles/electrical.json +16 -0
- contractor_bid/examples/profiles/fences-gates.json +16 -0
- contractor_bid/examples/profiles/hvac.json +70 -0
- contractor_bid/examples/profiles/plumbing.json +65 -0
- contractor_bid/examples/profiles/roofing.json +68 -0
- contractor_bid/learning.py +42 -0
- contractor_bid/mcp_server.py +845 -0
- contractor_bid/packets.py +326 -0
- contractor_bid/profile.py +185 -0
- contractor_bid/profiles/README.md +27 -0
- contractor_bid/profiles/concrete-flatwork.json +16 -0
- contractor_bid/profiles/division-03-concrete.json +68 -0
- contractor_bid/profiles/division-04-masonry.json +64 -0
- contractor_bid/profiles/division-05-metals.json +68 -0
- contractor_bid/profiles/division-06-wood-plastics-composites.json +64 -0
- contractor_bid/profiles/division-07-thermal-moisture-protection.json +68 -0
- contractor_bid/profiles/division-08-openings.json +67 -0
- contractor_bid/profiles/division-09-finishes.json +66 -0
- contractor_bid/profiles/division-10-specialties.json +65 -0
- contractor_bid/profiles/division-11-equipment.json +64 -0
- contractor_bid/profiles/division-12-furnishings.json +64 -0
- contractor_bid/profiles/division-13-special-construction.json +65 -0
- contractor_bid/profiles/division-14-conveying-equipment.json +63 -0
- contractor_bid/profiles/division-21-fire-suppression.json +63 -0
- contractor_bid/profiles/division-22-plumbing.json +64 -0
- contractor_bid/profiles/division-23-hvac.json +69 -0
- contractor_bid/profiles/division-25-integrated-automation.json +64 -0
- contractor_bid/profiles/division-26-electrical.json +66 -0
- contractor_bid/profiles/division-27-communications.json +65 -0
- contractor_bid/profiles/division-28-electronic-safety-security.json +66 -0
- contractor_bid/profiles/division-31-earthwork.json +67 -0
- contractor_bid/profiles/division-32-exterior-improvements.json +69 -0
- contractor_bid/profiles/division-33-utilities.json +68 -0
- contractor_bid/profiles/drywall-framing.json +16 -0
- contractor_bid/profiles/electrical.json +16 -0
- contractor_bid/profiles/fences-gates.json +16 -0
- contractor_bid/profiles/hvac.json +16 -0
- contractor_bid/profiles/plumbing.json +16 -0
- contractor_bid/profiles/roofing.json +16 -0
- contractor_bid/project.py +101 -0
- contractor_bid/scripts/generate-csi-starters.py +94 -0
- contractor_bid/scripts/install.ps1 +78 -0
- contractor_bid/scripts/install.sh +123 -0
- contractor_bid/sendoff.py +76 -0
- contractor_bid/skills/README.md +13 -0
- contractor_bid/skills/bid-tracker/SKILL.md +99 -0
- contractor_bid/skills/concrete-flatwork-bid-scope/SKILL.md +62 -0
- contractor_bid/skills/division-03-concrete-bid-scope/SKILL.md +67 -0
- contractor_bid/skills/division-04-masonry-bid-scope/SKILL.md +65 -0
- contractor_bid/skills/division-05-metals-bid-scope/SKILL.md +66 -0
- contractor_bid/skills/division-06-wood-plastics-composites-bid-scope/SKILL.md +65 -0
- contractor_bid/skills/division-07-thermal-moisture-protection-bid-scope/SKILL.md +66 -0
- contractor_bid/skills/division-08-openings-bid-scope/SKILL.md +66 -0
- contractor_bid/skills/division-09-finishes-bid-scope/SKILL.md +65 -0
- contractor_bid/skills/division-10-specialties-bid-scope/SKILL.md +65 -0
- contractor_bid/skills/division-11-equipment-bid-scope/SKILL.md +65 -0
- contractor_bid/skills/division-12-furnishings-bid-scope/SKILL.md +64 -0
- contractor_bid/skills/division-13-special-construction-bid-scope/SKILL.md +65 -0
- contractor_bid/skills/division-14-conveying-equipment-bid-scope/SKILL.md +64 -0
- contractor_bid/skills/division-21-fire-suppression-bid-scope/SKILL.md +64 -0
- contractor_bid/skills/division-22-plumbing-bid-scope/SKILL.md +65 -0
- contractor_bid/skills/division-23-hvac-bid-scope/SKILL.md +66 -0
- contractor_bid/skills/division-25-integrated-automation-bid-scope/SKILL.md +65 -0
- contractor_bid/skills/division-26-electrical-bid-scope/SKILL.md +66 -0
- contractor_bid/skills/division-27-communications-bid-scope/SKILL.md +65 -0
- contractor_bid/skills/division-28-electronic-safety-security-bid-scope/SKILL.md +66 -0
- contractor_bid/skills/division-31-earthwork-bid-scope/SKILL.md +66 -0
- contractor_bid/skills/division-32-exterior-improvements-bid-scope/SKILL.md +66 -0
- contractor_bid/skills/division-33-utilities-bid-scope/SKILL.md +65 -0
- contractor_bid/skills/drywall-framing-bid-scope/SKILL.md +62 -0
- contractor_bid/skills/electrical-bid-scope/SKILL.md +63 -0
- contractor_bid/skills/fences-gates-bid-scope/SKILL.md +64 -0
- contractor_bid/skills/hvac-bid-scope/SKILL.md +72 -0
- contractor_bid/skills/plumbing-bid-scope/SKILL.md +69 -0
- contractor_bid/skills/roofing-bid-scope/SKILL.md +71 -0
- contractor_bid/templates/00-scope-reference-index-template.md +45 -0
- contractor_bid/templates/02-proposal-letter-template.md +48 -0
- contractor_bid/templates/project-readme-template.md +38 -0
- contractor_bid/templates/review-pages-template.md +7 -0
- contractor_bid/templates/scope-pages-sources-template.json +19 -0
- contractor_bid/templates/takeoff-template.json +48 -0
- contractor_bid/tracker.py +382 -0
- contractor_bid/triage.py +483 -0
- contractor_bid/util.py +90 -0
- contractor_bid/validate.py +251 -0
- contractor_bid/workbook.py +328 -0
- contractor_bid-0.2.0.dist-info/METADATA +402 -0
- contractor_bid-0.2.0.dist-info/RECORD +137 -0
- contractor_bid-0.2.0.dist-info/WHEEL +4 -0
- contractor_bid-0.2.0.dist-info/entry_points.txt +3 -0
- contractor_bid-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -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
|
+
}
|
contractor_bid/.mcp.json
ADDED
contractor_bid/AGENTS.md
ADDED
|
@@ -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.
|
contractor_bid/CLAUDE.md
ADDED
|
@@ -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.
|
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
|
+
}
|