Files
magnus919_agent-skills/writers-helper/scripts/submission-tracker.py
Magnus HedemarkGitHubfactory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
6fe5aed86e feat(skill): rename writing skill to writers-helper (#302)
Rename the skill directory to writers-helper and update the name field,
eval manifest skill_name, skill README title and example paths, root
README catalog entry, and the skill-triggers index. Regenerate llms.txt,
.claude-plugin/marketplace.json, .codex-plugin/plugin.json, and
.agents/plugins/marketplace.json from their generators. Content is
unchanged.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-08-08 13:06:46 -04:00

281 lines
9.1 KiB
Python

#!/usr/bin/env python3
"""Track submissions to agents and publishers.
Maintains a JSONL ledger (default ~/.writing-submissions.jsonl) of queries,
proposals, and manuscript submissions. Reports the pipeline by status and
flags follow-ups due. Non-interactive; --dry-run previews mutations.
Examples:
submission-tracker.py --add "The Glass Orchard" "Agent Name" query 2026-08-08 sent
submission-tracker.py --update 1 status partial-request
submission-tracker.py --list
submission-tracker.py --status --follow-up-days 21
"""
import argparse
import json
import os
import sys
from datetime import date, datetime, timedelta
from pathlib import Path
DEFAULT_FILE = os.path.join(Path.home(), ".writing-submissions.jsonl")
VALID_STATUSES = {
"sent",
"no-response",
"partial-request",
"full-request",
"rejection",
"offer",
"accepted",
"withdrawn",
}
SORT_ORDER = {
"offer": 0,
"accepted": 0,
"full-request": 1,
"partial-request": 2,
"sent": 3,
"no-response": 3,
"rejection": 4,
"withdrawn": 5,
}
def load_records(path):
records = []
if not path.is_file():
return records
with path.open(encoding="utf-8") as handle:
for line_number, line in enumerate(handle, start=1):
line = line.strip()
if not line:
continue
try:
record = json.loads(line)
except json.JSONDecodeError as error:
print(
f"warning: skipping malformed line {line_number} in {path}: {error}",
file=sys.stderr,
)
continue
records.append(record)
return records
def save_records(path, records):
with path.open("w", encoding="utf-8") as handle:
for record in records:
handle.write(json.dumps(record, sort_keys=True) + "\n")
def add_record(path, title, target, submission_type, sent_date, status, notes):
records = load_records(path)
next_id = max((int(r["id"]) for r in records), default=0) + 1
record = {
"id": next_id,
"title": title,
"target": target,
"type": submission_type,
"sent_date": sent_date,
"status": status,
"notes": notes,
}
records.append(record)
save_records(path, records)
return record
def update_record(path, record_id, field, value):
records = load_records(path)
for record in records:
if int(record["id"]) == record_id:
record[field] = value
save_records(path, records)
return record
return None
def parse_date(value):
try:
return datetime.strptime(value, "%Y-%m-%d").date()
except ValueError as error:
raise ValueError(f"invalid date {value!r}, expected YYYY-MM-DD") from error
def status_report(records, follow_up_days):
counts = {}
for record in records:
status = record.get("status", "sent")
counts[status] = counts.get(status, 0) + 1
cutoff = date.today() - timedelta(days=follow_up_days)
follow_ups = []
for record in records:
status = record.get("status", "sent")
if status not in {"sent", "no-response"}:
continue
try:
sent = parse_date(record.get("sent_date", ""))
except ValueError:
continue
if sent <= cutoff:
follow_ups.append(
{
"id": record["id"],
"title": record.get("title", ""),
"target": record.get("target", ""),
"sent_date": record.get("sent_date", ""),
"days_outstanding": (date.today() - sent).days,
}
)
total = len(records)
active = sum(
counts.get(s, 0) for s in ("sent", "no-response", "partial-request", "full-request")
)
return {
"total": total,
"active": active,
"by_status": counts,
"follow_ups_due": follow_ups,
"follow_up_window_days": follow_up_days,
}
def main(argv=None):
parser = argparse.ArgumentParser(description="Track submissions to agents and publishers.")
parser.add_argument("--file", default=DEFAULT_FILE, help="Path to the JSONL ledger.")
parser.add_argument(
"--add",
nargs="+",
metavar="VALUE",
help="Add a submission: TITLE TARGET TYPE SENT-DATE STATUS [NOTES...]",
)
parser.add_argument(
"--update",
nargs=3,
metavar=("ID", "FIELD", "VALUE"),
help="Update a field on a submission.",
)
parser.add_argument("--list", action="store_true", help="List all submissions.")
parser.add_argument("--status", action="store_true", help="Report the pipeline.")
parser.add_argument("--follow-up-days", type=int, default=21, help="Follow-up window in days.")
parser.add_argument("--json", action="store_true", help="Emit JSON.")
parser.add_argument(
"--dry-run", action="store_true", help="Preview a mutation without writing."
)
args = parser.parse_args(argv)
path = Path(args.file).expanduser()
if args.add:
if len(args.add) < 4:
parser.error("--add needs TITLE TARGET TYPE SENT-DATE STATUS [NOTES...]")
title, target, submission_type = args.add[0], args.add[1], args.add[2]
try:
sent_date = parse_date(args.add[3]).isoformat()
except ValueError as error:
parser.error(str(error))
status = args.add[4]
if status not in VALID_STATUSES:
parser.error(f"invalid status {status!r}; valid: {', '.join(sorted(VALID_STATUSES))}")
notes = " ".join(args.add[5:])
if args.dry_run:
print(
f"would add to {path}: {title!r} -> {target} ({submission_type}) "
f"sent {sent_date}, status {status}"
)
return 0
try:
record = add_record(path, title, target, submission_type, sent_date, status, notes)
except OSError as error:
print(f"error: cannot write {path}: {error}", file=sys.stderr)
return 1
if args.json:
print(json.dumps(record, indent=2))
else:
print(
f"added #{record['id']}: {record['title']} -> {record['target']} ({record['status']})"
)
return 0
if args.update:
try:
record_id = int(args.update[0])
except ValueError:
parser.error("ID must be an integer")
field, value = args.update[1], args.update[2]
if field == "status" and value not in VALID_STATUSES:
parser.error(f"invalid status {value!r}; valid: {', '.join(sorted(VALID_STATUSES))}")
if field == "sent_date":
try:
value = parse_date(value).isoformat()
except ValueError as error:
parser.error(str(error))
if args.dry_run:
print(f"would update #{record_id} in {path}: {field} = {value!r}")
return 0
try:
record = update_record(path, record_id, field, value)
except OSError as error:
print(f"error: cannot write {path}: {error}", file=sys.stderr)
return 1
if record is None:
print(f"error: no submission with id {record_id}", file=sys.stderr)
return 1
if args.json:
print(json.dumps(record, indent=2))
else:
print(f"updated #{record['id']}: {field} = {value!r}")
return 0
try:
records = load_records(path)
except OSError as error:
print(f"error: cannot read {path}: {error}", file=sys.stderr)
return 1
if args.list:
records = sorted(records, key=lambda r: SORT_ORDER.get(r.get("status", "sent"), 5))
if args.json:
print(json.dumps(records, indent=2))
return 0
if not records:
print("no submissions recorded")
return 0
for record in records:
print(
f"#{record['id']} [{record.get('status', 'sent')}] {record.get('title', '')} "
f"-> {record.get('target', '')} ({record.get('type', '')}, sent {record.get('sent_date', '')})"
)
return 0
if args.status:
if args.follow_up_days < 1:
parser.error("--follow-up-days must be >= 1")
report = status_report(records, args.follow_up_days)
if args.json:
print(json.dumps(report, indent=2))
return 0
print(f"Pipeline: {report['total']} total, {report['active']} active")
for status, count in sorted(report["by_status"].items()):
print(f" {status}: {count}")
if report["follow_ups_due"]:
print(f"Follow-ups due (>{args.follow_up_days} days):")
for item in report["follow_ups_due"]:
print(
f" #{item['id']} {item['title']} -> {item['target']} "
f"({item['days_outstanding']} days)"
)
else:
print("No follow-ups due")
return 0
parser.error("one of --add, --update, --list, or --status is required")
return 1
if __name__ == "__main__":
sys.exit(main())