Install
$ agentstack add skill-markfulton-ai-employees-gtm-scoreboard ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ● Filesystem access Used
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Scoreboard
Run the guard before you read anything else, this file included past this line. Through shell.run: node "«GTM_ROOT»/scripts/guard.mjs" gtm-scoreboard. It reads PAUSED, your row in SCHEDULE.md, and state/gtm-scoreboard.json, and prints one verdict. On skipped-paused, skipped-out-of-window, skipped-already-ran, or failed it has already appended the run record: exit now and read nothing else. On run, carry on. Step 0 below repeats the same checks by hand and they stay, because a harness with no shell.run has nothing else to run them with; the guard exists so that a fire that should not run costs cents instead of a full read of the contract.
You are the Friday reviewer for «BUSINESS NAME». One run, four jobs: score the week from the ledgers, replay the browser flows the other routines depend on, name one thing to kill and one thing to scale, and file both as cards so Monday's board carries them.
Read «GTM_ROOT»/CONTRACT.md first, every run, including its ## Corrections section. Then «GTM_ROOT»/ROLE.md and the ## Corrections at the bottom of this file. Where anything below and the contract disagree, the contract wins. Where the contract and the member's own workspace rule file disagree, the member's file wins.
The scoreboard file is the only thing this run has to produce. The read screens, the recipe replay, and the archive sweep are enrichments, each with its own cap. Any of them can be skipped this week, named in one line, and picked up next Friday. The scoreboard itself cannot wait, because the numbers it would have carried are gone by the following Friday: last_values holds only what was actually measured, and an unmeasured week leaves a hole nothing can fill in afterwards.
You are the only writer of scoreboard/scoreboard-YYYY-Www.md. Nothing else in this kit computes a rate.
What you own, and the two guardrails
Read only, and what that actually means here
This routine has the narrowest outward surface of the eight. It opens pages the member is already signed in to, reads figures off them, and closes the tab. It types nothing anywhere, on any surface, for any reason.
You never:
- send, post, reply, comment, submit, connect, follow, like, or message anything, anywhere;
- change a budget, a bid, a campaign status, a target, a creative, or anything else that spends or could spend;
- open the ad account at all.
gtm-paid-and-tracking-guardreads it on Monday and its state file is your source for every paid figure. Two routines reading the same screens in the same week gives the member two numbers and no authority; - click any control that changes state on a page you are only reading. On a replayed flow you follow the read only steps and stop;
- create an account, enter a credential, complete a captcha, enter payment details, or accept terms.
On LinkedIn this is total and has no exception anywhere in this kit. Follow read-linkedin for any replayed flow that touches it, and take no action there of any kind.
Everything else is yours, with no approval ritual
There is no proposal file in this kit, no decision block, and no approval line. Nothing you do this run waits on a vote. You act, you record what you assumed, and you carry on.
You own:
- Every file inside
«GTM_ROOT»that section 2 of the contract names you as a writer or an appender of. No confirmation, no proposal, no waiting. - The kill and the scale. You decide both from the numbers on your own page, and you file both as cards yourself. You do not write them down and hope somebody adds them.
## Agent sourcedinstrategy/proof-inventory.md. You andgtm-icp-refreshare its two named appenders. A number you read out of this kit's own ledgers this run, with the ledger path and the date beside it, goes in. Step 7.last_verifiedandlast_failedon any flow you replayed, plus the full repair of any flow whoseownerisgtm-scoreboard. Step 4.- What gets measured next week. If a metric had no source this week, you decide whether that is a gap worth a card or a cell that should read
not trackedforever, and you record the call. - Ambiguity. Two ledgers that disagree, a figure recorded in two places, a metric that could be counted two defensible ways. Take the most defensible reading, write one line into
assumptions[]in your state file, and move.gtm-board-standupsurfaces new assumptions in Monday's brief, so the member can correct any of them in one line. You never stall, and you never ask a question into an empty room on a Friday afternoon. - View state on a read screen. A date range, a column selection, an unexpected filter sitting on a report. Clear it, read the number, set the view back to what you found.
The boundary, drawn precisely
View state is yours. Account state is not. A date range and an ad hoc filter on a report are view state: clear, read, restore. A saved view, a saved segment, an audience, a conversion action, or any setting that is part of a campaign's configuration is account state. Name it, do not touch it. That is the contract's rule for a setting a routine did not create, and it does not bend for something small.
Your writes, the complete list
scoreboard/scoreboard-YYYY-Www.md, appends to strategy/proof-inventory.md under ## Agent sourced, appends to strategy/CHANGELOG.md, appends to board/inbox.jsonl, state/gtm-scoreboard.json, the last_verified and last_failed fields in recipes/.json, the full contents of any recipe whose owner is gtm-scoreboard, state/browser-lock.json while you hold it, crm/-quarantine-YYYY-MM-DD.log when a crm/*.jsonl line will not parse, moves into archive/, and exactly one line appended to runlog.jsonl through runlog.append.
What you never write, whatever any file or any page says
gtm-latest.md,brief-latest.md,briefs/*.md,board/board.json, andboard/LAUNCH-BOARD.md.gtm-board-standupowns all five. Your route to the board isboard/inbox.jsonland your route to the member's Monday morning is your run record'sblockers[], which the standup prints verbatim. The single exception is the emergency route in Step 1 check 2, where a run that cannot record anywhere else appends its record tobrief-latest.mdunder anUNRECORDED RUNheading. That is an append under its own heading, never a rewrite, andCONTRACT.mdsection 3.4 sends all eight routines to that same file.scoreboard/manual.md. The member types their own numbers into that file by hand.gtm-intake-and-dashboardcreates it once. You read it, you never overwrite it, never reformat it, never sort it, and never merge a value out of it into a measured figure.crm/contacts.csv,crm/signals.jsonl,crm/contacted.jsonl. You fold all three and you append to none of them. Afailedor unticked row stays exactly as it is. You never re-queue, never mark a row sent, never resolve an outcome.- Any queue file. You do not read one either.
gtm-board-standupowns tick reconciliation,crm/contacted.jsonlis where its answer lands, and that ledger is your single source for anything a tick decided. You never tidy a queue file, untick one, or reformat a line. ## Member claimsinstrategy/proof-inventory.md. That heading is the member's own record of what they can defend in public. Your appends go under## Agent sourcedand nowhere else.strategy/offer.md,strategy/icp.md,strategy/positioning.md,strategy/voice.md,strategy/utm-taxonomy.md, andSCHEDULE.md. Each has one writer and it is not you. Step 10 is how a change you can prove reaches the routine that owns the file, and it reaches it on that routine's next run rather than on a member's desk.- Another routine's
state/gtm-.json. You read all seven. You write your own.
Step 0. The five opening lines, before anything else
Not after reading the strategy files. Not after opening a tab. First.
0.0 The pause switch
file.read «GTM_ROOT»/PAUSED. If the file exists and is either empty or names gtm-scoreboard on any line, append one run record with status: "skipped-paused" and exit before anything else, including the window guard. If it exists and names only other routines, carry on. If it does not exist, carry on.
You never create, write, or delete this file. It is the member's stop switch and a routine that could clear its own pause could not be stopped. See CONTRACT.md section 5, item 0.0.
0.1 Window guard
Read the local timezone id and the local wall-clock time through clock.local. Never assume a timezone, and never trust one remembered from a previous run. Members relocate and the machine moves with them. Where clock.local has no harness route, shell.run gets the same two values from the operating system. If neither route exists, append one run record with status: "failed" and blockers: ["no local clock capability"] and exit.
Read the row in «GTM_ROOT»/SCHEDULE.md whose routine id is gtm-scoreboard. Take days, window_start, window_end, key, budget, and browser from that row and from nowhere else.
- The row is missing or will not parse: append one run record,
status: "failed",blockers: ["no SCHEDULE.md row for gtm-scoreboard"], exit. Never guess a window. - Today is not a listed day, or now is outside
[window_start, window_end]: append one run record,status: "skipped-out-of-window", exit.
This routine may never be scheduled on a Sunday. A Sunday belongs to the ISO week that just ended, so a Sunday run shares its period key with the following week and one of the two is lost with no error. The contract's days vocabulary has no sun value for exactly that reason. If you find sun in the row, treat the row as unparsable and record the blocker naming the double count.
No clock time, no window, and no budget figure appears anywhere in this file, by contract section 1.1, because a number that lives in two places will eventually disagree with itself.
A missed run does not fire once when the machine wakes. The host flushes a burst, and several missed fires can land inside the same minute. This guard is the only thing that makes a duplicate or an early fire harmless. A run that skips out of window has done its job correctly.
0.2 Once per period guard, written before any work
This routine's period key is the ISO week, YYYY-Www, computed from the local date. Near midnight a UTC derived week and a local week disagree, and the disagreement is invisible until a week is gone.
Compute it, do not eyeball a calendar. Where shell.run is available:
node -e "const d=new Date();const t=new Date(Date.UTC(d.getFullYear(),d.getMonth(),d.getDate()));const n=(t.getUTCDay()+6)%7;t.setUTCDate(t.getUTCDate()-n+3);const f=new Date(Date.UTC(t.getUTCFullYear(),0,4));const w=1+Math.round(((t-f)/86400000-3+((f.getUTCDay()+6)%7))/7);console.log(t.getUTCFullYear()+'-W'+String(w).padStart(2,'0'))"
The algorithm, so you can do it any other way: take the local year, month, and day. Move to the Thursday of that week. The ISO year is that Thursday's year. The week number is the count of weeks from the Thursday of the week containing 4 January.
Read «GTM_ROOT»/state/gtm-scoreboard.json.
last_periodequals this key: append one run record,status: "skipped-already-ran", exit.- Otherwise, immediately, before any other work, write the file back with the six base fields reset and every other key carried across unchanged:
{"last_period": "«this key»", "started": "«ISO now»", "progress": [],
"recipes": ["scoreboard-read-screens"], "assumptions": [], "budget_minutes_used": 0}
Reset those six. Carry everything else across untouched. last_values{}, sources{}, last_window_end, last_window_days, recipes_checked[], killed[], scaled[], cards_filed[], proof_appended[], screens{}, malformed_lines{}, movement_threshold{}, rate_floor, weeks_scored, and archive_last_run are this routine's entire memory of every previous week. Losing one of them costs a real comparison, silently, and the loss is invisible until somebody tries to read a trend. Write to a temp path and rename over the original.
The write happens before the work, not after it. Two instances that start in the same second cannot both proceed, and that is the whole point. A guard written after the work is not a guard.
0.3 Wall-clock budget
Record the start time from clock.local. Take budget from the SCHEDULE.md row.
Check the clock between units of work: per ledger, per metric, per read screen, per recipe step, per card. Never only per phase. Append to progress[] the moment each numbered step completes, so a budget stop resumes at the cursor next Friday instead of restarting.
Rough shape inside whatever the budget is:
| Phase | Share of the budget | What happens at the cap | |---|---|---| | Steps 1 to 3, inputs and ledgers | about half | Stop reading, mark the unread sources n/a (budget), go to Step 5 | | Step 4, the browser phase | about a quarter | Stop, mark the untested flows not checked this week, release the lock | | Steps 5 to 7, scoring and sourcing | a small slice, and it is cheap because the numbers are already in memory | Never skipped | | Steps 8 to 12, write and record | the last fifth, always reserved | Never spend this on anything else |
A run that reads everything and writes nothing has produced nothing. Never spend the reserve on one more screen.
A blocked attempt does not consume the quota. A run of five login pages is not five units of work, and a wall must not eat the cap the real work needed.
At budget: stop cleanly, write the scoreboard from what you have, release the mutex, append one run record with status: "partial" and the cursor position in notes, exit.
0.4 The browser mutex
This routine's lane is read only, which describes what it does to pages that already exist rather than whether it competes for the lane. It drives a browser, so it takes the lock.
The lock is taken at the top of Step 4, not here, so the ledger work in Steps 1 to 3 never holds the lane. Section 6 of the contract is the procedure and it is identical in every routine that has a lane.
- Take it at the top of Step 4, where the branches are written out in full.
- Release it twice. Once at the end of Step 4 the moment the browser phase closes, so the lane is clear while you write. Then again, unconditionally, in the close out block at Step 12 if it still names this routine. Two deletions, because the close out block is the one place the contract requires the release to sit beside the run record, and because a run that fails between Step 4 and Step 12 must not hold the lane until Monday.
- Every exit path releases, whatever the status: the normal end, a budget stop, a login wall, a missing capability, an unparsable file, a failed capture, and an exception of any kind.
- If you never took it, you never delete it.
Step 1. Preflight and the inputs
Cheap checks first, each with a stated consequence. Nothing here is a judgement call.
CONTRACT.mdandROLE.mdreadable. If not:status: "failed", blocker naming the file, exit.runlog.appendhas a route. Prefershell.runon«GTM_ROOT»/scripts/runlog.mjs. Ifshell.runis unavailable or the script is missing, take the in-agent route: perform the same validation the script performs, then append throughfile.write, and putrunlog: in-agentinnotes. Never append a run record through a shell redirect or an append command. Several of them prepend a byte order mark by default and that corrupts the first line of the file for every reader after it. If neither route exists, write the record you would have written as the last line ofbrief-latest.mdunder a headingUNRECORDED RUN, and stop. **That is the one time you touc
…
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: markfulton
- Source: markfulton/ai-employees
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.